使用ASP.NET MVC 4在运行时动态应用验证规则

时间:2013-09-20 14:51:40

标签: c# validation asp.net-mvc-4

我在WebForms工作多年,但我对.NET的MVC很新。我试图找出如何在运行时将动态验证规则应用于我的模型的成员。出于这个问题的目的,这些是我正在使用的类的简化版本:

public class Device
{
   public int Id {get; set;}
   public ICollection<Setting> Settings {get; set;}
}

public class Setting
{
   public int Id {get; set;} 
   public string Value {get; set;}
   public bool IsRequired {get; set;}
   public int MinLength {get; set;}
   public int MaxLength {get; set;}
}

在我看来,我将使用每个编辑器遍历Settings集合,并在运行时应用每个Setting实例中包含的验证规则,以实现与我在模型上使用DataAnnotations时获得的相同的客户端和服务器端验证。编译时间。在WebForms中,我只是将相应的Validator附加到相关字段,但我在MVC4中找不到类似的机制。有没有办法实现这个目标?

4 个答案:

答案 0 :(得分:9)

我的解决方案是扩展ValidationAttribute类并实现IClientValidatable接口。以下是一个完整的示例,有一些改进空间:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Web.Mvc;

namespace WebApplication.Common
{
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public class RuntimeRequiredAttribute : ValidationAttribute, IClientValidatable
    {
        public string BooleanSwitch { get; private set; }
        public bool AllowEmptyStrings { get; private set; }

        public RuntimeRequiredAttribute(string booleanSwitch = "IsRequired", bool allowEmpytStrings = false ) : base("The {0} field is required.")
        {
            BooleanSwitch = booleanSwitch;
            AllowEmptyStrings = allowEmpytStrings;
        }

            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
            {
                PropertyInfo property = validationContext.ObjectType.GetProperty(BooleanSwitch);

                if (property == null || property.PropertyType != typeof(bool))
                {
                    throw new ArgumentException(
                        BooleanSwitch + " is not a valid boolean property for " + validationContext.ObjectType.Name,
                        BooleanSwitch);
                }

                if ((bool) property.GetValue(validationContext.ObjectInstance, null) &&
                    (value == null || (!AllowEmptyStrings && value is string && String.IsNullOrWhiteSpace(value as string))))
                {
                    return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
                }

                return ValidationResult.Success;
            }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata,
            ControllerContext context)
        {
            object model = context.Controller.ViewData.Model;
            bool required = (bool)model.GetType().GetProperty(BooleanSwitch).GetValue(model, null);

            if (required)
            {
                yield return
                    new ModelClientValidationRequiredRule(
                        FormatErrorMessage(metadata.DisplayName ?? metadata.PropertyName));
            }
            else
            //we have to return a ModelCLientValidationRule where
            //ValidationType is not empty or else we get an exception
            //since we don't add validation rules clientside for 'notrequired'
            //no validation occurs and this works, though it's a bit of a hack
            {
                yield return
                    new ModelClientValidationRule {ValidationType = "notrequired", ErrorMessage = ""};
            }
        }
    }
}

上面的代码将在模型上查找用作验证开关的属性(IsRequired是默认值)。如果要用作开关的布尔属性设置为true,则对使用RuntimeRequiredValdiationAttribute修饰的属性执行客户端和服务器端验证。重要的是要注意,该类假定用于验证开关的模型的任何属性都不会显示给最终用户进行编辑,即这不是RequiredIf验证器。

实际上还有另一种方法可以实现ValidationAttribute以及客户端验证as outlined here。为了进行比较,我上面所做的IClientValidatable路由是outlined by the same author here

请注意这当前不适用于嵌套对象,例如,如果属性修饰了另一个对象包含的对象上的属性,则它将无效。有一些方法可以解决这个缺点,但到目前为止我没有必要这样做。

答案 1 :(得分:1)

您可以使用RemoteAttribute。这应该对服务器执行不显眼的ajax调用以验证您的数据。

答案 2 :(得分:1)

正如我在上面的评论中所说,我使用反射做了类似的事情。您可以忽略其中的一些,例如,您可能不需要字典,因为这只是一种为其提供自定义可翻译消息的方式。

