If you have not read part one yet, then please do take a look as it offers a broad introduction into validation in ASP.NET MVC 3 and includes several topics that will be useful for this article.
WRITING OUR OWN CUSTOM VALIDATOR
There are four distinct parts to creating a fully functional custom validator that works on both the client and the server. First we subclass ValidationAttribute and add our server side validation logic. Next we implement IClientValidatable on our attribute to allow HTML5 data-* attributes to be passed to the client. Thirdly, we write a custom javascript function that performs validation on the client. Finally, we create an adapter to transform the HTML5 attributes into a format that our custom function can understand. Whilst this sounds like a lot of work, once you get started you will find it relatively straightforward.
SUBCLASSING VALIDATIONATTRIBUTE
In this example, we are going to write a NotEqualTo validator that simply checks that the value of one property does not equal the value of another. In effect, the opposite of the new CompareAttribute. We are going to use it on our password property to ensure that the password is not the same as the username which is a common security requirement. Below is our initial implementation:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class NotEqualToAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "{0} cannot be the same as {1}.";
public string OtherProperty { get; private set; }
public NotEqualToAttribute(string otherProperty)
: base(DefaultErrorMessage)
{
if (string.IsNullOrEmpty(otherProperty))
{
throw new ArgumentNullException("otherProperty");
}
OtherProperty = otherProperty;
}
public override string FormatErrorMessage(string name)
{
return string.Format(ErrorMessageString, name, OtherProperty);
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
if (value != null)
{
var otherProperty = validationContext.ObjectInstance.GetType()
.GetProperty(OtherProperty);
var otherPropertyValue = otherProperty
.GetValue(validationContext.ObjectInstance, null);
if (value.Equals(otherPropertyValue))
{
return new ValidationResult(
FormatErrorMessage(validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
}
If you have read the previous article and examined the code for the ValidatePasswordLengthAttribute, you will find the code very familiar. Let's examine this code in detail, starting from the top.
Our validator subclasses the built-in abstract class ValidationAttribute. The class is decorated with an AttributeUsage attribute that tell .NET how the attribute can be used. Here, the NotEqualToAttribute attribute is only permitted on properties and only a single instance of the attribute may appear on each property.
The next thing of note is the default error message constant. If we examine this value, we can see that it is in the form of a format string with placeholders for dynamic content (the name of the two properties being compared). The default error message is passed into the constructor of the base class, giving the user the option to override this default by setting either ErrorMessage or ErrorMessageResourceName and ErrorMessageResourceType which are properties on the base class. We also override FormatErrorMessage allowing us to add the two dynamic property names into the error message string.
Unlike the ValidatePasswordLengthAttribute example that we saw in part 1, our NotEqualToAttribute needs some configuration. Therefore, the name of the property that we are to check must be passed to the class constructor. We have a null check in place as this property is always required.
The next thing we need to do is actually implement the validation logic. This is done by overriding the IsValid method. It is generally a good idea to only validate if the property has a value, so we add a null check and return ValidationResult.Success if the value is null. Otherwise we access the object with validationContext.ObjectInstance and use reflection to obtain the value of the other property. Finally, we compare the two property values and return a validation failure if they match.
In some situations, you might be tempted to use the second constructor overload of ValidationResult that takes in an IEnumerable of member names. For example, you may decide that you want to display the error message on both fields being compared, so you change the code to this:
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName), new[] { validationContext.MemberName, OtherProperty });
If you run your code, you will find absolutely no difference. This is because although this overload is present and presumably used elsewhere in the .NET framework, the MVC framework completely ignores ValidationResult.MemberNames.
It is important to note that the ValidationAttribute class is not new, but the .NET 4.0 version has important differences from its' .NET 3.5 counterpart making it a much more powerful tool. The important difference that makes the .NET 4.0 version far more useful lies in the signature of the IsValid method. The .NET 3.5 method looked like this:
public abstract bool IsValid(object value);
Using this version, if you needed access to multiple properties within a validator, you had to declare the AttributeUsage as Class rather than Property and thus put the attribute on the model rather than an individual property of the model. When you did this, the value argument of the IsValid method contained the whole object instance so you could use this object to access the properties using reflection:
public override bool IsValid(object value)
{
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
return Object.Equals(originalValue, confirmValue);
}
Whilst this worked, it sets your validation error against the model rather than a property. This means that at the UI, none of the model properties would display the validation error and in fact, the error would only be displayed at all if you had added a validation summary in your view.
In .NET 4.0 we have two methods:
public virtual bool IsValid(object value);
protected virtual ValidationResult IsValid(object value, ValidationContext validationContext);
The .NET 4.0 version still has the single value method signature (for backward compatability) but it is no longer marked as abstract meaning that we can choose to override the second overload which includes a validationContext argument. This (amongst other things) gives us access to the whole model even when the validator is at the property level. The massive advantage of using a property level validator is that the error is set against the property itself instead of the class removing the requirement to use an Html.ValidationSummary. Since the validation error is set against the property correctly, your normal Html.ValidationFor helpers will pick up and display the error against the invalid form field.
That is our initial server side validation code complete. Let's add the new attribute to the password property of the RegisterModel and run the application.
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
[NotEqualTo("UserName")]
public string Password { get; set; }
If you enter the same value for username and password, then on post back you will find the server side NotEqualToAttribute validation fires and returns a validation failure to the client. Now try entering different values and you will find that after a short delay, the username validates successfully and the error message disappear. We have our server side validation working, but for a better user experience, it makes sense for our custom validator to behave in the same way as the built-in validators and offer client side validation. This is exactly what we are going to do next.
Figure 1:NotEqual Validator Server Side Validation
IMPLEMENTING ICLIENTVALIDATABLE
ASP.NET MVC 2 had a mechanism for adding client side validation but it was not very pretty. Thankfully in MVC 3, things have improved and the process is now fairly trivial and thankfully does not involve changing the Global.asax as in the previous version.
The first step is for your custom validation attribute to implement IClientValidatable. This is a simple, one method interface:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var clientValidationRule = new ModelClientValidationRule()
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "notequalto"
};
clientValidationRule.ValidationParameters.Add("otherproperty", OtherProperty);
return new[] { clientValidationRule };
}
The GetClientValidationRules method that you need to implement has one simple function. Using the metadata parameter, you are required to construct one or more ModelClientValidationRules which are returned from the method and used by the framework to output the client-side HTML5 data-* attributes that are necessary for client-side validation. Therefore, ModelClientValidationRule must contain all data necessary for validation to be performed on the client.
The ModelClientValidationRule class has three properties, two of which must always be set: ErrorMessage and ValidationType. For ErrorMessage, we simply call the same method as our server side validation making use of the ModelMetadata argument to obtain the DisplayName. ValidationType is a string unique to the type of validator and is used as a key to get the right client side validation code. There is also a ValidationParameters collection which will contain zero or more entries depending on your validator type. In this example, the client only requires one additional piece of data in order to carry out validation: the name of the other property.
Something that might catch you out is the difference between metadata.DisplayName and metadata.GetDisplayName(). The former only returns the value of a DisplayName or Display(Name=...) attribute if present on the property. If neither of these attributes is present, nothing will be returned. In our example, if we remove the DisplayAttribute from the password property, our client error message would be displayed as ' cannot be the same as UserName.' which is obviously not what we want. Therefore, always use the GetDisplayName() method instead. This has the same functionality as the property unless the attributes are not present in which case, it will default to the name of the property.
If you run the application now and view source, you will see that the password input html now contains your notequalto data attributes:
<div class="editor-field">
<input data-val="true" data-val-notequalto="Password cannot be the same as UserName."
data-val-notequalto-otherproperty="UserName"
data-val-regex="Weak password detected."
data-val-regex-pattern="^(?!password$)(?!12345$).*"
data-val-required="The Password field is required."
id="Password" name="Password" type="password" />
<span class="hint">Enter your password here</span>
<span class="field-validation-valid" data-valmsg-for="Password"
data-valmsg-replace="true"></span>
</div>
If you happen to point reflector at the MVC 3 dll, you might find that there are a number of classes in the form ModelClientValidation{Attribute Name}Rule such as ModelClientValidationRequiredRule and ModelClientValidationRegexRule. These are subclassed from the ModelClientValidationRule that we used above and just provide a constructor which sets the base values. I do not really see the benefit of this approach in our situation where reuse is not an option, but if you do want to follow this convention, then you would change the code to:
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
return new[] { new ModelClientValidationNotEqualToRule(FormatErrorMessage(metadata.DisplayName), OtherProperty) };
}
public class ModelClientValidationNotEqualToRule : ModelClientValidationRule
{
public ModelClientValidationNotEqualToRule(string errorMessage, string otherProperty)
{
ErrorMessage = errorMessage;
ValidationType = "notequalto";
ValidationParameters.Add("otherproperty", otherProperty);
}
}
That's the c# code complete. The next stage is the javascript. The good news is that since the move to jQuery and jQuery.validate, the javascript code is easier than ever.
CREATING A CUSTOM JQUERY VALIDATE FUNCTION
There are two parts to client side validation. The first part is the creation of the validation function itself and the second is writing an adapter that converts the HTML5 data-* attributes into a form that jQuery.validate can handle.
All of this code is best placed in a separate javascript file, so in our example, create a new customValidation.js file and put in the following:
(function ($) {
$.validator.addMethod("notequalto", function (value, element, params) {
if (!this.optional(element)) {
var otherProp = $('#' + params)
return (otherProp.val() != value);
}
return true;
});
$.validator.unobtrusive.adapters.addSingleVal("notequalto", "otherproperty");
} (jQuery));
Don't make the mistake of putting your custom validation functions and adapters in the jQuery document ready function. This is too late in the process. Here we are just wrapping our code in a javascript closure and passing in the jQuery object aliased as $.
Within the closure, the majority of the rest of the code (all but the last line) is the validation function. It is fairly easy to understand. First we check that the element has a value using jquery.validate's optional method. If so, we look up the other property and compare the values. The method returns true if the value is valid or false if it is invalid.
All validation functions can take in three arguments: value, element and params though you can omit params from your function if you do not require any further information other than the element being validated. If your validation function requires a single value (as in our example) then the params argument will be the value itself. On the other hand if your validation function requires multiple values, then you can access each one using the syntax param.{parametername}. For example, param.otherproperty or param.whatever. We will look at a more complicated scenario later in the article but in this case, we only require one parameter - the name of the other property.
Depending on your validation requirements, you may find that the jquery.validate library already has the code that you need for the validation itself. There are lots of validators in jquery.validate that have not been implemented or mapped to data annotations, so if these fulfil your need, then all you need to write in javascript is an adapter or even a call to a built-in adapter which can be as little as a single line. Take a look inside jquery.validate.js to find out what is available.
USING AN EXISTING JQUERY.VALIDATE.UNOBTRUSIVE ADAPTER
The job of the adapter is to read the HTML5 data-* attributes on your form element and convert this data into a form that can be understood by jquery.validate and your custom validation function. You are not required to do all the work yourself though and in many cases, you can call a built-in adapter. jquery.validate.unobtrusive declares three built-in adapters which can be used in the majority of situations. These are:
- jQuery.validator.unobtrusive.adapters.addBool - used when your validator does not need any additional data.
- jQuery.validator.unobtrusive.adapters.addSingleVal - used when your validator takes in one piece of additional data.
- jQuery.validator.unobtrusive.adapters.addMinMax - used when your validator deals with minimum and maximum values such as range or string length.
If your validator does not fit into one of these categories, you are required to write your own adapter using the jQuery.validator.unobtrusive.adapters.add method. This is not as difficulty as it sounds and we'll see an example later in the article.
Brad Wilson goes into much more detail about these adapters in his blog post Unobtrusive Client Validation in ASP.NET MVC 3
So, in our case, we make use of the addSingleVal method, passing in the name of the adapter and the name of the single value that we want to pass. Should the name of the validation function differ from the adapter, you can pass in a third parameter (ruleName):
jQuery.validator.unobtrusive.adapters.addSingleVal("notequalto", "otherproperty", "mynotequaltofunction");
At this point, our custom validator is complete. If we run the application once more, we should have our server side and client side validation working.
Figure 2:NotEqual Validator Client Side Validation
A MORE COMPLEX CUSTOM VALIDATOR
Now that we have created our NotEqualToAttribute, we can take a look at a slightly more complicated example. We are going to create a RequiredIfAttribute that allows a field to be mandatory only if another field contains a certain value. This is a common UI requirement where for example, if you tick a checkbox, then a text field must be filled in. A typical real world example is a dropdown list that contains multiple values including 'Other'. If the user selects this option, then some javascript displays a 'Please specify...' text input which is a required field.
SUBCLASSING VALIDATIONATTRIBUTE AGAIN
As before, we will start with the C# code before moving on to the javascript validator and finally joining them together with the adapter. Given that we have already been through this process already with the NotEqualToAttribute, I will not explain every line of code and will instead concentrate on the important differences between this example and the previous one.
public enum Comparison
{
IsEqualTo,
IsNotEqualTo
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
private const string DefaultErrorMessageFormatString = "The {0} field is required.";
public string OtherProperty { get; private set; }
public Comparison Comparison { get; private set; }
public object Value { get; private set; }
public RequiredIfAttribute(string otherProperty, Comparison comparison, object value)
{
if (string.IsNullOrEmpty(otherProperty))
{
throw new ArgumentNullException("otherProperty");
}
OtherProperty = otherProperty;
Comparison = comparison;
Value = value;
ErrorMessage = DefaultErrorMessageFormatString;
}
private bool Validate(object actualPropertyValue)
{
switch (Comparison)
{
case Comparison.IsNotEqualTo:
return actualPropertyValue == null || !actualPropertyValue.Equals(Value);
default:
return actualPropertyValue != null && actualPropertyValue.Equals(Value);
}
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
if (value == null)
{
var property = validationContext.ObjectInstance.GetType()
.GetProperty(OtherProperty);
var propertyValue = property.GetValue(validationContext.ObjectInstance, null);
if (!Validate(propertyValue))
{
return new ValidationResult(
string.Format(ErrorMessageString, validationContext.DisplayName));
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata,
ControllerContext context)
{
return new[]
{
new ModelClientValidationRequiredIfRule(string.Format(ErrorMessageString,
metadata.GetDisplayName()), OtherProperty, Comparison, Value)
};
}
}
public class ModelClientValidationRequiredIfRule : ModelClientValidationRule
{
public ModelClientValidationRequiredIfRule(string errorMessage,
string otherProperty,
Comparison comparison,
object value)
{
ErrorMessage = errorMessage;
ValidationType = "requiredif";
ValidationParameters.Add("other", otherProperty);
ValidationParameters.Add("comp", comparison.ToString().ToLower());
ValidationParameters.Add("value", value.ToString().ToLower());
}
}
The C# code is very similar to the previous validator. The main difference is that the constructor takes in three parameters instead of one. These parameters are subsequently used in server side validation and also returned from GetClientValidationRules in a custom ModelClientValidationRule built as a container object for all the data that should be rendered as HTML5 data-* attributes. I am not going to say anything further about this code as it is fairly readable and follows exactly the sanme pattern as the NotEqualAttribute that we created earlier in the article.
Before we look at the javascript side of things, we need to change our model to use the new validator. In the existing RegisterModel class, there is really nowhere that the validator can be added, so let's make a couple of modifications. Firstly we are going to add two new fields to the model. HasPromotionalCode is a boolean and will map to a checkbox. If the user indicates that they do have a promotional code, then we want the PromotionalCode string property to be required.
[Display(Name = "Promotional code?")]
public bool HasPromotionalCode { get; set; }
[Display(Name = "Promotional code")]
[RequiredIf("HasPromotionalCode", Comparison.IsEqualTo, true)]
public string PromotionalCode { get; set; }
Next we change our view to display the two new properties:
<div class="editor-label">
@Html.LabelFor(m => m.PromotionalCode)
</div>
<div class="editor-field">
@Html.TextBoxFor(m => m.PromotionalCode)
@Html.ValidationMessageFor(m => m.PromotionalCode)
</div>
Whilst this works, if we want to create a really friendly UI then we might want to hide the PromotionalCode text input until the user checks the HasPromotionalCode field. This is very easy to achieve with jQuery. We'll just wrap the label and text input in a hidden div and add some javascript to show/hide the div when the checkbox value changes.
<div id="promotionalCodeDiv" style="display: none;">
<div class="editor-label">
@Html.LabelFor(m => m.PromotionalCode)
</div>
<div class="editor-field">
@Html.TextBoxFor(m => m.PromotionalCode)
@Html.ValidationMessageFor(m => m.PromotionalCode)
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
$('#HasPromotionalCode').click(function () {
$('#promotionalCodeDiv').toggle();
});
});
</script>
If you run the application at this point, you will find all your fields present and correct. Let's move on to the javascript validation code now which needs a little more explanation.
CREATING ANOTHER CUSTOM JQUERY VALIDATE FUNCTION
jQuery.validator.addMethod("requiredif", function (value, element, params) {
if ($(element).val() != '') return true
var $other = $('#' + params.other);
var otherVal = ($other.attr('type').toUpperCase() == "CHECKBOX") ?
($other.attr("checked") ? "true" : "false") : $other.val();
return params.comp == 'isequalto' ? (otherVal != params.value)
: (otherVal == params.value);
});
CREATING A JQUERY.VALIDATE.UNOBTRUSIVE ADAPTER
As our client validation method requires three different values, we cannot use any of the built-in adapters this time and must write our own version. As previously mentioned, the unobtrusive validation library contains an add method that we can use to register our adapter.
jQuery.validator.unobtrusive.adapters.add("requiredif", ["other", "comp", "value"],
function (options) {
options.rules['requiredif'] = {
other: options.params.other,
comp: options.params.comp,
value: options.params.value
};
options.messages['requiredif'] = options.message;
}
);
It is not obvious what is going on here until I explain to you that the job of the adapter is two-fold. Firstly it must map parameters from options.params.* to options.rules['rulename'] and secondly it must map the error message from options.message to options.messages['rulename']. There is often more to it than that, but as Brad Wilson has already done a comprehensive post on the intricacies of unobtrusive adapters, there is no point in me repeating it here so I suggest you head over there to fill in the gaps.
We are finished and now have a RequiredIf validation attribute that validates input at the client using javascript and at the server, just like the built-in validators. This provides the ultimate in user experience with immediate feedback as soon as you tab out of a field, plus the security of the server side checks which cannot be circumvented.
Figure 3:The Completed RequiredIf Validator
To show that adapters are not always so simple, imagine a scenario where our requirements are not as advanced and we just want the RequiredIf validator to check for the presence of any value in another field. In this case, we would not need a custom validation function at all and would only be required to write an adapter. JQuery.validate's built-in required function takes in a dependency expression. Typically we pass true which means that a field is always required but we can optionally pass a selection filter:
jQuery.validator.unobtrusive.adapters.add("requiredif", ["other"], function (options) {
var $other = $('#' + options.params.other);
options.rules['required'] = "#" + options.params.other + (($other.attr('type').toUpperCase() == "CHECKBOX") ?
":checked" : ":filled");
options.messages['required'] = options.message;
});
In this situation, the adapter needs to map the server generated data-val-requiredif-other attribute to the selection filter that the required validator can accept. We check if the other field is a checkbox. If so, we generate a selection filter in the format#otherpropertyname:checked, otherwise we use #otherpropertyname:filled. See the jQuery.validate required documentation for more information about selection filters. I'll leave it as an exercise for the reader to make this a bit more sophisticated!
EXPLORING ALTERNATIVES WITH IVALIDATABLEOBJECT
ASP.NET MVC 3 also introduces an alternative mechanism for validation involving multiple properties: IValidatableObject. This interface contains a single method:
IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
Given that this interface is implemented by the viewmodel itself, you have direct access to all model properties making your code even more straightforward than the subclassing of ValidationAttribute approach that we used above. Instead of using our custom NotEqualToAttribute, we can achieve the same validation functionality on the server by implementing IValidatableObject:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (UserName == Password)
{
yield return new ValidationResult("Password cannot be the same as username", new[] { "Password" });
}
}
This definitely results in more readable and more concise code. IValidatableObject looks especially useful for one-off validation situations where re-use is never going to happen - where creating a specific validation attribute seems like overkill.
Unfortunately, there are a couple of reasons why you might want to think twice before using IValidatableObject:
TWO STAGE VALIDATION
If you wish mix data annotations with IValidatableObject which I imagine would be a very common scenario, it is important to know that your IValidatableObject validation will only be called if your data annotations validation validates successfully. This can result in a rather poor user experience:
- a) user submits form
- b) a single validation error is displayed (data annotations)
- c) user corrects mistake and resubmits.
- d) a different, totally unrelated error is displayed (IValidatableObject).
- e) user corrects second mistake and resubmits
- f) success
When this experience is compared with the alternative (ValidationAttribute) approach where all validation errors are returned in one go, you may find it somewhat lacking. You may be wondering if this is relevant if you have enabled client-side validation. More bad news I am afraid...
CLIENT-SIDE VALIDATION
Unlike the simple IClientValidatable implementation required for the ValidationAttribute method, I have not found any way of enabling client-side validation when using IClientValidatable. This was recently confirmed by a couple of guys at Microsoft. Without client-side validation, I simply cannot see IValidatableObject being used for the vast majority of validation scenarios. If you can replicate the validation logic on the client, you would subclass ValidationAttribute. Otherwise, you would use the RemoteAttribute that we discussed in part 2 of this series. Both of these approaches result in immediate feedback for the user which is simply not possible with IValidatableObject.
CODE DOWNLOAD
You can now download all the code from this article here.
CONCLUSION
In our second part of this series on MVC 3 validation, we have built several custom validators that validate user input both at the client (using JavaScript) and at the server. We have dealt with scenarios where we have utilised existing validation functions and adapters. We have also developed totally custom validators where we were required to write everything ourselves. We also briefly looked at an alternative to custom validation attributes - IValidatableObject. We loved the clean server side code that we could use with this approach, but were sorely disappointed by the two-stage validation process and the lack of client-side integration. Have I missed anything out? Almost certainly, so there is something related to MVC 3 validation that I did not cover, please get in touch and let me know and I will address it in a future post. I hope that you have found this series helpful. We will be back with more in-depth .NET articles soon. To ensure that you do not miss out on future posts, why not subscribe to the DevTrends RSS Feed in your favourite RSS reader.
No comments:
Post a Comment