Asp.Net MVC中Html.TextBox扩展,其行为也许不是你期望的

用ASP.Net MVC做网站,用的最多的扩展,大概就是Html.TextBox扩展,但在某些特定的情况下,其行为也许不是你所期望的那样。下面我们来看看这种特定的情况吧。

为了说明问题,我们假设有这样一个Modal类型:

    /// <summary>
    /// 我的对象
    /// </summary>
    public class MyObject
    {
        /// <summary>
        /// ID
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
    }

然后你在Controller的Action中获得了该对象的一个实例和一个列表,并通过

ViewData["myObjects"] = myObjects;
return View(myObject);

把实例和这个列表输出到了View中,在View中,你想编辑这个列表中的每个对象,于是,你可能写了类似与下面所示的代码:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyObject>" %>
<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文件中,先看我们上面用到的那个扩展的签名和实现:

public static string TextBox(this HtmlHelper htmlHelper, string name, object value) {
           return TextBox(htmlHelper, name, value, (object)null /* htmlAttributes */);
       }

我们就跟着它走吧,最后一直到

return InputHelper(htmlHelper, InputType.Text, name, value, (value == null) /* useViewData */, false /* isChecked */,
true /* setId */, true /* isExplicitValue */, htmlAttributes);
这儿了。那么我们去看看那个InputHelper函数的实现吧:
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); }
从上面代码中我删除了许多与TextBox设置value值无关的代码,我们不管那些逻辑控制的代码,就看它是如何为input类型为text、hidden的标签设置value的:
string valueParameter = Convert.ToString(value, CultureInfo.CurrentCulture);

首先,上面这行代码把我们设置的任意类型的value转换为string类型,接下来,看default中的两行:

string attemptedValue = (string)htmlHelper.GetModelStateValue(name, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(name) : valueParameter), isExplicitValue);
现在,你看到最关键的代码了,它在做什么?它不管我们是否已经显示的设置了value参数,试图去某个地方取值,而且,取出来的这个值有最高的优先级,如果不是null,它就是<input>标签的value值了,那么这个地方是哪儿呢? 好,我们去看看htmlHelper.GetModelStateValue(name, typeof(string))是怎么取值的呢?
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):

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这个类的一个方法吧:

protected internal ViewResult View(object model) {
            return View(null /* viewName */, null /* masterName */, model);
        }
这个我们常用的方法,调用了
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>标签吧,也不是很麻烦。

备注:本文不讨论文中描述的应用出现的场景。

posted @ 2009-09-13 05:27  cokkiy  阅读(1711)  评论(0编辑  收藏  举报