8

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 at runtime. For purposes of this question these are simplified versions of the classes I'm working with:

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

In my view I would iterate through the Settings collection with editors for each and apply the validation rules contained in each Setting instance at runtime to achieve the same client and server-side validation that that I get from using DataAnnotations on my model at compile-time. In WebForms I would have just attached the appropriate Validator to the associated field but I'm having trouble finding a similar mechanism in MVC4. Is there a way to achieve this?

joelmdev
  • 11,083
  • 10
  • 65
  • 89
  • I wrote something similar to this the other day using reflection. We have a form where different people can specify different required fields. Is that sort of thing that you are after? If so i can paste the code as an answer. – Gaz Winter Sep 20 '13 at 15:19
  • @GazWinter that sounds like it's on the right track. – joelmdev Sep 20 '13 at 15:30
  • Did you have any luck based on the code that i sent? – Gaz Winter Sep 22 '13 at 17:19
  • 1
    @GazWinter I'm actually extending the ValidationAttribute class to achieve the desired effect. I'll post a complete code example shortly. – joelmdev Sep 23 '13 at 13:00

4 Answers4

9

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.

joelmdev
  • 11,083
  • 10
  • 65
  • 89
  • I was referred to your answer from here: http://stackoverflow.com/questions/20401389/find-property-value-of-complex-object-in-getclientvalidationrules/20402203?noredirect=1#comment30466813_20402203. I understand what you did here. I did something similar in the past, but did you ever have to do it dynamically? Why is the metadata parameter null? – Fabio Milheiro Dec 05 '13 at 15:05
  • 2
    Note that in the code example, **context.Controller.ViewData.Model** is the model of the main view, which means that this won't work if you want to make the check in a child partial view with a different model. – Nikolay Arhangelov Jun 27 '14 at 15:08
  • @NikolayArhangelov You may be able to get this to work. Try switching context.Controller.ViewData.Model to metadata.Container. I'm not sure the implications, but it appears to work. – alex Jun 16 '17 at 23:03
3

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

neeKo
  • 4,280
  • 23
  • 31
1

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.

Gaz Winter
  • 2,924
  • 2
  • 25
  • 47
0

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;}
}
Xaruth
  • 4,034
  • 3
  • 19
  • 26
Adn12
  • 11
  • 3
    You'll see that this isn't what I'm looking for if you re-read the question. This assigns validation rules to the model at compile time. I need to assign rules at runtime. – joelmdev Sep 20 '13 at 15:34