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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2022年4月6日

CPOL

7分钟阅读

viewsIcon

14991

ASP.NET Core MVC 入门

引言

在本文中,我们将创建一个购物体验,这对于任何在线购物过的人来说都应该很熟悉。购物体验可以像这样:

  • 在目录的每本书旁边都会显示一个“添加到购物车”按钮。点击此按钮将显示客户到目前为止已选择的书籍摘要,包括总成本。
  • 此时,用户可以点击“继续购物”按钮返回图书目录,或者点击“立即结账”按钮完成订单并结束购物会话。

“立即结账”按钮以及其他一些高级体验将在下一篇文章中介绍。顺便说一句,到目前为止,我们已经完成了构建BooksStore应用程序旅程的 3 个部分。

Using the Code

配置购物车页面

在本节中,我们将使用Razor Pages来实现购物车。为此,第一步,我们将配置Startup类以在BooksStore应用程序中启用Razor Pages

using System;
using BooksStore.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace BooksStore
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        // This method gets called by the runtime. 
        // Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddDbContext<BooksStoreDbContext>(opts => {
                opts.UseSqlServer(
                    Configuration["ConnectionStrings:SportsStoreConnection"]);
            });
            services.AddScoped<IBooksStoreRepository, EFBooksStoreRepository>();
            services.AddRazorPages();
        }
        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. 
                // You may want to change this for production scenarios, 
                // see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            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/Page{bookPage}",
                    new { Controller = "Home", action = "Index", bookPage = 1 });
                endpoints.MapDefaultControllerRoute();
                endpoints.MapRazorPages();
            });
            SeedData.EnsurePopulated(app);
        }
    }
}

AddRazorPages方法设置Razor Pages使用的服务,而MapRazorPages方法将Razor Pages注册为 URL 路由系统可以用来处理请求的终结点。

下一步,我们在BooksStore项目中添加一个名为*Pages*的文件夹,这是Razor Pages的约定位置。我们将向*Pages*文件夹添加一个名为*\_ViewImports.cshtml*的文件,其中包含以下代码:

@namespace BooksStore.Pages
@using Microsoft.AspNetCore.Mvc.RazorPages
@using BooksStore.Models
@using BooksStore.MyTagHelper
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

这些表达式设置了Razor Pages所属的命名空间,并允许在不指定命名空间的情况下在Razor Pages中使用BooksStore类。

接下来,我们将向*Pages*文件夹添加一个名为*\_ViewStart.cshtml*的文件,其中包含以下代码:

@{
    Layout = "_MyCartLayout";
}

Razor Pages有自己的配置文件,这个文件指定BooksStore项目中的Razor Pages将默认使用名为*\_MyCartLayout*的布局文件。

最后,为了提供Razor Pages将使用的布局,向*Pages*文件夹添加一个名为*\_MyCartLayout.cshtml*的文件,其中包含以下标记:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Books Store</title>
    <link href="~/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <div class="bg-dark text-white p-2">
        <span class="navbar-brand ml-2">BOOKS STORE</span>
    </div>
    <div class="m-1 p-1">
        @RenderBody()
    </div>
</body>
</html>

创建购物车页面

 

在 Visual Studio 中,我们将添加Razor Page模板项,并将项名称设置为*MyCart.cshtml*到*Pages*文件夹。这将创建一个*MyCart.cshtml*文件和一个*MyCart.cshtml.cs*类文件。将*MyCart.cshtml*文件的内容替换为以下标记:

@page
<h4>Hello, I am the Cart Page</h4>

使用 URL https://:yourport/mycart(我的 URL:https://:44333/mycart)运行应用程序。

创建“添加到购物车”按钮

在实现购物车功能之前,我们需要创建将书籍添加到购物车的按钮。为此,我们将向*MyTagHelper*文件夹添加一个名为*UrlExtensions.cs*的类文件,并使用以下代码定义扩展方法:

using Microsoft.AspNetCore.Http;
namespace BooksStore.MyTagHelper
{
    public static class UrlExtensions
    {
        public static string PathAndQuery(this HttpRequest request) =>
           request.QueryString.HasValue
                ? $"{request.Path}{request.QueryString}"
                : request.Path.ToString();    
    }
}

PathAndQuery扩展方法操作HttpRequest类,ASP.NET Core 使用该类来描述 HTTP 请求。该扩展方法生成一个 URL,购物车更新后浏览器将返回到该 URL,并考虑查询字符串(如果存在)。

