Dynamically apply validation rules at runtime with ASP.NET MVC 4

前端 未结 4 1754
遇见更好的自我
遇见更好的自我 2020-12-14 13:02

I\'ve been working in WebForms for years but I\'m fairly new to .NET\'s flavor of MVC. I am trying to figure out how to apply dynamic validation rules to members of my model

相关标签:
4条回答
  • 2020-12-14 13:03

    My solution was to extend the ValidationAttribute class and implement the IClientValidatable interface. Below is a complete example with some room for improvement:

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

    The code above will look for a property on the model to use as a switch for the validation (IsRequired is default). If the boolean property to be used as a switch is set to true, then both client and server-side validation are performed on the property decorated with the RuntimeRequiredValdiationAttribute. It's important to note that this class assumes that whatever property of the model is being used for the validation switch will not be displayed to the end user for editing, i.e. this is not a RequiredIf validator.

    There is actually another way to implement a ValidationAttribute along with client-side validation as outlined here. For comparison, the IClientValidatable route as I have done above is outlined by the same author here.

    Please note that this doesn't currently work with nested objects, eg if the attribute decorates a property on an object contained by another object, it won't work. There are some options for solving this shortcoming, but thus far it hasn't been necessary for me.

    0 讨论(0)
  • 2020-12-14 13:05

    As i said in my comment above i have done something similar using reflection. You can ignore some of it, you probably don't need the dictionary for example, as that was just a way of giving them custom translatable messages.

    Server side code:

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

    On the model we have a List<RequiredField> which is basically a class with two strings, one for the field name and one for the error message.

    Once you have passed the model into the view you need a bit of jQuery to add the validation stuff to the page if you want to do the check server side.

    Client side code:

       $("#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
    
                     }
    
              }
    

    Apologies if any of this is not very clear. Feel free to ask any questions and I will do my best to explain.

    0 讨论(0)
  • 2020-12-14 13:16

    I havent been working with MVC4 for a long time, so forgive me if i am wrong, but you can server side and client side validation using jquery-val (already available to you if you used the "internet application" template when creating your project) and attributes:

    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;}
    }
    
    0 讨论(0)
  • 2020-12-14 13:29

    You could use RemoteAttribute. This should perform unobtrusive ajax call to the server to validate your data.

    0 讨论(0)
提交回复
热议问题