服务器端代码

 private static Dictionary<string, ILocalisationToken> _requiredValidationDictionary;

 private static Dictionary<string, ILocalisationToken> RequiredValidationDictionary(UserBase model)
 {
      if (_requiredValidationDictionary != null)
          return _requiredValidationDictionary;

      _requiredValidationDictionary = new Dictionary<string, ILocalisationToken>
      {
             { model.GetPropertyName(m => m.Publication), ErrorMessageToken.PublicationRequired},
             { model.GetPropertyName(m => m.Company), ErrorMessageToken.CompanyRequired},
             { model.GetPropertyName(m => m.JobTitle), ErrorMessageToken.JobTitleRequired},
             { model.GetPropertyName(m => m.KnownAs), ErrorMessageToken.KnownAsRequired},
             { model.GetPropertyName(m => m.TelephoneNumber), ErrorMessageToken.TelephoneNoRequired},
             { model.GetPropertyName(m => m.Address), ErrorMessageToken.AddressRequired},
             { model.GetPropertyName(m => m.PostCode), ErrorMessageToken.PostCodeRequired},
             { model.GetPropertyName(m => m.Country), ErrorMessageToken.CountryRequired}
      };
      return _requiredValidationDictionary;

  }

  internal static void SetCustomRequiredFields(List<string> requiredFields, UserBase model, ITranslationEngine translationEngine)
  {
      if (requiredFields == null || requiredFields.Count <= 0) return;
      var tokenDictionary = RequiredValidationDictionary(model);
      //Loop through requiredFields and add Display text dependant on which field it is.
  foreach (var requiredField in requiredFields.Select(x => x.Trim()))
  {
      ILocalisationToken token;

      if (!tokenDictionary.TryGetValue(requiredField, out token))
         token = LocalisationToken.GetFromString(string.Format("{0} required", requiredField));

      //add to the model.
      model.RequiredFields.Add(new RequiredField
      {
         FieldName = requiredField,
         ValidationMessage = translationEngine.ByToken(token)
      });
      }
  }

  internal static void CheckForRequiredField<T>(ModelStateDictionary modelState, T fieldValue, string fieldName,                                                            IList<string> requiredFields,                                                          Dictionary<string, ILocalisationToken> tokenDictionary)
   {
        ILocalisationToken token;
        if (!tokenDictionary.TryGetValue(fieldName, out token))
           token = LocalisationToken.GetFromString(string.Format("{0} required", fieldName));
        if (requiredFields.Contains(fieldName) && (Equals(fieldValue, default(T)) || string.IsNullOrEmpty(fieldValue.ToString())))
             modelState.AddModelError(fieldName, token.Translate());
   }

  internal static void CheckForModelErrorForCustomRequiredFields(UserBase model,                                                                             Paladin3DataAccessLayer client, ICache cache,                                                                             ModelStateDictionary modelState)
  {

      var requiredFields = Common.CommaSeparatedStringToList                          (client.GetSettingValue(Constants.SettingNames.RequiredRegistrationFields, cache: cache, defaultValue: String.Empty, region: null)).Select(x => x.Trim()).ToList();
      var tokenDictionary = RequiredValidationDictionary(model);

      foreach (var property in typeof(UserBase)             .GetProperties(BindingFlags.Instance |                                               BindingFlags.NonPublic |                                               BindingFlags.Public))
      {
            CheckForRequiredField(modelState, property.GetValue(model, null), property.Name, requiredFields, tokenDictionary);
      }
  }

在模型上我们有一个List<RequiredField>,它基本上是一个有两个字符串的类,一个用于字段名,一个用于错误信息。

将模型传递到视图后,如果要执行检查服务器端,则需要一些jQuery将验证内容添加到页面中。

客户端代码

   $("#YOURFORM").validate();
        for (var x = 0; x < requiredFields.length; x++) {
            var $field = $('#' + requiredFields[x].FieldName.trim());

            if ($field.length > 0) {
                $field.rules("add", {
                      required: true,
                      messages: {
                           required: "" + requiredFields[x].ValidationMessage  
                           //required: "Required Input"
                      }
                });

            $field.parent().addClass("formRequired"); //Add a class so that the user knows its a required field before they submit

                 }

          }

道歉,如果其中任何一项不是很清楚。随意提出任何问题,我会尽力解释。

答案 3 :(得分:0)

我很长一段时间没有使用过MVC4,所以请原谅我,如果我错了,你可以使用jquery-val进行服务器端和客户端验证(如果你使用“互联网应用程序”模板已经可以使用了创建项目)和属性:

public class Device
{
    public int Id {get; set;}
    public ICollection<Setting> Settings {get; set;}
}

public class Setting
{
    [Required]
    public int Id {get; set;} 
    [Range(1,10)]
    public string Value {get; set;}
    [Required]
    public bool IsRequired {get; set;}
    public int MinLength {get; set;}
    public int MaxLength {get; set;}
}