通过实例模拟ASP.NET MVC的Model绑定机制:简单类型+复杂类型
2012-05-30 09:18
1031 查看
[code] public class DefaultModelBinder
{
public IValueProvider ValueProvider{ get; private set;}
public DefaultModelBinder(IValueProvider valueProvider)
{
this.ValueProvider = valueProvider;
}
public IEnumerable<object> GetParameterValues(ActionDescriptor actionDescriptor)
{
foreach (ParameterDescriptor parameterDescriptor in actionDescriptor.GetParameters())
{
string prefix = parameterDescriptor.BindingInfo.Prefix ?? parameterDescriptor.ParameterName;
yield return GetParameterValue(parameterDescriptor, prefix);
}
}
public object GetParameterValue(ParameterDescriptor parameterDescriptor, string prefix)
{
object parameterValue = BindModel(parameterDescriptor.ParameterType, prefix);
if (null == parameterValue && string.IsNullOrEmpty(parameterDescriptor.BindingInfo.Prefix))
{
parameterValue = BindModel( parameterDescriptor.ParameterType, "");
}
return parameterValue ?? parameterDescriptor.DefaultValue;
}
public object BindModel(Type parameterType, string prefix)
{
if (!this.ValueProvider.ContainsPrefix(prefix))
{
return null;
}
return this.ValueProvider.GetValue(prefix).ConvertTo(parameterType);
}
}
[/code]
[/code]
方法GetParameterValues根据指定的用于描述Action方法的ActionDescriptor获取最终执行该方法的所有参数值。在该方法中,我们通过调用ActionDescriptor的GetParameters方法得到用于描述其参数的所有ParameterDescriptor对象,并将每一个ParameterDescriptor作为参数调用GetParameterValue方法得到具体某个参数的值。GetParameterValue除了接受一个类型为ParameterDescriptor的参数外,还接受一个用于表示前缀的字符串参数。如果通过ParameterDescriptor的BindingInfo属性表示的ParameterBindingInfo对象具有前缀,则采用该前缀;否则采用参数名称作为前缀。
对于GetParameterValue方法来说,它又通过调用另一个将参数类型作为参数的BindModel方法来提供具体的参数值,BindModel方法同样接受一个表示前缀的字符串作为其第二个参数。GetParameterValue最初将通过ParameterDescriptor获取到的参数值和前缀作为参数调用BindModel方法,如果返回值为Null并且参数并没有显示执行前缀,会传入一个空字符串作为前缀再一次调用BindModel方法,这实际上模拟了之前提到过的去除前缀的后备Model绑定机制(针对于ModelBindingContext的FallbackToEmptyPrefix属性)。如果最终得到的对象不为Null,则将其作为参数值返回;否则返回参数的默认值。
BindModel方法的逻辑非常简单。先将传入的前缀作为参数调用ValueProvider的ContainsPrefix方法判断当前的ValueProvider保持的数据是否具有该前缀。如果返回之为False,直接返回Null,否则以此前缀作为Key调用GetValue方法得到一个ValueProviderResult调用,并最终调用ConvertTo方法转换为参数类型并返回。
为了验证我们自定义的DefaultModelBinder能够真正地用于针对简单参数类型的Model绑定没我们将它应用到一个具体的ASP.NET MVC应用中。在通过Visual Studio的ASP.NET MVC项目模板创建的空Web应用中,我们创建了如下一个默认的HomeController。HomeController具有一个ModelBinder属性,其类型正是我们自定义的DefaultModelBinder,该属性通过方法GetValueProvider提供。
[code] [code] public class HomeController : Controller
{
public DefaultModelBinder ModelBinder{ get; private set;}
public HomeController()
{
this.ModelBinder = new DefaultModelBinder(GetValueProvider());
}
private void InvokeAction(string actionName)
{
ControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController));
ReflectedActionDescriptor actionDescriptor = (ReflectedActionDescriptor)controllerDescriptor
.FindAction(ControllerContext, actionName);
actionDescriptor.MethodInfo.Invoke(this,this.ModelBinder.GetParameterValues(actionDescriptor).ToArray());
}
public void Index()
{
InvokeAction("Action");
}
private IValueProvider GetValueProvider()
{
NameValueCollection requestData = new NameValueCollection();
requestData.Add("foo", "abc");
requestData.Add("bar", "123");
requestData.Add("baz", "123.45");
return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
}
public void Action(string foo, [Bind(Prefix="baz")]double bar)
{
Response.Write(string.Format("{0}:{1}<br/>", "foo", foo));
Response.Write(string.Format("{0}:{1}<br/>", "bar", bar));
}
}
[/code]
[/code]
InvokeAction方法用于执行指定的Action方法。在该方法中我们先根据当前Controller的类型创建一个ControllerDescriptor对象,并通过调其FindAction方法得到用于描述指定Action方法的ActionDescriptor对象。通过之前的介绍我们知道这是一个ReflectedActionDescriptor对象,所以我们将其转化成ReflectedActionDescriptor类型得到Action方法对应的MethodInfo对象。最后调用DefaultModelBinder的GetParameterValues方法得到目标Action方法所有的参数,将其传入MethodInfo的Invoke方法以反射的形式对指定的Action方法进行执行。
默认的Action方法Index中我们通过执行InvokeAction方法来执行定义在HomeController的Action方法。通过上面的代码片断可以看出,该方法的两个参数foo和bar均为简单类型(string和double),在参数bar上还应用了BindAttribute并指定了相应的前缀(“baz”)。在该Action方法中,我们将两个参数值呈现出来。
而在用于提供ValueProvider的GetValueProvider方法返回的是一个NameValueCollectionValueProvider对象。作为数据源的NameValueCollection对象包含三个名称为foo、bar和baz的数据(abc、123、123.45),我们可以将它们看成是Post的标单输入元素。
当我们运行该程序的时候会在浏览器中得到如下的输出结果。我们可以看到目标Action方法的两个参数值均通过我们自定义的DefaultModelBinder得到了有效的绑定。而实际上参数值的提供最终是通过ValueProvider实现的,它在默认的情况下会根据参数名称进行匹配(foo参数),如果参数应用BindAttribute并显式指定了前缀,则会按照这个前缀进行匹配(bar参数)。
[code] [code] foo: abc
bar: 123.45
[/code]
[/code]
二、复杂类型
对于简单类型的参数来说,由于支持与字符串类型之间的转换,相应ValueProvider可以直接从数据源中提取相应的数据并直接转换成参数类型。所以针对简单类型的Model绑定是一步到位的过程,但是针对复杂类型的Model绑定就没有这么简单了。复杂对象可以表示为一个树形层次化结构,其对象本身和属性代表相应的节点,叶子节点代表简单数据类型属性。而ValueProvider采用的数据源是一个扁平的数据结构,它通过基于属性名称前缀的Key实现与这个对象树中对应叶子节点的映射。[code] [code] public class Contact
{
public string Name{ get; set;}
public string PhoneNo{ get; set;}
public string EmailAddress{ get; set;}
public Address Address{ get; set;}
}
public class Address
{
public string Province{ get; set;}
public string City{ get; set;}
public string District{ get; set;}
public string Street{ get; set;}
}
[/code]
[/code]
以上面定于得这个Contact类型为例,它具有三个简单类型的属性(Name、PhoneNo和EmailAddress)和复杂类型Address的属性;而Address属性具有四个简单类型的属性。一个Contact对象的数据结构可以通过如下图所示的树来表示,这个树种的所有叶子节点均为简单类型。如果我们需要通过一个ValueProvider来构建一个完整的Contact对象,它必须能够提供所有所有叶子节点的数值,而ValueProvider通过基于属性名称前缀的Key实现与对应的叶子节点的映射。
实际上当我们调用HtmlHelper<TModel>的模板方法EditorFor/EditorForModel的时候就是按照这样的匹配方式对标单元素进行命名的。假设在将Contact作为Model类型的强类型View中,我们通过调用HtmlHelper<TModel>的扩展方法EditorFor将Model对象的所有信息以编辑的模式呈现出来。
[code] [code] @model Contact
@Html.EditorFor(m => m.Name)
@Html.EditorFor(m => m.PhoneNo)
@Html.EditorFor(m => m.EmailAddress)
@Html.EditorFor(m => m.Address.Province)
@Html.EditorFor(m => m.Address.City)
@Html.EditorFor(m => m.Address.District)
@Html.EditorFor(m => m.Address.Street)
[/code]
[/code]
下面的代码片断代表了作为Model对象的Contact在最终呈现出来的View中代表的HTML,我们可以清楚地看到这些<input>表单元素完全是根据属性名称和类型层次结构进行命名的。随便提一下,对于基于提交表单的Model绑定来说,作为匹配的是表单元素的name属性而非id属性,所以这里的命名指的是name属性而非id属性。
[code] [code] <input id="Name" name="Name" type="text" ... />
<input id="PhoneNo" name="PhoneNo" type="text" ... />
<input id="EmailAddress" name="EmailAddress" type="text" ... />
<input id="Address_Province" name="Address.Province" type="text" ... />
<input id="Address_City" name="Address.City" type="text" ... />
<input id="Address_District" name="Address.District" type="text" ... />
<input id="Address_Street" name="Address.Street" type="text"... />
[/code]
[/code]
对于用于模拟默认Model绑定机制的自定义DefaultModelBinder来说,我们仅仅提供了针对简单类型的绑定,现在我们对其进行完善是之可以提供对复杂类型的Model绑定。如下面的代码片断所示,在BindModel方法中我们创建了一个基于参数类型的ModelMetadata对象,并根据其IsComplexType属性判断参数类型是否为复杂类型。
[code][code] public class DefaultModelBinder
{
//其他成员
public object BindModel(Type parameterType, string prefix)
{
if (!this.ValueProvider.ContainsPrefix(prefix))
{
return null;
}
ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => null, parameterType);
if (!modelMetadata.IsComplexType)
{
return this.ValueProvider.GetValue(prefix).ConvertTo(parameterType);
}
object model = CreateModel(parameterType);
foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(parameterType))
{
string key = string.IsNullOrEmpty(prefix) ? property.Name : prefix + "." + property.Name;
property.SetValue(model, BindModel(property.PropertyType, key));
}
return model;
}
private object CreateModel(Type modelType)
{
Type type = modelType;
if (modelType.IsGenericType)
{
Type genericTypeDefinition = modelType.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof(IDictionary<,>))
{
type = typeof(Dictionary<,>).MakeGenericType(modelType.GetGenericArguments());
}
else if (((genericTypeDefinition == typeof(IEnumerable<>)) || (genericTypeDefinition == typeof(ICollection<>))) ||(genericTypeDefinition == typeof(IList<>)))
{
type = typeof(List<>).MakeGenericType(
modelType.GetGenericArguments());
}
}
return Activator.CreateInstance(type);
}
}
[/code]
[/code]
如果参数为复杂类型,则通过调用CreateModel方法以反射的方式创建Model对象。CreateModel方法会被用于后面我们会介绍的基于集合和字典的Model绑定,所以我们这里还针对泛型的IDictionary<,>、IEnumerable<>、ICollection<>和IList<>类型作了相应地处理。具体来说,如果参数类型为IDictionary<,>,则创建一个Dictionary<,>对象,而对后三者则创建一个List<>对象,具体的泛型参数根据参数类型获取。对于一般的类型,我们直接通过Activator的CreateInstance方法根据参数类型创建相应的Model对象。
通过CreateModel方法创建的是针对参数类型的“空”对象,我们需要通过Model绑定对它的相关属性进行初始化。在BindModel方法中,我们遍历参数类型的所有属性,并在现有前缀的基础上加上“.{属性名称}”(如果当前前缀为空,则直接采用属性名称)作为绑定对应属性的前缀递归地调用BindModel方法得到属性值。我们最终通过反射的方式将得到值对属性进行赋值。
现在我们采用我们完善后的DefaultModelBinder来进行针对复杂类型的Model绑定。如下面的代码片断所示,我们对HomeController的Action方法进行了相应的修改使之具有两个Contact类型的参数foo和bar。在Action方法中,我们将这两个参数代表的Contact对象的相关信息呈现出来。
[code] [code] public class HomeController : Controller
{
public DefaultModelBinder ModelBinder{ get; private set;}
public HomeController()
{
this.ModelBinder = new DefaultModelBinder(GetValueProvider());
}
private IValueProvider GetValueProvider()
{
NameValueCollection requestData = new NameValueCollection();
requestData.Add("Name", "张三");
requestData.Add("PhoneNo", "123456789");
requestData.Add("EmailAddress", "zhangsan@gmail.com");
requestData.Add("Address.Province", "江苏");
requestData.Add("Address.City", "苏州");
requestData.Add("Address.District", "工业园区");
requestData.Add("Address.Street", "星湖街328号");
return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
}
public void Index()
{
InvokeAction("Action");
}
public void Action(Contact foo, Contact bar)
{
Response.Write("Foo<br/>");
Response.Write(string.Format("{0}:{1}<br/>", "Name", foo.Name));
Response.Write(string.Format("{0}:{1}<br/>", "PhoneNo", foo.PhoneNo));
Response.Write(string.Format("{0}:{1}<br/>", "EmailAddress", foo.EmailAddress));
Response.Write(string.Format("{0}:{1}{2}{3}{4}<br/><br/>", "Address",
foo.Address.Province, foo.Address.City, foo.Address.District,
foo.Address.Street));
Response.Write("Bar<br/>");
Response.Write(string.Format("{0}:{1}<br/>", "Name", bar.Name));
Response.Write(string.Format("{0}:{1}<br/>", "PhoneNo", bar.PhoneNo));
Response.Write(string.Format("{0}:{1}<br/>", "EmailAddress", bar.EmailAddress));
Response.Write(string.Format("{0}:{1}{2}{3}{4}<br/>", "Address",
bar.Address.Province, bar.Address.City, bar.Address.District,
bar.Address.Street));
}
}
[/code]
[/code]
通过GetValueProvider方法提供的依然是一个NameValueCollectionValueProvider对象,我们将一个Contact对象包含的信息包含在它对应的NameValueCollection对象中。对于添加到NameValueCollection中的针对Contact对象的某个属性的数据条目,我们按照上面介绍的匹配规则对其命名。运行我们的实例程序,我们会在浏览器中得到如下所示的输出结果,我们从中可以看到Action方法的两个参数foo和bar通过我们自定义的DefaultModelBinder进行了正确地绑定,并且它们具有相同的值。
[code] [code] Foo
Name: 张三
PhoneNo: 123456789
EmailAddress: zhangsan@gmail.com
Address: 江苏 苏州 工业园区 星湖街328号
Bar
Name: 张三
PhoneNo: 123456789
EmailAddress: zhangsan@gmail.com
Address: 江苏 苏州 工业园区 星湖街328号
[/code]
[/code]
之所以同一个Action方法中两个相同类型的参数会绑定相同的数据,使缘于之前介绍的去除前缀的后备Model绑定机制。由于请求数据中并不包含针对某个参数的前缀,所以在针对参数名称作为前缀的Model绑定失败的情况下,后备Model绑定会前缀为空字符串的情况下再次进行。
[code] [code] public class HomeController : Controller
{
//其他成员
private IValueProvider GetValueProvider()
{
NameValueCollection requestData = new NameValueCollection();
requestData.Add("foo.Name", "Foo");
requestData.Add("foo.PhoneNo", "123456789");
requestData.Add("foo.EmailAddress", "Foo@gmail.com");
requestData.Add("bar.Name", "Bar");
requestData.Add("bar.PhoneNo", "987654321");
requestData.Add("bar.EmailAddress", "Bar@gmail.com");
return new NameValueCollectionValueProvider(requestData, CultureInfo.InvariantCulture);
}
public void Action(Contact foo, Contact bar)
{
Response.Write("Foo<br/>");
Response.Write(string.Format("{0}:{1}<br/>", "Name", foo.Name));
Response.Write(string.Format("{0}:{1}<br/><br/>", "PhoneNo", foo.PhoneNo));
Response.Write(string.Format("{0}:{1}<br/>", "EmailAddress", foo.EmailAddress));
Response.Write("Bar<br/>");
Response.Write(string.Format("{0}:{1}<br/>", "Name", bar.Name));
Response.Write(string.Format("{0}:{1}<br/>", "PhoneNo", bar.PhoneNo));
Response.Write(string.Format("{0}:{1}<br/>", "EmailAddress", bar.EmailAddress));
}
}
[/code]
[/code]
在如上所示的代码中,我们为NameValueCollectionValueProvider设置了基于“foo”和“bar”的前缀的两套数据,目的在为Action方法的foo和bar参数提供不同的数据。运行我们的程序后会在浏览器上得到如下所示的输出结果,可以看出Action方法的两个参数被绑定了不同的值。
[code] [code] Foo
Name: Foo
PhoneNo: 123456789
EmailAddress: Foo@gmail.com
Bar
Name:Bar
PhoneNo: 987654321
EmailAddress: Bar@gmail.com
[/code]
[/code]
通过实例模拟ASP.NET MVC的Model绑定的机制:简单类型+复杂类型
通过实例模拟ASP.NET MVC的Model绑定的机制:数组
通过实例模拟ASP.NET MVC的Model绑定的机制:集合+字典
相关文章推荐
- 通过实例模拟ASP.NET MVC的Model绑定机制:简单类型+复杂类型
- 通过实例模拟ASP.NET MVC的Model绑定机制[上篇]
- 通过实例模拟ASP.NET MVC的Model绑定的机制:集合+字典
- 通过实例模拟ASP.NET MVC的Model绑定机制:数组
- 通过实例模拟ASP.NET MVC的Model绑定机制:数组
- 通过实例模拟ASP.NET MVC的Model绑定的机制:集合+字典
- 如何应用Asp.Net Mvc内建功能(DefaultModelBinder)实现简单类型、复杂类型、集合类型,以及字典类型的自动绑定
- ASP.NET MVC 3 Model【通过一简单实例一步一步的介绍】
- ASP.NET MVC 3 Model【通过一简单实例一步一步的介绍】
- ASP.NET MVC 3 Model【通过一简单实例一步一步的介绍】
- ASP.NET MVC 3 Model【通过一简单实例一步一步的介绍】
- ASP.NET MVC 3 Model【通过一简单实例一步一步的介绍】
- ASP.NET MVC 3 Model【通过一简单实例一步一步的介绍】
- ASP.NET MVC 3 Model【通过一简单实例一步一步的介绍】【续Model验证部分】
- 在ASP.NET MVC中使用Knockout实践02,组合View Model成员、Select绑定、通过构造器创建View Model,扩展View Model方法
- asp.net mvc源码分析-DefaultModelBinder 自定义的普通数据类型的绑定和验证
- ASP.NET MVC Model绑定的简单应用
- asp.net mvc源码分析-DefaultModelBinder 自定义的普通数据类型的绑定和验证
- 【原创】Asp.net MVC学习笔记之-基于类型来绑定Model的属性
- Model绑定机制1:简单类型+复杂类型