接下来,我们还将命名空间,其中包含扩展方法,添加到视图导入文件,以便我们可以在部分视图中使用它,方法是将BooksStore.MyTagHelper命名空间添加到*BooksStore/Views*文件夹中的*\_ViewImports.cshtml*文件中。

@using BooksStore
@using BooksStore.Models
@using BooksStore.MyTagHelper
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BooksStore

最后,我们将更新描述每本书的部分视图,使其包含一个“添加到购物车”按钮,方法是将按钮添加到*BooksStore/Views/Shared*文件夹中的*BookTemplate.cshtml*文件视图中,使用以下标记:

@model Book
<div class="card" style="width: 100%;">
    <div class="card-body">
        <h5 class="card-title">
            @Model.Title
            <span class="badge badge-pill badge-primary">
                <small>@Model.Price.ToString("c")</small>
            </span>
        </h5>
        <form id="@Model.BookID" asp-page="/MyCart" method="post">
            <input type="hidden" asp-for="BookID" />
            <input type="hidden" name="returnUrl"
                   value="@ViewContext.HttpContext.Request.PathAndQuery()" />
            <div class="card-text p-1">
                <strong>@Model.Genre</strong>
                <p>@Model.Description</p>
                <button type="submit"
                    class="btn btn-warning btn-sm pull-right">
                    Add To Cart
                </button>
            </div>
        </form>
    </div>
</div>

在前面的标记中:

  • 我们添加了一个form元素,其中包含隐藏的input元素,指定视图模型中的BookID值以及购物车更新后浏览器应返回的 URL。
  • form元素和其中一个input元素使用内置的 Tag Helper 进行配置,Tag Helper 是一种生成包含模型值并以控制器或 Razor Pages为目标的表单的有用方法。
  • 另一个input元素使用我们创建的扩展方法来设置返回 URL。我们还添加了一个button元素,该元素将提交表单到应用程序。

启用 Session

我们将使用 Session 状态来存储用户购物车的详细信息,Session 状态是与用户所做的一系列请求相关联的数据。有多种存储 Session 状态的方法,但在本项目中,我们将将其存储在内存中。这具有简单性的优点,但意味着在应用程序停止或重新启动时,Session 数据将丢失。在BooksStore项目中的*Startup.cs*文件中,我们将添加服务和中间件以启用 Session:

...
public void ConfigureServices(IServiceCollection services)
   {
       ...
      services.AddRazorPages();
      services.AddDistributedMemoryCache();
      services.AddSession();
    }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
 {
       ...
      app.UseStaticFiles();
      app.UseSession();
      app.UseRouting();
       ...
 }

AddDistributedMemoryCache方法调用设置内存数据存储。AddSession方法注册用于访问 Session 数据所需的服务,而UseSession方法允许 Session 系统在请求从客户端到达时自动将请求与 Session 相关联。

使用购物车功能

现在,为了使用购物车功能,我们将向BooksStore项目中的*Models*文件夹添加一个名为*MyCart.cs*的类文件,并使用它来定义具有以下代码的类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BooksStore.Models
{
    public class MyCart
    {
        public List<CartLine> Lines { get; set; } = new List<CartLine>();

        public void AddItem(Book book, int quantity)
        {
            CartLine line = Lines
                .Where(b => b.Book.BookID == book.BookID)
                .FirstOrDefault();
            if (line == null)
            {
                Lines.Add(new CartLine
                {
                    Book = book,
                    Quantity = quantity
                });
            }
            else
            {
                line.Quantity += quantity;
            }
        }

        public void RemoveLine(Book book) =>
            Lines.RemoveAll(l => l.Book.BookID == book.BookID);

        public decimal ComputeTotalValue() =>
            Lines.Sum(e => e.Book.Price * e.Quantity);

        public void Clear() => Lines.Clear();
    }

    public class CartLine
    {
        public int CartLineID { get; set; }
        public Book Book { get; set; }
        public int Quantity { get; set; }
    }
}

MyCart类使用同一文件中定义的CartLine类来表示客户选择的书籍以及用户想要购买的数量。我们定义了向购物车添加商品、从购物车中删除已添加的商品、计算购物车中商品总成本以及通过删除所有商品来重置购物车的方法。

