目录
介绍
在第1部分中,BooksStore应用程序可以在单个页面上显示数据库中的书籍,在第2部分中,它可以在一个页面上显示较少数量的书籍,并且用户可以从一个页面移动到另一个页面以查看整个目录。在本文中,我们将添加对按类型浏览书籍的支持。
使用代码
过滤图书对象
首先,要按Book类型过滤对象,我们将通过更改Models/ViewModels文件夹中的BooksListViewModel.cs文件来添加一个名为CurrentGenre的视图模型属性:
using System.Collections.Generic;
namespace BooksStore.Models.ViewModels
{
public class BooksListViewModel
{
public IEnumerable<Book> Books { get; set; }
public PagingInfo PagingInfo { get; set; }
public string CurrentGenre { get; set; }
}
}
下一步是更新Home控制器,以便Index操作方法按genres过滤Book对象,并使用我们添加到视图模型的属性来指示使用以下代码选择了哪个genres:
public IActionResult Index(string genre, int bookPage = 1)
=> View(new BooksListViewModel
{
Books = repository.Books
.Where(p => genre == null || p.Genre == genre)
.OrderBy(p => p.BookID)
.Skip((bookPage - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo
{
CurrentPage = bookPage,
ItemsPerPage = PageSize,
TotalItems = repository.Books.Count()
},
CurrentGenre = genre
});
在前面的代码中:
- 我们添加了一个名为genre的参数。此参数用于增强LINQ查询:如果类型不是null,则仅选择具有匹配Genre属性的Book对象。
- 我们还设置了CurrentGenre属性的值。
运行应用程序:
使用以下URL选择自助类型:http://localhost:44333/?genre=Self-Help
但是,这些更改意味着PagingInfo.TotalItems的值计算不正确,因为它没有考虑genres过滤器。而且,很明显,我们和我们的用户不希望使用URL导航到genres。
改进URL方案
我们将通过更改Startup类Configure方法中的路由配置来改进URL方案,以使用以下代码创建一组更有用的URL:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("genpage",
"{genre}/{bookPage:int}",
new { Controller = "Home", action = "Index" });
endpoints.MapControllerRoute("page", "{bookPage:int}",
new { Controller = "Home", action = "Index", bookPage = 1 });
endpoints.MapControllerRoute("genre", "{genre}",
new { Controller = "Home", action = "Index", bookPage = 1 });
endpoints.MapControllerRoute("pagination",
"Books/{bookPage}",
new { Controller = "Home", action = "Index", bookPage = 1 });
endpoints.MapDefaultControllerRoute();
});
通过使用ASP.NET Core路由系统来处理传入请求和生成传出URL,我们可以确保应用程序中的所有URL都是一致的。
现在我们需要一种方法来从视图中接收额外的信息,而不必向标签助手类添加额外的属性。幸运的是,标签助手有一个很好的特性,它允许在一个集合中一起接收具有公共前缀的属性。BooksStore /MyTagHelper文件夹中的MyPageLink.cs文件中的前缀值,代码如下:
public class MyPageLink : TagHelper
{
private IUrlHelperFactory urlHelperFactory;
public MyPageLink(IUrlHelperFactory helperFactory)
{
urlHelperFactory = helperFactory;
}
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public PagingInfo PageModel { get; set; }
public string PageAction { get; set; }
[HtmlAttributeName(DictionaryAttributePrefix = "page-url-")]
public Dictionary<string, object> PageUrlValues { get; set; }
= new Dictionary<string, object>();
public bool PageClassesEnabled { get; set; } = false;
public string PageClass { get; set; }
public string PageClassNormal { get; set; }
public string PageClassSelected { get; set; }
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(ViewContext);
TagBuilder result = new TagBuilder("div");
for (int i = 1; i <= PageModel.TotalPages; i++)
{
TagBuilder tag = new TagBuilder("a");
PageUrlValues["bookPage"] = i;
tag.Attributes["href"] = urlHelper.Action(PageAction, PageUrlValues);
tag.Attributes["href"] = urlHelper.Action(PageAction,
new { bookPage = i });
if (PageClassesEnabled)
{
tag.AddCssClass(PageClass);
tag.AddCssClass(i == PageModel.CurrentPage
? PageClassSelected : PageClassNormal);
}
tag.InnerHtml.Append(i.ToString());
result.InnerHtml.AppendHtml(tag);
}
output.Content.AppendHtml(result.InnerHtml);
}
}
我们使用的HtmlAttributeName属性允许我们为元素上的属性名称指定前缀,在本例中为page-url-。名称以此前缀开头的任何属性的值都将添加到分配给该PageUrlValues属性的字典中,然后将其传递给该IUrlHelper.Action方法以生成标签助手生成的a元素的href属性的URL。
在BooksStore/Views/Home文件夹中的Index.cshtml文件中,我们将为标签助手处理的div元素添加一个新属性,指定将用于生成URL的类型,并带有以下标记:
<div page-model="@Model.PagingInfo" page-action="Index" page-classes-enabled="true"
page-class="btn" page-class-normal="btn-outline-dark"
page-class-selected="btn-primary" page-url-genre="@Model.CurrentGenre"
class="btn-group pull-right m-1">
</div>
我们只向视图添加了一个新属性,但任何具有相同前缀的属性都将添加到字典中。
运行应用程序并请求https://localhost:44333/Self-Help
为分页链接生成的链接如下所示:http://localhost:44333/1。如果用户单击这样的页面链接,类型过滤器将丢失,应用程序将显示一个包含所有类型书籍的页面。通过添加从视图模型中获取的当前genres,我们生成这样的URL:http://localhost:44333/Self-Help/1。当用户点击此类链接时,会将当前genres传递给Index操作方法,并保留过滤。
创建导航视图组件
ASP.NET Core具有视图组件的概念,非常适合创建可重用导航控件等项目。我们将创建一个视图组件来呈现导航菜单并通过从共享布局中调用该组件将其集成到应用程序中。
我们将在BooksStore项目中创建一个名为ViewComponents的文件夹,它是视图组件的常规主页,并向其中添加一个名为GenreNavigation.cs的类文件,我们使用以下代码定义该类:
using Microsoft.AspNetCore.Mvc;
namespace BooksStore.ViewComponents
{
public class GenreNavigation : ViewComponent
{
public string Invoke()
{
return "Hello from the Genre Navigation.";
}
}
}
当组件在Razor视图中使用时,会调用视图组件的Invoke方法,并将Invoke方法的结果插入到发送给浏览器的HTML中。我们希望genres列表出现在所有页面上,因此我们将在共享布局中使用视图组件。为此,我们将使用BooksStore/Views/Shared文件夹中的_Layout.cshtml文件中的视图组件,并带有以下标记:
<body>
<div>
<div class="bg-dark text-white p-2">
<span class="navbar-brand ml-2">BOOKS STORE</span>
</div>
<div class="row m-1 p-1">
<div id="genres" class="col-3">
<p>The BooksStore homepage helps you explore Earth's Biggest
Bookstore without ever leaving the comfort of your couch.</p>
<vc:genre-navigation />
</div>
<div class="col-9">
@RenderBody()
</div>
</div>
</div>
</body>
我们添加了插入视图组件的vc:genre-navigation元素。此元素将其连字符连接起来,从而vc:genre-navigation指定GenreNavigation类。
运行应用程序:
创建genres导航
我们可以使用视图组件生成组件列表,然后使用更具表现力的Razor语法来呈现将显示它们的HTML。第一步是使用以下代码更新BooksStore/ViewComponents文件夹中GenreNavigation.cs文件中的视图组件:
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BooksStore.Models;
namespace BooksStore.ViewComponents
{
public class GenreNavigation : ViewComponent
{
private IBooksStoreRepository repository;
public GenreNavigation(IBooksStoreRepository repo)
{
repository = repo;
}
public IViewComponentResult Invoke()
{
return View(repository.Books
.Select(x => x.Genre)
.Distinct()
.OrderBy(x => x));
}
}
}
在前面的代码中,构造函数定义了一个IBooksStoreRepository参数。在该Invoke方法中,我们使用LINQ选择和排序存储库中的genres集,并将它们作为参数传递给该View方法,该方法呈现默认的Razor局部视图,其详细信息从使用IViewComponentResult对象的方法返回。
Razor使用不同的约定来定位视图组件选择的视图。视图的默认名称和搜索视图的位置都与用于控制器的不同。为此,我们将在BooksStore项目中创建Views/Shared/Components/GenreNavigation文件夹,并向其中添加一个名为Default.cshtml的Razor视图,我在其中添加了具有以下标记的内容:
@model IEnumerable<string>
<a class="btn btn-block btn-outline-primary" asp-action="Index"
asp-controller="Home" asp-route-genre="">
Home
</a>
@foreach (string genre in Model)
{
<a class="btn btn-block btn-outline-primary"
asp-action="Index" asp-controller="Home"
asp-route-genre="@genre"
asp-route-bookPage="1">
@genre
</a>
}
运行应用程序以查看genres导航按钮。如果单击按钮,项目列表将更新为仅显示所选genres的项目,如下图所示:
指示当前genres
我们需要向用户提供一些清晰的视觉反馈,以表明选择了哪种类型。为此,第一步,我们将使用该RouteData属性访问请求数据,以获取当前所选genres的值。在下面的代码中,我们将在BooksStore/ViewComponents文件夹中的GenreNavigation.cs文件中传递选定的genres。
public IViewComponentResult Invoke()
{
ViewBag.SelectedGenre = RouteData?.Values["genre"];
return View(repository.Books
.Select(x => x.Genre)
.Distinct()
.OrderBy(x => x));
}
在Invoke方法内部,我们为ViewBag对象动态分配了一个SelectedGenre属性,并将其值设置为当前genres,该genres是通过该RouteData属性返回的上下文对象获取的。ViewBag是一个动态对象,它允许我们简单地通过为它们分配值来定义新属性。
接下来,我们可以更新视图组件选择的视图,并改变用于样式链接的CSS类,以便代表当前类型的类是不同的。为此,我们将使用以下标记更改Views/Shared/Components/GenreNavigation文件夹中的Default.cshtml文件:
@model IEnumerable<string>
<a class="btn btn-block btn-outline-primary" asp-action="Index"
asp-controller="Home" asp-route-genre="">
Home
</a>
@foreach (string genre in Model)
{
<a class="btn btn-block
@(genre == ViewBag.SelectedGenre
? "btn-primary": "btn-outline-primary")"
asp-action="Index" asp-controller="Home"
asp-route-genre="@genre"
asp-route-bookPage="1">
@genre
</a>
}
我们在class属性中使用了Razor表达式,将btn-primary类应用于表示所选genres的元素,否则应用到btn-outline-primary类。运行应用程序并请求自助类型:
修复分页
目前,页面链接的数量取决于存储库中的书籍总数,而不是所选类型的书籍数量。这意味着我们可以点击Self-Help类型的第2页的链接,因为没有足够的书来填满两页,所以最终会得到一个空白页,如下图所示:
我们可以通过更新Home控制器中的Index操作方法来解决这个问题,以便分页信息使用以下代码传递genres:
public IActionResult Index(string genre, int bookPage = 1)
=> View(new BooksListViewModel
{
Books = repository.Books
.Where(p => genre == null || p.Genre == genre)
.OrderBy(p => p.BookID)
.Skip((bookPage - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo
{
CurrentPage = bookPage,
ItemsPerPage = PageSize,
TotalItems = genre == null ?
repository.Books.Count() :
repository.Books.Where(e =>
e.Genre == genre).Count()
},
CurrentGenre = genre
});
运行应用程序:
兴趣点
我们添加了对分页的支持,以便视图在一个页面上显示较少数量的书籍,并且用户可以从一个页面移动到另一个页面以查看整个目录。我们使用Bootstrap来设计应用程序的外观,我们还添加了对按类型导航书籍的支持。在下一篇文章中,我们将添加一个购物车,它是电子商务应用程序的重要组件。
https://www.codeproject.com/Articles/5327794/An-Introduction-to-ASP-NET-Core-MVC-through-an-E-3