MVC3 项目总结

开发 项目管理
最常见的验证方式是:在实体的属性上加 特性(Attribute) 的方式来完成基本的数据验证. 比如 Required, StringLength, Range 等. 为了保持实体类的POCO ( Plain Old CLR Objects, 所谓的POCO就是那些不包括INSERT、ADD、DEL等数据持久化操作的以及不包括任何业务逻辑功能的原始类。

1.验证 Validation

多样化验证规则

最常见的验证方式是:在实体的属性上加 特性(Attribute) 的方式来完成基本的数据验证. 比如 Required, StringLength, Range 等. 为了保持实体类的POCO ( Plain Old CLR Objects, 所谓的POCO就是那些不包括INSERT、ADD、DEL等数据持久化操作的以及不包括任何业务逻辑功能的原始类。只包含最基本的GETTER 和SETTER).,一般是对实体类声明一个伴随类(MetadataTypeAttribute),在伴随类里声明各种特性.但是伴随类只能声明一个(可以尝试对实体类加多个 MetadataType 看看).

实际情况下, 不同的系统可能要求不一样的验证规则,但是又用的是同一套实体.

比如旅客信息的邮件地址,电话号码等, 在线下系统下是非必填的,但是在线上预订的时候,又是必填的. 你可以定意两个不同的实体. 我采用的是另外一种方法:

先声明一个伴随类:

  1. public class EContactMetadata {  
  2.  
  3. [RegularExpression(@"\d{8}" , ErrorMessage = "請輸入8位有效的號碼")]  
  4.  
  5. [Required]  
  6.  
  7. public object PhoneNo1 { get; set; }  
  8.  
  9. }  

在 Global 里把这个伴随类注册到实体类上:

  1. protected void Application_Start() {  
  2.  
  3. …  
  4.  
  5. TypeDescriptor.AddProviderTransparent(new AssociatedMetadataTypeTypeDescriptionProvider(typeof(EContact) , typeof(EContactMetadata)) , typeof(EContact));  
  6.  
  7. …  
  8.  
  9. }  

用这种方式可以对同一个实体类添加多个伴随类,后注册的会覆盖先注册的.没有发生覆盖的会保留.

部分验证

某个实体类的属性对应到数据库里的某个字段是必填的,但是在填写界面里,又用不到这个字段,而且这个字段暂时也没有办法生成.

比如旅客信息的 OrderNo 是必填的,但是在填写信息页面, OrderNo 还没有生成. 所以在 Action 里, ModelState.IsValid 一直是 false. 为了避免这个 false, 你可以新定义一个类, 但是又不能每一种变化都搞个类出来吧.

使用 BindAttribute 接合 ModelBinder

BindAttribute 有 Include 和 Exclude , Include 是只接收 Include 指定的属性, Exclude 是排除.

  1. [HttpPost]  
  2.  
  3. public ActionResult Reserve([Bind(Include = "FG,FB,Hotels,OptionUseDates")]Reserve r , string act) {  
  4.  
  5. …  

意思是参数 r 只接收 Post 过来的 FG, FB,Hotels,OptionUseDates 数据,其它的传过来也不要

在ModelBinder 中把非 Include 的或 Exclude 的验证错误剔除

  1. public class SmartModelBinder : DefaultModelBinder {  
  2. protected override void OnModelUpdated(ControllerContext controllerContext , ModelBindingContext bindingContext) {  
  3. Dictionary<string , bool> startedValid = new Dictionary<string , bool>(StringComparer.OrdinalIgnoreCase);  
  4. //获取模型的验证结果  
  5. var results = ModelValidator.GetModelValidator(bindingContext.ModelMetadata , controllerContext).Validate(bindingContext.Model);  
  6. foreach(ModelValidationResult validationResult in results) {  
  7. string subPropertyName = CreateSubPropertyName(bindingContext.ModelName , validationResult.MemberName);  
  8. //if(bindingContext.PropertyFilter(subPropertyName)) {  
  9. //bindingContext.PropertyFilter 是一个 delegate, 如果指定的 member 在 BindAttribute 的 Include 的列表内(或者非 Exclude 的列表内),返回 true, 否则为 false  
  10. //部分验证的功能就是通过它的结果来实现的  
  11. if(bindingContext.PropertyFilter(validationResult.MemberName)) {  
  12. if(!startedValid.ContainsKey(subPropertyName)) {  
  13. startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName);  
  14. }  
  15. if(startedValid[subPropertyName]) {  
  16. bindingContext.ModelState.AddModelError(subPropertyName , validationResult.Message);  
  17. }  
  18. }  
  19. }  
  20. }  

在 Global 里注册该 ModelBinder

  1. protected void Application_Start() {  
  2. …  
  3. ModelBinders.Binders.DefaultBinder = new SmartModelBinder();  
  4. … 

自验证 IValidatableObject

某些情况下,简单的在用 DataAnnoation 不足以做完业务逻辑的验证。比如旅客信息中,儿童的年龄要在返程日期的为准,2 到 11 岁之间, 当机票订票要求是护照时,出生日期、护照号码,签发国家、签发地为必填;当要求是回乡证时,回乡证号必填;当要求护照或回乡证时,其中之一的信息必须完整。

这种情况下,就需要用到 IValidatableObject , 只要按需求实现 Validate 方法就可以了。

  1. public class Traveller : IValidatableObject {  
  2. public int TravellerID { get; set; }  
  3. [Required]  
  4. public string FirstName { get; set; }  
  5. [Required]  
  6. public string LastName { get; set; }  
  7. public DateTime Birthday { get; set; }  
  8. public bool IsAdult { get; set; }  
  9. public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {  
  10. if(!this.IsAdult && this.Birthday.Date < DateTime.Now.AddYears(-12)) {  
  11. yield return new ValidationResult("儿童年龄必须在 12 岁以内");  
  12. }  
  13. }  

另外注意,如果标注有 DataAnnation 的属性没有验证没有通过,是不会去执行 Validate 方法的。

手动验证

暂未整理

启用客服端验证

AppSetting 里

  1. <!--啟用客戶端校驗--> 
  2. <add key="ClientValidationEnabled" value="true"/> 
  3. <!--启用非介入式验证--> 
  4. <add key="UnobtrusiveJavaScriptEnabled" value="true"/> 

  1. Html.EnableClientValidation(true);  
  2. Html.EnableUnobtrusiveJavaScript(true) 

另外,如果以启用了客户端验证,但是表单项并没有在 Form (Html.BeginForm()) 内,也是不会有客户端验证的.

自定义客户端验证

具体请参考该 js 文件。

[[84103]]

以下所述的客户端验证都是基于 jQuery.validate 1.9 + jquery.validate.unobtrusive (项目自带) 的

默认的,要在客户端实时看到验证信息(js 验证),需要使用 Html.ValidationMessageFor() 来生成一个容器,供 js 往里填写错误信息.但是,一个页面如果有很多个表单对象,而又不能使用 EditorForModel , 一个一个敲,也是一件很头痛的事。其实可以通过修改,来自动生成: 重写 jQuery validator 的 setting 中的 errorPlacement 和 success 。写这两个是做到最小的修改,如果有必要,你也可以重写 其它的方法。

  1. mode1_error 即 errorPlacement  
  2. var mode1_error = function(error, inputElement){  
  3. //查找错误提示容器  
  4. var container = $("span[data-valmsg-for='" + inputElement[0].name + "']", inputElement[0].form);  
  5. //如果没有,生成一个  
  6. if(container.length == 0){  
  7. container = $("<span>")  
  8. .attr({"data-valmsg-replace":true, "data-valmsg-for":inputElement[0].name}).appendTo(form)  
  9. }  
  10. container.addClass("floatValidationError shadow radius hide");  
  11. //调用原来的 errorPlacement  
  12. errorPlacement(error, inputElement);  
  13. //这块为选择器做的  
  14. var flow = inputElement;  
  15. var flag = inputElement.attr("data-flag");  
  16. if(flag != undefined){  
  17. container.data("data-for",flag);  
  18. var display = $("*[data-for='" + flag + "']", inputElement[0].form);  
  19. display.addClass("input-validation-error");  
  20. if(display.length > 0){  
  21. flow = display;  
  22. }  
  23. }  
  24. //下拉列表不能设置border,需要包装一个 span ,在 span 上显示border  
  25. if(inputElement[0].tagName == "SELECT" && inputElement.hasClass("input-validation-error") && !inputElement.parent().hasClass("input-validation-error-wrapper")){  
  26. inputElement.wrap("<span class='input-validation-error-wrapper' />")  
  27. }  
  28. //这一步能过 css hide 做,优化性能  
  29. //container.hide();  
  30. flow.focus(elementFocus).blur(elementBlur);  
  31. }  
  32. mode2_success 即 success  
  33. var mode2_success = function(error){  
  34. var container = error.data("unobtrusiveContainer");  
  35. //调用原有的 success  
  36. success(error);  
  37. //为选择器所做  
  38. var dataFor = container.data("data-for");  
  39. if(dataFor != undefined){  
  40. $("*[data-for='" + dataFor + "']").removeClass("input-validation-error");  
  41. }  
  42. //如果该error对应的表单对象是下拉列表,要清除包装的 span   
  43. var ele = $("[name='" + container.data("valmsg-for") + "']");  
  44. if(ele[0].tagName == "SELECT" && ele.hasClass("input-validation-error")){  
  45. var par = ele.parent();  
  46. ele.appendTo(par.parent());  
  47. par.remove();  
  48. }  

使用这个 js 后,即使不Html.ValidationMessageFor() 也一样会在客户端有个提示

效果如下:

 

优化

如果一个页面里的表单很多,每个表单又有N个验证规则,在 IE7(包括)以下,会提示:

是否停止运行此脚本?

此页面上的脚本造成 Internet Explorer 运行速度减慢。XXXX

为了该问题,测试打了很多小报告,我也很烦,jquery validation 造成的问题,我有什么办法,但是问题总归要解决,我花了两天时间,用 ie 调试工具里的探测器及dynaTrace 收集了很多运行数据,***发现,验证是很快,但是显示错误却很慢,因为有大量的非Id查找的 jquery selector。具体表现在 jquery.validate 的 showLabel 方法,和 jquery.validate.unobtrusive 的 onError 方法。我对这两个地方做了修改,在测,整个验证过程在 ie7下执行的很快!

具体改动如下:

jquery.validate.unobtrusive.js

第40行由:

  1. var container = $(this).find("[data-valmsg-for='" + inputElement[0].name + "']"), 

改为:

  1. var container = $(this).find("span[data-valmsg-for='" + inputElement[0].name + "']"), 

目的:减少 dom 节少遍历次数

第48行由:

  1. error.removeClass("input-validation-error").appendTo(container); 

改为:

  1. error  
  2. //.removeClass("input-validation-error")  
  3. .appendTo(container); 

原因:在 jquery.validate 1.9 中的 showLabel 方法的

  1. var label = this.errorsFor(element) 

这个方法又调 errors() 方法

errors 方法又是跟据 errorClass 去查找

errorClass 即 input-validation-error

在 showLabel 方法里,如果

label = this.errorsFor(element) 没有结果,就又会新建一个 label (DOM创建)

所以在 jquery.validate.unobtrusive 的第48行不应该 removeClass("input-validation-error")

jquery.validate.js 改动:

showLabel 方法:

在开头处加入

  1. if (message == undefined)  
  2. return

errorsFor 方法改为:

  1. errorsFor: function (element) {  
  2. var name = this.idOrName(element);  
  3. return $(this.settings.errorElement + "." + this.settings.errorClass + "[for='" + name + "']", this.errorContext);  
  4. // return this.errors().filter(function () {  
  5. // return $(this).attr('for') == name;  
  6. // });  
  7. }, 

修改后的 js 文件:

[[84104]][[84105]]

#p#

扩展 DataTypeAttribute

DataTypeAttribute 只是用来显示值,而不用于验证(不知是否有误,请指教)。

但是我就是想要用 DataTypeAttribute 做验证怎么办呢?自定义一个 DataTypeValidator

  1. public class DataTypeValidator : DataAnnotationsModelValidator<DataTypeAttribute> {  
  2.  
  3. //和 jQuery 里的email 验证保持一致  
  4.  
  5. private static readonly string EmailReg = @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$";  
  6.  
  7. private static readonly string UrlReg = @"^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$";  
  8.  
  9. public DataTypeValidator(ModelMetadata metadata , ControllerContext context , DataTypeAttribute attribute)  
  10. : base(metadata , context , attribute) {  
  11. this.message = attribute.ErrorMessage;  
  12. }  
  13. string message;  
  14. public override IEnumerable<ModelValidationResult> Validate(object container) {  
  15. var value = Metadata.Model;  
  16. if(value != null) {  
  17. var dataType = (DataType)Enum.Parse(typeof(DataType) , Metadata.DataTypeName);  
  18. var flag = true;  
  19. switch(dataType) {  
  20. case DataType.EmailAddress:  
  21. if(!Regex.IsMatch(value.ToString() , EmailReg, RegexOptions.IgnoreCase)) {  
  22. flag = false;  
  23. }  
  24. break;  
  25. case DataType.Url:  
  26. if(!Regex.IsMatch(value.ToString() , UrlReg , RegexOptions.IgnoreCase))  
  27. flag = false;  
  28. break;  
  29. }  
  30. if(!flag) {  
  31. yield return new ModelValidationResult() {  
  32. //下面这句不能加,会影响 SmartModelBinder 里的 CreateSubPropertyName  
  33. //MemberName = Metadata.PropertyName ,  
  34. Message = ErrorMessage 
  35. };  
  36. }  
  37. }  
  38. }  
  39. public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() {  
  40. List<ModelClientValidationRule> rules = new List<ModelClientValidationRule>();  
  41. ModelClientValidationRule rule;  
  42. switch(Attribute.DataType) {  
  43. case DataType.EmailAddress:  
  44. rule = new ModelClientValidationRule() { ErrorMessage = message , ValidationType = "email" };  
  45. //rule.ValidationParameters.Add("email" , "true");  
  46. rules.Add(rule);  
  47. break;  
  48. case DataType.Url:  
  49. rule = new ModelClientValidationRule() { ErrorMessage = message , ValidationType = "url" };  
  50. //rule.ValidationParameters.Add("url" , "true");  
  51. rules.Add(rule);  
  52. break;  
  53. case DataType.Date:  
  54. rule = new ModelClientValidationRule() { ErrorMessage = message , ValidationType = "date" };  
  55. //rule.ValidationParameters.Add("date" , "true");  
  56. rules.Add(rule);  
  57. break;  
  58. }  
  59. return rules;  
  60. }  

然后在 Global 里:

  1. DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(DataTypeAttribute) , typeof(DataTypeValidator)); 

该类的方法:GetClientValidationRules 里,将 Date 的 ValidationType 设为 date 。但是在 jquery.validate 里,date 方法是去验证 new Date(value) 是不是 NaN, 这在 ie 下有问题, 比如 new Date("2012-01-01") 是 NaN ,也许你说可以用 dateISO 去验证,但是 dateISO 要求如下格式:yyyy-MM-dd 或 yyyy/MM/dd, 换一种格式呢?当然是验证不通过了!

这种情况下,只能重写 jquery.validate 的 date 验证规则了(在上文我贴出的 site.js 里有):

  1. var checkDate = function(value, element){  
  2. return this.optional(element) || value.toDate() != null;  
  3. }  
  4. …  
  5. $.validator.addMethod("date", checkDate); 

该部分是覆盖 jquery.validate 里的 date 处理方法, toDate方法在我贴出的 site.js 里有,请自行参考。另外,我还提供了 DateRange 验证,用以解决复杂的验证逻辑,用法如下:

  1. @Html.TextBoxFor(m => m[i].BirthDay , new { Value = Model[i].BirthDay.ToString(dateFmt) , @data_val_required = "必填" , @data_val_dateRange_min = min.ToString(dateFmt) , @data_val_dateRange_max = max.ToString(dateFmt) , @data_val_dateRange = @msg , title = "輸入格式:2012/01/01" }) 

RequiredIf 验证

做复杂验证的时候,可以像上面的 DateRange 的用法一样去处理大部分问题,但是当某值为XX时,某XX为必填,DataAnnoation 里没提供,jquery.validate 里也没有提供, 那只能自定义了:

  1. [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]  
  2. public class RequiredIfAttribute : ValidationAttribute , IClientValidatable {  
  3.  
  4. private RequiredAttribute required = new RequiredAttribute();  
  5.  
  6. /// <summary>  
  7. /// 依赖于哪个属性  
  8. /// <remarks>必须是本对象下的一个属性</remarks>  
  9. /// </summary>  
  10. public string DependentProperty { get; set; }  
  11.  
  12. private List<object> targetValues = new List<object>();  
  13. /// <summary>  
  14. /// 依赖属性为哪些值时,该属性为必填  
  15. /// </summary>  
  16. public List<object> TargetValues {  
  17. get {  
  18. return this.targetValues;  
  19. }  
  20. set {  
  21. this.targetValues = value;  
  22. }  
  23. }  
  24.  
  25. /// <summary>  
  26. ///   
  27. /// </summary>  
  28. /// <param name="dependentProperty"></param>  
  29. /// <param name="targetValue"></param>  
  30. public RequiredIfAttribute(string dependentProperty , object targetValue)  
  31. this(dependentProperty , targetValue , null) {  
  32. }  
  33.  
  34. public RequiredIfAttribute(string dependentProperty , params object[] targetValues)  
  35. this(dependentProperty , targetValues , null) {  
  36. }  
  37.  
  38. /// <summary>  
  39. ///   
  40. /// </summary>  
  41. /// <param name="dependentProperty"></param>  
  42. /// <param name="targetValue"></param>  
  43. /// <param name="errorMessage"></param>  
  44. public RequiredIfAttribute(string dependentProperty , object targetValue , string errorMessage)  
  45. : base(errorMessage) {  
  46. this.DependentProperty = dependentProperty;  
  47. this.TargetValues.Add(targetValue);  
  48. }  
  49.  
  50. /// <summary>  
  51. ///   
  52. /// </summary>  
  53. /// <param name="dependentProperty"></param>  
  54. /// <param name="targetValues"></param>  
  55. /// <param name="errorMessage"></param>  
  56. public RequiredIfAttribute(string dependentProperty , object[] targetValues , string errorMessage)  
  57. : base(errorMessage) {  
  58. this.DependentProperty = dependentProperty;  
  59. this.TargetValues.AddRange(targetValues);  
  60. }  
  61.  
  62. /// <summary>  
  63. ///   
  64. /// </summary>  
  65. /// <param name="metadata"></param>  
  66. /// <param name="context"></param>  
  67. /// <returns></returns>  
  68. public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata , ControllerContext context) {  
  69. var rule = new ModelClientValidationRule() {  
  70. ErrorMessage = this.FormatErrorMessage(metadata.GetDisplayName()) ,  
  71. //只能是小写  
  72. ValidationType = "requiredif" //要在 jquery.validate 里实现 requiredIf 规则  
  73. };  
  74.  
  75. var tvs = this.TargetValues.Select(v => {  
  76. if(v.GetType() == typeof(bool))  
  77. return v.ToString().ToLower();  
  78. else 
  79. return v.ToString();  
  80. });  
  81.  
  82. var ser = new JavaScriptSerializer();  
  83. var values = ser.Serialize(tvs);  
  84. //只能是小写  
  85. rule.ValidationParameters.Add("dependencyvalue" , values);  
  86. //不要试图获取该对象输出成 html 的表单前缘,我试了很多方法,都不能获取,特别是当 Model 是一个集合的时候  
  87. rule.ValidationParameters.Add("dependency" , string.Format("*.{0}" , this.DependentProperty));  
  88. yield return rule;  
  89. }  
  90. /// <summary>  
  91. ///   
  92. /// </summary>  
  93. /// <param name="name"></param>  
  94. /// <returns></returns>  
  95. public override string FormatErrorMessage(string name) {  
  96. if(!String.IsNullOrEmpty(this.ErrorMessageString))  
  97. required.ErrorMessage = this.ErrorMessageString;  
  98. return required.FormatErrorMessage(name);  
  99. }  
  100. /// <summary>  
  101. ///   
  102. /// </summary>  
  103. /// <param name="value"></param>  
  104. /// <param name="validationContext"></param>  
  105. /// <returns></returns>  
  106. protected override ValidationResult IsValid(object value , ValidationContext validationContext) {  
  107. var containerType = validationContext.ObjectInstance.GetType();  
  108. var field = containerType.GetProperty(this.DependentProperty);  
  109. if(field == null)  
  110. throw new MissingMemberException(containerType.Name , this.DependentProperty);  
  111. var dependentvalue = field.GetValue(validationContext.ObjectInstance , null);  
  112. if((dependentvalue == null && (this.TargetValues == null || this.TargetValues.Count == 0)) || (dependentvalue != null && this.TargetValues.Any(t => t.Equals(dependentvalue)))) {  
  113. i(!required.IsValid(value))  
  114. return new ValidationResult(FormatErrorMessage(validationContext.DisplayName) , new[] { validationContext.MemberName });  
  115. }  
  116. return ValidationResult.Success;  
  117. }  
  118. Gloal  
  119. DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute) , typeof(MyRequiredAttributeAdapter)); 

扩展 jquery.validate

处理方法:

  1. var requiredIf = function (value, element, params) {  
  2. var prefix = getModelPrefix(element.name);  
  3. var fullDependencyName = appendModelPrefix(params["dependency"], prefix);  
  4. var dependency = $(element.form).find(":input[name='" + fullDependencyName + "']");  
  5. var dependencyValue = params["dependencyvalue"];  
  6.  
  7. var acturalValue = null;  
  8. if(dependency.attr("type") == "checkbox"){  
  9. acturalValue = dependency.attr("checked") == true ? dependency.val() : null;  
  10. }else{  
  11. acturalValue = dependency.val();  
  12. }  
  13.  
  14. eval("dvs = " + dependencyValue);  
  15. if(dvs.indexOf2(acturalValue) >= 0){  
  16. return $.validator.methods.required.call(this, value, element, params);  
  17. }  
  18. return true;  
  19. }  
  20. Adapter  
  21. var requiredIfAdapter = function (options) {  
  22. options.rules["requiredIf"] = options.params;  
  23. options.messages["requiredIf"] = options.message;  

注册:

  1. $.validator.addMethod('requiredIf',requiredIf);  
  2. $.validator.unobtrusive.adapters.add('requiredIf',['dependency''dependencyvalue'],requiredIfAdapter); 

使用:

  1. …  
  2. [RequiredIf("CertificateType" , 2)]  
  3. public object Nation { get; set; }  
  4.  
  5. [RequiredIf("CertificateType" , 1 , 2)]  
  6. public object IDNumber { get; set; }  
  7. … 

RequiredIf 所涉及到的 js 方法同样在 site.js 里也有提供

处理中英文混合的 RequiredAttribute 提示

将项目部署到服务器上,有个奇怪的现象,Required 的提示居然是中英文混合的(和 jQuery 无关,因为 required 的错误信息是写在 html 里的),其原因是,设置了 DisplayAttribute ,也设置了 Required, 并且也带 ErrorMessage (是通过 ErrorMessageResourceType 指向其它DLL里的,我没有引用这个DLL,也不推荐这样做)。

要解决这个问题,只能扩展 RequiredAttributeAdapter 了

  1. public class MyRequiredAttributeAdapter : RequiredAttributeAdapter {  
  2. public MyRequiredAttributeAdapter(ModelMetadata metadata , ControllerContext context , RequiredAttribute attribute)  
  3. : base(metadata , context , attribute) {  
  4. //ErrorMessage 和 ErrorMessageResourceName 不可同时存在  
  5. if(attribute.ErrorMessage == null) {  
  6. //attribute.ErrorMessage = null;  
  7. attribute.ErrorMessageResourceType = typeof(CustomMessage);  
  8. attribute.ErrorMessageResourceName = "PropertyValueRequired";  
  9. }  
  10. }  

其中,ErrorMessage 和 ErrorMessageResourceName 不能同时存在,这个需要注意。

然后注册这个适配器:

  1. Global  
  2.  
  3. DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(RequiredAttribute) , typeof(MyRequiredAttributeAdapter));  

在处理这个问题的时候,我搜索到另外一个问题:PropertyValueInvalid 的本地化

1, 在站点下新建一个全局资源文件夹: App_GlobalResources

2,新建一个资源文件:CustomMessage.rexs (名随便)

3, 加一个字符串资源:PropertyValueInvalid, 值为:{0} 不是有效的 {1}

4, Global 里:

DefaultModelBinder.ResourceClassKey = "CustomMessage";

CustomMessage 即第二步建的资源文件

RedirectToAction 后,ModelState 丢了!

假如说你的某个action 往 ViewBag 里写了好多东西进去。当这个 action 的 post 方法发生,并且 model 验证不通过,为了不在写那么多 ViewBag , 你必定会用到 RedirectToAction, 但是这一 Redirect ,你的验证错误就丢了,用户输入的内容也没有了,redirect 的结果同新打开的页面一样,看不到错误提示,看不到用户输入的内容,咋办呢?在 post 方法里写 ViewBag ?避免使用 RedirectToAction ?

重写 Controller 的 OnActionExecuted

  1. protected override void OnActionExecuted(ActionExecutedContext filterContext) {  
  2.  
  3. if(TempData["ModelState"] != null && !ModelState.Equals(TempData["ModelState"]))  
  4.  
  5. ModelState.Merge((ModelStateDictionary)TempData["ModelState"]);  
  6.  
  7.  
  8. base.OnActionExecuted(filterContext);  
  9.  
  10. }  

当ModelState.IsValid 为 false 时:

  1. if(this.ModelState.IsValid) {  
  2. ...  
  3. else {  
  4. ...  
  5. this.TempData.Add("ModelState" , this.ModelState);  
  6. return RedirectToAction("Infomation" , new { booking = "direct" });  

#p#

页面 Html

Javascript ***出现在 body 结束的地方

如果你随意放置 javascript 代码在页面里,会造成什么后果?这个我就不说了,反正按我的规则,我都是把 javascript 代码统一放到 body 结束的地方。在 MVC 里有神马办法能做到这个统一?搜索了很多,***我使用了 ViewContext.HttpContext

  1. /// <summary>  
  2. /// 注册脚本  
  3. /// </summary>  
  4. /// <param name="htmlHelper"></param>  
  5. /// <param name="template"></param>  
  6. /// <returns></returns>  
  7. public static MvcHtmlString Script(this HtmlHelper htmlHelper , Func<object , HelperResult> template) {  
  8. htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;  
  9. return MvcHtmlString.Empty;  
  10. }  
  11. /// <summary>  
  12. /// 渲染脚本  
  13. /// </summary>  
  14. /// <param name="htmlHelper"></param>  
  15. /// <returns></returns>  
  16. public static IHtmlString RenderScripts(this HtmlHelper htmlHelper) {  
  17. foreach(object key in htmlHelper.ViewContext.HttpContext.Items.Keys) {  
  18. if(key.ToString().StartsWith("_script_")) {  
  19. var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object , HelperResult>;  
  20. if(template != null) {  
  21. htmlHelper.ViewContext.Writer.Write(template(null));  
  22. }  
  23. }  
  24. }  
  25. return MvcHtmlString.Empty;  

在 _Layout.cshtml 里:

  1. ....  
  2. @Html.RenderScripts()  
  3. </body>  
  4. </html> 

在每个 View 甚至 Partical View 里:

  1. @Html.Script(  
  2. @<script type="text/javascript">  
  3. $(":regex(id, /^travellers__\\d+__CertificateType$/)").live("change", BookingHelper.CertificateTypeChange).trigger("change");  
  4. </script>  

这样就保证了所有这样写的 script 代码都在 body 结束的位置

DisplayAttribute

首先,我们的实体类的每个属性都有指定 DisplayAttribute , 这样,我们就可以用 Html.LabelFor 来统一属性的显示名称。但是,你的 Display 定义的准不准呢?是不是客户想要的文字呢?如果不是,你还要一个一个的去把 Display 给修改?会不会影响到其它人?

为了做到最小影响,我加了一个 DataAnnoationsModelMetaDataProvider

  1. public class DisplayNameMetadataProvider : DataAnnotationsModelMetadataProvider {  
  2.  
  3. private ResourceManager resourceManager = null;  
  4.  
  5. public DisplayNameMetadataProvider(ResourceManager manager) {  
  6. this.resourceManager = manager;  
  7. }  
  8. public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor , Type containerType , string propertyName) {  
  9. var meadata = base.GetMetadataForProperty(modelAccessor , containerType , propertyName);  
  10.  
  11. var key = string.Format("{0}_{1}" , containerType.Name , propertyName);  
  12. var value = this.resourceManager.GetString(key);  
  13. if(value != null)  
  14. metadata.DisplayName = value;  
  15. return metadata;  
  16. }  
  17. }  

 

  1. Globa  
  2. ModelMetadataProviders.Current = new DisplayNameMetadataProvider(OnlineUI.ResourceManager);  

OnlineUI 是一个资源文件。这样做的好处是:只有 OnlineUI 里有对应的 ClassName_PropertyName 的时候,才会去对原有的 DisplayAttribute 去覆盖,不存在的话,继续沿用原有的。

CheckBoxFor 的神秘随从

用 CheckBoxFor 的时候,你是否注意到它有一个你想不到的地方?它会生成一个 hidden input, 它的name和 checkbox 的name 一样。

原因我也不清楚,给你一个参考:

http://forums.asp.net/t/1314753.aspx

AdditionalViewData 的困惑

public static MvcHtmlString EditorFor<TModel , TValue>(this HtmlHelper<TModel> html , Expression<Func<TModel , TValue>> expression , string templateName , object additionalViewData);

该方法的参数 additionalViewData 的说明:

additionalViewData:

An anonymous object that can contain additional view data that will be merged

into the System.Web.Mvc.ViewDataDictionary<TModel> instance that is created

for the template.

大意是说 additionalViewData 将会被合并到 ViewBag 内。

比如:Traveller.cshtml

  1. @Html.EditorFor(m => m[i].Nation , "Nation", new { Disabled = true }) 

我声明了一个 additionalViewData :Disabled, 然后在 EditorTemplates 下的

Nation.cshtml 里:

  1. @model string  
  2. @{  
  3. Layout = null;  
  4. var nation = Model == null ? "HK" : Model;  
  5. var required = (bool)(ViewBag.Required ?? false);  
  6. var disabled = (bool)(ViewBag.Disabled ?? false);  
  7. ...  
  8. ... 

但是,不要太过于自信,将 additionalViewData 合并到 ViewBag 内,是有条件的:如果上面说的 Traveller.cshtml 是通过 Html.Action("Traveller",....) 调用的,这个 additionalViewData 是死活也合并不进 ViewBag 内的,除非用 Html.Partical("Traveller",....)

至于为什么,我没有去考证,只知道通过 Html.Action 是死活都不行。

Post 方法的参数是一个字典

Post 方法如下:

  1. [HttpPost]  
  2.  
  3. public ActionResult Option([Bind(Include = "Date,Qty")]Dictionary<long , SelectedOption> opts) {  
  4.  
  5. ... 

那要怎样处理 html ,才能将Post 的值绑定到 opts 参数里?

首先,要有一个 key,

  1. <input type="hidden" name="opts[@idx].Key" value="@item.Master.ResourceID" /> 

idx 必须从 0 开始, 这个key 的 value 就是 dictionary 的 key

字典的 value

  1. @Html.DropDownList(string.Format("opts[{0}].Value.Qty" , idx) , options.Select(o => new SelectListItem() { Text = o.ToString() , Value = o.ToString(), Selected = o == selectedQty })) 

也就是说: 字典的 key 必须要有一个 name 为 opts[0].Key 的html表单对象来存放

字典的 value 的值必须要用这样的 name 的 html 表单:opts[0].Value.Qty

HtmlFieldPrefix

暂未整理

异常处理 ExceptionFilter

异常处理程序的执行顺序

关于顺序,可以参考:

http://msdn.microsoft.com/zh-cn/library/gg416513(v=vs.98).aspx

OnActionExecuting(ActionExecutingContext)、OnResultExecuting(ResultExecutingContext) 和 OnAuthorization(AuthorizationContext) 筛选器以正向顺序运行。 OnActionExecuted(ActionExecutedContext)、OnResultExecuting(ResultExecutingContext) 和 OnException(ExceptionContext) 筛选器以反向顺序运行。

也就是说***注册的 ExceptionFilter(不指定 Order 的情况下,认为Order 是相同的) ***执行,我在这个上面走了一个弯路,一开始,在 Global 里,我将自定义的异常处理程序优先于HandleErrorAttribute 注册(就是放到它的前面),没有指定 Order ,在 IISExpress 里执行,是能定位到自定义的处理程序里,但是发布到服务器上后,却是 HandleErrorAttribute 先执行...

#p#

怎样显示错误页面?

你想怎么显示错误页面?跳转?如果是跳转,你的 Exception 怎么传递?

我是像默认的 Error.cshtml 一样,将自定义的错误页面也放到 shared 文件下。但是这样就要考虑怎样才能将 XXX.cshtml 给展示出来,参考了一下 HandleErrorAttribute 的写法,其实挺简单:

  1. public class HandleOtherErrorAttribute : FilterAttribute , IExceptionFilter {  
  2. public void OnException(ExceptionContext filterContext) {  
  3. if(!filterContext.ExceptionHandled) {  
  4. if(filterContext.Exception is HttpRequestValidationException) {  
  5. string controllerName = (string)filterContext.RouteData.Values["controller"];  
  6. string actionName = (string)filterContext.RouteData.Values["action"];  
  7. HandleErrorInfo model = new HandleErrorInfo(new Exception("非法字符串") , controllerName , actionName);  
  8. ViewResult result = new ViewResult {  
  9. ViewName = "Error" ,  
  10. ViewData = new ViewDataDictionary<HandleErrorInfo>(model) ,  
  11. TempData = filterContext.Controller.TempData  
  12. };  
  13. filterContext.Result = result;  
  14. filterContext.ExceptionHandled = true;  
  15. filterContext.HttpContext.Response.Clear();  
  16. filterContext.HttpContext.Response.StatusCode = 500;  
  17. filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;  
  18. }  
  19. }  
  20. }  

其它

实现 Server.Transfer 一样的效果

某个动作如下:

  1. /Search?CityID=1&ThemeID=2&xxx=yyy 

我想将 /Search?CityID=1 这样的地址自成一派,变成 /City/1 ,但是又不想让看到地址的变化(就是不跳转), 那只能用 Server.Transfer 了。

  1. public class TransferResult : ActionResult {  
  2. public string Url { get; private set; }  
  3. public TransferResult(string url) {  
  4. this.Url = url;  
  5. }  
  6.  
  7. public override void ExecuteResult(ControllerContext context) {  
  8. if(context == null)  
  9. throw new ArgumentNullException("context");  
  10.  
  11. var httpContext = HttpContext.Current;  
  12. // MVC 3 running on IIS 7+  
  13. if(HttpRuntime.UsingIntegratedPipeline) {  
  14. httpContext.Server.TransferRequest(this.Url , true);  
  15. } else {  
  16. // Pre MVC 3  
  17. httpContext.RewritePath(this.Url , false);  
  18. IHttpHandler httpHandler = new MvcHttpHandler();  
  19. httpHandler.ProcessRequest(httpContext);  
  20. }  
  21. }  
  22. }  
  23.  
  24. public class TransferToRouteResult : ActionResult {  
  25. public string RouteName { get; set; }  
  26. public RouteValueDictionary RouteValues { get; set; }  
  27. public TransferToRouteResult(RouteValueDictionary routeValues)  
  28. : this(null , routeValues) {  
  29. }  
  30. public TransferToRouteResult(string routeName , RouteValueDictionary routeValues) {  
  31. this.RouteName = routeName ?? string.Empty;  
  32. this.RouteValues = routeValues ?? new RouteValueDictionary();  
  33. }  
  34. public override void ExecuteResult(ControllerContext context) {  
  35. if(context == null)  
  36. throw new ArgumentNullException("context");  
  37. var urlHelper = new UrlHelper(context.RequestContext);  
  38. var url = urlHelper.RouteUrl(this.RouteName , this.RouteValues);  
  39. var actualResult = new TransferResult(url);  
  40. actualResult.ExecuteResult(context);  
  41. }  

Controller 的扩展方法:

  1. public static ActionResult TransferToAction( this Controller controller, string actionName, string controllerName = null , object routeValues = null){  
  2. var rd = new RouteValueDictionary( routeValues );  
  3. if(!rd.ContainsKey("action"))  
  4. rd.Add("action" , actionName);  
  5. else  
  6. rd["action"] = actionName;  
  7. if(controllerName != null) {  
  8. if(!rd.ContainsKey("controller"))  
  9. rd.Add("controller" , controllerName);  
  10. else  
  11. rd["controller"] = controllerName;  
  12. }  
  13. return new TransferToRouteResult(rd);  

使用:

  1. public ActionResult City(int id , string eng) {  
  2. ...  
  3. return this.TransferToAction("List" , routeValues: new { CityID = id });  

原文链接:http://www.cnblogs.com/xling/archive/2012/07/11/2587002.html

责任编辑:林师授 来源: 博客园
相关推荐

2012-06-26 09:37:54

Web

2012-06-23 20:24:33

Web

2015-06-23 16:42:21

2012-07-16 13:02:01

2014-04-16 11:03:36

MVC3JSON

2012-08-27 10:11:43

ASP.NET

2012-03-13 09:11:46

Web

2011-04-14 09:19:22

ASP.NET MVC

2011-04-18 09:35:59

ASP.NET MVC

2010-10-14 09:05:36

ASP.NET MVC

2012-03-27 14:34:07

Visual Stud微软MVC

2010-11-15 08:44:38

ASP.NET MVC

2009-09-27 14:01:29

Spring MVC

2015-06-17 10:16:30

ASP.NET

2013-05-23 10:39:29

MVC架构MVC项目搭建

2012-08-15 09:22:49

JavaScript

2013-01-21 10:19:50

JavaScript项目管理JS

2011-01-15 23:07:59

2012-06-17 20:19:29

2010-10-12 09:52:02

ASP.NET MVC
点赞
收藏

51CTO技术栈公众号