Techniques for extending the MVC 2 framework
|125843 MVC 2 Validation Samples.zip|
Developers are commonly familiar with the validator control set available in the .NET framework, such as the RequiredFieldValidator, CustomValidator, and so on. These validator controls set up input validation in web forms very quickly, protecting the server from potential malicious or invalid user input. ASP.NET added a similar feature, but using its own flavor.
ASP.NET MVC 2 features extends the existing validation components of MVC 1.0 to create a pretty comprehensive validation package, which validates user input on both the client and server. The validation framework includes the new syntaxes of MVC 2.0, leveraging the lambda expression statements for identifying properties within the model. It also shifts from an input validation approach to a model validation approach, utilizing the model as the source of the validation, for the object's validation needs. Typically, data annotations define the validations you want to specify at the global level, as we'll see in this article. For more information on this, Brad Wilson has a great write-up available here.
This is a great new feature because the validation doesn't have to be embedded in the UI, and the validation components don't need to explicitly define the validations. For instance, the validation component doesn't explicitly define the rules to validate. In using Web Forms as an example, it doesn't explicitly define a RequiredFieldValidator, a RegularExpressionValidator, and such.
How Validation Works
Let's begin by looking at how the validation within the framework works by creating a simple example, shown in Figure 1. The following sample form sets up validation using the new MVC 2.0 constructs. These validations only get applied when the form posts back to the server. If invalid, the message for the validation appears in the validation summary, and the asterisk is injected at the spot of the rendering.
Note how the text box and the validation message both reference the same property within the model; the validation message does not necessarily link to the text box as the validator controls did in web forms. Rather, the validation message specifies the placeholder to put an error message when the validation fails and the metadata to use for the validation. You may be wondering how this linkage occurs.
Model binding is a pretty powerful and convenient feature in the MVC framework. When the submit button posts the form back to the server, the model binding process validates the data posted back against any validations specified on the model. The source of the validation rules is tucked away within a group of data annotations, or declarations of the requirements for the data used for this model. For example, check out the validation rules applied for our test class in Figure 2. All the fields in this example are required, whereas the email address goes an extra mile because it needs to ensure that the data is in a proper format.
When invalid data is posted to the server, the framework appends the errors to the ModelState object, a dictionary that stores the errors that have occurred. Its IsValid property is the accurate determination of whether the page is in a valid state (similar to Page.IsValid in web forms). From this, you can make an accurate decision of what needs to happen regarding the action that needs to take place, as shown in Figure 3.
Figure 4 adds a new script file that we need to make the application work on the client, plus a call to the new EnableClientValidation method. This new method performs some extra work behind the scenes to prep the UI, as client-side validation has some needs (supplying a unique ID for every form element in the view and others). If one of these two options is missing, some interesting errors may occur.
Note: It's important to call EnableClientValidation before the declaration of the form, as this routine must perform some extra setup work on every form within the view to ensure the validation happens smoothly; otherwise, you may experience an error.
Client-side validation hooks into the submission of the form (form submit event on the client). The client validation only allows the server to post back if its validations succeed; otherwise, the validation summary displays the errors immediately. Otherwise, if the input is OK, the submit event occurs as normal, the data posts back to the server, and another validation happens on the server.
This process works seamlessly, and the validation components are well integrated. But this begs the question: How do we customize this process? The process is much more than just adding an attribute definition, a script file, and hoping everything works out OK. The truth is there are several components involved making these features integrate out of the box, and it is possible for you to create your own customized components to handle your own custom business-validation needs.
Creating Custom Validation Components
To illustrate, let's set up an attribute for validating a ZIP code as in Figure 5. A ZIP code is five digits or nine digits with a dash; the validation can be configured to require or ignore the four-digit ZIP code extension by setting RequireExtension to the appropriate value. Note the base class it inherits from, as this is common among all validation attributes.
Although not all data annotations use the ValidationAttribute base class, attributes specific to the validation feature do. The key piece to this component is the IsValid method, which makes the server-side determination of whether a feature is valid. This implementation uses regular expressions to check the ZIP code, but you can use whatever mechanism you like. This base class also has some other key properties, such as the ErrorMessage property that stores the message to display when the validation fails.
Let's look at how we use this attribute, in the sample model shown in Figure 6. It's important to note that the information you pass to the attribute's properties is static. You can see how that works in this example; for instance, we are passing in true/false statically in the attribute definition, not by using a method call or a variable assignment. However, attributes can instantiate objects within the class or access static classes.
For instance, our IsValid implementation can use the static Regex.IsMatch method because it's a static method. We could also access the HTTP context via HttpContext.Current because, again, it's static. We can instantiate an object and invoke its method because that instance is local to our ZipCodeAttribute. However, you cannot directly pass controller variable references to the attribute, or any other local object, to affect how the process works. You can, however, store these important objects in the items collection in the current context (HttpContext.Current.Items, a dictionary) and retrieve them from this collection within the attribute.
The issue with the Items collection is timing; because Items is short-lived (not persisted across requests to the server), you have to ensure the code within the attribute runs after the assignment to the items collection, which you often can't do because of the MVC framework's architecture. The ViewData or TempData may be valid options, too, but there are similar concerns with these collections.
The validation feature requires additional customization support; the feature also needs a model validator. Since we are creating a custom data annotations validator, a special base class of the model validator attaches to the attribute and manages the client and server integration, as shown in Figure 7. There are other uses for this, too, but here we need only its GetClientValidationRules method to handle the client-server integration.
Other than specifying the type of attribute we need through the generic argument, the GetClientValidationRules method is important because does a few things. First, it passes the error message from the attribute (or uses a default message if null) to the ModelClientValidationRule object, as well as a unique identifier for the validation type. This unique identifier is important because it's used to identify the correct validation type on the client side. Additionally, any parameters to pass to the client are specified here.
If you are familiar with the ASP.NET AJAX framework, you will see some parallels of this process to the script control or extender description process, which is a process to identify the client-side object and define any parameters to pass from the server to the client. The validation type is almost equivalent to defining the control type, whereas the validation parameters are equivalent to describing the properties of an AJAX control.
The last piece to the validation component, if we want the client-side validation integration, requires a client-side component using the Microsoft ASP.NET AJAX library. This component must follow a definition implemented by the other client-side validation components. Checking out the MicrosoftMvcValidation.debug.js script in an MVC 2 project is helpful; this feature has a specialized architecture. My implementation mimics Microsoft's approach to designing these components.
Every validator defines a constructor, and the constructor is where any of the validation parameters are passed to. Here we take the requireExtension parameter as specified in our validation attribute and described in the model validator. Additionally, the framework uses a static create method to instantiate the component. The rule is a JsonValidationRule containing the rule parameters/information. All this is illustrated in Figure 8.
The interesting part is how the create method finishes; it returns a delegate that points to the custom validator's validate method. If you are unfamiliar with the createDelegate method syntax, the first parameter specifies the context of the method call (the object available in the method's body via the "this" pointer), followed by the method to invoke when the delegate is executed. This way, the caller can work with the delegate directly and not worry about the actual invocation details.
The body of the prototype contains only one required method validate, the important method that the previously mentioned delegate points to. Outside of this method, everything else you see are supporting properties and methods I created. On the client, we use the same approach for validation; a regular expression is executed to see whether the input matches the content. The MVC components contain some nice helpers for checking whether the array of matches returned from the regular expression check have any matches, as shown in Figure 9.
That's one of the inherent problems with client-to-server integration: Often we find ourselves duplicating the logic to work on both, not being able to integrate the two processes. But I digress.
Finally, we need a way to tell the in-built ValidatorRegistry component (also in the MVC validation script) to use our custom validator. The way to do that is to attach a new reference of our ZIP code component to its validators collection; the statement in Figure 10 adds our validator in a client-side reference to zipCode.
Figure 10: Registering the custom validator on the client
Sys.Mvc.ValidatorRegistry.validators\\["zipCode"\\] = Function.createDelegate(null, Nucleo.Validators.ZipCodeValidator.create);
This is an important statement, because it matches the name and casing of the ValidationType property of the object returned from the ZipCodeModelValidator class. Casing is very important as it has to match the case exactly; copy and paste for safety's sake.
Note we defined another delegate, to point to the create method. This links up the static create method to the associated validator. This is how the framework requires the setup to be.
There is one final hook up to make everything work together. In the Application_Start method in the global.asax file, we need to register these new components. The DataAnnotationsValidationProvider contains some static containers for storing the validation components in a collection, as shown in Figure 11.
This RegisterAdapter method registers the attribute with the provider. Although the two are linked through the ZipCodeModelValidator, RegisterAdapter still requires defining both types.
Now we can finally use our new validation component. A sample form is shown in Figure 12, one requiring a nine-digit ZIP code and one requiring a five-digit ZIP code.
Check out the downloadable sample code accompanying this article to see how it works. (Click the Download the code link at the beginning of this article.) The validation component prevents a submission to the server when the data is in an invalid state, and re-evaluates the data on the server.
Putting It All Together
Although there are a lot of moving parts, and a tight integration, once you understand how the pieces integrate, the process is easy to implement.
Brian Mains (firstname.lastname@example.org) is a Microsoft MVP and consultant with Computer Aid, where he works with nonprofit and state government organizations.