Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
264 views
Welcome To Ask or Share your Answers For Others

1 Answer

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...