前天,我在開發中碰到一個奇怪的問題。和往常一樣,我在視圖中這樣寫:
@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。