ASP.NET Core应用程序11:使用模型绑定

  模型绑定是使用从 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 方法的参数是将被模型绑定的对象、值的前缀和一系列选择将包含在流程中的属性的表达式。

posted @ 2024-06-15 15:53  一纸年华  阅读(50)  评论(0编辑  收藏  举报