65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2022年3月18日

CPOL

7分钟阅读

viewsIcon

9509

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 日:初始版本
© . All rights reserved.