ASP.NET Core应用程序12:使用模型验证

  模型验证是确保请求中提供的数据是有效的。
  本章描述 ASP.NET Core 数据验证功能。解释了如何显式地执行验证,如何使用属性来播述验证约束,以及如何验证单个属性和整个对象。演示了如何向用户显示验证消息,以及如何通过客户端和远程验证改进用户的验证体验。

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>
    <div class="form-group">
        <label>CategoryId</label>
        <input class="form-control" asp-for="CategoryId" />
    </div>
    <div class="form-group">
        <label>SupplierId</label>
        <input class="form-control" asp-for="SupplierId" />
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

  替换 Controllers 文件夹中 FormController.cs 。

public class FormController : Controller
{
    private DataContext context;
    public FormController(DataContext dbContext)
    {
        context = dbContext;
    }
    public async Task<IActionResult> Index(long? id)
    {
        var result = await context.Products
            .FirstOrDefaultAsync(p => id == null || p.ProductId == id);
        return View("Form", result);
    }
    [HttpPost]
    public IActionResult SubmitForm(Product product)
    {
        TempData["name"] = product.Name;
        TempData["price"] = product.Price.ToString();
        TempData["categoryId"] = product.CategoryId.ToString();
        TempData["supplierId"] = product.SupplierId.ToString();
        return RedirectToAction(nameof(Results));
    }
    public IActionResult Results()
    {
        return View(TempData);
    }
}

2 理解对模型验证的需要

  验证数据最直接的方法是在操作或处理程序方法中进行验证。

[HttpPost]
public IActionResult SubmitForm(Product product)
{
    if (string.IsNullOrEmpty(product.Name))
    {
        ModelState.AddModelError(nameof(Product.Name), "Enter a name");
    }
    if (ModelState.GetValidationState(nameof(Product.Price))
            == ModelValidationState.Valid && product.Price < 1)
    {
        ModelState.AddModelError(nameof(Product.Price),
            "Enter a positive price");
    }
    if (!context.Categories.Any(c => c.CategoryId == product.CategoryId))
    {
        ModelState.AddModelError(nameof(Product.CategoryId),
            "Enter an existing category ID");
    }
    if (!context.Suppliers.Any(s => s.SupplierId == product.SupplierId))
    {
        ModelState.AddModelError(nameof(Product.SupplierId),
            "Enter an existing supplier ID");
    }
    if (ModelState.IsValid)
    {
        TempData["name"] = product.Name;
        TempData["price"] = product.Price.ToString();
        TempData["categoryId"] = product.CategoryId.ToString();
        TempData["supplierId"] = product.SupplierId.ToString();
        return RedirectToAction(nameof(Results));
    }
    else
    {
        return View("Form");
    }
}

  对于创建的 Product 参数的每个属性,检查用户提供的值,并记录使用从 ControllerBase 类承的 ModelState 属性返回的 ModelStateDictionary 对象所发现的任何错误。ModelStateDictionary 类是一个字典,用于跟踪模型对象状态的详细信息,重点关注验证错误。
  如下 ModelStateDictionary 成员:

名称 描述
AddModelError(property, message) 此方法用于记录指定属性的模型验证错误
GetValidationState(property) 此方法用于确定特定属性是否存在模型验证错误,相应属性表示为ModelValidationState 枚举的值
IsValid 如果所有模型属性都有效,该属性返回 true,否则返回 false
Clear() 此属性清除验证状态

  Product 类的验证要求之一是确保用户为 Name 属性提供一个值,如果 Name 属性是 null 或空符串,那么使用 Modelstate.AddModelEmor 方法来注册一个证错误,指定属性的名称(Name)和显示给用户的消息。
  在模型绑定过程中,还使用 ModelStateDictionary 来记录查找和为模型属性赋值的任何问题GetValidationstate 方法用于査看模型属性是否存在任何错误记录,这些错误可能来自模型绑定过程,也可能是因为 AddModelEmor 方法在操作方法的显式验证期间调用。Getvalidationsiae 方从 ModelValidationState 枚举中返回一个值。
  ModelValidationState 值:

名称 描述
Unvalidated 未验证此值,说明没有对模型属性执行任何验证,这通常是因为请求中没有与属性名称对应的值
Valid 此值表示与该属性关联的请求值有效
Invalic 此值意味着与该属性关联的请求值无效,不应使用
Skipped 此值说明没有处理模型属性,这通常意味着存在太多验证错误,没有必要继续执行验证检查

  在验证 Product对象中的所有属性之后,检査 ModelState.IsValid 属性,以查看是否存在错误。

2.1 向用户显示验证错误

  在点击提交按钮,标签助手会使用返回的验证错误来给 input 元素 class 属性添加。将值验证失败的元素添加到 input-validation-error 类中,然后可以对其进行解释,以向用户突出显示问题。

