ASP.NET MVC的Controller有个很不错的方法:UpdataModel (相对应的还有TryUpdateModel)。它能够把提交的数据(Form, QueryString, RouteData)自动更新到实体,例如:
如果提交的数据键值与Customer的属性相对应,就可以实现对Customer的属性进行更新。而一般在ASP.NET WebForm中,我们可能要写上很多类似这样的语句(在实际开发中每个页面可不止两三个赋值语句):
据我所知,有不少企业早就利用反射开发出像UpdateModel的方法,基本上符合中小型项目的需要,毕竟对于由UpdateModel带来的一点性能损失,远远比不上由于没有UpdateModel而导致需要写一堆赋值语句所带来的损失。但是,绝大多数企业的程序员还是一直在写这样的赋值语句,即使已经受尽了这种类似“机器人在打字”的折磨(因为可以夸张的说,实在看不出当写这些赋值语句时需要调用人类大脑哪些神经,所以说类似“机器人在打字”,大脑处于麻木状态),或许有些程序员喜欢这样,也乐此不疲,那么我只能说“佩服”。我是一个很很很“懒惰”的人,不喜欢干些类似“重复”性的活,我总是想着法子怎么写出通用的代码去让我能够在以后可以“偷懒”,就算是写一个小小的代码生成工具也好。 刚才说过“有不少企业早就利用反射开发出像UpdateModel的方法”,遗憾的是直到现在,我都是耳闻而没有真正看到WebForm下类似MVC的UpdateModel(注:本文说提及的WebForm 和MVC 都是ASP.NET的范畴,而UpdateModel虽然实际上只是Controller里的一个方法,但本文它还作为数据绑定机制的代名词,只是不想打太多文绉绉的名称,以防自己都看不懂)。
前面已经提到,UpdateModel其本质无非是把提交的数据(NameValueCollection 类型)转化成实体,相信每个用过反射的人都知道怎么写。但是,想写好可不简单。这里的“好”是至少包含以下几点:可扩展性(是否可以通过override每个方法,或者继承某个基类或接口来实现修改数据绑定的机制),泛化(是否可以支持复杂类型的实体,例如泛型集合),健壮性等等。
说了这么多废话,其实是想引出下面的话题: 既然MVC已经有一个优秀的UpdateModel,而且它同样是ASP.NET下的好孩子,何不“借”来给WebForm用用?说借就借,MVC挺大方的,还再三叮嘱:当初没想到老兄会对我的UpdateModel感兴趣啊,所以都是写成protected internal的访问形式了,您觉得没问题就改改凑合凑合吧。于是MVC随手掏出以下的C#文件:
没有IController这个接口,Controller怎么“活”呀?没错,WebForm只是想借“UpdateModel”,可没打算背着其他多余的东西,所以凡是与UpdateModel无关的东西都被干掉了,包括Controller后面一大堆的接口。另外,还需要清除以下的东东(如果需要添加代码肯定把我累倒,幸好只是做清除动作,暗喜中…):
1. ControllerBase 去掉IController接口,当然把与之相关的Execute,ExecuteCore方法去掉,甚至去掉Initialize方法;TempData,ViewData等等都不要了(题外话:其实TempData也是挺不错的,但是WebForm什么场景下能用到呢,苦想半天脑海中也只有零零星星几个场景,暂忽略之)。
2. ControllerContext 只保留HttpContext 和 Controller
3. Controller 最惨, 几乎减掉一半重量,只剩下Binders ,ModelState 和UpdateModel, TryUpdateModel 的一堆重载方法(如果你不需要记录数据绑定错误而带来的异常,可把ModelState也请走)。
4. 其他的改动已经忘了,印象中改动较大的就是把与RouteData, ViewData 相关的东东全部请走。
万事俱备,只欠东风。谁是东风呢?其实是为了让WebForm 中调用UpdateModel的写法与MVC一致,这里“东风”就是“扩展方法”!
(经过一番测试, UpdateModel如果由空值string.Empty向数字或日期类型转化,基本上100%都是抛异常的,但这不影响Model其他属性的更新,你也可以用TryUpdateModel)
我们应该是时候写一个Hello UpdateModel 来测试一番了:
(上一篇文章里,不少朋友抱怨我的VS 的IDE样式太难看,大家先凑合看一眼,反正都是一些无关紧要的代码)
后台测试代码:
最后点击Submit按钮成功输出:Name: Bruce Lee, Age: 123
而UpdateModel的其他重载方法都可以像MVC那样正常使用,至于QueryString更新到Model的例子,有兴趣的朋友可以自己试试,这里不一一列举。
这里想引出另外一个问题: 在WebForm里,有多少人在对控件命名时是不写前缀的呢?即实际开发中,一般控件都类似这样写:“txtName”,“txtAge”, UpdateModel可以应付吗?可能有些朋友开始失望了,也有甚者可能会说:很多人都明白MVC不该用WebForm的服务端控件,那么同样WebForm也不要去想着从MVC那里借鉴什么了。暂时来说前者我非常认可,后者我可不觉得有什么不可借鉴的。如果因为这个前缀而让UpdateModel就此划上句号的话,那么就不会是我写这篇文章的初衷。
俗话说,入乡随俗。MVC的UpdateModel来到WebForm的世界就要做点变通,顺应WebForm的一贯做法。以上提到,我对好代码的其中一个观点是易于扩展。前面的代码已经被我“砍”得“惨不忍睹”了,不想再为此而大动手术,我们就来个扩展好不好?(注:并非指.Net扩展方法,大家可以理解为拓展)
ValueProviderDictionary里有一个方法PopulateDictionary,其主要作用就是把Form,QueryString等数据统一添加到内置的字典里,而作为UpdateModel的ValueProvider。聪明的你可能已经想到,由于控件ID作为字典的key, 所以只要改动此方法,在添加数据到内置字典前去掉前缀再作为key不就成功了么?因为ValueProviderDictionary是作为默认的ValueProvider, 我不想改动它,所以另外写了一个ValueProvider: PrefixableValueProvider,其意思是:支持前缀的ValueProvider。
首先定义可能出现的前缀枚举类型:1. None,2. Camel,3. Three。1 和 3 容易理解,这里说说Camel,从字面上用Camel可能不是很妥当,详细请参考。这里是指控件的前缀都是小写字母,而随后的英文单词首字母大写。例如:txtName,lblCustomer,这是多数企业使用的控件命名规范。
PrefixableValueProvider同样实现“IDictionary<string,ValueProviderResult>”接口,里面只有PopulateDictionary方法与ValueProviderDictionary不同。 对于Camel规则,因为前缀都是小写字母,所以只要找到第一个非小写字母,就从这个字母开始截取字符串作为key即可。下面给出了一个简单的实现代码,大家可以完全不用看,以免被我的低效率和考虑不周全的代码给误导。
上面的代码考虑到这样的场景:当控件ID有下面几个:“Name”, “txtName”…,如果“Name”先被加入,而当后面发现有“txtName”,那么就会移除“Name”而使用“txtName”的值;如果先发现“txtName”,而后面不管有叫“Name”的控件还是叫“Name”的查询字符串都不会覆盖前者。
除此之外,PrefixableValueProvider百分之九十九的代码都是跟ValueProviderDictionary一样。可能有人问,这不煞费苦心吗?我也想继承ValueProviderDictionary,而override它的PopulateDictionary方法呀,谁知道它原来是private的。不过等真正用于实际开发时,我肯定把private换成protected virtual,然后在PrefixableValueProvider中override它。
瞎扯的太远差点忘了说怎么使用这个PrefixableValueProvider了,其实就是要指定Controller的ValueProvider是PrefixableValueProvider就可以:
把前面例子的控件ID改成“txtName”和“txtAge”测试就可以了,这里不详细写了。
任何圣人都有缺点,何况只是一个UpdateModel。它的缺点是:
1. 由于控件ID需要与Model符合某种约束,所以在为控件命名时就不能那么“自由”了。
2. 当页面要提交的数据跟Model不是那么一一对应时,可能需要另外处理。
上面的贴图可能真的由于IDE的样式和图片压缩的原因而惨不忍睹了,可以下载本文完整的源代码:UpdateModel1.0.rar