Request被处理到ActionInvoker时,ActionInvoker找到目标Action,方法列表的参数是怎么传递的? 这就需要理解Model Binding过程了。
看一个普通的action:
public ViewResult Person(
int
id)
{
var myPerson =
new Person();
return View(myPerson);
}
请求http://mydomain.com/Home/Person/1 经过Route系统解析后,执行到Person。id的参数值解析过程为: Request.Form["id"] -> RouteData.Values["id"] -> Request.QueryString["id"] -> Request.Files["id"]。 当然了,这里解析完第RouteData后就找到目标,退出后边的解析了。
看参数的一般解析过程:
执行过程从上到下,找到即中止,直到找到;否则,如果参数为值类型,并且必选,则报错。另外,参数解析过程中,对于简单类型,DefaultModelBinder利用System.ComponentModel.TypeDescriptor类将解析出的字符串转换为目标类型。如果转换失败,model binding失败。
对于上述Person方法,加入对应的view为:
@using ModelBinding.Models
@model Person
@{
ViewBag.Title =
"
Person
";
Layout =
"
~/Views/Shared/_Layout.cshtml
";
}
@{
var myPerson =
new Person() {FirstName =
"
Jane
", LastName =
"
Doe
"};
}
@using (Html.BeginForm()) {
@Html.EditorFor(m => myPerson)
@Html.EditorForModel()
<input type=
"
submit
" value=
"
Submit
" />
}
标黄部分,实际上render了2个Person对象实例。那么它的postback对应的action方法怎么写?
[HttpPost]
public ActionResult Person(Person firstPerson, [Bind(Prefix =
"
myPerson
")] Person secondPerson)
//
Exclude="IsApproved, Role"
//
FirstName,LastName
{
//
do sth.
return Content(
"
Success
");
}
它可以接收2个Person实例参数。其中,第二个参数加了一个Bind设置,Prefix后还可以加入Exclude、Include设置。它们分别表示model所绑定的数据来源前缀、忽略赋值的属性、必须赋值的属性。
如果要传值多个字符串,因为是同类型,可以讲它们作为数组传值么?先看view:
Enter your three favorite movies:
@using (Html.BeginForm()) {
@Html.TextBox(
"
movies
",
"
m1
",
new{id=
"
m1
", name=
"
m1
"})
@Html.TextBox(
"
movies
",
"
m2
",
new { id =
"
m2
", name =
"
m2
" })
@Html.TextBox(
"
movies
",
"
m3
",
new { id =
"
m3
", name=
"
m3
" })
<input type=
"
submit
" />
}
对应的action为:
[HttpPost]
public ActionResult Movies(
string
[] movies)
{
return Content(
"
done
");
}
测试成功。Asp.net MVC Framework貌似足够强大,它model binding系统能够智能识别和提取model参数。看model为多个对象实例的list时:
@model List<ModelBinding.Models.Person>
@{
ViewBag.Title = "PersonList";
Layout = "~/Views/Shared/_Layout.cshtml";
}
@using (Html.BeginForm())
{
for (int i = 0; i < Model.Count; i++)
{
<h4>Person Number: @i</h4>
@:First Name: @Html.EditorFor(m => m[i].FirstName)
@:Last Name: @Html.EditorFor(m => m[i].LastName)
}
<input type="submit"/>
}
对应的action为:
[HttpPost]
public ActionResult PersonList(
List<Person> list)
{
return Content(
"
done
");
}
view中的多个person配置,在页面被postback会server后,就被model binding为List<Person>类型的list了。
如果要手动来触发绑定,改进如下:
[HttpPost]
[ActionName(
"
PersonList
")]
public ActionResult PersonListResult()
{
var list =
new List<Person>();
//
Person myPerson = (Person)DependencyResolver.Current.GetService(typeof(Person));
UpdateModel(list);
return Content(
"
done
");
}
可以看到,参数占位已经被移除,然后在action内部,先创建一个list,然后调用controller基类的UpdateModel方法来更新list。而UpdateModel方法作用就是根据request传回来的值,同步model。基于之前讲述的model binding寻值的4个源,这里可以直接配置数据源。将上面标黄的代码改为:
UpdateModel(list,
new FormValueProvider(ControllerContext));
即可。这时,model数据源直接从Form中获取。
因为FormCollection实现了IValueProvider接口,上面的实现还可以继续改进为:
[HttpPost]
[ActionName(
"
PersonList
")]
public ActionResult PersonListResult(FormCollection formData)
{
var list =
new List<Person>();
//
UpdateModel(list, formData);
if(TryUpdateModel(list, formData))
{
return Content(
"
done
");
}
else
{
//
...provide UI feedback based on ModelState
//
var isValid = ModelState.IsValid;
return View();
}
//
return Content("done");
}
其中,TryUpdateModel方法可以避免binding失败时抛出异常。
利用Model Binding,怎么来上传文件?如下:
<form action=
"
@Url.Action(
"
UploadFile")" method="post" enctype="multipart/form-data">
Upload a photo: <input type=
"
file
" name=
"
photo
" />
<input type=
"
submit
" />
</form>
action的写法:
[HttpPost]
public ActionResult UploadFile(HttpPostedFileBase file)
{
//
Save the file to disk on the server
string filename =
"
myfileName
";
//
... pick a filename
file.SaveAs(filename);
///
/ ... or work with the data directly
//
byte[] uploadedBytes = new byte[file.ContentLength];
//
file.InputStream.Read(uploadedBytes, 0, file.ContentLength);
///
/ Now do something with uploadedBytes
return Content(
"
done
");
}
最后,来自己定义Model Binding系统吧。先基于IValueProvider接口,实现一个:
public
class CurrentTimeValueProvider : IValueProvider
{
public
bool ContainsPrefix(
string prefix)
{
return
string.Compare(
"
CurrentTime
", prefix,
true) ==
0;
}
public ValueProviderResult GetValue(
string key)
{
return ContainsPrefix(key)
?
new ValueProviderResult(DateTime.Now,
null, CultureInfo.InvariantCulture)
:
null;
}
}
创建一个factory:
public
class CurrentTimeValueProviderFactory : ValueProviderFactory
{
public
override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
return
new CurrentTimeValueProvider();
}
}
Application_Start中注册:
ValueProviderFactories.Factories.Insert(
0,
new CurrentTimeValueProviderFactory());
因为它的位置为0,优先被利用。看一个action:
public ActionResult Clock(DateTime currentTime)
{
return Content(
"
The time is
" + currentTime.ToLongTimeString());
}
显然,它需要一个datetime参数,但是通过http://mydomain.com/home/clock/ 可以访问,这就是CurrentTimeValueProvider的功劳。
自定义一个Dependency-Aware(不知道怎么翻译)的ModelBinder:
public
class DIModelBinder : DefaultModelBinder
{
protected
override
object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
return DependencyResolver.Current.GetService(modelType) ??
base.CreateModel(controllerContext, bindingContext, modelType);
}
}
注册使用:
ModelBinders.Binders.DefaultBinder =
new DIModelBinder();
为Person创建一个专用的ModelBinder:
public
class PersonModelBinder : IModelBinder
{
public
object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//
see if there is an existing model to update and create one if not
Person model = (Person)bindingContext.Model ?? (Person)DependencyResolver.Current.GetService(
typeof(Person));
//
find out if the value provider has the required prefix
bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
string searchPrefix = (hasPrefix) ? bindingContext.ModelName +
"
.
" :
"";
//
populate the fields of the model object
model.PersonId =
int.Parse(GetValue(bindingContext, searchPrefix,
"
PersonId
"));
model.FirstName = GetValue(bindingContext, searchPrefix,
"
FirstName
");
model.LastName = GetValue(bindingContext, searchPrefix,
"
LastName
");
model.BirthDate = DateTime.Parse(GetValue(bindingContext, searchPrefix,
"
BirthDate
"));
model.IsApproved = GetCheckedValue(bindingContext, searchPrefix,
"
IsApproved
");
model.Role = (Role)Enum.Parse(
typeof(Role), GetValue(bindingContext, searchPrefix,
"
Role
"));
return model;
}
private
string GetValue(ModelBindingContext context,
string prefix,
string key)
{
ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
return vpr ==
null ?
null : vpr.AttemptedValue;
}
private
bool GetCheckedValue(ModelBindingContext context,
string prefix,
string key)
{
bool result =
false;
ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
if (vpr !=
null)
{
result = (
bool)vpr.ConvertTo(
typeof(
bool));
}
return result;
}
}
注册使用:
ModelBinders.Binders.Add(
typeof(Person),
new PersonModelBinder());
自定义一个ModelBindProvider:
public
class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
return modelType ==
typeof(Person) ?
new PersonModelBinder() :
null;
}
}
注册使用:
ModelBinderProviders.BinderProviders.Add(
new CustomModelBinderProvider());
基于上面的代码,你也可以直接给对象类加上Model Binding:
[ModelBinder(
typeof(PersonModelBinder))]
public class Person
源码download