在开始这个主题之前,我先简要介绍一下如何在ActionMethod中通过Form使用Post的方式进行传递参数。
原生类型参数传递
public ActionResult SimplePost(string number) { ViewData["Title"] = "SimplePost Page"; ViewData["Message"] = "Increase :"; #region Increase SimplePostModel model = new SimplePostModel(); int result; if (!string.IsNullOrEmpty(number)) { if (int.TryParse(number, out result)) { model.SimplePostResult = result; ViewData["number"] = model.Increase(); } else { ViewData["number"] = number; } } else { ViewData["number"] = model.SimplePostResult; } #endregion return View(); }
先看一个简单的示例:
<%@ Page Language="C#" AutoEventWireup="true" MasterPageFile="~/Views/Shared/Site.Master" CodeBehind="SimplePost.aspx.cs" Inherits="MvcAppWarningPostWithHtmlHelper.Views.Home.SimplePost" %> <%@ Import Namespace="MvcAppWarningPostWithHtmlHelper.Models" %> <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server"> <h2> <%= Html.Encode(ViewData["Message"]) %>h2> <% using (Html.BeginForm("SimplePost", "Home", "post")) {%> <input type="text" name="number" value="<%= ViewData["number"] %>" /> <br /> <input type="submit" value="Increase" title="Update Form"/> <%} %> asp:Content>
该示例通过在页面放置一个Form再用一个Submit进行提交,在Form中,通过向Controller SimplePost发送Form表单,在public ActionResult SimplePost(string number) 的参数中我们将获得从客户端传回的number输入框的值,由此我们就可以展开后续的工作了,具体的设置用法等,大家可以参考以上代码。
那么我们是否仅限于传递类似string这样的简单类型了呢?答案当然是否定的,如果仅限于此就完全没有多少值得探讨的地方了。
下面我们用一个自定义的类型来看看我们是如何完成这个任务的。
首先在页面上放置了一个表单,并在Submit按钮点击后,向服务端提交表单,并将数据整理成一个User对象,而不是几个字符串,然后再从服务端输出到Submit按钮下方的空白处,返回回来。
我们的HTML一定是一组文本串,这一点毋庸置疑,在将表单提交回服务端的时候,服务端最常用的手段就可以根据传回来的值获得用户输入的数据,但是服务端并不知道如何将这些数据组织成我们所需要的复合对象,而这一点就需要我们有所作为。在MVC框架中,默认为我们提供了DefaultModelBinder,这个类继承自IModelBinder接口,目的只有一个,就是将客户端的数据组合成我们需要的Model的类型。因为是复杂类型,具体该怎么组合,程序并不知道,因此我们引入自己定义的ModelBinder,这个ModelBinder继承自IModelBinder,目的旨在一个BindModel方法上,我们在方法内通过传递的参数得到ModelBindingContext,然后从提交的数据中进行分析,就可以随意组合成我们自己的复杂对象了。(详细代码请下载后查阅)而就表单提交而言,则是通过Form的方式从页面中获取对应已知元素的值,这些值即是我们所要捕获的数据。
但是一定有朋友会质疑,那我的系统有无数的Model类型,那么我不是也要很多很多的ModelBinder类了吗?当然也不是,因为系统既然有DefaultModelBinder,它肯定不是仅为一种特殊的类服务的。DefaultModelBinder采用了反射的方式,通过分析我们的ActionMethod的参数名,通过我们的指定的参数名和相应类型,它可以在“可查询的值范围”内查找相应的值。而这里“可查询的值范围”默认就是DefalultValueProvider,DefaultValueProvider会从RouteData,QueryString,Form中查询,查询优先级也是依照这个,可以看出Url的优先级优于表单。
通过IValueProvider接口我们不难看出(只有一个方法),它通过键值的方式进行取值,值被保存在ValueProviderResult中返回。因此我们可以简单地认为只要能够在所提供的ValueProvider中获取到相应的值,并且符合对应的类型(类型转换将由ModelBinder来完成),即可完成相应的供值任务。而在取到值之后则由ModelBinder进行组装最后将这些值返回给我们的ActionMethod即完成了自定义转换值的任务。
上图是ControllerActionInvoker中InvokeAction中的一段方法,其中的代码
IDictionary<string, object> parameters = GetParameterValues(methodInfo);
就是为了将ActionMethod的参数转换为一个键值对,而这里的键就是我们的参数名,而值则是对应类型的值,感兴趣的朋友可以在这里设置断点自行跟踪。
已知元素
在上文我用下划线标注了一个“已知元素”,我们知道我们通过Form方式进行取值,我们必须知道页面元素的名字,但是我们使用DefaultModelBinder的时候,也就是我们没有对复合类型指定任何的自定义ModelBinder的时候,我们从未提供任何的值用于指定我们的元素对应规则,那么框架是如何为我们提供取值的呢?
通过反射的方式,利用我们提供的ActionMethod的参数名,在Form中寻找对应的名字以及它的属性。虽然我们从未提供对应属性的值,但是根据反射,即可获得对应属性名,再依照约定将这些名字组合成对应的Form Key,即可从Form中获取相应的元素值了。具体对应规则则为:
假设我们的User对象定义为如下形式
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
而我们的参数名为User user,那么在所提交的表单中,name=”user.Name”, name=”user.Age”的元素的value将会对应user.Name和user.Age的属性值。而所有那些复杂的对应组合关系就统统交给DefaultModelBinder去帮忙分析好了,只要类型不够简单,它就会继续递归向下直到所有的类型都满足不可递归为止。
虽然我们总是诟病于反射的性能,但是就易用性而言,这种透明的方式显然看起来更友好。友好吗?其实不然。如果我们不了解转换的实质,我们就可能掉进那些特殊的例子中,这一个个的陷阱都只能让我们木讷,但还好我们现在好像看清了本质。
既然我们说它是用递归的方式对参数类型以及参数名进行了分析和递归,那么就必然牵涉到递归的终点问题,递归啥时候停?刚才说到都是简单类型的时候就会停,但是如果出现了循环引用的情况,就必将导致程序的崩溃。这里我们就必须提供自己的ModelBinder用来做这些比复杂更复杂的转换,而即便你再懒也需要懂得如何避开它。其实实现一个CustomModelBinder很容易,因为只有一个方法,唯一的不好就是它增加了我们管理项目文件的成本,在一个大型系统中,这样的管理甚至有点可怕,不过即便如此这个死穴我们也得保护起来,总不能让错误运行在我们的视线范围吧?
给User加一个配偶,这样User就涵盖了另一个User,因为“配偶”本身就是个循环引用,所以我们的实体也是个天然的循环引用体。按照我们的分析,DefaultModelBinder对这种实体天生就有抵触情绪。下面这个类就是我们增加用来自定义ModelBinder的一个示例。
成员
namespace MvcAppWarningPostWithHtmlHelper.Models { public class UserModelBinder : IModelBinder { public UserModelBinder() { NameUniqueID = "user$Name"; AgeUniqueID = "user$Age"; SpouseNameUnique = "userSpouse$Name"; SpouseAgeUnique = "userSpouse$Age"; } private string NameUniqueID { get; set; } private string AgeUniqueID { get; set; } private string SpouseNameUnique { get; set; } private string SpouseAgeUnique { get; set; } private User UserConvert(string name, string age, User spouse) { int iAge = 0; int.TryParse(age, out iAge); if (spouse != null && spouse.Spouse == null) { spouse.Spouse = new User(name, iAge, spouse); } return new User(name, iAge, spouse); } #region IModelBinder 成员 public ModelBinderResult BindModel(ModelBindingContext bindingContext) { HttpRequestBase request = bindingContext.HttpContext.Request; if (request.Form != null && request.Form.HasKeys()) { return new ModelBinderResult(UserConvert(request.Form.GetValues(NameUniqueID)[0], request.Form.GetValues(AgeUniqueID)[0], UserConvert(request.Form.GetValues(SpouseNameUnique)[0], request.Form.GetValues(SpouseAgeUnique)[0], null))); } return null; } #endregion } }