Mvc 模型
Mvc 模型
模型绑定概念
- 官方文档:https://learn.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-7.0
- 模型绑定系统:
- 从各种源(如 Route 、Form 和 QueryString)中检索数据。
- 将数据提供给 Controller 和 Razor Pages 的方法参数(与 Action 形参名称对应)和公共属性(与模型属性名称对应)。
- 将字符串数据转换为 .NET 基本类型或者更新模型类的属性(简单类型)
- 模型类的属性的为自定义类型或不能直接有字符串转换时需要自定义模型绑定器
使用方法
创建模型类
为模型类绑定属性
视图中使用
asp-for
绑定属性<!-- Id为需要绑定的属性名称 --> <input id="id" asp-for="Id"/>
在模型类中或者在
Action
中指定可以绑定属性【二选一】[HttpPost] // 从表单接收需要使用 Post public IActionResult Next([Bind("Id")]MyViewModel vm) { // 接收到的 vm 只有 Id 属性是从视图传来的,其余属性为默认值 // 模型类必须具有无参构造函数,mvc框架通过无参函数创建实例,再给实例对应属性赋值 /* other code */ }
[BindProperties] // 将绑定模型中所有的公共属性 public class MyViewModel { [BindProperty] // 绑定当前属性() public long Id { get; set; } [BindNever] // 即使在 Action 或 [BindProperties] 指定了绑定,也永远不会绑定该属性 public OptTag? MySelect { get; set; } }
Controller/Action
中接收绑定的模型或数据
注意事项
默认情况下,不会绑定 HTTP GET 请求的属性。
使用
SupportsGet
属性支持 GET 请求[BindProperty(Name = "ai_user", SupportsGet = true)]
默认绑定器只能绑定可以使用 TypeConverter 或
TryParse
方法从单个字符串转换为该属性的简单类型。对于无法由字符串转换而来或者需要 多个字符串源 的属性类型,需要自定义转化器
简单类型包括:
Boolean ,Byte , SByte ,Char ,Enum ,Guid ,Single DateOnly ,DateTime ,DateTimeOffset ,TimeOnly ,TimeSpan Decimal ,Double ,Int16, Int32, Int64 ,UInt16, UInt32, UInt64 Uri ,Version
TryParse
数据类:定义静态方法
TryParse
public class DateRange { public DateRange(string from, string to) { /* code */ } public static bool TryParse(string? value, out DateRangeTP? result) { // value 传入的需要转换的数据 // result 转换的结果 // return 转换是否成功 } public static bool TryParse(string value, IFormatProvider provider, T out result) { /* code */ } }
Action 接收参数
// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022 public IActionResult ByRange([FromQuery] DateRange range) {/* code */}
默认绑定复杂类型属性
模型或控制器中标记该复杂属性为可绑定的
视图页使用
[前缀].[属性名]
,如果没有找到,则继续查找不含前缀的属性如果直接绑定到 Action 参数,则前缀为
参数名
:asp-for="instructorToUpdate.ID"
public IActionResult OnPost(int? id, Instructor instructorToUpdate)
如果绑定到模型属性,则前缀为模型的属性名
<input id="chapter" asp-for="Question.Chapter" value="@Model.Question.Chapter" />
自定义前缀:
[Bind(Prefix = "Instructor")] Instructor instructorToUpdate
集合与字典
对于是简单类型集合或字典的目标,模型绑定将查找视图页或 QueryString 中同名参数的数据封装到集合中。
对于字典,通过
[keyName]
指定键名:selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
记录类型
支持记录类型的绑定和验证,需要满足以下条件:
- 只有一个公共构造函数
- 具有同名属性和同类型属性的参数。
- 名称不区分大小写。
记录类型使用参数上的验证元数据和绑定元数据,忽略属性上的同名元数据,但使用属性时会通过属性读取值。
public record Person(string Name) { private readonly string _name; [BindProperty(Name = "SomeName")] // This does not get used [Required] // This does not get used public string Name { get; init => _name = value ?? string.Empty; } // Name 永远不为 null }
绑定没有无参数构造函数的模型
- 模型绑定要求复杂类型具有无参数构造函数
- 基于
System.Text.Json
和Newtonsoft.Json
的输入格式支持不具有无参数构造函数的类绑定。
绑定格式化数据源
框架自带 Json 格式数据源解析
对于 xml 格式数据
- 需要添加解析器
builder.Services.AddControllers().AddXmlSerializerFormatters();
- 控制器或操作方法使用
[Consumes("application/xml")]
- 需要添加解析器
自定义 Json 解析
创建转换器:
internal class ObjectConverter : JsonConverter<T>
为模型应用转换器:
[JsonConverter(typeof(ObjectConverter ))] public record ObjectId(int Id);
自定义模型绑定
通常自定义类型不应于字符串与特定类型的转换,而应选择用 TypeConverter 来完成此操作。
创建自定义绑定器,实现
IModelBinder
接口public class QuestionModerBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext ctx) { // 安全性检查,其余检查略 if (ctx == null) throw new ArgumentNullException(nameof(ctx)); // ctx.ModelName 为 ModelBinder 指定的 Name // 与视图组件的 name 属性与 ctx.ModelName 一致的数据将被封装到 ValueProviderResult 中 var valueProviderResult = ctx.ValueProvider.GetValue(ctx.ModelName); // valueProviderResult.Values 为数据集合 // 获取数据进行处理,并创建模型实例 var param = valueProviderResult.Values[0]; var model = new MyViewModel(param); // 将模型实例设置到绑定器结果中 ctx.Result = ModelBindingResult.Success(model); return Task.CompletedTask; } }
视图页面表单中指定需要绑定的数据
<!-- name 属性作为源数据识别的名字,与指定绑定器的特性中的 Name 一致 --> <input name="qt" value="@Model.Question.A.Text" /> <input name="qt" value="@Model.Question.B.Text" />
【方案一】使用特性为模型指定自定义绑定器
// Name 属性作为识别数据源的依据 // BinderType 用于指定自定义绑定器 public IActionResult Next([ModelBinder(BinderType = typeof(QuestionModerBinder), Name = "qt")]Question question) { /*...*/ } // 特性也可以用于模型或模型中的特定属性 [ModelBinder(BinderType = typeof(AuthorBinder))] public class Author { public int Id { get; set; } [ModelBinder(BinderType = typeof(AuthorNameBinder), Name = "AName")] public string Name { get; set; } }
【方案二】使用
ModelBinderProvider
为指定类型提供绑定器- 这是内置框架绑定器的实现方式
- 不需要应用
[ModelBinder]
特性就能实现
public class AuthorEntityBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); // 检查目标类型,返回对应的自定义绑定器 if (context.Metadata.ModelType == typeof(Author)) return new BinderTypeModelBinder(typeof(AuthorEntityBinder)); return null; } }
// 注册服务 services.AddControllers(options => options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider()));
模型验证
- 验证特性可以设置验证规则以及错误消息
- 控制器和视图会自动选取验证规则对数据进行验证
ModelState.IsValid
- 如果前端启用 JavaScript 验证(默认),如果数据不通过则不会提交表单到后台
- 如果前端禁言 JavaScript 验证,提交到后端的数据仍然会在
ModelState.IsValid
位置进行验证,不通过会重新返回此表单
内置验证特性
[ValidateNever]
指示应从验证中排除属性或参数[EmailAddress]
验证属性是否具有电子邮件格式[PhoneAttribute]
指定数据字段值是格式标准的电话号码[Url]
验证 属性是否具有 URL 格式[Compare]
验证模型中的两个属性是否匹配[Required]
属性必须有值,对于有默认值的值类型也可是使用,有利于提示数据有效性[MinLength]
设置数组或字符串最小长度[StringLength]
设置字符串属性的长度[RegularExpression]
使用正则表达式匹配[Range]
将值限制在指定范围内[Remote]
调用服务端方法进行验证[Remote(action: "VerifyEmail", controller: "Users")]
[DataType]
特性System.ComponentModel.DataAnnotations
命名空间还提供格式特性DataType
不提供任何验证- 应用程序还可通过
DataType
特性自动提供类型特定的功能,比如:日期选择器、区域适用的货币符号、创建mailto:
链接等
jQuery 验证不适用于
Range
属性的DateTime
,比如[Range(typeof(DateTime), "1/1/1966", "1/1/2020")]
验证始终失败
自定义验证
自定义特性
继承自 ValidationAttribute 类,重写 IsValid 方法
public class ClassicMovieAttribute : ValidationAttribute { public int Year { get; } public ClassicMovieAttribute(int year) => Year = year; protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { // 获取被验证的实例 var movie = (Movie)validationContext.ObjectInstance; // 验证不通过返回错误信息 if (movie.Genre == Genre.Classic && ((DateTime)value!).Year > Year) return new ValidationResult($"year must later than {Year}"); // 验证通过返回成功 return ValidationResult.Success; } }
使用 IValidatableObject
模型类中实现 IValidatableObject 接口
public class ValidatableMovie : IValidatableObject { /* other code */ public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (/* 验证不通过的条件 */) { yield return new ValidationResult( "错误信息", new[] { "nameof(验证不通过的属性名)" }); } } }
客户端验证
客户端验证将阻止提交,直到表单变为有效为止。
框架模板的
_ValidationScriptsPartial.cshtml
已经自动引入了 jQuery Unobtrusive Validation 验证脚本在需要验证的视图中引入验证脚本文件
_ValidationScriptsPartial.cshtml
@section scripts{ @{await Html.RenderPartialAsync("_ValidationScriptsPartial"); } }
标记帮助程序 和 HTML 帮助程序 将自动使用模型属性中的验证特性和类型元数据,生成对应的 HTML 5
data-
特性对数据进行校验。同时需要在服务器也进行验证,因为如果客户端禁用了验证,请求会跳过客户端验证直接到控制器
[HttpPost] [ValidateAntiForgeryToken] // 检查验证 token public IActionResult Next(ExerciseViewModel vm) { if(ModelState.IsValid) { /* 验证通过后的逻辑 */ } }