ASP.NET Core 中的 Session 状态功能仅存储intstringbyte[]值。由于我们要存储MyCart对象,因此我们需要为ISession接口定义扩展方法,该接口提供对 Session 状态数据的访问,以便将MyCart对象序列化为JSON并将其转换回来。我们将向*MyTagHelper*文件夹添加一个名为*SessionExtensions.cs*的类文件,并使用以下代码定义扩展方法:

using Microsoft.AspNetCore.Http;
using System.Text.Json;

namespace BooksStore.MyTagHelper
{
    public static class SessionExtensions
    {
        public static void SetJson(this ISession session, string key, object value)
        {
            session.SetString(key, JsonSerializer.Serialize(value));
        }
        public static T GetJson<T>(this ISession session, string key)
        {
            var sessionData = session.GetString(key);
            return sessionData == null
                ? default(T) : JsonSerializer.Deserialize<T>(sessionData);
        }
    }
}

这些方法将对象序列化为JavaScript 对象表示法格式,从而可以轻松地存储和检索MyCart对象。

在*Pages*文件夹中名为*MyCart.cshtml.cs*的类文件中,该文件是由 Visual Studio 中的Razor Page模板项创建的,使用以下代码定义该类:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using BooksStore.MyTagHelper;
using BooksStore.Models;
using System.Linq;

namespace BooksStore.Pages
{
    public class MyCartModel : PageModel
    {
        private IBooksStoreRepository repository;

        public MyCartModel(IBooksStoreRepository repo)
        {
            repository = repo;
        }

        public MyCart myCart { get; set; }

        public string ReturnUrl { get; set; }

        public void OnGet(string returnUrl)
        {
            ReturnUrl = returnUrl ?? "/";
            myCart = HttpContext.Session.GetJson<MyCart>("mycart") ?? new MyCart();
        }

        public IActionResult OnPost(long bookId, string returnUrl)
        {
            Book book = repository.Books
                .FirstOrDefault(b => b.BookID == bookId);
            myCart = HttpContext.Session.GetJson<MyCart>("mycart") ?? new MyCart();
            myCart.AddItem(book, 1);
            HttpContext.Session.SetJson("mycart", myCart);
            return RedirectToPage(new { returnUrl = returnUrl });
        }
    }
}

在前面的代码中:

  • 页面模型类,名为MyCartModel,定义了一个OnPost处理程序方法,该方法被调用以处理 HTTP POST请求。它通过从数据库检索Book、从 Session 数据中检索用户购物车,并使用Book更新其内容来实现此目的。
  • GET请求由OnGet处理程序方法处理,该方法在 Razor 内容部分呈现页面后,设置ReturnUrlmyCart属性的值。
  • 处理程序方法使用与*BookTemplate.cshtml*视图生成的 HTML 表单中的输入元素匹配的参数名称。

MyCart Razor Page 将接收浏览器在用户点击“添加到购物车”按钮时发送的 HTTP POST请求。它将使用请求表单数据从数据库获取Book对象,并使用它来更新用户的购物车,该购物车将作为 Session 数据存储,供将来的请求使用。将*BooksStore/Pages*文件夹中的*MyCart.cshtml*文件的内容更新为以下标记:

@page
@model MyCartModel

<h2>Your cart</h2>

<table class="table table-bordered">
    <thead class="thead-light">
        <tr>
            <th>Quantity</th>
            <th>Item</th>
            <th class="text-right">Price</th>
            <th class="text-right">Subtotal</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var line in Model.myCart.Lines)
        {
            <tr>
                <td class="text-center">@line.Quantity</td>
                <td class="text-left">@line.Book.Title</td>
                <td class="text-right">@line.Book.Price.ToString("c")</td>
                <td class="text-right">
                    @((line.Quantity * line.Book.Price).ToString("c"))
                </td>
            </tr>
        }
    </tbody>
    <tfoot>
        <tr>
            <td colspan="3" class="text-right">Total:</td>
            <td class="text-right">
                @Model.myCart.ComputeTotalValue().ToString("c")
            </td>
        </tr>
    </tfoot>
</table>
<div class="text-center">
    <a class="btn btn-info" href="@Model.ReturnUrl">Continue shopping</a>
</div>

运行应用程序

点击“添加到购物车”按钮

点击“继续购物”按钮会将用户返回到他们之前的页面。

关注点

在本文中,我们将创建带有“添加到购物车”“继续购物”按钮的购物车体验。“立即结账”按钮以及其他一些高级体验将在下一篇文章中介绍。

历史

  • 2022 年 4 月 6 日:初始版本
© . All rights reserved.