class="form-control input-validation-error"

  可以通过在样式表中定义定制的 CSS 样式来实现这一点,但是如果想使用诸如 Bootstrap 库提供的内置验证样式,则需要做一些额外的工作。
  给 Views/Shared 文件夹添加一个名为 _Validation.cshtml 的文件。

<script src="/lib/jquery/jquery.min.js"></script>
<script type="text/javascript">
    $(document).ready(function () {
        $("input.input-validation-error").addClass("is-invalid");
    });
</script>

  在 Views/Form 文件夹的 Form.cshtml文件中包含部分视图。

<partial name="_validation" />

  请求localhost:5000/controllers/Form。只有当表单与可以被模型浏览器解析的数据一起提交,并通过操作方法中的显式验证检查时,用户才会看到结果视图。在此之前,提交表单视图以突出显示的验证错误形式呈现。

2.2 显示验证消息

  标签助手应用于输入元素的 CSS 类表明,表单字段存在问题,但没有告诉用户问题是什么。向用户提供更多信息需要使用不同的标签助手,从而将问题摘要添加到视图中。

<div asp-validation-summary="All" class="text-danger"></div>

  ValidationSummaryTagHelper 类检测 div 元素上的 asp-validation-summary 特性,并通过添加描述已记录的任何验证错误的消息来响应。asp-validation-summary 特性的值来自 ValidationSummay枚举。

名称 描述
All 该值用于显示已记录的所有验证错误
ModelOnly 该值用于仅显示整个模型的验证错误,不包括那些已经为单个属性记录的错误
None 此值用于禁用标签助手,以便它不会转换 HTML 元素

2.3 显示属性级的验证消息

  尽管自定义错误消息比默认错误消息更有意义,但它仍然没有多大帮助,因为它不能清楚指出问题与哪个字段相关。对于这类错误,更有用的是在包含问题数据的 HTML 元素旁边显示验证错误消息。
  这可以使用 ValidationMessageTag 标签助手来完成,它査找具有 asp-validation-for 特性的 span 元素,该特性用于指定应该显示错误消息的属性。
  在Views/Form 文件夹的 Form.cshtml,为表单中的每个 input 元素添加了属性级验证消息元素。

<label asp-for="Name"></label>
<div><span asp-validation-for="Name" class="text-danger"></span></div>
<input class="form-control" asp-for="Name" />

3 使用元数据指定验证规则

  验证过程支持使用特性在模型类中直接表达模型验证规则,确保无论使用哪种操作方法处理请求,都将应用相同的验证规则集。

[Required]
[Display(Name = "Name")]
public string Name { get; set; }
[Column(TypeName = "decimal(8, 2)")]
[Required(ErrorMessage = "Please enter a price")]
[Range(1, 999999, ErrorMessage = "Please enter a positive price")]
public decimal Price { get; set; }

  在 Product 类上使用验证属性允许删除对 Name 和 Price 属性的显式验证检查。

[HttpPost]
public IActionResult SubmitForm(Product product)
{
    if (!context.Categories.Any(c => c.CategoryId == product.CategoryId))
    {
        ModelState.AddModelError(nameof(Product.CategoryId),
            "Enter an existing category ID");
    }
    if (!context.Suppliers.Any(s => s.SupplierId == product.SupplierId))
    {
        ModelState.AddModelError(nameof(Product.SupplierId),
            "Enter an existing supplier ID");
    }
    if (ModelState.IsValid)
    {
        TempData["name"] = product.Name;
        TempData["price"] = product.Price.ToString();
        TempData["categoryId"] = product.CategoryId.ToString();
        TempData["supplierId"] = product.SupplierId.ToString();
        return RedirectToAction(nameof(Results));
    }
    else
    {
        return View("Form");
    }
}

  验证属性是在调用操作方法之前应用的,这意味着在执行模型级验证时,仍然可以依赖模型状态来确定各个属性是否有效。要查看验证属性的作用,请求http:/localhost:5000/controllers/form,清除Name 和 Price 字段,并提交表单。响应将包括属性生成的验证错误。

创建自定义属性的验证属性
  可以通过创建扩展 ValidationAtribute 类的属性来扩展验证过程。创建 Validation 文件夹,并在其中添加了一个名为 PrimaryKeyAttribute.cs 的类文件。

public class PrimaryKeyAttribute : ValidationAttribute
{
    public Type ContextType { get; set; }
    public Type DataType { get; set; }
    protected override ValidationResult IsValid(object value,
            ValidationContext validationContext)
    {
        DbContext context = validationContext.GetService(ContextType) as DbContext;
        if (context.Find(DataType, value) == null)
        {
            return new ValidationResult(ErrorMessage
                ?? "Enter an existing key value");
        }
        else
        {
            return ValidationResult.Success;
        }
    }
}

  自定义属性重写了 IsValid 方法和 ValidationContext 对象,该方法通过要检査的值来调用 ValidationContext 对象提供关于验证过程的上下文,并通过 GetService 方法提供对应用程序服务的访问。
  在 IsValid 方法中,属性获得上下文类的一个实例,并使用它查询数据库,以确定该值是否用作主键值。
  在 Validation 文件夹中添加 PhraseAndPriceAtribute.cs。

