ASP.NET MVC 4中如何在运行时动态应用验证规则?

8

我已经在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;}
}

在我看来,我会通过为每个设置集合迭代并应用其中包含的验证规则来实现运行时的客户端和服务器端验证,以达到在编译时使用DataAnnotations在我的模型上获得的相同的验证效果。在WebForms中,我只需将适当的验证器附加到相关字段,但我在MVC4中找不到类似的机制。有没有办法实现这一点?

我前几天用反射写了类似的东西。我们有一个表单,不同的人可以指定不同的必填字段。这是你想要的吗?如果是的话,我可以将代码粘贴为答案。 - Gaz Winter
@GazWinter,那听起来就像是在正确的轨道上。 - joelmdev
你根据我发送的代码有什么进展了吗? - Gaz Winter
1
@GazWinter 我正在扩展ValidationAttribute类以实现所需的效果。我很快会发布完整的代码示例。 - joelmdev
4个回答

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和客户端验证,如此处所述。作为比较,我所做的IClientValidatable路线也由同一作者在此处进行了描述
请注意,这目前无法使用嵌套对象,例如,如果该属性装饰了包含在另一个对象中的对象的属性,则它将无法工作。解决这个缺点的方案有一些选项,但迄今为止,对我来说还没有必要。

我是从这里引用您的答案的:http://stackoverflow.com/questions/20401389/find-property-value-of-complex-object-in-getclientvalidationrules/20402203?noredirect=1#comment30466813_20402203。我理解您在这里所做的事情。我过去也做过类似的事情,但您是否曾经需要动态地执行它?为什么元数据参数为空? - Fabio Milheiro
2
请注意,在代码示例中,context.Controller.ViewData.Model 是主视图的模型,这意味着如果您想在具有不同模型的子部分视图中进行检查,则此方法将无法正常工作。 - Nikolay Arhangelov
@NikolayArhangelov 你可以尝试让它工作。尝试将context.Controller.ViewData.Model切换为metadata.Container。我不确定其影响,但似乎可以工作。 - alex

3
你可以使用 RemoteAttribute。这将执行不显眼的 Ajax 调用以验证你的数据。

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

                 }

          }

如果有任何不清楚的地方,我很抱歉。随时提出问题,我会尽力解释。


0

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

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;}
}

3
如果您重新阅读问题,就会发现这不是我要找的内容。这将在编译时将验证规则分配给模型。我需要在运行时分配规则。 - joelmdev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接