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





5.00/5 (3投票s)
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 状态功能仅存储int
、string
和byte[]
值。由于我们要存储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
处理程序方法,该方法被调用以处理 HTTPPOST
请求。它通过从数据库检索Book
、从 Session 数据中检索用户购物车,并使用Book
更新其内容来实现此目的。 GET
请求由OnGet
处理程序方法处理,该方法在 Razor 内容部分呈现页面后,设置ReturnUrl
和myCart
属性的值。- 处理程序方法使用与*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 日:初始版本