3. 自定义 ASP.NET Core MVC 网站
现在您已经了解了基本 MVC 网站的结构,您将对其进行自定义和扩展。您已经为 Northwind 数据库注册了一个 EF Core 模型,因此下一个任务是在主页上输出一些数据。
3.1 定义自定义样式
主页将显示 Northwind 数据库中77 种产品的列表。为了有效利用空间,我们希望以三列显示列表。为此,我们需要为网站自定义样式表:
在 wwwroot\css 文件夹中,打开 site.css 文件。
在文件的底部,添加将应用于具有product-columns(产品列) ID 的元素的新样式,如以下代码所示:
#product-columns {
column-count: 3;
}
3.2 设置类别图像
Northwind 数据库包括一个包含八个类别的表格,但它们没有图像,并且网站上有一些彩色图片看起来更好:
在 wwwroot 文件夹中,创建一个名为 images 的文件夹。
在images文件夹中添加category1.jpeg、category2.jpeg等八个图片文件,直至category8.jpeg。
您可以通过以下链接从本书的 GitHub 存储库下载图像:https://github.com/markjprice/cs10dotnet6/tree/master/Assets/Categories
3.3 了解 Razor 语法
在我们自定义主页视图之前,让我们回顾一个示例 Razor 文件,该文件具有一个初始 Razor 代码块,该代码块使用价格和数量实例化订单,然后在网页上输出有关订单的信息,如以下标记所示:
@{
Order order = new()
{
OrderId = 123,
Product = "Sushi",
Price = 8.49M,
Quantity = 3
};
}
Your order for @order.Quantity of @order.Product has a total cost of [email protected] * @order.Quantity
前面的 Razor 文件会导致以下不正确的输出:
Your order for 3 of Sushi has a total cost of $8.49 * 3
尽管 Razor 标记可以使用 @object 包含任何单个属性的值。属性语法,您应该将表达式括在括号中,如以下标记所示:
Your order for @order.Quantity of @order.Product has a total cost of $@(order.Price * order.Quantity)
前面的Razor 表达式导致以下正确输出:
Your order for 3 of Sushi has a total cost of $25.47
3.4 定义typed view类型视图
要在编写视图时改进 IntelliSense,您可以使用顶部的 @model 指令定义视图可以期望的类型:
在 Views\Home 文件夹中,打开 Index.cshtml。
在文件的顶部,添加一条语句来设置模型类型以使用
HomeIndexViewModel,如下代码所示:
@model HomeIndexViewModel
现在,每当我们在此视图中键入 Model 时,您的代码编辑器都会知道模型的正确类型,并会为其提供 IntelliSense。在视图中输入代码时,请记住以下几点:
让我们继续自定义主页的视图。
声明模型的类型,使用@model(带有小写的m)
与模型实例交互,使用@Model(带有大写的M)
在最初的 Razor 代码块中,添加一条语句为当前项目声明一个字符串变量,并在现有的
@using Packt.Shared
@using Northwind.Common
@model HomeIndexViewModel //小写model
@{
ViewData["Title"] = "Home Page";
string currentItem = "";
WeatherForecast[]? weather = ViewData["weather"] as WeatherForecast[];
}
@if (Model is not null) // 大写Model
{
Northwind
We have had @Model?.VisitorCount visitors this month.
Query customers from a service
Query products by price
@if (Model is not null)
{
Products
@foreach (Product p in @Model.Products)
{
-
@p.ProductName costs
@(p.UnitPrice is null ? "zero" : p.UnitPrice.Value.ToString("C"))
}
}
}
查看前面的 Razor 标记时,请注意以下事项:
• 很容易将
3.5 查看自定义主页
让我们看看我们定制的主页的结果:
启动Northwind.Mvc网站项目。
请注意主页有一个旋转的轮播图,显示类别、随机数量的访问者和三列的产品列表,如图 15.4 所示:
现在,单击任何类别或产品链接都会出现 404 Not Found 错误,所以让我们看看如何实现使用传递的参数查看产品或类别详细信息的页面。
3. 关闭 Chrome 并关闭网络服务器。
3.6 使用路由值传递参数
传递简单参数的一种方法是使用默认路由中定义的 id 段:
在 HomeController 类中,添加一个名为 ProductDetail 的 action 方法,如下代码所示:
public async Task ProductDetail(int? id)
{
if (!id.HasValue)
{
return BadRequest("You must pass a product ID in the route, for example, /Home/ProductDetail/21");
}
Product? model = await db.Products
.SingleOrDefaultAsync(p => p.ProductId == id);
if (model == null)
{
return NotFound($"ProductId {id} not found.");
}
return View(model); // 将模型传递给视图,然后返回结果
}
请注意以下事项:
• 此方法使用称为模型绑定的ASP.NET Core 功能自动将路由中传递的id 与方法中名为id 的参数相匹配。
• 在该方法内部,我们检查id 是否没有值,如果是,我们调用BadRequest 方法返回一个400 状态代码,其中包含解释正确URL 路径格式的自定义消息。
• 否则,我们可以连接到数据库并尝试使用 id 值检索产品。
• 如果我们找到一个产品,我们将它传递给一个视图;否则,我们调用 NotFound 方法返回 404 状态代码和一条自定义消息,说明在数据库中未找到具有该 ID 的产品。
在 Views/Home 文件夹中,添加一个名为 ProductDetail.cshtml 的新文件。
修改内容,如下标记所示:
@model Packt.Shared.Product
@{
ViewData["Title"] = "Product Detail - " + Model.ProductName;
}
Product Detail
- Product Id
- @Model.ProductId
- Product Name
- @Model.ProductName
- Category Id
- @Model.CategoryId
- Unit Price
- @Model.UnitPrice.Value.ToString("C")
- Units In Stock
- @Model.UnitsInStock
启动 Northwind.Mvc 项目。
当主页出现产品列表时,单击其中一个,例如,第二个产品 Chang。
注意浏览器地址栏中的URL路径,浏览器选项卡中显示的页面标题,以及商品详情页,如图15.5所示:
查看开发者工具。
在Chrome地址栏编辑URL,请求一个不存在的产品ID,比如99,注意404 Not Found状态码和自定义错误响应。
3.7 更详细地了解模型绑定器
模型绑定器功能强大,默认的绑定器可以为您做很多事情。在默认路由标识要实例化的控制器类和要调用的操作方法之后,如果该方法有参数,则需要设置这些参数的值。
模型绑定器通过查找在 HTTP 请求中作为以下任何类型的参数传递的参数值来执行此操作:
• 路由参数,如上一节我们做的id,如下URL路径所示:/Home/ProductDetail/2
• 查询字符串参数,如下URL路径所示:/Home/ProductDetail?id=2
• 表单参数,如以下标记所示:
模型绑定器几乎可以填充任何类型:
• 简单类型,如 int、string、DateTime 和bool。
• 类、记录或结构定义的复杂类型。
• 集合类型,如数组和列表。
让我们创建一个有点人为的例子来说明使用默认模型绑定器可以实现什么:
在 Models 文件夹中,添加一个名为 Thing.cs 的新文件。
修改内容,定义一个类,该类具有两个属性,一个为可为空的整数Id,一个为字符串,名为Color,如下代码所示:
using System.ComponentModel.DataAnnotations; // [Range], [Required], [EmailAddress]
namespace Northwind.Mvc.Models;
public class Thing
{
[Range(1, 10)]
public int? Id { get; set; }
[Required]
public string? Color { get; set; }
[EmailAddress]
public string? Email { get; set; }
}
在 HomeController 中,添加两个新的操作方法,一个用于显示带有表单的页面,一个用于使用您的新模型类型显示带有参数的事物,如以下代码所示:
public IActionResult ModelBinding()
{
return View(); // the page with a form to submit
}
[HttpPost]
public IActionResult ModelBinding(Thing thing)
{
HomeModelBindingViewModel model = new(
thing,
!ModelState.IsValid,
ModelState.Values
.SelectMany(state => state.Errors)
.Select(error => error.ErrorMessage)
);
return View(model);
}
在 Views\Home 文件夹中,添加一个名为 ModelBinding.cshtml 的新文件。
修改其内容,如以下标记所示:
@model Northwind.Mvc.Models.HomeModelBindingViewModel
@{
ViewData["Title"] = "Model Binding Demo";
}
@ViewData["Title"]
Enter values for your thing in the following form:
@if (Model != null)
{
Submitted Thing
- Model.Thing.Id
- @Model.Thing.Id
- Model.Thing.Color
- @Model.Thing.Color
- Model.Thing.Email
- @Html.DisplayFor(model => model.Thing.Email)
@if (Model.HasErrors)
{
@foreach (string errorMessage in Model.ValidationErrors)
{
@errorMessage
}
}
}
在 Views/Home 中,打开 Index.cshtml,在第一个
启动网站。
在首页点击绑定。
注意关于不明确匹配的未处理异常,如图 15.6 所示:
关闭 Chrome 并关闭网络服务器。
3.8 消除动作方法的歧义
虽然 C# 编译器可以通过注意到签名signatures 的不同来区分这两种方法,但从 HTTP 请求的路由角度来看,这两种 (ModelBinding) 方法都是潜在的匹配。我们需要一种特定于 HTTP 的方法来消除操作方法的歧义。
为此,我们可以为操作 (actions)创建不同的名称,或者指定一种方法应用于特定的 HTTP 动词,如 GET、POST 或 DELETE。这就是我们解决问题的方法:
在HomeController中,装饰第二个ModelBinding action方法,指明它应该用于处理HTTP POST请求,即提交表单时,如下代码高亮显示:
[HttpPost]
public IActionResult ModelBinding(Thing thing)
其他 ModelBinding 操作方法将隐式用于所有其他类型的 HTTP 请求,如 GET、PUT、DELETE 等。
启动网站。
在首页点击绑定。
单击提交按钮并注意 Id 属性的值是从查询字符串参数设置的,颜色属性的值是从表单参数设置的,如图 15.7 所示:
关闭 Chrome 并关闭网络服务器。
3.9 传递路由参数
现在我们将使用路由参数设置属性:
修改表单的操作以将值 2 作为路由参数传递,如以下标记中突出显示的所示:
启动网站。
在首页点击 Binding。
单击 Submit 按钮,注意 Id 属性的值是从路由参数设置的,Color 属性的值是从表单参数设置的。
关闭 Chrome 并关闭网络服务器。
现在我们将使用表单参数设置属性:
修改表单的操作以将值 1 作为表单参数传递,如以下标记中突出显示的所示:
启动网站。
在首页点击 Binding.
单击 Submit 按钮并注意 Id 和 Color 属性的值都是从表单参数中设置的。
良好实践:如果您有多个同名参数,请记住表单参数具有最高优先级,查询字符串参数具有自动模型绑定的最低优先级。
3.11 验证模型 Validating the model
模型绑定的过程可能会导致错误,例如,如果模型已使用验证规则进行修饰,则数据类型转换或验证错误。绑定了哪些数据以及任何绑定或验证错误都存储在 ControllerBase.ModelState 中。
让我们通过对绑定模型应用一些验证规则然后在视图中显示无效数据消息来探索我们可以对模型状态做些什么:
在模型文件夹中,打开 Thing.cs。
导入 System.ComponentModel.DataAnnotations 命名空间。
用验证属性(validation attribute)装饰 Id 属性,将允许的数字范围限制在 1 到 10,一个确保访问者提供颜色,并添加一个新的 Email 属性,使用正则表达式进行验证,如以下代码中突出显示的 :
using System.ComponentModel.DataAnnotations; // [Range], [Required], [EmailAddress]
namespace Northwind.Mvc.Models;
public class Thing
{
[Range(1, 10)]
public int? Id { get; set; }
[Required]
public string? Color { get; set; }
[EmailAddress]
public string? Email { get; set; }
}
在 Models 文件夹中,添加一个名为 HomeModelBindingViewModel.cs 的新文件。
修改其内容以定义一个记录,该记录具有用于存储绑定模型的属性、一个指示存在错误的标志以及一系列错误消息,如以下代码所示:
namespace Northwind.Mvc.Models;
public record HomeModelBindingViewModel //定义一个记录
(
Thing Thing, //用于存储绑定模型的属性
bool HasErrors, //指示存在错误的标志
IEnumerable ValidationErrors //一系列错误消息
);
在 HomeController 中,在处理HTTP POST的 ModelBinding 方法中,将之前传递东西给视图的语句注释掉,改为添加创建视图模型实例的语句。 验证模型并存储错误消息数组,然后将视图模型传递给视图,如以下代码中突出显示的所示:
[HttpPost]
public IActionResult ModelBinding(Thing thing)
{
HomeModelBindingViewModel model = new(
thing,
!ModelState.IsValid,
ModelState.Values
.SelectMany(state => state.Errors)
.Select(error => error.ErrorMessage)
);
return View(model);
}
在 Views\Home 中,打开 ModelBinding.cshtml。
修改模型类型声明以使用视图模型类,如以下标记所示:
@model Northwind.Mvc.Models.HomeModelBindingViewModel
添加
@model Northwind.Mvc.Models.HomeModelBindingViewModel
@{
ViewData["Title"] = "Model Binding Demo";
}
@ViewData["Title"]
Enter values for your thing in the following form:
@if (Model != null)
{
Submitted Thing
- Model.Thing.Id
- @Model.Thing.Id
- Model.Thing.Color
- @Model.Thing.Color
- Model.Thing.Email
- @Html.DisplayFor(model => model.Thing.Email)
@if (Model.HasErrors)
{
@foreach (string errorMessage in Model.ValidationErrors)
{
@errorMessage
}
}
}
启动网站。
在首页点击Binding。
单击 Submit 按钮并注意 1、红色和 [email protected] 是有效值。
输入Id为13,清除颜色文本框,删除邮箱地址中的@,点击提交按钮,注意错误信息,如图15.8所示:
关闭 Chrome 并关闭网络服务器。
最佳实践:Microsoft 使用什么正则表达式来实现 EmailAddress 验证属性?在以下链接中查找:https://github.com/microsoft/referencesource/blob/5697c29004a34d80acdaf5742d7e699022c64ecd/System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs#L54
3.12 理解视图助手方法
Understanding view helper methods
在为 ASP.NET Core MVC 创建视图时,您可以使用 Html 对象及其方法来生成标记。
一些有用的方法包括:
• ActionLink:使用它来生成一个锚点 元素,其中包含指定控制器和操作的 URL 路径。例如,Html.ActionLink(linkText: "Binding", actionName: "ModelBinding", controllerName: "Home") 将生成 Binding。您可以使用锚标记助手实现相同的结果:Binding。
• AntiForgeryToken:在,其中包含将在提交表单时验证的防伪令牌。
• Display 和 DisplayFor:使用它为相对于使用显示模板的当前模型的表达式生成HTML 标记。有用于 .NET 类型的内置显示模板,并且可以在 DisplayTemplates 文件夹中创建自定义模板。文件夹名称在区分大小写的文件系统上区分大小写。
• DisplayForModel:使用它为整个模型而不是单个表达式生成 HTML 标记。
• Editor 和EditorFor:使用它为相对于使用编辑器模板的当前模型的表达式生成HTML 标记。有用于使用