前天,我在开发中碰到一个奇怪的问题。和往常一样,我在视图中这样写:
@Html.TextBoxFor(m => m.LoginName)
因为Login模型是有Required标注的,那么输出的HTML理当带有对应的data-val-required属性,从而启用客户端验证。但在页面上却没有验证信息,查看HTML源码发现,期望的属性并未生成。这是为什么呢?
我的第一个反应是去确认ClientValidationEnabled和UnobtrusiveJavaScriptEnabled设置是否被关闭了,可是并没有。我从来未曾、也没有理由关闭这些设置。那么问题在哪呢?
求之不得,只好请问google了。Stackoverflow上的一个类似的问题引起了我的关注,帖中提到原因可能是因为控件不在FormContext的上下文中。这提醒了我,我的form是手写的html,像这样:
<form action="@Url.Content("Login")" method="post"> </form>
而不是用Html.BeginForm()。在我印象里,BeginForm()的作用只是输出一段HTML,最终结果和手写标签应该是完全等效的。难道这个方法还有什么其他特殊的行为吗?为了确认,反汇编了一下ASP.NET MVC的源代码,发现还真的是这样。
TextBoxFor()是一个扩展方法,它的实现调用了HtmlHelper.InputHelper辅助方法,其中有这么一行:
tagBuilder.MergeAttributes<string, object>(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
很明显,该方法设置了客户端验证需要的属性。来看看方法的实现:
public IDictionary<string, object> GetUnobtrusiveValidationAttributes(string name, ModelMetadata metadata) { Dictionary<string, object> dictionary = new Dictionary<string, object>(); if (!this.ViewContext.UnobtrusiveJavaScriptEnabled) { return dictionary; } FormContext formContextForClientValidation = this.ViewContext.GetFormContextForClientValidation(); if (formContextForClientValidation == null) { return dictionary; } ......
可以看到,方法会在ViewContext中查找FormContext,如果找不到的话则直接返回,而不会设置任何属性。那么这个FormContext又是在哪设置的呢?最直接的想法是,它应该在Html.BeginForm()当中。
来看看BeginForm()方法,它调用了FormHelper方法,其实现大概是这样的:
private static MvcForm FormHelper(this HtmlHelper htmlHelper, string formAction, FormMethod method, IDictionary<string, object> htmlAttributes) { TagBuilder tagBuilder = new TagBuilder("form"); tagBuilder.MergeAttributes<string, object>(htmlAttributes); tagBuilder.MergeAttribute("action", formAction); tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true); bool flag = htmlHelper.ViewContext.ClientValidationEnabled && !htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled; if (flag) { tagBuilder.GenerateId(htmlHelper.ViewContext.FormIdGenerator()); } htmlHelper.ViewContext.Writer.Write(tagBuilder.ToString(TagRenderMode.StartTag)); MvcForm result = new MvcForm(htmlHelper.ViewContext); if (flag) { htmlHelper.ViewContext.FormContext.FormId = tagBuilder.Attributes["id"]; } return result; }
前面的代码似乎和FormContext没有太大关系,但是后面就调用了ViewContext.FormContext,这说明FormContext必定是在这一行之前创建的。是不是在于new MvcForm(...)这一句呢?再来看看MvcForm的构造方法:
public MvcForm(ViewContext viewContext)
{
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
this._viewContext = viewContext;
this._writer = viewContext.Writer;
this._originalFormContext = viewContext.FormContext;
viewContext.FormContext = new FormContext();
}
果然!
结论:要让ASP.NET MVC客户端生效,不仅ClientValidationEnabled和UnobtrusiveJavaScriptEnabled应该设置为true,而且要创建的input控件必须用BeginForm()/EndForm()包围起来,否则不会生成data-val属性。所以,建议你使用Html.BeginForm()辅助方法,而应该避免手写form标签,以免碰到和我类似的bug。