模型绑定是使用从 HTTP 请求获得的数据值,创建操作方法和页面处理程序所需的对象的过程。本章描述模型绑定系统的工作方式;显示它如何绑定简单类型、复杂类型和集合;并演示如何控制流程,以指定请求的哪一部分提供应用程序所需的数据值。
本章介绍了模型绑定特性,展示了如何使用带有参数和属性的模型绑定,如何绑定简单和复杂类型,以及绑定到数组和集合所需的约定。还解释了如何控制请求的哪一部分用于模型绑定,以及如何控制何时执行模型绑定。
1 准备工作
本章继续使用上一章项目。
修改 Views/Form 文件夹中 Form.cshtml。
@model Product
@{
Layout = "_SimpleLayout";
}
<h5 class="bg-primary text-white text-center p-2">HTML Form</h5>
<form asp-action="submitform" method="post" id="htmlform">
<div class="form-group">
<label asp-for="Name"></label>
<input class="form-control" asp-for="Name" />
</div>
<div class="form-group">
<label asp-for="Price"></label>
<input class="form-control" asp-for="Price" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
在 Models 文件夹的 Product.cs,注释掉已应用于 Product 模型类的 DisplayFormat 属性。
//[DisplayFormat(DataFormatString = "{0:c2}", ApplyFormatInEditMode = true)]
浏览器请求http://localhost:5000/controllers/form
。
2 理解模型绑定
模型绑定是 HTTP 请求和操作或页面处理程序方法之间的桥梁。大多数 ASP.NET Core 应用程序在某种程度上依赖于模型绑定。
通过使用浏览器请求 http://localhost:5000/controllers/form/index/5
。这个 URL, 包含想要査看的 Product 对象的 Productld 属性值。URL 的这一部分对应于控制器路由模式定义的 id 参数名称匹配。
public async Task<IActionResult> Index(long id = 1)
在 MVC 框架调用操作方法之前需要为 id 参数设置一个值,而找到一个合适的值是模型绑定系统得职责。模型绑定系统依赖于模型绑定器,模型绑定器是负责从请求或应用程序的某个部分提供数据值的组件。
默认的模型绑定在以下四个地方寻找数据值:
- 表单数据
- 请求主体(仅适用于用 ApiController 装饰的控制器).
- 路由段变量。
- 查询字符串
按顺序检查每个数据源,直至找到参数的值为止。示例请求中没有表单数据,因此在那里不会找到任何值,并且表单控制器没有使用 ApiController 属性装饰,因此不会检査请求主体,下一步是检查路由数据,它包含一个名为 id 的段变量。这允许模型绑定系统提供一个值来允调用索引操作方法。在找到合适的数据值后停止搜索,所以就不会搜索查询字符串数据值。
知道寻找数据值的顺序是很重要的,因为一个请求可以包含多个值,比如这个URL:http://localhost:5000/controllers/form/index/3?id=1
。
路由系统将处理请求,并将 URL 模板中的 id 段与值 3 匹配,查询字符串包含 id 值 1,由于搜索查询字符串之前的路由数据,因此索引操作方法将接收值 3,而忽略査询字符串的值。另一方面,如果请求没有 id 段的 URL:http://localhost:5000/controllers/form/index?id=1
,则将检査查询字符串。
3 绑定简单数据类型
请求数据值必须转换为C#值,这样它们才能用于调用操作或页面处理程序方法。简单类型是源自请求中的一项数据的值,该数据项可以从字符串中解析。这包括数值、bool值、日期和字行串值。
用于简单类型的数据绑定很容易从请求中提取单个数据项,而不必通过上下文数据查找定义的位置。日系如下 FormController.cs 向 Form 控制器方法定义的 SubmitForm 操作方法添加了参数,以便模型绑定器用于提供 name 和 price 值。
[HttpPost]
public IActionResult SubmitForm(string name, decimal price)
{
TempData["name param"] = name;
TempData["price param"] = price.ToString();
return RedirectToAction(nameof(Results));
}
绑定 Razor Pages 中的简单数据类型
Razor Pages 可以使用模型绑定,但是必须注意确保表单元素的 name 属性的值与处理程序方法参数的名称相匹配,如果 asp-for 属性用来选择嵌套属性,则可能不会出现这种情况。为了确保名称匹配,可以显式定义 name 属性。修改 Pages 文件下 FormHandler.cshtml。
<div class="form-group">
<label>Name</label>
<input class="form-control" asp-for="Product.Name" name="name" />
</div>
<div class="form-group">
<label>Price</label>
<input class="form-control" asp-for="Product.Price" name="price" />
</div>
public IActionResult OnPost(string name, decimal price)
{
TempData["name param"] = name;
TempData["price param"] = price.ToString();
return RedirectToPage("FormResults");
}
4 绑定复杂类型
模型绑定系统在处理复杂类型时非常出色,复杂类型是不能从单个字符串值解析的任何类型。可使用绑定器创建完整的 Product 对象,而不是处理像 name 和 price 一样的单独的值。修改 FormController.cs。
[HttpPost]
public IActionResult SubmitForm(Product product)
{
TempData["product"] = System.Text.Json.JsonSerializer.Serialize(product);
return RedirectToAction(nameof(Results));
}
请求提交后,返回一个json字符串 {"ProductId":0,"Name":"Kayak","Price":100.00,"CategoryId":0,"Category":null,"SupplierId":0,"Supplier":null}
,示例提供了 Name 和 Price 属性的值,但是 Produclid、Caiegoryld 和 SupplierId 属性为 0,而 Category 和 Supplier 属性为空。
4.1 绑定到属性
使用参数进行模型绑定不适合 Razor 页面开发风格,因为参数经常重复页面模型类定义的属性。更好的方式是使用现有属性进行模型绑定。修改 Pages 文件下 FormHandler.cshtml 如下。
...
<input class="form-control" asp-for="Product.Name"/>
...
<input class="form-control" asp-for="Product.Price"/>
...
[BindProperty]
public Product Product { get; set; }
public IActionResult OnPost()
{
TempData["product"] = System.Text.Json.JsonSerializer.Serialize(Product);
return RedirectToPage("FormResults");
}
用 BindProperty 修饰属性表明它的属性应该服从模型绑定过程,这意味着 OnPost 处理程序方法可以在不声明参数的情况下获得它需要的数据。当使用 BindProperty 特性时,模型绑定器在定位数据值时使用属性名,因此不需要添加到输入元素的显式 name 特性。
默认情况下,BindProperty 不会绑定 GET 请求的数据,但这可以通过将 BindProperty 特性的 SupportsGet 参数设置为 tmue 来更改。
4.2 绑定嵌套的复杂类型
如果使用复杂类型来定义受模型绑定约束的属性,则使用属性名作为前缀重复模型绑定过程。例如,Product 类定义 Category 属性,其 Category 是复杂类型,修改 Vews/Fom 文件夹的 Form.cshtml 如下。
<div class="form-group">
<label>Category Name</label>
<input class="form-control" name="Category.Name" value="@Model.Category.Name" />
</div>
name 特性组合了由句点分隔的属性名称。在本例中,元素用于给视图模型的 Categoy 属指定的对象的 Name 属性,因此 Name 属性设置为 Category.Name。当应用 asp-for 特性时,输入元素标签助手将自动为 name 特性使用这种格式,如下所示。
<input class="form-control" asp-for="Category.Name" />
4.3 选择性的绑定属性
一些模型类定义了一些敏感的属性,用户不应该为这些属性指定值。为了防止模型绑定器使用敏感属性的值,可以指定应该绑定的属性列表。
在 Controllers 文件夹的 FormController.cs 文件中有选择地绑定属性。
[HttpPost]
public IActionResult SubmitForm([Bind("Name", "Category")] Product product)
{
TempData["name"] = product.Name;
TempData["price"] = product.Price.ToString();
TempData["category name"] = product.Category.Name;
return RedirectToAction(nameof(Results));
}
为操作方法参数返回了 Product 类型,该参数用 Bind 特性修饰,以指定应该包含在模型绑定过程中的属性名称。这个示例告诉模型绑定特性寻找Name 和 Category 属性的值,这将从流程中排除其他任何属性。
另外 BindNever 特性可以从模型绑定器中排除了一个属性,与上面的效果相同。
using Microsoft.AspNetCore.Mvc.ModelBinding;
[BindNever]
public decimal Price { get; set; }
5 绑定到数组和集合
5.1 绑定到数组
给 Pages 文件夹添加一个名为 Bindings.cshtml 的 Razor Pages。
@page "/pages/bindings"
@model BindingsModel
@using Microsoft.AspNetCore.Mvc
@using Microsoft.AspNetCore.Mvc.RazorPages
<div class="container-fluid">
<div class="row">
<div class="col">
<form asp-page="Bindings" method="post">
<div class="form-group">
<label>Value #1</label>
<input class="form-control" name="Data" value="Item 1" />
</div>
<div class="form-group">
<label>Value #2</label>
<input class="form-control" name="Data" value="Item 2" />
</div>
<div class="form-group">
<label>Value #3</label>
<input class="form-control" name="Data" value="Item 3" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-secondary" asp-page="Bindings">Reset</a>
</form>
</div>
<div class="col">
<ul class="list-group">
@foreach (string s in Model.Data.Where(s => s != null))
{
<li class="list-group-item">@s</li>
}
</ul>
</div>
</div>
</div>
@functions
{
public class BindingsModel : PageModel
{
[BindProperty(Name = "Data")]
public string[] Data { get; set; } = Array.Empty<string>();
}
}
数组的模型绑定需要将 name 特性设置为将提供数组值的所有元素的相同值。这个页面显示三个输入元素,他们的 name 特性值都是 Data。为让模型绑定器找到数组值,用 BindProperty 特性装饰了页面模型的 Data 属性,并使用了 Name 参数。
提交 HTML 表单时,将创建一个新数组,并使用来自所有三个输入元素的值填充该数组,这些值将显示给用户。要查看绑定过程,请求 http://localhost:5000/pages/bindings
。
默认情况下,数组是按照从浏览器接收表单值的顺序填充的,这个顺序通常是定义 HTML 元素的顺序。如果需要覆盖默认值,则可以使用 name 特性指定数组中值的位置。
<input class="form-control" name="Data[1]" value="Item 1" />
<input class="form-control" name="Data[0]" value="Item 2" />
<input class="form-control" name="Data[2]" value="Item 3" />
5.2 绑定到简单集合
模型绑定流程可以创建集合和数组。对于序列集合,例如列表和集合,只更改模型绑定器用的属性或参数的类型。
[BindProperty(Name = "Data")]
public SortedSet<string> Data { get; set; } = new SortedSet<string>();
将 Data 属性的类型改为 SortedSet
5.3 绑定到字典
模型绑定到字典时为键值对。为集合提供值的所有元素都必须共享一个公共前缀(在本例中为 Data),后面跟着方括号中的键值。
<input class="form-control" name="Data[first]" value="Item 1" />
<input class="form-control" name="Data[second]" value="Item 2" />
<input class="form-control" name="Data[third]" value="Item 3" />
<div class="col">
<table class="table table-sm table-striped">
<tbody>
@foreach (string key in Model.Data.Keys)
{
<tr>
<th>@key</th>
<td>@Model.Data[key]</td>
</tr>
}
</tbody>
</table>
</div>
[BindProperty(Name = "Data")]
public Dictionary<string, string> Data { get; set; }
= new Dictionary<string, string>();
5.4 绑定到复杂类型的集合
@page "/pages/bindings"
@model BindingsModel
@using Microsoft.AspNetCore.Mvc
@using Microsoft.AspNetCore.Mvc.RazorPages
<div class="container-fluid">
<div class="row">
<div class="col">
<form asp-page="Bindings" method="post">
@for (int i = 0; i < 2; i++)
{
<div class="form-group">
<label>Name #@i</label>
<input class="form-control" name="Data[@i].Name"
value="Product-@i" />
</div>
<div class="form-group">
<label>Price #@i</label>
<input class="form-control" name="Data[@i].Price"
value="@(100 + i)" />
</div>
}
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-secondary" asp-page="Bindings">Reset</a>
</form>
</div>
<div class="col">
<table class="table table-sm table-striped">
<tbody>
<tr><th>Name</th><th>Price</th></tr>
@foreach (Product p in Model.Data)
{
<tr>
<th>@p.Name</th>
<td>@p.Price</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@functions
{
public class BindingsModel : PageModel
{
[BindProperty(Name = "Data")]
public Product[] Data { get; set; } = Array.Empty<Product>();
}
}
6 指定模型绑定源
默认的模型绑定过程在四个地方查找数据,但也可以覆盖默认搜索,指定绑定源。
模型绑定源特性:
名称 | 描述 |
---|---|
FromForm | 该属性用子选择表单数据作为绑定数册的源 |
FromRoute | 该属性用于选择作为绑定数据源的路由系统 |
FromQuery | 该属性用于选择查询字符串作为绑定数据的源 |
FromHeader | 该属性用于选择一个请求头作为绑定数据的源 |
FromBody | 该属性用于指定应该将请求体用作绑定数据的源 |
模型绑定源特性:
此URL:http://localhost:5000/controllers/Form/Index/5?id=1
按照默认会查出 id 为 5 的数据,id 为 1 将被忽略。但如果将 FromQuery 特性应用于索引操作方法定义的 id 参数覆盖默认,那将查询 id 为 1的数据。
public async Task<IActionResult> Index([FromQuery] long? id)
7 手动模式绑定
模型绑定源特性:
当为操作或处理程序方法定义参数或应用 BindProperty 属性时,将自动应用模型绑定。如果始终如一地遵循名称约定,并且总是希望应用该过程,那么自动模型绑定可以很好地工作。如果需要控制绑定过程,或者希望有选择地执行绑定,那么可以手动执行模型绑定。
@page "/pages/bindings"
@model BindingsModel
@using Microsoft.AspNetCore.Mvc
@using Microsoft.AspNetCore.Mvc.RazorPages
<div class="container-fluid">
<div class="row">
<div class="col">
<form asp-page="Bindings" method="post">
<div class="form-group">
<label>Name</label>
<input class="form-control" asp-for="Data.Name" />
</div>
<div class="form-group">
<label>Price</label>
<input class="form-control" asp-for="Data.Price"
value="@(Model.Data.Price + 1)" />
</div>
<div class="form-check m-2">
<input class="form-check-input" type="checkbox" name="bind"
value="true" checked />
<label class="form-check-label">Model Bind?</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-secondary" asp-page="Bindings">Reset</a>
</form>
</div>
<div class="col">
<table class="table table-sm table-striped">
<tbody>
<tr><th>Name</th><th>Price</th></tr>
<tr>
<td>@Model.Data.Name</td>
<td>@Model.Data.Price</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@functions {
public class BindingsModel : PageModel
{
public Product Data { get; set; }
= new Product() { Name = "Skis", Price = 500 };
public async Task OnPostAsync([FromForm] bool bind)
{
if (bind)
{
await TryUpdateModelAsync<Product>(Data,
"data", p => p.Name, p => p.Price);
}
}
}
}
手动模型绑定使用 TryUpdateModelAsync 方法执行,该方法由 PageModel 和 ControllerBase类提供,这意味着它对 Razor Pages 和 MVC 控制器都可用。
这个例子混合了自动和手动的模型绑定。OnPostAsync 方法使用自动模型绑定来接收其绑定参数的值,该参数已经用 FromFomm 特性进行了修饰。如果参数的值为 true,则使用 TryUpdateModelAsync 方法应用模型绑定。TryUpdateModelAsync 方法的参数是将被模型绑定的对象、值的前缀和一系列选择将包含在流程中的属性的表达式。