六、表单和模型

在本章中,我们将学习如何构建表单以显示和捕获应用中使用的数据,如何将控件绑定到模型,以及如何使用验证技术排除无效数据。我们将介绍客户端提交的数据,即 HTML 表单及其服务器端对应项、模型和文件。通过这些,我们将学习如何处理用户提交的数据。

具体来说,我们将讨论以下内容:

  • 使用表单上下文
  • 使用模型
  • 了解模型元数据并使用元数据影响表单生成
  • 我们如何使用 HTML 助手生成 HTML
  • 使用模板
  • 将表单绑定到对象模型
  • 验证模型
  • 使用 AJAX
  • 上传文件

技术要求

为了实现本章中介绍的示例,您需要.NET Core 3 SDK 和文本编辑器。当然,VisualStudio2019(任何版本)满足我们的所有要求,但您也可以使用 VisualStudio 代码。

本章的源代码可从 GitHub 的检索 https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition

开始

因为视图本质上是 HTML,所以没有什么可以阻止您手动将标记添加到视图中,其中可以包括通过模型、视图包或临时数据从控制器获得的值。但是,与以前的版本一样,ASP.NET Core 具有内置的方法来帮助您生成与模型(结构和内容)匹配的 HTML,并显示模型验证错误和其他有用的模型元数据。

因为所有这些都在模型之上工作,为了使框架能够提取任何相关信息,我们需要使用强类型视图,而不是动态视图;这意味着向具有适当模型类型的视图添加@model@inherits指令。需要明确的是,模型是您传递给从控制器返回的ViewResult对象的对象,可能是从View方法返回的,它必须匹配视图中声明的@model指令或其@inherit声明。

让我们先看看表单上下文,然后看看如何获得有关模型的信息。

使用表单上下文

视图上下文对象(ViewContext在视图组件(将在第 9 章可重用组件中讨论)中可用,并且作为 Razor Pages 的属性(IRazorPage),这意味着您可以在视图中访问它。在其中,除了通常的上下文属性(例如HttpContextModelStateDictionaryRouteDataActionDescriptor之外,您还可以访问表单上下文FormContext对象)。此对象提供以下属性:

  • CanRenderAtEndOfFormbool):表示表单最后是否可以呈现额外内容(EndOfFormContent)。
  • EndOfFormContentIList<IHtmlContent>):在表单末尾(在</form>标记之前)添加的内容集合。
  • FormDataIDictionary<string, object>:提交的表单数据。
  • HasAntiforgeryTokenbool):表示表单是否呈现防伪令牌,具体取决于BeginForm方法的调用方式。默认为true
  • HasEndOfFormContentbool):表示是否添加了表单结尾内容。
  • HasFormDatabool):表示FormData字典是否已经使用,是否包含数据。

此外,它还提供了一个方法RenderedField,带有两个重载:

  • 返回是否已在当前视图中呈现表单字段的指示的一种
  • 为特定字段设置此标志的另一个字段(通常由基础结构调用)

开发人员可以利用表单上下文来呈现表单中的其他数据,例如验证脚本或其他字段。

现在我们已经看到了全局上下文的样子,让我们看看如何提取有关模型的信息。

使用模型

ASP.NET Core 框架使用模型元数据提供程序从模型中提取信息。此元数据提供程序可以通过HtmlMetadataProperty访问,并公开为IModelMetadataProvider。默认设置为DefaultModelMetadataProvider实例,可通过依赖注入框架进行更改,其合约只定义了两种相关方法:

  • GetMetadataForTypeModelMetadata:返回模型类型本身的元数据
  • GetMetadataForPropertiesIEnumerable<ModelMetadata>:所有公共模型属性的元数据

