转载自:http://msdn.microsoft.com/zh-cn/magazine/hh781022.aspx
虽然模型绑定看起来很简单,但实际上是一个相对较复杂的框架,由许多共同创建和填充控制器操作所需对象的部件组成。
同时,你还会看到一些经常被忽视的模型绑定技术,并了解如何避免一些最常见的模型绑定错误。
模型绑定基础知识
为了了解什么是模型绑定,让我们首先看看从 ASP.NET 应用程序请求值填充对象的传统方法,如图 1 所示。
图 1 从请求直接检索值
public ActionResult Create()
{
var product = new Product() {
AvailabilityDate = DateTime.Parse(Request[ "availabilityDate"]),
CategoryId = Int32.Parse(Request["categoryId"]),
Description = Request[ "description"],
Kind = (ProductKind)Enum.Parse( typeof(ProductKind),
Request[ "kind"]),
Name = Request[ "name"],
UnitPrice = Decimal.Parse(Request[ "unitPrice"]),
UnitsInStock = Int32.Parse(Request["unitsInStock"]),
};
// ...
}
然后将图 1 和图 2 中的操作进行对比,图 2 利用模型绑定生成相同的结果。
图 2 原始值的模型绑定
public ActionResult Create(
DateTime availabilityDate, int categoryId,
string description, ProductKind kind, string name,
decimal unitPrice, int unitsInStock
)
{
var product = new Product() {
AvailabilityDate = availabilityDate,
CategoryId = categoryId,
Description = description,
Kind = kind,
Name = name,
UnitPrice = unitPrice,
UnitsInStock = unitsInStock,
};
// ...
}
使用模型绑定,控制器操作可以专注于提供业务值,并避免在普通请求映射和解析上浪费时间。
复杂对象的绑定
幸运的是,ASP.NET MVC 可以处理原始类型和复杂类型。
下面的代码在 Create 操作中多执行了一次传递,跳过原始值并直接绑定到 Product 类:
public ActionResult Create(Product product)
{
// ...
}
该代码说明了模型绑定的实际强大功能。
分解模型绑定
既然你已经了解操作中的模型绑定,现在是时候分解构成模型绑定框架的组件了。
这些步骤分别由值提供程序和模型绑定程序来完成。
值提供程序
在运行时,ASP.NET MVC 使用 ValueProviderFactories 类中注册的值提供程序计算模型绑定程序可以使用的请求值。
默认情况下,值提供程序集合按下面的顺序计算来自各种源的值:
- 以前绑定的操作参数(当该操作为子操作时)
- 表单字段 (Request.Form)
- JSON 请求主体中的属性值 (Request.InputStream),但仅当该请求为 AJAX 请求时
- 路由数据 (RouteData.Values)
- 查询字符串参数 (Request.QueryString)
- 已发布文件 (Request.Files)
你甚至可以创建自己的自定义值提供程序。
自定义值提供程序
创建自定义值提供程序的最低要求非常简单: 创建实现 System.Web.Mvc.ValueProviderFactory 接口的新类。
例如,图 3 演示了从用户的 cookie 中检索值的自定义值提供程序。
图 3 检查 Cookie 值的自定义值提供程序工厂
public class CookieValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider
(
ControllerContext controllerContext
)
{
var cookies = controllerContext.HttpContext.Request.Cookies;
var cookieValues = new NameValueCollection();
foreach (var key in cookies.AllKeys)
{
cookieValues.Add(key, cookies[key].Value);
}
return new NameValueCollectionValueProvider(
cookieValues, CultureInfo.CurrentCulture);
}
}
CookieValueProviderFactory 简单地检索用户的 cookie 并利用 NameValueCollectionValueProvider 将这些值向模型绑定框架公开,而不是从头构建一个全新的值提供程序。
在创建自定义值提供程序之后,你需要通过 ValueProviderFactories.Factories 集合将其添加到值提供程序的列表中:
var factory = new CookieValueProviderFactory();
ValueProviderFactories.Factories.Add(factory);
ASP.NET MVC 提供的现有值提供程序集能够很好地公开 HttpRequest 中大多数可用数据(可能 cookie 除外),并且通常提供足够的数据以满足大多数情况的需要。
要确定新建值提供程序对于特定情况来说是否是正确的,请回答以下问题: 现有值提供程序提供的信息集是否包括我需要的所有数据(但可能采用错误格式)?
本文的其余部分介绍如何执行此操作。
负责使用值提供程序提供的值创建和填充模型的 ASP.NET MVC 模型绑定框架主要组件被称为模型绑定程序。
默认模型绑定程序
它通过对目标模型的各个属性使用相对较简单的递归逻辑来实现该目的:
- 例如,名称为 UnitPrice.Amount 的表单字段包含 UnitPrice 属性的 Amount 字段的值。
- 从属性名称的注册值提供程序获取 ValueProviderResult。
- 默认的转换逻辑利用属性的 TypeConverter 从字符串类型的源值转换为目标类型。
- 但如果属性为复杂类型,则执行递归绑定。
递归模型绑定
使用此方法,DefaultModelBinder 能够遍历整个复杂对象图表,甚至填充深度嵌套的属性值。
图 4 显示两个类。
图 4 带复杂 Unitprice 属性的 Product 类
public class Product
{
public DateTime AvailabilityDate { get; set; }
public int CategoryId { get; set; }
public string Description { get; set; }
public ProductKind Kind { get; set; }
public string Name { get; set; }
public Currency UnitPrice { get; set; }
public int UnitsInStock { get; set; }
}
public class Currency
{
public float Amount { get; set; }
public string Code { get; set; }
}
执行此更新时,模型绑定程序将查找名为 UnitPrice.Amount 和 UnitPrice.Code 的值以便填充复杂的 Product.UnitPrice 属性。
到目前为止,你见过了位于对象层次结构的一个层级深度的复杂对象,DefaultModelBinder 可以轻松处理此对象。 要演示递归模型绑定的实际强大功能,请将名为 Child 的新属性添加到相同类型的 Product:
public class Product {
public Product Child { get; set; }
// ...
}
然后,将新字段添加到表单(应用点表示法指示每一层),根据所需层数创建层。 例如:
<input type="text" name="Child.Child.Child.Child.Child.Child.Name"/>
当绑定程序完成所有工作后,它将创建一个与图 5 中代码相似的对象图表。
图 5 从递归模型绑定创建的对象图表
new Product {
Child = new Product {
Child = new Product {
Child = new Product {
Child = new Product {
Child = new Product {
Child = new Product {
Name = "MADNESS!"
}
}
}
}
}
}
}
如果你可以创建表单字段名表示要填充的值,通过使用递归模型绑定,不管该值位于对象层次结构中的哪个位置,模型绑定程序都会找到并绑定它。
模型绑定不适用的情况
然而,还存在默认模型绑定逻辑看似无法运行、但实际上只要使用得当仍可正常运行的情况,并且这种情况相当普遍。
下面提供了开发人员往往认为 DefaultModelBinder 无法处理的一些最常见的情况,并说明了如何仅使用 DefaultModelBinder 来实现这些情况。
复杂集合 现有的 ASP.NET MVC 值提供程序将所有请求字段名称视为表单发布值对待。 例如,表单发布中的原始值集合,其中每个值都需要其自己的唯一索引(添加了空格以增强可读性):
MyCollection[0]=one &
MyCollection[ 1]=two &
MyCollection[ 2]=three
相同方法还可应用于复杂对象集合。 要演示此功能,请通过将 UnitPrice 属性更改为 Currency 对象集合来更新 Product 类以便支持多种货币:
public class Product : IProduct
{
public IEnumerable<Currency> UnitPrice { get; set; }
// ...
}
更改之后,需要下面的请求参数来填充更新后的 UnitPrice 属性:
UnitPrice[0].Code=USD &
UnitPrice[ 0].Amount=100.00 &
UnitPrice[1].Code=EUR &
UnitPrice[1].Amount=73.64
请记住,模型绑定程序要求属性名称遵循表单发布命名语法,而不管请求是 GET 还是 POST。
例如,前面的 UnitPrice 集合的 JSON 负载。 用于该数据的纯 JSON 数组语法应表示为:
[
{ "Code": "USD", "Amount": 100.00 },
{ "Code": "EUR", "Amount": 73.64 }
]
但是,默认值提供程序和模型绑定程序要求将数据表示为 JSON 表单发布:
{
"UnitPrice[0].Code": "USD",
"UnitPrice[0].Amount": 100.00,
"UnitPrice[1].Code": "EUR",
"UnitPrice[1].Amount": 73.64
}
然而,在你了解了相对较简单的语法来发布复杂集合之后,处理这些情况要容易得多。
发生这些情况时,许多开发人员抓住机会使用模型绑定框架的可扩展性模型,并构建其自己的自定义模型绑定程序。
同样,更新 Create 控制器操作以使其接受新的 IProduct 接口,而不是具体的 Product 实现,如图 6 所示。
图 6 绑定到接口
public interface IProduct
{
DateTime AvailabilityDate { get; }
int CategoryId { get; }
string Description { get; }
ProductKind Kind { get; }
string Name { get; }
decimal UnitPrice { get; }
int UnitsInStock { get; }
}
public ActionResult Create(IProduct product)
{
// ...
}
尽管图 6 中显示的更新后的 Create 操作是完全合法的 C# 代码,但会导致 DefaultModelBinder 引发异常: “无法创建接口的实例。”由于 DefaultModelBinder 无法得知要创建的 IProduct 的具体类型,因此模型绑定程序引发此异常完全合乎情理。
图 7 显示了 ProductModelBinder,即知道如何创建和绑定 IProduct 接口实例的自定义模型绑定程序。
图 7 ProductModelBinder(紧密耦合的自定义模型绑定程序)
public class ProductModelBinder : IModelBinder
{
public object BindModel
(
ControllerContext controllerContext,
ModelBindingContext bindingContext
)
{
var product = new Product() {
Description = GetValue(bindingContext, "Description"),
Name = GetValue(bindingContext, "Name"),
};
string availabilityDateValue =
GetValue(bindingContext, "AvailabilityDate");
if(availabilityDateValue != null)
{
DateTime date;
if (DateTime.TryParse(availabilityDateValue, out date))
product.AvailabilityDate = date;
}
string categoryIdValue =
GetValue(bindingContext, "CategoryId");
if (categoryIdValue != null)
{
int categoryId;
if (Int32.TryParse(categoryIdValue, out categoryId))
product.CategoryId = categoryId;
}
// Repeat custom binding code for every property
// ...
return product;
}
private string GetValue(
ModelBindingContext bindingContext, string key)
{
var result = bindingContext.ValueProvider.GetValue(key);
return (result == null) ?
null : result.AttemptedValue;
}
}
此外,这些自定义绑定程序的常见问题还包括:侧重于特定模型类、在框架和业务层之间创建紧密耦合,以及限制重复使用以支持其他模型类型。
此方法通常可以为两个领域的提供最佳功能。
请考虑更高级别的目标: 针对非具体类型开发控制器操作并动态确定每个请求的具体类型的功能。
图 8 显示通用模型抽象模型绑定程序的示例。
图 8 通用抽象模型绑定程序
public class AbstractModelBinder : DefaultModelBinder
{
private readonly string _typeNameKey;
public AbstractModelBinder(string typeNameKey = null)
{
_typeNameKey = typeNameKey ?? "
__type__";
}
public override object BindModel
(
ControllerContext controllerContext,
ModelBindingContext bindingContext
)
{
var providerResult =
bindingContext.ValueProvider.GetValue(_typeNameKey);
if (providerResult != null)
{
var modelTypeName = providerResult.AttemptedValue;
var modelType =
BuildManager.GetReferencedAssemblies()
.Cast<Assembly>()
.SelectMany(x => x.GetExportedTypes())
.Where(type => !type.IsInterface)
.Where(type => !type.IsAbstract)
.Where(bindingContext.ModelType.IsAssignableFrom)
.FirstOrDefault(type =>
string.Equals(type.Name, modelTypeName,
StringComparison.OrdinalIgnoreCase));
if (modelType != null)
{
var metaData =
ModelMetadataProviders.Current
.GetMetadataForType(null, modelType);
bindingContext.ModelMetadata = metaData;
}
}
// Fall back to default model binding behavior
return base.BindModel(controllerContext, bindingContext);
}
}
例如,该键可以定义为路由的一部分(在路由数据中),指定为查询字符串参数,或者甚至表示为表单发布数据中的字段。
AbstractModelBinder 使用该新的元数据取代描述初始抽象模型类型的现有 ModelMetadata 属性,有效地导致模型绑定程序忘记它在一开始曾绑定到非具体类型。
在 AbstractModelBinder 使用绑定到正确模型所需的所有信息取代模型元数据后,它会完全将控制权交还给基本的 DefaultModelBinder 逻辑以使其处理其余工作。
AbstractModelBinder 是一个很好的示例,演示了如何通过直接从 IModelBinder 接口派生,使用你自己的自定义逻辑扩展默认绑定逻辑,而不需要重复进行基本的工作。
模型绑定程序选择
大多数教程都向你演示了两种注册自定义模型绑定程序的方法。
全局 ModelBinders 集合覆盖特定类型的模型绑定程序的一般推荐方法是将类型到绑定程序的映射注册到 ModelBinders.Binders 字典。
下面的代码段通知框架使用 AbstractModelBinder 绑定 Currency 模型:
- ModelBinders.Binders.Add(typeof(Currency), new AbstractModelBinder());
覆盖默认模型绑定程序或者,你也可以通过将模型绑定程序分配给 ModelBinders.Binders.DefaultBinder 属性来替换全局默认处理程序。 例如:
ModelBinders.Binders.DefaultBinder = new AbstractModelBinder();
尽管这两种方法适用于许多情况,但是 ASP.NET MVC 还允许你使用其他方法为类型注册模型绑定程序: 属性和提供程序。
使用自定义属性修饰模型
图 9 显示创建 AbstractModelBinder 的 CustomModelBinderAttribute 实现。
图 9 CustomModelBinderAttribute 实现
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Enum |
AttributeTargets.Interface | AttributeTargets.Parameter |
AttributeTargets.Struct | AttributeTargets.Property,
AllowMultiple = false, Inherited = false
)]
public class AbstractModelBinderAttribute : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new AbstractModelBinder();
}
}
然后,你可以将 AbstractModelBinderAttribute 应用到任何模型类或属性,例如:
public class Product
{
[AbstractModelBinder]
public IEnumerable<CurrencyRequest> UnitPrice { get; set; }
// ...
}
现在,当模型绑定程序尝试为 Product.UnitPrice 查找合适的绑定程序时,它将发现 AbstractModelBinderAttribute 并使用 AbstractModelBinder 绑定 Product.UnitPrice 属性。
此外,自定义模型绑定程序属性可以应用于所有类和单个属性的事实意味着你可以精确控制模型绑定过程。
问绑定程序!
因此,他们在用于单个模型类型的显式模型绑定程序注册、基于属性的静态注册和用于所有类型的已设定默认模型绑定程序之间提供了绝佳的中间地带。
下面的代码演示了如何创建为所有端口和抽象类型提供 AbstractModelBinder 的 IModelBinderProvider:
public class AbstractModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
if (modelType.IsAbstract || modelType.IsInterface)
return new AbstractModelBinder();
return null;
}
}
如果该类型是一个具体类型,AbstractModelBinder 不适用;请返回空值以指示模型绑定程序与该类型不匹配。
实现 .GetBinder 逻辑时需要记住的重要一点是将针对作为模型绑定候选项的所有属性执行该逻辑,所以请务必精简该逻辑,否则很容易为你的应用程序带来性能问题。
为了开始使用模型绑定程序提供程序,将其添加到在 ModelBinderProviders.BinderProviders 集合中维护的提供程序列表。 例如,按如下方式注册 AbstractModelBinder:
var provider = new AbstractModelBinderProvider();
ModelBinderProviders.BinderProviders.Add(provider);
这非常简单,你已经在整个应用程序中为非具体类型添加了模型绑定支持。
模型绑定方法将确定适当的模型绑定程序的任务从框架转移到最合适的位置 — 模型绑定程序本身,从而使模型绑定选择 更具有动态性。
关键扩展点
此外,模型绑定分离填充对象的逻辑和使用填充对象的逻辑,从而有助于更好地分离问题。
花时间了解 ASP.NET MVC 模型绑定以及如何正确使用它可以对所有应用程序(甚至是最简单的应用程序)带来重大影响。
Jess Chadwick 是一名专攻 Web 技术的独立软件顾问。 他具有 10 多年的开发经验,其涉及范围从新兴企业的嵌入式设备到财富 500 强公司的企业级 Web 场。 他是一名 ASP 专家、微软 ASP.NET 最佳职员,以及书籍和杂志作者。 他积极参与开发社团,经常在用户组和会议中发言,并领导 NJDOTNET 新泽西州中部 .NET 用户组。
衷心感谢以下技术专家对本文进行了审阅:Phil Haack
谢谢浏览!