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.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…