你通常不会调用这些方法;它们由框架在内部调用。他们返回的ModelMetadata类(实际上可能是派生类,例如DefaultModelMetadata)应该是我们更感兴趣的。此元数据返回以下内容:

  • 类型或属性的显示名称和说明(DisplayName
  • 数据类型(DataType
  • 文本占位符(Placeholder
  • 空值情况下显示的文本(NullDisplayText
  • 显示格式(DisplayFormatString
  • 是否需要该属性(IsRequired
  • 属性是否为只读(IsReadOnly
  • 绑定是否需要该属性(IsBindingRequired

  • 型号活页夹(BinderType

  • 活页夹型号名称(BinderModelName
  • 模型的绑定源(BindingSource
  • 属性的包含类(ContainerType

HTML 助手在为模型生成 HTML 时使用这些属性,它们会影响模型的生成方式。

默认情况下,如果未提供模型元数据提供程序且不存在属性,则元数据属性将假定为安全值或空值。但是,可以覆盖它们。让我们了解如何使用这些属性。

我们将首先查看显示名称(DisplayName和说明(Description)。这些可以由来自System.ComponentModel.DataAnnotations名称空间的[Display]属性控制。此属性还设置属性(Placeholder的占位符/水印):

[Display(Name = "Work Email", Description = "The work email", 
    Prompt = "Please enter the work email")]
public string WorkEmail { get; set; }

通过[Required]实现按要求标记属性(IsRequired)。还可以提供从ValidationAttribute继承的所有其他验证属性(例如RequiredMaxLength,如下所示:

[Required]
[Range(1, 100)]
public int Quantity { get; set; }

属性是否可编辑(IsReadOnly)由属性是否有 setter 和是否应用[Editable]属性控制(默认值为true

[Editable(true)]
public string Email { get; set; }

字符串中包含的数据类型(DataType)可以通过应用[DataType]属性或从中继承的属性来定义:

[DataType(DataType.Email)]
public string Email { get; set; }

有几个从DataTypeAttribute继承的属性类可以替代它使用:

  • [EmailAddress]:同DataType.EmailAddress
  • [CreditCard]DataType.CreditCard
  • [Phone]DataType.PhoneNumber
  • [Url]DataType.Url
  • [EnumDataType]DataType.Custom
  • [FileExtensions]DataType.Upload

DataType has several other possible values; I advise you to have a look into it.

显示值是否为nullNullDisplayText)的文本和显示格式(DisplayFormatString)都可以通过[DisplayFormat]属性设置:

[DisplayFormat(NullDisplayText = "No birthday supplied", DataFormatString = "{0:yyyyMMdd}")]
public DateTime? Birthday { get; set; }

当涉及到将表单字段绑定到类属性时,[ModelBinder]可以用于指定自定义模型绑定器类型(BinderType属性)以及要绑定到的模型中的名称(ModelBinderName;通常,您不提供模型的名称,因为假定该名称与特性名称相同:

[ModelBinder(typeof(GenderModelBinder), Name = "Gender")]
public string Gender { get; set; }

这里,我们指定一个自定义模型绑定器,它将尝试从请求中检索一个值并将其转换为适当的类型。下面是一个可能的实现:

public enum Gender
{
    Unspecified = 0,
    Male,
    Female
}

public class GenderModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.
        ValueProvider.GetValue(modelName);

        if (valueProviderResult != ValueProviderResult.None)
        {
            bindingContext.ModelState.SetModelValue(modelName, 
            valueProviderResult);

            var value = valueProviderResult.FirstValue;

            if (!string.IsNullOrWhiteSpace(value))
            {
                if (Enum.TryParse<Gender>(value, out var gender))
                {
                    bindingContext.Result = ModelBindingResult.
                    Success(gender);
                }
                else
                {
                    bindingContext.ModelState.TryAddModelError
                    (modelName, "Invalid gender.");
                }
            }
        }

        return Task.CompletedTask;
    }
}

这样做的目的是使用当前值提供程序查找传递的表单名称,如果设置了,则检查它是否与Gender枚举匹配。如果是,则设置为返回值(bindingContext.Result;否则,它会添加一个模型错误。

如果通过设置[Bind][BindRequired][BindingBehavior][BindNever]需要一个属性,则IsBindingRequired将是true

[BindNever]  //same as [BindingBehavior(BindingBehavior.Never)]
public int Id { get; set; }
[BindRequired]  //same as [BindingBehavior(BindingBehavior.Required)]
public string Email { get; set; }
[BindingBehavior(BindingBehavior.Optional)]  //default, try to bind if a 
                                            //value is provided
public DateTime? Birthday { get; set; }

[Bind]应用于类本身或参数,以指定哪些属性应该绑定或从绑定中排除。在这里,我们要提到的是,哪些应该受到约束:

[Bind(Include = "Email")]
public class ContactModel
{
    public int Id { get; set; }
    public string Email { get; set; }
}

如果我们使用IBindingSourceMetadata属性之一,则设置BindingSource属性:

  • [FromBody]
  • [FromForm]
  • [FromHeader]
  • [FromQuery]
  • [FromRoute]
  • [FromServices]

默认的模型元数据提供程序可以识别这些属性,但是您当然可以推出自己的提供程序并以任何其他方式提供属性。

有时不应将属性应用于模型属性,例如,自动生成模型类时。在这种情况下,您可以应用一个[ModelMetadataType]属性,通常在另一个文件中,您可以在其中指定用于从以下文件检索元数据属性的类:

public partial class ContactModel
{
    public int Id { get; set; }
    public string Email { get; set; }
}

可以从另一个文件向同一类添加属性:

[ModelMetadataType(typeof(ContactModelMetadata))]
public partial class ContactModel
{
}

在以下示例中,我们指定了要绑定的各个属性:

public sealed class ContactModelMetadata
{
    [BindNever]
    public int Id { get; set; }
    [BindRequired]
    [EmailAddress]
    public string Email { get; set; }
}

Besides using the model, it is also possible to bind properties on the controller itself. All that is said also applies, but these properties need to take the [BindProperty] attribute. See Chapter 4, Controllers and Actions, for more information.

现在让我们看看如何处理匿名类型。

使用匿名类型的模型

与以前版本的 ASP.NET MVC 一样,您不能将匿名类型作为模型传递给视图。即使可以,该视图也无法访问其属性,即使该视图设置为使用dynamic作为模型类型。您可以使用这样的扩展方法将您的匿名类型转换为ExpandoObject,这是dynamic的常见实现:

public static ExpandoObject ToExpando(this object anonymousObject)
{
    var anonymousDictionary = HtmlHelper.
    AnonymousObjectToHtmlAttributes(anonymousObject);
    IDictionary<string, object> expando = new ExpandoObject();

    foreach (var item in anonymousDictionary)
    {
        expando.Add(item);
    }

    return expando as ExpandoObject;
}

您可以在控制器中使用此选项:

return this.View(new { Foo = "bar" }.ToExpando());

在视图文件中,按如下方式使用它:

@model dynamic

<p>@Model.Foo</p>

我们现在已经完成了模型绑定,所以让我们继续使用 HTML 帮助程序。

使用 HTML 助手

HTML 助手是视图的Html对象(IHtmlHelper对象)的方法,其存在是为了帮助生成 HTML。我们可能不知道路由的确切语法和 URL 可能很难生成,但我们使用它们还有两个更重要的原因。HTML 帮助程序根据模型元数据生成用于显示和编辑目的的适当代码,并且还包括错误和描述占位符。重要的是要记住,它们始终基于模型。

通常,内置 HTML 帮助程序有两个重载:

  • 采用强类型模型的模型(例如,EditorFor(x => x.FirstName)
  • 另一种以字符串形式获取动态参数的方法(例如,EditorFor("FirstName")

此外,它们都采用可选参数htmlAttributes,可用于向呈现的 HTML 元素添加任何属性(例如,TextBoxFor(x => x.FirstName, htmlAttributes: new { @class = "first-name" }))。由于这个原因,当我们浏览不同的 HTML 帮助程序时,我将跳过htmlAttributes参数。

形式

为了提交值,我们首先需要一个表单;HTMLform元素可用于此目的。BeginForm助手为我们生成一个:

@using (Html.BeginForm())
{
 <p>Form goes here</p>
}

返回一个IDisposable实例;因此,应在using块中使用。这样,我们可以确保它被正确终止。

此方法有多个重载,其中,它可以采用以下参数:

  • actionNamestring:控制器动作的可选名称。如果存在,controllerName参数也必须提供。
  • controllerNamestring:控制器的可选名称;它必须与actionName配合使用。
  • methodFormMethod:可选的 HTML 表单方法(GETPOST);如果未提供,则默认为POST

  • routeNamestring:可选路由名称(通过 fluent 配置注册的路由名称)。

  • routeValuesobject:可选对象实例,包含routeName特有的路由值。
  • antiForgerybool?):表示表单中是否应该包含防伪令牌(后面会详细介绍);如果未提供,则默认情况下会包含它。

还有另一种表单生成方法BeginRouteForm,它更关注路由,因此它总是采用routeName参数。它所做的任何事情都可以通过BeginForm实现。

有两种方法可用于定义表单提交的目标:

  • actionNamecontrollerName:将表单提交到的操作和可选控制器名称。如果省略控制器名称,它将默认为当前名称。
  • routeName:路由表中定义的路由名称,由控制器和动作组成。

必须选择其中之一。

单行文本框

所有基本的.NET 类型都可以通过文本框进行编辑。我所说的文本框是指具有适当的type属性的<input>元素。为此,我们有TextBoxForTextBox方法,前者用于强类型版本(基于模型使用 LINQ 表达式的版本),另一个用于基于字符串的版本。这些方法可按如下方式使用:

@Html.TextBoxFor(x => x.FirstName)
@Html.TextBox("FirstName")

这些方法具有多个重载,这些重载采用了format参数。

formatstring:用于呈现类型实现IFormattable的情况的可选格式字符串

例如,如果要呈现的值表示金钱,我们可以有一行,如下所示:

@Html.TextBoxFor(model => model.Balance, "{0:c}");

此处,c用于设置货币格式。

TextBoxTextBoxForHTML 帮助程序呈现一个值为type<input>标记,该值取决于属性的实际类型及其数据类型元数据(DefaultModelMetadata.DataTypeName

  • text:对于没有任何特定DataType属性的字符串属性
  • datedatetime:对于DateTime属性,取决于DataType的存在,其值为DateDateTime
  • number:用于数字属性
  • email:与EmailAddressDataType属性关联时的字符串属性
  • url:具有DataType属性Url的字符串属性
  • timeTimeSpan属性或DataType属性为Time的字符串属性
  • tel:具有DataType属性PhoneNumber的字符串属性

<input>标记的类型是 HTML5 支持的值之一。您可以在上了解更多信息 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input

多行文本框

如果我们想要呈现多行文本框,我们必须使用TextAreaTextAreaFor方法。这些呈现 HTMLtextarea元素及其参数:

  • rowsint):要生成的行(textarea``rows属性)
  • columnsintcols属性

在此之后,我们继续了解密码是如何工作的。

密码

密码(<input type="password">PasswordPasswordFor方法之一产生。他们可以接受的唯一可选值是初始密码,即初始密码valuestring

接下来是下拉列表。

下拉菜单

DropDownListDropDownListFor方法使用SelectListItem项集合形式指定的值呈现<select>元素。参数如下:

  • selectListIEnumerable<SelectListItem>:要显示的项目列表
  • optionLabelstring:默认为空项

SelectListItem类公开了以下属性:

  • Disabledbool):表示该项目是否可用。默认值为false
  • GroupSelectListGroup:可选组。
  • Selectedbool):表示是否选中该项目。只能有一个项目标记为选中;因此,默认值为false
  • Textstring:要显示的文本值。
  • Valuestring:要使用的值。

SelectListGroup类提供两个属性:

  • Namestring:必填组名,用于将多个列表项分组。
  • Disabledbool):表示该组是否禁用。默认为false

有两个助手方法,GetEnumSelectListGetEnumSelectList<>,它们以IEnumerable<SelectListItem>的形式返回枚举字段的名称和值。如果我们希望使用它们来提供下拉列表,这将非常有用。

列表框

ListBoxListBoxFor方法与下拉列表类似。唯一的区别是生成的<select>元素的multiple属性设置为true。只需要一个参数selectListIEnumerable<SelectListItem>)就可以显示项目。

单选按钮

对于单选按钮,我们有RadioButtonRadioButtonFor两种方法,它们将<input>呈现为radio类型:

  • valueobject:用于单选按钮的值
  • isCheckedbool?):表示单选按钮是否勾选(默认)

单选按钮组名称将是生成该组的属性的名称,例如:

@Html.RadioButtonFor(m => m.Gender, "M" ) %> Male
@Html.RadioButtonFor(m => m.Gender, "F" ) %> Female

复选框

也可以通过CheckBoxCheckBoxForCheckBoxForModel方法来考虑复选框。这一次,他们呈现了一个类型为checkbox<input>标记。唯一参数如下所示:

  • isCheckedbool?):表示复选框是否勾选。默认值为false

同样,组名将来自属性,单选按钮也是如此。

使用复选框时需要记住的一点是,我们通常会将复选框值绑定到一个bool参数。在这种情况下,我们不能忘记为复选框提供一个值true;否则,表单将不包含其字段的数据。

隐藏值

HiddenHiddenForHiddenForModel呈现一个<input type="hidden">元素。可以使用以下参数显式重写模型或其属性:

  • valueobject):要包含在隐藏字段中的值

另一个选项是使用[HiddenInput]属性装饰模型类属性,如下例所示:

[HiddenInput(DisplayValue = false)] 
public bool IsActive { get; set; } = true;

使用自动模型编辑器时,DisplayValue参数会导致属性不作为标签输出。

链接

如果我们想要生成特定控制器动作的超链接(<a>,我们可以使用ActionLink方法。它有多个重载,可接受以下参数:

  • linkTextstring:链接文本
  • actionNamestring:动作名称
  • controllerNamestring:控制器名称,必须与actionName一起提供
  • routeValuesobject:包含路由值的可选值(POCO 类或字典)
  • protocolstring):可选的 URL 协议(如httphttps等)
  • hostnamestring:可选的 URL 主机名
  • fragmentstring:可选的 URL 锚(例如#anchorname
  • portint:可选的 URL 端口

如我们所见,此方法可以为 web 应用的同一主机或不同主机生成链接。

另一种选择是使用路由名称,为此目的,有RouteLink方法;唯一的区别是,它不使用actionNamecontrollerName参数,而是使用routeName参数,如routeNamestring),即为其生成链路的路由的名称。

接下来,我们有标签。

标签

LabelLabelForLabelForModel使用模型的文本表示或可选文本呈现<label>元素:

  • labelTextstring:要添加到标签的文本

在标签之后,我们有原始的 HTML。

原始 HTML

这将呈现 HTML 编码的内容。其唯一参数如下:

  • valuestringobject:HTML 编码后显示的内容

我们将要学习的下一个特性是 ID、名称和值。

ID、名称和值

从生成的 HTML 元素、生成的 ID 和名称中提取某些属性时,这些属性通常很有用。JavaScript 通常需要这样做:

  • IdIdForIdForModel:返回id属性的值
  • NameNameForNameForModelname属性的值
  • DisplayNameDisplayNameForDisplayNameForModel:给定属性的显示名称
  • DisplayTextDisplayTextFor:属性或模型的显示文本
  • ValueValueForValueForModel:视图包中的第一个非空值

通用编辑器和显示

我们已经看到,我们可以对单个模型属性或模型本身使用模板。要呈现显示模板,我们有DisplayDisplayForDisplayForModel方法。它们都接受以下可选参数:

  • templateNamestring:将覆盖模型元数据中模板的名称(DefaultModelMetadata.TemplateHint
  • additionalViewDataobject):合并到视图包中的对象或IDictionary
  • htmlFieldNamestring:生成的 HTML<input>字段的名称

属性只有在其元数据声明为显示模式时才会呈现(DefaultModelMetadata.ShowForDisplay

对于编辑模板,方法类似:EditorEditorForEditorForModel。这些参数与相应的显示参数完全相同。值得一提的是,仅为根据元数据定义的可编辑属性生成编辑器(DefaultModelMetadata.ShowForEdit

效用方法与性质

IHtmlHelper类还公开了一些其他实用方法:

  • Encode:HTML 使用配置的 HTML 编码器对字符串进行编码
  • FormatValue:呈现传递值的格式化版本

此外,它还公开了以下上下文属性:

  • IdAttributeDotReplacement:用于生成 ID 值的点替换字符串(来自MvcViewOptions.HtmlHelperOptions.IdAttributeDotReplacement
  • Html5DateRenderingMode:HTML5 日期呈现模式(从MvcViewOptions.HtmlHelperOptions.Html5DateRenderingMode开始)
  • MetadataProvider:模型元数据提供者
  • TempData:临时数据

  • ViewDataViewBag:强/松类型视图包

  • ViewContext:视图的所有上下文,包括 HTTP 上下文(HttpContext)、路由数据(RouteData)、表单上下文(FormContext)和解析模型(ModelStateDictionary

接下来是验证消息。

验证消息

可以显示单个已验证属性的验证消息,也可以显示所有模型的摘要。对于显示单个消息,我们使用ValidationMessageValidationMessageFor方法,它们接受以下可选属性:

  • messagestring:覆盖验证框架中的错误消息的错误消息

对于验证总结,我们有ValidationSummary,它接受以下参数:

  • excludePropertyErrorsbool):如果设置,则仅显示模型级(顶部)错误,而不显示单个属性的错误
  • messagestring):显示单个错误的消息
  • tagstring:用于覆盖MvcViewOptions.HtmlHelperOptions.ValidationSummaryMessageElement的 HTML 标记)

在验证之后,我们继续下一个特性,即自定义帮助程序。

海关助理

一些 HTML 元素没有相应的 HTML 帮助程序,例如,button。不过,添加一个是很容易的。那么,让我们在IHtmlHelper上创建一个扩展方法:

public static class HtmlHelperExtensions
{
    public static IHtmlContent Button(this IHtmlHelper html, string text)
    {
        return html.Button(text, null);
    }

    public static IHtmlContent Button(this IHtmlHelper html, string
    text, object htmlAttributes)
    {
        return html.Button(text, null, null, htmlAttributes);
    }

    public static IHtmlContent Button(
        this IHtmlHelper html,
        string text,
        string action,
        object htmlAttributes)
    {
        return html.Button(text, action, null, htmlAttributes);
    }

    public static IHtmlContent Button(this IHtmlHelper html, string
    text, string action)
    {
        return html.Button(text, action, null, null);
    }

    public static IHtmlContent Button(
        this IHtmlHelper html,
        string text,
        string action,
        string controller)
    {
        return html.Button(text, action, controller, null);
    }

    public static IHtmlContent Button(
        this IHtmlHelper html,
        string text,
        string action,
        string controller,
        object htmlAttributes)
    {
        if (html == null)
        {
            throw new ArgumentNullException(nameof(html));
        }

        if (string.IsNullOrWhiteSpace(text))
        {
            throw new ArgumentNullException(nameof(text));
        }

        var builder = new TagBuilder("button");
        builder.InnerHtml.Append(text);

        if (htmlAttributes != null)
        {
            foreach (var prop in htmlAttributes.GetType()
            .GetTypeInfo().GetProperties())
            {
                builder.MergeAttribute(prop.Name, 
                    prop.GetValue(htmlAttributes)?.ToString() ?? 
                    string.Empty);
            }
        }

        var url = new UrlHelper(new ActionContext(
            html.ViewContext.HttpContext,  
            html.ViewContext.RouteData, 
            html.ViewContext.ActionDescriptor));

        if (!string.IsNullOrWhiteSpace(action))
        {
            if (!string.IsNullOrEmpty(controller))
            {
                builder.Attributes["formaction"] = url.Action(
                action, controller);
            }
            else
            {
                builder.Attributes["formaction"] = url.Action(action);
            }
        }

        return builder;
    }
}

此扩展方法使用所有其他 HTML 帮助程序的通用准则:

  • 每个可能参数的多个重载
  • 有一个名为htmlAttributesobject类型的参数,用于我们希望添加的任何自定义 HTML 属性
  • 使用UrlHelper类为控制器操作生成正确的路由链接(如果提供)
  • 返回一个IHtmlContent的实例

使用它很简单:

@Html.Button("Submit")

它还可以与特定动作和控制器一起使用:

@Html.Button("Submit", action: "Validate", controller: "Validation")

它甚至可以与一些自定义属性一起使用:

@Html.Button("Submit", new { @class = "save" })

由于 ASP.NET Core 不提供任何用于提交表单的 HTML 帮助程序,我希望您觉得这很有用!

我们对定制助手的研究到此结束。现在让我们重点讨论为常用的标记编写模板。

使用模板

当调用DisplayDisplayFor<T>DisplayForModelHTML 助手方法时,ASP.NET Core 框架会以特定于该属性(或模型类)的方式呈现目标属性(或模型)值,并且会受到其元数据的影响。例如,ModelMetadata.DisplayFormatString用于以所需格式呈现属性。然而,假设我们想要一个稍微复杂一点的 HTML,例如,在复合属性的情况下。输入显示模板!

显示模板是一个剃须刀功能;基本上,它们是部分视图,存储在Views\Shared下名为DisplayTemplates的文件夹中,它们的模型被设置为以.NET 类为目标。让我们想象一下,我们有一个Location类存储LatitudeLongitude值:

public class Location
{
    public decimal Latitude { get; set; }
    public decimal Longitude { get; set; }
}

如果我们想为此使用自定义显示模板,可以使用局部视图,如下所示:

@model Location
<div><span>Latitude: @Model.Latitude</span> - <span>Longitude:
@Model.Longitude</span></div>

因此,该文件存储在Views/Shared/DisplayTemplates/Location.cshtml中,但现在您需要将Location类与之关联,您可以通过将[UIHint]应用于该类型的属性来实现:

[UIHint("Location")]
public Location Location { get; set; }

The [UIHint] attribute accepts a view name. It is searched in the Views\Shared\DisplayTemplates folder.

与显示模板类似,我们有编辑器模板。编辑器模板由EditorEditorForEditorForModel呈现,它们与显示模板的主要区别在于部分视图文件存储在Views\Shared\EditorTemplates中。当然,在这些模板中,您可能会添加 HTML 编辑器元素,即使使用自定义 JavaScript 也是如此。对于Location类,我们可以有以下内容:

@model Location
<div>
    <span>Latitude: @Html.TextBoxFor(x => x.Latitude)</span>
    <span>Longitude: @Html.TextBoxFor(x => x.Longitude)</span>
</div>

There can be only one [UIHint] attribute specified, which means that both templates—display and editor—must use the same name. Also, custom templates are not rendered by EditorForModel or DisplayForModel; you need to explicitly render them using EditorFor and DisplayFor.

好的,我们已经了解了如何为常用的标记元素使用模板,这从重用的角度来看非常有用。现在让我们看一看模型绑定。

强制模型绑定

ASP.NET Core 尝试自动填充(设置其属性和字段的值)操作方法的任何参数。这是因为它有一个内置的(虽然是可配置的)模型绑定器提供程序,它创建了一个模型绑定器。这些模型绑定器知道如何将来自多个绑定源(前面讨论过)的数据以多种格式绑定到 POCO 类。

模型粘合剂

模型绑定器提供程序接口是IModelBinderProvider,模型绑定器是IModelBinder,这并不奇怪。模型装订商在MvcOptionsModelBinderProviders集合中注册:

services.AddMvc(options =>
{
    options.ModelBinderProviders.Add(new CustomModelBinderProvider());
});

包括的供应商如下:

  • BinderTypeModelBinderProvider:定制型号活页夹(IModelBinder
  • ServicesModelBinderProvider[FromServices]
  • BodyModelBinderProvider[FromBody]
  • HeaderModelBinderProvider[FromHeader]
  • SimpleTypeModelBinderProvider:使用类型转换器的基本类型
  • CancellationTokenModelBinderProviderCancellationToken
  • ByteArrayModelBinderProvider:将 Base64 字符串反序列化为字节数组
  • FormFileModelBinderProvider[FromForm]
  • FormCollectionModelBinderProviderIFormCollection
  • KeyValuePairModelBinderProviderKeyValuePair<TKey, TValue>
  • DictionaryModelBinderProviderIDictionary<TKey, TValue>
  • ArrayModelBinderProvider:对象数组
  • CollectionModelBinderProvider:对象集合(ICollection<TElement>IEnumerable<TElement>IList<TElement>
  • ComplexTypeModelBinderProvider:嵌套属性(例如TopProperty.MidProperty.BottomProperty

这些提供程序帮助将值分配给以下类型:

  • 使用类型转换器的简单属性
  • POCO 类
  • 嵌套的 POCO 类
  • POCO 类的数组
  • 辞典
  • POCO 类集合

例如,以以下类别的模型为例:

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public OrderState State { get; set; }
    public DateTime Timestamp { get; set; }
    public List<OrderDetail> Details { get; set; }
}

public enum OrderState
{
    Received,
    InProcess,
    Sent,
    Delivered,
    Cancelled,
    Returned
}

public class OrderDetail
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
}

public class Location
{
    public int X { get; set; }
    public int Y { get; set; }
}

这里,我们有不同类型的属性,包括基本类型、枚举和 POCO 类集合。当我们为这样的模型生成表单时,可能使用前面描述的 HTML 帮助程序,您将获得包含以下值的 HTML 表单元素:

Id=43434
CustomerId=100
State=InProcess
Timestamp=2017-06-15T20:00:00
Details[0]_ProductId=45
Details[0]_Quantity=1
Details[1]_ProductId=47
Details[1]_Quantity=3
X=10
Y=20

请注意分隔子属性名称的_字符,默认情况下,它被配置为替换MvcViewOptions.HtmlHelper.IdAttributeDotReplacement属性中的点(.。如您所见,ASP.NET Core 甚至可以绑定一些复杂的情况。

模型绑定源

因此,我们将模型(或单个基类型参数)声明为操作方法的参数,并且可以应用模型绑定源属性来指示 ASP.NET Core 从特定位置获取值。同样,这些措施如下:

  • [FromServices]:将从依赖项注入容器插入对象。
  • [FromBody]:该值将来自POST请求的有效负载,通常为 JSON 或 XML。
  • [FromForm]:该值将来自已发布的表单。
  • [FromQuery]:从查询字符串中获取值。
  • [FromHeader]:该值将从请求头读取。
  • [FromRoute]:该值将作为命名模板项来自路由。

可以在同一方法上混合不同的模型绑定源属性,如下所示:

public IActionResult Process(
    [FromQuery] int id, 
    [FromHeader] string contentType, 
    [FromBody] Complex complex) { ... }

[FromPost]将采用multipart/form-dataapplication/x-www-form-urlencoded格式的键值对。

您需要记住的一点是,只有一个参数具有[FromBody]属性,这是有意义的,因为主体是唯一的,将其绑定到两个不同的对象是没有意义的。将其应用于 POCO 类也是有意义的。[FromBody]与注册的输入格式化程序一起工作;它试图通过遍历每个输入格式化程序来反序列化发送的任何有效负载(通常由POSTPUT)。第一个响应为非空值的函数将生成结果。输入格式化程序查看请求的Content-Type头(例如,application/xmlapplication/json,以确定是否可以处理请求并将其反序列化为目标类型。我们将在第 8 章API 控制器中更详细地介绍输入格式化程序。

您可以使用[FromQuery]从查询字符串构造 POCO 对象。如果您在查询字符串上为 POCO 的每个属性提供一个值,ASP.NET Core 足够聪明,可以做到这一点,如下所示:

//call this with: SetLocation?X=10&Y=20
public IActionResult SetLocation([FromQuery] Location location) { ... }

其中一些属性采用可选的Name参数,可用于显式声明源名称,如下所示:

[FromHeader(Name = "User-Agent")]
[FromQuery(Name = "Id")]
[FromRoute(Name = "controller")]
[FromForm(Name = "form_field")]

如果不指定源名称,它将使用参数的名称。

如果未指定任何属性,ASP.NET Core 在尝试绑定值时将采用以下逻辑:

  1. 如果请求是一个POST值,它将尝试绑定表单中的值(与[FromForm]一样)。
  2. 然后,它将路由值([FromRoute]
  3. 然后查询字符串([FromQuery]

因此,[FromBody][FromServices][FromHeader]从未自动使用。您始终需要应用属性(或定义约定)。

如果在使用默认逻辑或任何属性的操作方法中找不到参数的值,则该值将接收默认值:

  • 值类型的默认值(0表示整数,false表示布尔值,等等)
  • 类的实例化对象

如果要在找不到参数值时强制模型状态无效,请对其应用[BindRequired]属性:

public IActionResult SetLocation(
    [BindRequired] [FromQuery] int x,
    [BindRequired] [FromQuery] int y) { ... }

在这种情况下,在不提供XY参数的情况下,尝试调用此操作时将出现错误。您还可以将其应用于模型类,在这种情况下,需要提供其所有属性,如下所示:

[BindRequired]
public class Location
{
    public int X { get; set; }
    public int Y { get; set; }
}

这也有一些限制,因为您无法绑定到抽象类、值类型(struct)或没有公共无参数构造函数的类。如果您想绑定到一个抽象类或一个没有公共、无参数构造函数的类,您需要展开自己的模型绑定器并自己返回一个实例。

动态绑定

如果您事先不知道请求将包含什么内容,例如,如果您希望接受任何已发布的内容,该怎么办?你基本上有三种方式来接受它:

  • 如果有效负载可以表示为字符串,请使用字符串参数。
  • 使用自定义模型活页夹。
  • 使用支持 JSON 的参数类型之一。

如果使用字符串参数,它将只按原样包含有效负载,但 ASP.NET Core 还支持将 JSON 有效负载绑定到dynamicSystem.Text.Json.JsonElement参数。如果您不熟悉的话,JsonElement是新的System.Text.JsonAPI 的一部分,它取代了JSON.NETNewtonsoft.Json)作为包含的 JSON 序列化程序。ASP.NET Core 可以将内容类型为application/jsonPOST绑定到这些参数类型之一,而无需任何额外配置,如下所示:

[HttpPost]
public IActionResult Process([FromBody] dynamic payload) { ... }

动态参数实际上是JsonElement的一个实例。除非使用自己的模型绑定器并从中返回构造的实例,否则不能将参数声明为接口或抽象基类。

现在,让我们继续验证绑定它的模型帖子。

JSON.NET is still available as an open source project from GitHub at https://github.com/JamesNK/Newtonsoft.Json. You can use it instead of the built-in JSON serializer. To do this, have a look at https://docs.microsoft.com/en-us/aspnet/core/migration/22-to-30?view=aspnetcore-3.1.

模型验证

我们都知道,通过验证页面而不必发布其内容的客户端验证是我们现在对 web 应用的期望。但是,对于禁用 JavaScript 的情况来说,这可能是不够的。在这种情况下,我们需要确保在实际使用数据之前在服务器端验证数据。ASP.NET Core 支持这两种方案;让我们看看如何。

服务器端验证

验证提交的模型的结果(通常通过POST进行验证)始终在ControllerBase类的ModelState属性中可用,并且在ActionContext类中也存在。考虑下面的代码片段:

if (!this.ModelState.IsValid)
{
    if (this.ModelState["Email"].Errors.Any())
    {
        var emailErrors = string.
            Join(Environment.NewLine, this.ModelState
            ["Email"].Errors.Select(e => e.ErrorMessage));
    }
}

如您所见,我们有全局验证状态(IsValid)和单个属性错误消息(例如,["Email"].Errors)。

使用所有基于System.ComponentModel.DataAnnotationsAPI 的内置验证程序,执行以下验证:

  • 基于属性的验证(ValidationAttribute-派生)
  • 基于IValidatableObject接口的验证

如果您碰巧更改了模型,则在发布表单或通过调用TryValidateModel显式调用表单时执行验证。ModelState属性属于ModelStateDictionary类型,它公开了以下属性:

  • ItemModelStateEntry:访问单个模型属性的状态
  • KeysKeyEnumerable:模型属性名称的集合
  • ValuesValueEnumerable:模型属性值
  • Countint:模型属性的计数
  • ErrorCountint:错误计数
  • HasReachedMaxErrorsbool:发现的错误是否达到配置的最大值
  • MaxAllowedErrorsint:配置的最大错误数(参见配置部分)
  • RootModelStateEntry:根对象的模型状态
  • IsValidbool:模型是否有效
  • ValidationStateModelValidationState:模型的验证状态(UnvalidatedInvalidValidSkipped

基于属性的验证与验证属性所在的属性相关联(某些验证属性也可以应用于类)。属性的名称将是键,属性的值将是ModelStateDictionary中的值。对于每个属性,一旦验证器失败,将不会触发任何其他最终验证器,并且模型状态将立即无效。每个属性公开一个或多个ModelError对象的集合:

IEnumerable<ModelError> errors = this.ModelState["email"];

此类有两个属性:

  • ErrorMessagestring):由属性验证器生成的消息(如果有)
  • ExceptionException:验证此特定属性时产生的任何异常

在这之后,我们转到它的配置。

配置

作为MvcOptions类的一部分,AddMvc方法提供了两个配置选项:

  • MaxModelValidationErrorsint:不再执行验证之前的最大验证错误数(默认为200)。
  • ModelValidatorProvidersIList<IModelValidatorProvider>:注册的模型验证提供商。默认情况下,它包含一个DefaultModelValidatorProvider实例和一个DataAnnotationsModelValidatorProvider实例。

这些内置提供程序基本上执行以下操作:

  • DefaultModelValidatorProvider:如果属性具有实现IModelValidator的属性,则使用该属性进行验证。
  • DataAnnotationsModelValidatorProvider:钩住要验证的属性可能具有的任何ValidatorAttribute实例。

数据注释验证

System.ComponentModel.DataAnnotations提供以下验证属性:

  • [Compare]:比较两个属性的值是否相同。
  • [CreditCard]:字符串属性必须具有有效的信用卡格式。
  • [CustomValidation]:通过外部方法进行自定义验证。
  • [DataType]:根据特定数据类型(DateTimeDateTimeDurationPhoneNumberCurrencyTextHtmlMultilineTextEmailAddressPasswordUrlImageUrlCreditCardPostalCodeUpload验证属性。
  • [EmailAddress]:检查字符串属性是否为有效的电子邮件地址。
  • [MaxLength]:字符串属性的最大长度。
  • [MinLength]:字符串属性的最小长度。
  • [Phone]:检查字符串属性是否具有类似电话的结构(仅限美国)。
  • [Range]:属性的最大值和最小值。
  • [RegularExpression]:使用正则表达式验证字符串属性。
  • [Remote]:使用控制器动作验证模型。
  • [Required]:检查属性是否设置了值。

  • [StringLength]:检查字符串的最大和最小长度;与一个[MinLength]值和一个[MaxLength]值相同,但是使用这个,您只需要一个属性。

  • [Url]:检查字符串属性是否为有效的 URL。

所有这些属性都由注册的DataAnnotationsModelValidatorProvider自动挂钩。

对于自定义验证,我们有两个选项:

  • 继承ValidationAttribute并实现其IsValid方法:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class IsEvenAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, 
    ValidationContext validationContext)
    {
        if (value != null)
        {
            try
            {
                var convertedValue = Convert.ToDouble(value);
                var isValid = (convertedValue % 2) == 0;

                if (!isValid)
                {
                    return new ValidationResult(this.ErrorMessage, 
                    new[] { validationContext.MemberName });
                }
            }
            catch { }
        }

        return ValidationResult.Success;
      }
 }
  • 实施验证方法:
[CustomValidation(typeof(ValidationMethods), "ValidateEmail")]
public string Email { get; set; }

在此ValidationMethods类中,添加以下方法:

public static ValidationResult ValidateEmail(string email, ValidationContext context)
{
    if (!string.IsNullOrWhiteSpace(email))
    {
        if (!Regex.IsMatch(email, @"^([\w\.\-]+)@([\w\-]+
        )((\.(\w){2,3})+)$"))
        {
            return new ValidationResult("Invalid email",
            new[] { context.MemberName });
        }
    }

    return ValidationResult.Success;
} 

需要注意的几点:

  • 此验证属性检查有效电子邮件;它没有检查所需的值。
  • ValidationContext属性具有一些有用的属性,例如当前正在验证的成员名称(MemberName)、其显示名称(DisplayName)和根验证对象(ObjectInstance)。
  • ValidationResult.Successnull

验证方法的签名可能会有所不同:

  • 第一个参数可以是强类型(例如,string)或松散类型(例如,object),但必须与要验证的属性兼容。
  • 它可以是static或实例。
  • 可以接受也可以不接受ValidationContext参数。

为什么选择其中一个呢?[CustomValidation]属性通过拥有一组可在不同上下文中使用的共享方法,潜在地促进了重用。此属性中还有一条错误消息。

[CustomValidation] can be applied to either a property or the whole class.

错误消息

有三种方法可用于设置在发生验证错误时显示的错误消息:

  • ErrorMessage:一个普通的旧错误消息字符串,没有附加任何魔法。
  • ErrorMessageString:一个格式字符串,可以根据实际的验证属性获取令牌(例如,{0}{1});令牌{0}通常是正在验证的属性的名称。
  • ErrorMessageResourceTypeErrorMessageResourceName:可以请求错误消息来自外部类型(ErrorMessageResourceType中声明的字符串属性(ErrorMessageResourceName);如果您想本地化错误消息,这是一种常见的方法。

在此之后,我们继续下一个功能。

自我验证

如果您需要的验证涉及一个类的多个属性,那么您将实现IValidatableObject(也由DataAnnotationsValidatorProvider支持),类似于您将[CustomValidation]应用于整个类所实现的。我们说这个类是自验证的。IValidatableObject接口指定了一个方法Validate,下面是一个可能的实现:

public class ProductOrder : IValidatableObject
{
    public int Id { get; set; }
    public DateTime Timestamp { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext 
    context)
    {
        if (this.Id <= 0)
        {
            yield return new ValidationResult("Missing id", new []
             { "Id" });
        }

        if (this.ProductId <= 0)
        {
            yield return new ValidationResult("Invalid product",
             new [] { "ProductId" });
        }

        if (this.Quantity <= 0)
        {
            yield return new ValidationResult("Invalid quantity",
            new [] { "Quantity" });
        }

        if (this.Timestamp > DateTime.Now)
        {
            yield return new ValidationResult("Order date
            is in the future",  new [] { "Timestamp" });
        }
    }
}

自我验证之后,让我们继续进行自定义验证。

自定义验证

自定义验证的另一个选项涉及挂钩一个新的模型验证器提供者和一个定制的模型验证器。模型验证程序提供程序是IModelValidatorProvider的实例,例如:

public sealed class IsEvenModelValidatorProvider : IModelValidatorProvider
{
    public void CreateValidators(ModelValidatorProviderContext context)
    {
        if (context.ModelMetadata.ModelType == typeof(string)
            || context.ModelMetadata.ModelType == typeof(int)
            || context.ModelMetadata.ModelType == typeof(uint)
            || context.ModelMetadata.ModelType == typeof(long)
            || context.ModelMetadata.ModelType == typeof(ulong)
            || context.ModelMetadata.ModelType == typeof(short)
            || context.ModelMetadata.ModelType == typeof(ushort)
            || context.ModelMetadata.ModelType == typeof(float)
            || context.ModelMetadata.ModelType == typeof(double))
        {
            if (!context.Results.Any(x => x.Validator is 
            IsEvenModelValidator))
            {
                context.Results.Add(new ValidatorItem
                {
                    Validator = new IsEvenModelValidator(),
                    IsReusable = true
                });
            }
        }
    }
}

这将检查目标属性(context.ModelMetadata是否为预期类型(数字或字符串)之一,然后添加一个IsEvenModelValidator属性。触发验证时,将调用此验证程序。

为了便于完成,以下是其代码:

public sealed class IsEvenModelValidator : IModelValidator
{
    public IEnumerable<ModelValidationResult> 
    Validate(ModelValidationContext context)
    {
        if (context.Model != null)
        {
            try
            {
                var value = Convert.ToDouble(context.Model);
                if ((value % 2) == 0)
                {
                    yield break;
                }
            }
            catch { }
        }

        yield return new ModelValidationResult(
            context.ModelMetadata.PropertyName, 
            $"{context.ModelMetadata.PropertyName} is not even.");
    }
}

此验证程序代码尝试将数字转换为double值(因为它更通用),然后检查数字是否为偶数。如果值为null或不可转换,则只返回空结果。

阻止验证

如果您不希望您的模型验证整个类或一个或多个属性,可以对其应用[ValidateNever]属性。这实现了IPropertyValidationFilter接口,该接口可用于在验证过程中选择性地包括或排除属性。然而,我发现[ValidateNever]属性的实现方式没有多大意义,因为它迫使您将其包含在模型类中,而不是模型参数中,我认为这更有意义。

自动验证

第 4 章控制器和动作中,我们看到了如何注册一个过滤器,用于触发自动模型验证,当您使用POST时,这种情况已经发生了——并相应地执行动作。更多信息请参阅本章。

客户端模型验证

因为服务器端验证需要 post,所以有时它更有用,并提供更好的用户体验,以便在客户端执行验证。让我们看看怎么做。

所有内置验证器还包括客户端行为;这意味着,如果使用默认情况下包含在 ASP.NET Core 模板中的 jQuery 的非干扰性验证,则会自动获得它。不引人注目的验证需要以下 JavaScript 模块:

实际的文件名可能略有不同(最小化版本或包含版本号),但仅此而已。它们默认安装为wwwroot\lib\jquerywwwroot\lib\jquery-validationwwwroot\lib\jquery-validation-unobtrusive

在幕后,包含的验证程序向每个属性添加 HTML5 属性(data-*,以验证 HTML 表单元素,并且在表单即将提交时,强制进行验证。客户端验证仅在启用时执行(下一主题将对此进行详细介绍)。

配置

客户端验证提供程序通过AddViewOptions方法配置,该方法采用一个 lambda 函数,该函数公开MvcViewOptions

  • ClientModelValidatorProvidersIList<IClientModelValidatorProvider>):注册的客户模型验证人;默认情况下,它包含一个DefaultClientModelValidatorProvider属性、一个DataAnnotationsClientModelValidatorProvider属性和一个NumericClientModelValidatorProvider属性。
  • HtmlHelperOptions.ClientValidationEnabledbool:是否启用客户端验证。默认为true,表示已启用。
  • ValidationMessageElementstring:用于插入每个已验证属性的验证错误消息的 HTML 元素。默认值为span
  • ValidationSummaryMessageElementstring:用于插入模型的验证错误消息摘要的 HTML 元素。默认值为span

包含的IClientModelValidatorProvider属性具有以下目的:

  • DefaultClientModelValidatorProvider:如果验证属性实现了IClientModelValidator,则无论是否有特定的客户机模型验证程序提供程序,都会使用它进行验证。
  • NumericClientModelValidatorProvider:限制文本框仅包含数值。
  • DataAnnotationsClientModelValidatorProvider:添加对所有包含的数据注释验证程序的支持。

自定义验证

您当然可以推出自己的客户端验证程序;它的核心是IClientModelValidatorIClientModelValidatorProvider接口。从前面看到的IsEvenAttribute属性开始,让我们看看如何在客户端实现相同的验证。

首先,让我们注册一个客户端模型验证程序提供程序

services
    .AddMvc()
    .AddViewOptions(options =>
     {
         options.ClientModelValidatorProviders.Add(new 
         IsEvenClientModelValidatorProvider());
     });

IsEvenClientModelValidatorProvider属性的代码如下:

public sealed class IsEvenClientModelValidatorProvider : IClientModelValidatorProvider
{
    public void CreateValidators(ClientValidatorProviderContext context)
    {
        if (context.ModelMetadata.ModelType == typeof(string)
            || context.ModelMetadata.ModelType == typeof(int)
            || context.ModelMetadata.ModelType == typeof(uint)
            || context.ModelMetadata.ModelType == typeof(long)
            || context.ModelMetadata.ModelType == typeof(ulong)
            || context.ModelMetadata.ModelType == typeof(short)
            || context.ModelMetadata.ModelType == typeof(ushort)
            || context.ModelMetadata.ModelType == typeof(float)
            || context.ModelMetadata.ModelType == typeof(double))
        {
            if (context.ModelMetadata.ValidatorMetadata.
            OfType<IsEvenAttribute>().Any())
            {
                if (!context.Results.Any(x => x.Validator is 
                IsEvenClientModelValidator))                        
                {
                    context.Results.Add(new ClientValidatorItem
                    {
                        Validator = new IsEvenClientModelValidator(),
                        IsReusable = true
                    });
                }
            }
        }
    }
}

这需要一些解释。调用CreateValidators基础结构方法,为客户机模型验证程序提供程序提供添加自定义验证程序的机会。如果当前正在检查的属性(context.ModelMetadata)属于受支持的类型(context.ModelMetadata.ModelType)、数字或字符串之一,并且同时包含IsEvenAttribute属性且不包含任何IsEvenClientModelValidator属性,我们将以包含IsEvenClientModelValidator属性的ClientValidatorItem形式向 validators 集合(context.Results中添加一个属性,可以安全地重复使用(IsReusable,因为它不保持任何状态。

现在,让我们看看IsEvenClientModelValidator属性是什么样子的:

public sealed class IsEvenClientModelValidator : IClientModelValidator
{
    public void AddValidation(ClientModelValidationContext context)
    {
        context.Attributes["data-val"] = true.ToString().
        ToLowerInvariant();
        context.Attributes["data-val-iseven"] = this.GetErrorMessage
        (context);
    }

    private string GetErrorMessage(ClientModelValidationContext context)
    {
        var attr = context
            .ModelMetadata
            .ValidatorMetadata
            .OfType<IsEvenAttribute>()
            .SingleOrDefault();

        var msg = attr.FormatErrorMessage(context.
        ModelMetadata.PropertyName);

        return msg;
    }
}

它的工作原理如下:

  1. 将向用于编辑模型属性的 HTML 元素添加两个属性:
    • data-val:这意味着该元素应该被验证。
    • data-val-iseven:元素无效时用于iseven规则的错误消息。
  2. IsEvenAttribute属性的FormatErrorMessage方法检索错误消息。我们知道有IsEvenAttribute;否则,我们就不会在这里了。

最后,我们需要以某种方式添加 JavaScript 验证代码,可能在一个单独的.js文件中:

(function ($) {
  var $jQval = $.validator;
  $jQval.addMethod('iseven', function (value, element, params) {
    if (!value) {
      return true;
    }

    value = parseFloat($.trim(value));

    if (!value) {
      return true;
    }

    var isEven = (value % 2) === 0;
      return isEven;
    });

    var adapters = $jQval.unobtrusive.adapters;
    adapters.addBool('iseven');
})(jQuery);

我们在这里所做的是在iseven名称下注册一个自定义 jQuery 验证函数,该函数在启动时检查值是否为空,并尝试将其转换为浮点数(这适用于整数和浮点数)。最后,它检查该值是否为偶数并适当返回。不用说,这个验证函数是由不引人注目的验证框架自动连接的,因此您不必担心它没有验证。

The error message is displayed in both the element-specific error message label and in the error message summary if it is present in the view.

您可能会发现这个过程有点复杂,在这种情况下,您会很高兴知道您可以将 validation 属性和IClientModelValidator实现添加到一起;它将同样工作,这是可能的,因为包含了DefaultClientModelValidatorProvider属性。然而,由于单一责任原则SRP)和关注点分离SoC),因此建议将其分开。

在本节中,我们了解了如何编写在客户端或服务器端工作的自定义验证器。现在,让我们看看如何实现 AJAX 体验。

使用 AJAX 进行验证

AJAX 是一个很久以前创造的术语,用来表示现代浏览器的一个特性,通过它,可以通过 JavaScript 或浏览器完成异步 HTTP 请求,而无需重新加载整个页面。

ASP.NET Core 不提供对 AJAX 的任何支持,这并不意味着您不能使用它,只是您需要手动操作。

下面的示例使用 jQuery 检索表单中的值,并将它们发送给操作方法。确保 jQuery 库包含在视图文件或布局中:

<form>
  <fieldset>
    <div><label for="name">Name: </label></div>
    <div><input type="text" name="name" id="name" />
    <div><label for="email">Email: </label></div>
    <div><input type="email" name="email" id="email" />
    <div><label for="gender">Gender: </label></div>
    <div><select name="gender" id="gender">
    <option>Female</option>
    <option>Male</option>
    </select></div>
 </fieldset>
</form>
<script>

$('#submit').click(function(evt) {
  evt.preventDefault();

  var payload = $('form').serialize();

  $.ajax({
    url: '@Url.Action("Save", "Repository")',
    type: 'POST',
    data: payload,
    success: function (result) {
      //success
    },
    error: function (error) {
      //error
    }
    });
  });

</script>

本节 JavaScript 代码执行以下操作:

  • 将单击事件处理程序绑定到 ID 为submit的 HTML 元素。
  • 序列化所有的form元素。
  • 在名为RepositoryController的控制器中,创建对名为Save的控制器操作的POSTAJAX 请求。
  • 如果 AJAX 调用成功,则调用success函数;否则,将调用一个error函数。

The URL to the controller action is generated by the Action method. It is important not to have it hardcoded but to instead rely on this HTML helper to return the proper URL.

现在让我们看看如何使用内置机制执行 AJAX 风格的验证。

验证

其中一个包含的验证属性[Remote]使用 AJAX 透明地在服务器端执行验证。当应用于模型的属性时,它采用一个控制器和一个必须引用现有控制器操作的action参数:

[Remote(action: "CheckEmailExists", controller: "Validation")]
public string Email { get; set; }

此控制器操作必须具有与此类似的结构,当然减去操作的参数:

[AcceptVerbs("Get", "Post")]
public IActionResult CheckEmailExists(string email)
{
    if (this._repository.CheckEmailExists(email))
    {
        return this.Json(false);
    }

    return this.Json(true);
}

本质上,如果验证成功,它必须返回 JSON 格式的值true,否则返回false

This validation can not only be used for a simple property of a primitive type (such as string) but also for any POCO class.

实施限制

在 ASP.NET MVC 的早期(预核心)版本中,有一个属性[AjaxOnly],可用于限制只能由 AJAX 调用的操作。虽然它不再存在,但通过编写一个资源过滤器很容易将其恢复,如下所示:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AjaxOnlyAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        if (context.HttpContext.Request.Headers["X-Requested-With"]
        != "XMLHttpRequest")
        {
            context.Result = new StatusCodeResult
            ((int)HttpStatusCode.NotFound);
        }
    }
}

此属性实现资源过滤器接口IResourceFilter,这将在第 10 章理解过滤器中讨论,基本上,它所做的是检查是否存在特定标头(X-Requested-With),如果其值为XMLHttpRequest,则表示当前请求正在由 AJAX 执行。如果不是,则设置响应结果,从而使任何其他可能的过滤器短路。要应用它,只需将其放置在要限制的操作旁边:

[AjaxOnly]
public IActionResult AjaxOnly(Model model) { ... }

For an overview of AJAX and the XMLHttpRequest object, please see https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest.

在此之后,我们继续学习如何从 AJAX 返回内容。

从 AJAX 返回内容

根据最佳实践,AJAX 端点应该返回数据;在现代世界,当涉及到 web 应用时,这些数据通常是 JSON 格式的。因此,您最有可能使用JsonResult类将内容返回到客户机代码。至于向服务器发送数据,如果您使用 jQuery,它将为您处理所有事情,并且可以正常工作。否则,您将需要将数据序列化为适当的格式,可能还有 JSON。设置适当的内容类型标题,然后退出。

上传文件

文件上传是一个过程,在这个过程中,我们将文件从计算机发送到一个运行 ASP.NET Core 的服务器。在 HTTP 中上载文件需要两件事:

  • 你必须使用POST动词。
  • 必须在表单上设置multipart/form-data编码。

就 ASP.NET Core 而言,包含的模型绑定器知道如何将任何发布的文件绑定到IFormFile对象(或对象集合)。例如,采取如下形式:

@using (Html.BeginForm("SaveForm", "Repository", FormMethod.Post, 
    new { enctype = "multipart/form-data" }))
{
    <input type="file" name="file" />
   <input type="submit" value="Save"/>
}

您可以使用以下操作方法检索文件:

[HttpPost("[controller]/[action]")]
public IActionResult SaveForm(IFormFile file)
{
 var length = file.Length;
 var name = file.Name;
 //do something with the file
 return this.View();
}

但是,HTML 文件上传规范(https://www.w3.org/TR/2010/WD-html-markup-20101019/input.file.html 还提到了使用multiple属性一次提交多个文件的可能性。在这种情况下,您可以将参数声明为IFormFile实例数组(集合也可以使用):

public IActionResult SaveForm(IFormFile[] file) { ... }

IFormFile界面为您提供了操作这些文件所需的一切:

  • ContentTypestring:投递文件的内容类型
  • ContentDispositionstring:包含 HTML 输入名称和所选文件名的内部内容处置头
  • HeadersIHeaderDictionary:随文件发送的任何头文件
  • Lengthlong:已发布文件的长度,以字节为单位

  • Namestring:发起文件上传的输入元素的 HTML 名称

  • FileNamestring:保存发布文件的文件系统中的临时文件名

通过使用CopyToCopyToAsync,您可以轻松地将发布文件的内容作为字节数组从Stream源复制到另一个源。OpenReadStream允许您查看实际文件内容。

默认的文件上传机制使用文件系统中的临时文件,但是您可以推出您的机制。有关更多信息,请参阅 Microsoft 发布的以下帖子:

https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads

直接访问提交的文件

还可以直接访问HttpContext.Request.Form.Files集合。此集合的原型为IFormFileCollection,它公开了一个IFormFile集合。

本章将结束介绍如何使用文件。大多数复杂的应用都需要这一点,因此了解这一点很有用。

总结

本章涉及来自用户的数据以及因此需要验证的数据;否则,即使格式不正确,也可能提交无效信息。读完本章后,您应该能够设计一个表单来接收复杂的数据结构并对其进行验证。

对于验证,如果需要,您可能应该坚持使用数据注释属性和IValidatableObject实现。这些 API 在大量其他.NET API 中使用,几乎是验证的标准。

最好实现客户端验证和 AJAX,因为它提供了更好的用户体验,但千万不要忘记在服务器端进行验证!

可能不需要定制模型活页夹,因为包含的活页夹似乎涵盖了大多数情况。

显示和编辑器模板非常方便,因此您应该尝试使用它们,因为它可能会减少每次需要添加的代码,特别是如果您希望重用它的话。

在本章中,我们了解了如何使用模型,为模型生成 HTML,包括在前端和后端使用模板验证模型,查看验证错误消息,以及如何将模型与 HTML 表单元素绑定。

在下一章中,我们将讨论一个完全不同的主题!

问题

您应该能够回答这些问题,答案见评估部分:

  1. 默认的验证提供程序是什么?
  2. 我们将用于呈现 HTML 字段的方法称为什么?
  3. 什么是模型元数据?
  4. ASP.NET Core 是否支持客户端验证?
  5. 可以绑定到上传文件的基本接口是什么?
  6. 什么是不引人注目的验证?
  7. 我们如何执行服务器端验证?