public class PhraseAndPriceAttribute : ValidationAttribute
{
    public string Phrase { get; set; }
    public string Price { get; set; }
    protected override ValidationResult IsValid(object value,
        ValidationContext validationContext)
    {
        Product product = value as Product;
        if (product != null && product.Price > decimal.Parse(Price) &&
            product.Name.StartsWith(Phrase, StringComparison.OrdinalIgnoreCase))
        {
            return new ValidationResult(ErrorMessage
                ?? $"{Phrase} products cannot cost more than ${Price}");
        }
        return ValidationResult.Success;
    }
}

  该属性配置了 Phrase 和 Price 属性,这些属性在 IsValid 方法中用于检査模型对象的Name 和 Price 属性。属性级的自定义验证属性直接应用于它们验证的属性,模型级属性应用于整个类。

[PhraseAndPrice(Phrase = "Small", Price = "100")]
public class Product
[PrimaryKey(ContextType = typeof(DataContext), DataType = typeof(Category))]
public long CategoryId { get; set; }
[PrimaryKey(ContextType = typeof(DataContext), DataType = typeof(Category))]
public long SupplierId { get; set; }

  自定义属性允许从表单控制器的操作方法中删除其余的显式验证语句。验证属性会在调用操作方法之前自动应用,这意味着可以通过读取 ModelState.IsValid 属性来确定验证结果。

[HttpPost]
public IActionResult SubmitForm(Product product)
{
    if (ModelState.IsValid)
    {
        TempData["name"] = product.Name;
        TempData["price"] = product.Price.ToString();
        TempData["categoryId"] = product.CategoryId.ToString();
        TempData["supplierId"] = product.SupplierId.ToString();
        return RedirectToAction(nameof(Results));
    }
    else
    {
        return View("Form");
    }
}

4 执行客户端验证

  在 Web 应用程序中,用户通常希望立即得到验证反馈--而不必向服务器提交任何内容。这称为客户端验证,使用 JavaScript 实现。
  安装处理验证的 javascipt 包。打开一个新的PowerShel命令提示符。

libman install jquery-validate@1.19.1 -d wwwroot/lib/jquery-validate
1ibman install jqvery-validation-unobtrusivee3.2.11 -d wwroot/lib/jquery-validation-unobtrusive

  修改 Views/Shared 文件夹的 _Validation.cshtml。

<script src="/lib/jquery/jquery.min.js"></script>
<script src="~/lib/jquery-validate/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js">
</script>
<script type="text/javascript">
    $(document).ready(function () {
        $("input.input-validation-error").addClass("is-invalid");
    });
</script>

  Javascipt代码査找具有 data-val 属性的元素,并在用户提交表单时在浏览器中执行本地验画而不向服务器发送 HTTP请求。

<input class="form-control" asp-for="Name" data-val="true" data-val-required="name必填!"/>

5 执行远程验证

  远程验证的一个常见示例是检查应用程序中是否有可用的用户名,在此过程中,向服务器发送一个异步请求验证用户名。
  在 Controllers 文件夹中添加 ValidationController.cs。

    [ApiController]
    [Route("api/[controller]")]
    public class ValidationController : ControllerBase
    {
        private DataContext dataContext;

        public ValidationController(DataContext context)
        {
            dataContext = context;
        }

        [HttpGet("categorykey")]
        public bool CategoryKey(string categoryId)
        {
            long keyVal;
            return long.TryParse(categoryId, out keyVal)
                && dataContext.Categories.Find(keyVal) != null;
        }

        [HttpGet("supplierkey")]
        public bool SupplierKey(string supplierId)
        {
            long keyVal;
            return long.TryParse(supplierId, out keyVal)
                && dataContext.Suppliers.Find(keyVal) != null;
        }
    }

  验证操作方法必须定义一个参数,该参数的名称与它们要验证的字段相匹配,这允许模型绑过程从请求查询字符串中提取要测试的值。操作方法的响应必须是JSON,并且只能为 true 或 false,以指示一个值是否可接受。操作方法接收候选值,并检查它们是否用作 Category 或 Supplier 对象的数据库键。
  为了使用远程验证方法,将 Remote 属性应用到 Product 类中的 Categoryld 和 Supplierld 属性。

[PrimaryKey(ContextType = typeof(DataContext), DataType = typeof(Category))]
[Remote("CategoryKey", "Validation", ErrorMessage = "Enter an existing key")]
public long CategoryId { get; set; }
[PrimaryKey(ContextType = typeof(DataContext), DataType = typeof(Category))]
[Remote("SupplierKey", "Validation", ErrorMessage = "Enter an existing key")]
public long SupplierId { get; set; }

  请求 http://localhost:5000/controllers/Form,在每次输入一个值后,都会验证输入元素的值。

posted @ 2024-06-16 16:47  一纸年华  阅读(46)  评论(0编辑  收藏  举报