ASP.NET Core MVC 示例入门(第三部分)





5.00/5 (1投票)
ASP.NET Core MVC 入门
引言
在第一部分中,BooksStore
应用程序可以在一个页面上显示数据库中的书籍;在第二部分中,它可以每页显示少量书籍,用户可以翻页查看整个目录。在本文中,我们将添加按类型浏览书籍的支持。
Using the Code
筛选图书对象
首先,为了按类型筛选 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
操作方法按类型筛选 Book
对象,并使用我们添加到视图模型的属性来指示已选择的类型,代码如下:
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 查询:如果 genre 不为null
,则仅选择具有匹配Genre
属性的Book
对象。 - 我们还设置了
CurrentGenre
属性的值。
运行应用程序
使用以下 URL 选择“自助”类型:https://:44333/?genre=Self-Help
然而,这些更改意味着 PagingInfo.TotalItems
的值计算不正确,因为它没有考虑类型过滤器。显然,我们和用户都不希望使用 URL 来导航类型。
改进 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://:44333/Self-Help
为分页链接生成的链接如下所示:https://:44333/1。如果用户点击这样的页面链接,类型过滤器就会丢失,应用程序会显示一个包含所有类型书籍的页面。通过添加从视图模型获取的当前类型,我们生成了如下 URL:https://:44333/Self-Help/1。当用户点击此类链接时,当前类型将被传递到Index
操作方法,并且筛选将得到保留。
创建导航视图组件
ASP.NET Core 有视图组件的概念,这非常适合创建可重用的导航控件等项目。我们将创建一个视图组件来渲染导航菜单,并通过从共享布局调用该组件来将其集成到应用程序中。
我们将创建一个名为ViewComponents的文件夹,这是视图组件的约定存放位置,在BooksStore
项目中,并向其中添加一个名为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 中。我们希望类型列表出现在所有页面上,因此我们将使用共享布局中的视图组件。为此,我们将使用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
类。
运行应用程序
创建类型导航
我们可以使用视图组件来生成组件列表,然后使用更具表现力的 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 来选择和排序存储库中的类型集合,并将它们作为参数传递给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>
}
运行应用程序以查看类型导航按钮。如果单击一个按钮,项目列表将更新为仅显示所选类型的项目,如下图所示:
指示当前类型
我们需要一些清晰的视觉反馈来指示用户已选择哪种类型。为此,第一步,我们将使用RouteData
属性访问请求数据,以获取当前所选类型的的值。在以下代码中,我们将把所选类型传递到BooksStore/ViewComponents文件夹中的GenreNavigation.cs文件中。
public IViewComponentResult Invoke()
{
ViewBag.SelectedGenre = RouteData?.Values["genre"];
return View(repository.Books
.Select(x => x.Genre)
.Distinct()
.OrderBy(x => x));
}
在Invoke
方法内部,我们动态地将SelectedGenre
属性分配给ViewBag
对象,并将其值设置为当前类型,该类型通过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
类应用于代表所选类型的元素,否则应用btn-outline-primary
类。运行应用程序并请求“自助”类型。
修复分页
目前,页面链接的数量是由存储库中图书的总数决定的,而不是由所选类型的图书数量决定的。这意味着我们可以单击“自助”类型的第 2 页的链接,但最终得到一个空页面,因为没有足够的图书来填充两页,如下图所示:
我们可以通过更新Home
控制器中的Index
操作方法来修复此问题,以便分页信息考虑类型,代码如下:
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 来设置应用程序的外观样式,并且还添加了按类型导航图书的支持。在下一篇文章中,我们将添加一个购物车,这是电子商务应用程序的一个重要组成部分。
历史
- 2022 年 3 月 18 日:初始版本