表單中的輸入項,有些是固定的,不變的驗證規則,比如字符長度,必填等。但有些是動態的,比如注冊用戶名是否存在這樣的檢查,這個需要訪問服務器后台才能解決。這篇文章將會介紹MVC中如何使用【RemoteAttribute】來解決這類驗證需求,同時會分析【RemoteAttribute】的不足,以及改進的方法.
本文相關的源代碼在這里 MVC-Remote-Validation.zip
一, RemoteAttribute驗證使用
如果需要用戶把整個表單填完后,提交到后台,然后才告訴用戶說,“你注冊的用戶已經被占用了,請換一個用戶名”,估計很多用戶都可能要飈臟話了. MVC中的Remote驗證是通過Ajax實現的,也就是說,當你填寫用戶名的時候,就會自動的發送你填寫的內容到后台,后台返回檢查結果。
1. 實現Remote驗證非常簡單,首先需要有個后台的方法來響應驗證請求, 也就是需要創建一個Controller, 這里我們用ValidationController:
public class ValidationController : Controller { public JsonResult IsEmployeeNameAvailable(string employeeName) { //這里假設已經存在的用戶是”justrun”, 如果輸入的名字不是justrun,就通過驗證 if (employeeName != "justrun") { return Json(true, JsonRequestBehavior.AllowGet); } return Json("The name 'justrun' is not available, please try another name.", JsonRequestBehavior.AllowGet); } }
2. 接着在我們的Employee Model上應用上RemoteAttribute
public class Employee { public int EmpId { get; set; } [DisplayName("Employee Name")] [Remote("IsEmployeeNameAvailable", "Validation")] //使用RemoteAttribute,指定驗證的Controller和Action public String EmployeeName { get; set; } }
3. 對應的View
@using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary() <fieldset> <legend>Registration Form</legend> <ol> <li> @Html.LabelFor(m => m.EmployeeName) @Html.EditorFor(m => m.EmployeeName) @Html.ValidationMessageFor(m => m.EmployeeName) </li> </ol> <input type="submit" value="Register" /> </fieldset> }
4. 最后,看看驗證的效果
通過firebug能夠看到,在填寫表單的過程中,會不斷的把表單的EmployeeName發送到我們指定的Controller, Action上做驗證。
二, RemoteAttribute的局限性
使用 【RemoteAttribute】 來做遠端驗證的確是很棒– 它會自動的發起AJAX請求去訪問后台代碼來實現驗證. 但是注意, 一旦表單提交了,就不會在存在這個驗證了。比如當我用上【Required】這個驗證標簽的時候,無論在客戶端還是服務器端,都存在着對於必填項的驗證。服務器端可以通過ModelState.IsValid非常容易地判斷,當前提交到后台的表單數據是否合法。但是【RemoteAttribute】只有客戶端驗證,而沒有服務器端驗證。 也就是說,如果用戶的瀏覽器中,關閉js,我們的Remote檢查就形同虛設。
是不是非常意外, 當接觸Remote驗證的時候,原以為默認的就會認為它會和其它驗證標簽一樣。所以使用RemoteAttribute驗證,是存在一定的安全隱患的。
三, RemoteAttribute的改進
先介紹一下對於RemoteAttribute的改進思路:
如果我們也想讓RemoteAttribute和其它的驗證特性一樣工作,也就是說,如果不符合Remote的驗證要求,我們希望ModelState.IsValid也是false, 同時會添加上相應的ModelError. 這里選擇在MVC的Model binding的時候,做這個事情,因為在Model Binding的時候,正是將表單數據綁定到對應的model對象的時候。只要在綁定的過程中,如果發現Model中的屬性有使用RemoteAttribute, 我們調用相應的驗證代碼。驗證失敗了,就添加上對於的ModelError.
由於涉及到了Model Binding和Atrribute的使用,如果有興趣的,可以先看看這2篇文章:
Asp.net MVC使用Model Binding解除Session, Cookie等依賴
.Net Attribute詳解(上)-Attribute本質以及一個簡單示例
1. 繼承RemoteAttribute, 創建CustomRemoteAttribute
public class CustomRemoteAttribute : RemoteAttribute { public CustomRemoteAttribute(string action, string controller) : base(action, controller) { Action = action; Controller = controller; } public string Action { get; set; } public string Controller { get; set; } }
看了上面的代碼,你也學會說,這不是什么都沒干嗎? 是的,這個CustomRemoteAttribute 的確是什么都沒干,作用只是公開了RemoteAttribute的Controller和Action屬性,因為只有這樣我們才能知道Model添加的remote驗證,是要訪問那段代碼。
2. 替換RemoteAttribute為CustomRemoteAttribute
這個非常簡單,沒有什么要解釋的。
public class Employee { public int EmpId { get; set; } [DisplayName("Employee Name")] [CustomRemote("IsEmployeeNameAvailable", "Validation")] public String EmployeeName { get; set; } }
3. 自定義的CustomModelBinder
下面的CustomModelBinder就是在Model綁定的時候,調用相應的Action方法做驗證,失敗了,就寫ModelError. 注釋中已經解釋了整個代碼的工作流程。
public class CustomModelBinder : DefaultModelBinder { protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { if (propertyDescriptor.PropertyType == typeof(string)) { //檢查Model綁定的屬性中,是否應用了CustomRemoteAttribute var remoteAttribute = propertyDescriptor.Attributes.OfType<CustomRemoteAttribute>() .FirstOrDefault(); if (remoteAttribute != null) { //如果使用了CustomRemoteAttribute, 就開始找到CustomAttribute中指定的Controller var allControllers = GetControllerNames(); var controllerType = allControllers.FirstOrDefault(x => x.Name == remoteAttribute.Controller + "Controller"); if (controllerType != null) { //查找Controller中的Action方法 var methodInfo = controllerType.GetMethod(remoteAttribute.Action); if (methodInfo != null) { //調用方法,得到驗證的返回結果 string validationResponse = callRemoteValidationFunction( controllerContext, bindingContext, propertyDescriptor, controllerType, methodInfo, remoteAttribute.AdditionalFields); //如果驗證失敗,添加ModelError if (validationResponse != null) { bindingContext.ModelState.AddModelError(propertyDescriptor.Name, validationResponse); } } } } } base.BindProperty(controllerContext, bindingContext, propertyDescriptor); } /// This function calls the indicated method on a new instance of the supplied /// controller type and return the error string. (NULL if not) private string callRemoteValidationFunction( ControllerContext controllerContext, ModelBindingContext bindingContext, MemberDescriptor propertyDescriptor, Type controllerType, MethodInfo methodInfo, string additionalFields) { var propertyValue = controllerContext.RequestContext.HttpContext.Request.Form[ bindingContext.ModelName + propertyDescriptor.Name]; var controller = (Controller)Activator.CreateInstance(controllerType); object result = null; var parameters = methodInfo.GetParameters(); if (parameters.Length == 0) { result = methodInfo.Invoke(controller, null); } else { var parametersArray = new List<string> {propertyValue}; if (parameters.Length == 1) { result = methodInfo.Invoke(controller, parametersArray.ToArray()); } else { if (!string.IsNullOrEmpty(additionalFields)) { foreach (var additionalFieldName in additionalFields.Split(',')) { string additionalFieldValue = controllerContext.RequestContext.HttpContext.Request.Form[ bindingContext.ModelName + additionalFieldName]; parametersArray.Add(additionalFieldValue); } if (parametersArray.Count == parameters.Length) { result = methodInfo.Invoke(controller, parametersArray.ToArray()); } } } } if (result != null) { return (((JsonResult)result).Data as string); } return null; } /// Returns a list of all Controller types private static IEnumerable<Type> GetControllerNames() { var controllerNames = new List<Type>(); GetSubClasses<Controller>().ForEach(controllerNames.Add); return controllerNames; } private static List<Type> GetSubClasses<T>() { return Assembly.GetCallingAssembly().GetTypes().Where( type => type.IsSubclassOf(typeof(T))).ToList(); } }
4. 在MVC項目中應Global.asax.cs用上CustomModelBinder
打開Global.asax.cs, 添加上這段代碼
protected void Application_Start() { //修改MVC默認的Model Binder為CustomBinder ModelBinders.Binders.DefaultBinder = new CustomModelBinder(); …… }
5. 關閉客戶端驗證,看看效果
打開web.config文件,ClientValidationEnabled設置成false, 關閉客戶端驗證
<appSettings> <add key="webpages:Version" value="2.0.0.0" /> <add key="webpages:Enabled" value="false" /> <add key="PreserveLoginUrl" value="true" /> <add key="ClientValidationEnabled" value="false" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" /> </appSettings>
最終的運行效果如下,能夠明顯的看到,頁面刷新,表單提交到了后台處理。