If your project is rather big, often you would like your validation to reside on the business layer instead of on the UI layer. So for example, using our previous code sample, it would make perfect sense if the custom attribute code resides in your business layer project (instead of on the UI project). BUT, to enable client validation (here for example), implementing IClientValidatable is required and IClientValidatable is within the System.Web.Mvc namespace. This means that your business layer needs to reference System.Web.Mvc. But why would you want to do that - referencing a UI related reference in your business layer project?
When we look at the built-in validator (such as RequiredAttribute), how did they do this? If we look at the documentation, it is within System.ComponentModel.DataAnnotations.ValidationAttribute namespace and deriving from System.Object - there is no dependency to System.Web.Mvc at all. We can use that validation attribute in WinForm, Silverlight, WebForm, WPF, etc.
There is no magic - the ASP.NET guys simply built a validation adapter for it in the System.Web.Mvc namespace. In this post, I am going to show you how to build one for our MagicNumber validator.
So let's clarify on our namespacing issue - in our business layer, let's call that MyApp.Library project (namespace: MyApp.Library), which includes MyClass.cs (namespace: MyApp.Library or MyApp.Library.MyClass to be complete) and our custom validation attribute MagicNumberAttribute.cs (namespace: MyApp.Library or MyApp.Library.MagicNumberAttribute to be complete).
Here is the code for MyApp class:
public class MyClass { // ... more code [MagicNumber(ErrorMessage = "Sum is not correct")] public int MyData { get; set; } // ... more code }Here is our code for the custom validation attribute in MagicNumberAttribute.cs:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] sealed public class MagicNumberAttribute : ValidationAttribute { // constructor to accept the comparison property name public MagicNumberAttribute(string thirtySevenProperty) : base() { if (thirtySevenProperty == null) { throw new ArgumentNullException("thirtySevenProperty"); } ThirtySevenProperty= thirtySevenProperty; } // property to store the comparison field name public string ThirtySevenProperty{ get; private set; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { // get property info of the "ThirtySevenProperty" PropertyInfo thirtySevenPropertyInfo = validationContext.ObjectType.GetProperty(ThirtySevenProperty); // check if that property exists / valid if (thirtySevenPropertyInfo == null) { return new ValidationResult(String.Format(CultureInfo.CurrentCulture, "UNKNOWN PROPERTY", ThirtySevenProperty)); } // get the value of the property object thirtySevenPropertyValue = thirtySevenPropertyInfo.GetValue(validationContext.ObjectInstance, null); // do comparison and return validation result with error if not equal if (!Equals(value, thirtySevenPropertyValue)) { return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); } // return null if everything is ok return null; } }We'll call our ASP.NET MVC project to be MyApp.UI.Mvc (namespace: MyApp.UI.Mvc). Here is our javascript from the previous post, we'll reuse it, put it in a js file:
jQuery.validator.unobtrusive.adapters.addSingleVal("magicnumber", "other"); jQuery.validator.addMethod("magicnumber", function (val, element, other) { var modelPrefix = element.name.substr(0, element.name.lastIndexOf(".") + 1) var otherVal = $("[name=" + modelPrefix + other + "]").val(); if (val && otherVal) { return val == otherVal; } return true; );So now, instead of implementing IClientValidatable, we will be creating a validation adapter. We will put this in a file in our MyApp.UI.Mvc project, let's name it "MagicNumberAttributeAdapter.cs". The code is simple:
public class MagicNumberAttributeAdapter : DataAnnotationsModelValidatorTo make the connection between the adapter and the validation attribute itself, we need to let MVC know about it - so we need to register it inside the global.asax.cs file:{ public MagicNumberAttributeAdapter(ModelMetadata metadata, ControllerContext context, MagicNumberAttribute attribute) : base(metadata, context, attribute) { } public override IEnumerable GetClientValidationRules() { var rule = new ModelClientValidationRule { ErrorMessage = this.ErrorMessage, ValidationType = "magicnumber", }; rule.ValidationParameters.Add("other", this.Attribute.ThirtySevenProperty); yield return rule; } }
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(MagicNumberAttribute), typeof(MagicNumberAttributeAdapter));Once all those are setup, our custom validator will fire on the client-side (as well as server-side), we have clear separation of concern - where the business layer does not reference the UI as dependency, and our custom validation attribute is also reusable. Awesome!
Additional reading: