用ASP.Net MVC做网站,用的最多的扩展,大概就是Html.TextBox扩展,但在某些特定的情况下,其行为也许不是你所期望的那样。下面我们来看看这种特定的情况吧。
为了说明问题,我们假设有这样一个Modal类型:
</>code
- /// <summary>
- /// 我的对象
- /// </summary>
- public class MyObject
- {
- /// <summary>
- /// ID
- /// </summary>
- public int Id { get; set; }
- /// <summary>
- /// 名称
- /// </summary>
- public string Name { get; set; }
- }
然后你在Controller的Action中获得了该对象的一个实例和一个列表,并通过
</>code
- ViewData["myObjects"] = myObjects;
- return View(myObject);
把实例和这个列表输出到了View中,在View中,你想编辑这个列表中的每个对象,于是,你可能写了类似与下面所示的代码:
</>code
- <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyObject>" %>
</>code
- <div class="clearfix">
- <div class="nameDiv">ID:</div>
- <div class="contentDiv"><%=Html.TextBox("id",Model.Id) %></div>
- </div>
- <div class="clearfix">
- <div class="nameDiv">名称:</div>
- <div class="contentDiv"><%=Html.TextArea("id",Model.Name) %></div>
- </div>
- </div>
<div class="clearfix">- <%foreach (var item in myObjects)
- {%>
- <div class="clearfix">
- <div class="nameDiv">ID:</div>
- <div class="contentDiv"><%=Html.TextBox("id",item.Id) %></div>
- </div>
- <div class="clearfix">
- <div class="nameDiv">名称:</div>
- <div class="contentDiv"><%=Html.TextBox("name",item.Name) %></div>
- </div>
- <%} %>
- </div>
如果你像我这样,在这种循环中,使用了Html.TextBox,Html.Hidden或任何input类型的扩展,以及Html.TextArea扩展,那么恭喜你,你会看到不希望看到的结果。
会看到什么结果呢?除了第一个MyObject输出了它的值以外,剩下的值也都变成了第一个对象的值。也即,假如你第一个对象的Name=”MyObject1”,那么你除了在第一个“名称”输入框内看到“MyObject1”以外,剩下的所有“名称”输入框内看到还是“MyObject1”。
为什么会这样呢?我们去看看Html.TextBox扩展的源代码吧!(我下的是RC版的代码,但问题在正式版V1中也一样)。
源代码在System.Web.Mvc项目的Mvc/Html文件夹的InputExtensions文件中,先看我们上面用到的那个扩展的签名和实现:
</>code
- public static string TextBox(this HtmlHelper htmlHelper, string name, object value) {
- return TextBox(htmlHelper, name, value, (object)null /* htmlAttributes */);
- }
我们就跟着它走吧,最后一直到
这儿了。那么我们去看看那个InputHelper函数的实现吧:</>code
- return InputHelper(htmlHelper, InputType.Text, name, value, (value == null) /* useViewData */, false /* isChecked */,
true /* setId */, true /* isExplicitValue */, htmlAttributes);
从上面代码中我删除了许多与TextBox设置value值无关的代码,我们不管那些逻辑控制的代码,就看它是如何为input类型为text、hidden的标签设置value的:</>code
- private static string InputHelper(this HtmlHelper htmlHelper, InputType inputType, string name, object value, bool useViewData,
bool isChecked, bool setId, bool isExplicitValue, IDictionary<string, object> htmlAttributes) {- if (String.IsNullOrEmpty(name)) {
- throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
- }
- TagBuilder tagBuilder = new TagBuilder("input");
- //略去属性name,type等
- string valueParameter = Convert.ToString(value, CultureInfo.CurrentCulture);
- switch (inputType) {
//case InputType.CheckBox://case InputType.Radio://case InputType.Password:- default:
- string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
- tagBuilder.MergeAttribute("value",
attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);- break;
- }
- if (setId) {
- tagBuilder.GenerateId(name);
- }
- return tagBuilder.ToString(TagRenderMode.SelfClosing);
- }
</>code
- string valueParameter = Convert.ToString(value, CultureInfo.CurrentCulture);
首先,上面这行代码把我们设置的任意类型的value转换为string类型,接下来,看default中的两行:
现在,你看到最关键的代码了,它在做什么?它不管我们是否已经显示的设置了value参数,试图去某个地方取值,而且,取出来的这个值有最高的优先级,如果不是null,它就是<input>标签的value值了,那么这个地方是哪儿呢? 好,我们去看看htmlHelper.GetModelStateValue(name, typeof(string))是怎么取值的呢?</>code
- string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
- tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
</>code
- internal object GetModelStateValue(string key, Type destinationType) {
- ModelState modelState;
- if (ViewData.ModelState.TryGetValue(key, out modelState)) {
- return modelState.Value.ConvertTo(destinationType, null /* culture */);
- }
- return null;
- }
我们看到,它是从htmlHelper.ViewData.ModelState中取值的,好,我们先看看ViewData.ModelState.TryGetValue(key, out modelState):
</>code
- public bool TryGetValue(string key, out ModelState value) {
- return _innerDictionary.TryGetValue(key, out value);
- }
现在,我们看到它是从一个内部词典中根据我们输入的key取值的,那这个内部词典的值是从哪儿来的呢?也就是htmlHelper.ViewData.ModelState是何时被创建,并构造它的内部词典呢?
我们知道,我们的每个View都有一个Html属性,而且我们用的还很顺手,这个东东就是htmlHelper,那么,我们的每个View怎么会有这个属性呢?因为每个View都是直接或间接从ViewPage<TModel>或从ViewPage 类型中继承来的。那么,我们就去看ViewPage<TModel>是怎么创建和初始化htmlHelper的吧。那么,View又是谁负责创建的呢?我们只是在Controller的一个方法中返回了一个ViewResult的实例。要讲清楚这个问题,我们需要把整个MVC模式讲一次,我们只是简单的看看Controller这个类的一个方法吧:
这个我们常用的方法,调用了</>code
- protected internal ViewResult View(object model) {
- return View(null /* viewName */, null /* masterName */, model);
- }
</>code
- protected internal virtual ViewResult View(string viewName, string masterName, object model) {
- if (model != null) {
- ViewData.Model = model;
- }
- return new ViewResult {
- ViewName = viewName,
- MasterName = masterName,
- ViewData = ViewData,
- TempData = TempData
- };
- }
我们看到,我们设置的model被赋给了ViewData.Model,那么是不是用这个构造我们上面看到的htmlHelper.ViewData.ModelState的内部词典的呢?我想是的。到这里,你应该明白了,为什么html.TextBox会表现的和我们预期的不一样,因为它始终想从内部ViewData中取值,而忽略我们显式设置的值。
在这种情况下,要么自己写一个TextBox扩展(也不难,是不是?),或者就用<input>标签吧,也不是很麻烦。
备注:本文不讨论文中描述的应用出现的场景。
如对本文有疑问,请提交到交流论坛,广大热心网友会为你解答!! 点击进入论坛