I am working on a way to make custom CSLA validation rules work with the extensible validation architecture of ASP.NET MVC. I have worked out how to extend MVC to inject the client-side rules, like this:
1. Create subclass of DataAnnotationsModelValidatorProvider, overriding GetValidators():
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context,
IEnumerable<Attribute> attributes)
{
var items = attributes.ToList();
if (AddImplicitRequiredAttributeForValueTypes && metadata.IsRequired &&
!items.Any(a => a is RequiredAttribute))
items.Add(new RequiredAttribute());
var validators = new List<ModelValidator>();
foreach (var attr in items.OfType<ValidationAttribute>())
{
// custom message, use the default localization
if (attr.ErrorMessageResourceName != null && attr.ErrorMessageResourceType != null)
{
validators.Add(new DataAnnotationsModelValidator(metadata, context, attr));
continue;
}
// specified a message, do nothing
if (attr.ErrorMessage != null && attr.ErrorMessage != WorkaroundMarker)
{
validators.Add(new DataAnnotationsModelValidator(metadata, context, attr));
continue;
}
var ctx = new MessageContext(attr, metadata.ContainerType, metadata.PropertyName,
Thread.CurrentThread.CurrentUICulture);
var errorMessage = validationMessageDataSource.GetMessage(ctx);
var formattedError = errorMessage == null
? GetMissingTranslationMessage(metadata, attr)
: FormatErrorMessage(metadata, attr, errorMessage);
var clientRules = GetClientRules(metadata, context, attr,
formattedError);
validators.Add(new LocalizedModelValidator(attr, formattedError, metadata, context, clientRules));
}
if (metadata.Model is IValidatableObject)
validators.Add(new ValidatableObjectAdapter(metadata, context));
return validators;
}
2. Create a ValidatableObjectAdapter class that extends ModelValidator. This class uses 2 interfaces IValidatableObject and IClientValidationRule (a custom one) to pull the validation data from CSLA:
/// <summary>
/// Adapter which converts the result from <see cref="IValidatableObject"/> to <see cref="ModelValidationResult"/>
/// </summary>
/// <remarks>Client side validation will only work if the rules from <see cref="IValidatableObject.Validate"/>
/// implements <see cref="IClientValidationRule"/></remarks>
public class ValidatableObjectAdapter
: ModelValidator
{
public ValidatableObjectAdapter(ModelMetadata metadata, ControllerContext controllerContext)
: base(metadata, controllerContext)
{
this.metadata = metadata;
}
private readonly ModelMetadata metadata;
/// <summary>
/// Gets or sets a value that indicates whether a model property is required.
/// </summary>
/// <returns>true if the model property is required; otherwise, false.</returns>
public override bool IsRequired
{
get { return true; }
}
/// <summary>
/// When implemented in a derived class, returns metadata for client validation.
/// </summary>
/// <returns>
/// The metadata for client validation.
/// </returns>
/// <remarks>Will only work if the rules from <see cref="IValidatableObject.Validate"/> implements <see cref="IClientValidationRule"/></remarks>
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var validator = (IValidatableObject)metadata.Model;
var validationResults = validator.Validate(new ValidationContext(metadata.Model, null, null));
foreach (var validationResult in validationResults)
{
var clientRule = validationResult as IClientValidationRule;
if (clientRule == null)
continue;
var rule = new ModelClientValidationRule
{
ErrorMessage = clientRule.ErrorMessage,
ValidationType = clientRule.ValidationType
};
foreach (var kvp in clientRule.ValidationParameters)
{
rule.ValidationParameters.Add(kvp);
}
yield return rule;
}
}
/// <summary>
/// When implemented in a derived class, validates the object.
/// </summary>
/// <param name="container">The container.</param>
/// <returns>
/// A list of validation results.
/// </returns>
public override IEnumerable<ModelValidationResult> Validate(object container)
{
var validator = (IValidatableObject)metadata.Model;
var validationResults = validator.Validate(new ValidationContext(metadata.Model, null, null));
foreach (var validationResult in validationResults)
{
bool hasMemberNames = false;
foreach (var memberName in validationResult.MemberNames)
{
hasMemberNames = true;
var item = new ModelValidationResult
{
MemberName = memberName,
Message = validationResult.ErrorMessage
};
yield return item;
}
if (!hasMemberNames)
yield return new ModelValidationResult
{
MemberName = string.Empty,
Message = validationResult.ErrorMessage
};
}
}
}
Here is the custom interface:
public interface IClientValidationRule
{
/// <summary>
/// Gets complete error message (formatted)
/// </summary>
string ErrorMessage { get; set; }
/// <summary>
/// Gets or sets parameters required for the client validation rule
/// </summary>
IDictionary<string, object> ValidationParameters { get; }
/// <summary>
/// Gets client validation rule (name of the jQuery rule)
/// </summary>
string ValidationType { get; set; }
}
3. Modify CSLA to implement the 2 interfaces, IValidatableObject and IClientValidationRule.
This is where I am running into a wall. Ideally, I would like to make implementing the IClientValidationRule part of the business rule itself so that implementing this optional interface and creating a jQuery library are all that are needed to support client-side validation of CSLA rules.
However, there doesn't seem to be a way to iterate the rules that are registered using BusinessRules.AddRule() to get at those client rules to return in GetClientValidationRules(). Ideally, I would cast the rule to the interface type and return the values that are defined in the rule itself. CSLA seems to only allow access to the broken rules, which don't seem to have any reverse mapping to the rule object that created them. Is there a way to get the original rule type (or better yet, instance) from BrokenRules?
BrokenRules has an internal constructor - I tried subclassing it but its internal constructor sets a private property. Furthermore, the BrokenRules property of BusinessBase is not virtual.
Another question: Why isn't this already part of CSLA? If CSLA had its own ModelValidatorProvider out of the box, people wouldn't need to pull their hair out to do this sort of thing. A mixed bag of client and server-side validation usually isn't acceptable because the server validation rules only fire when the all of the client ones fail. Ideally, we would run them all on the client and with MVC it is possible, provided CSLA had a facility to define the metadata for those rules.