Data validation is a very important part of any enterprise application. ASP.NET has a validation framework but it is very limited in scope and starts falling apart as soon as you need to perform more complex validations. Problems with the out of the box ASP.NET validation framework are well documented by Peter Blum on his web site, so we are not going to repeat them here. Peter has also built a nice replacement for the standard ASP.NET validation framework, which is worth looking into if you prefer the standard ASP.NET validation mechanism to the one offered by Spring.NET for some reason. Both frameworks will allow you to perform very complex validations but we designed the Spring.NET validation framework differently for the reasons described below.
On the Windows Forms side the situation is even worse. Out of the box data validation features are completely inadequate as pointed out by Ian Griffiths in this article. One of the major problems we saw in most validation frameworks available today, both open source and commercial, is that they are tied to a specific presentation technology. The ASP.NET validation framework uses ASP.NET controls to define validation rules, so these rules end up in the HTML markup of your pages. Peter Blum's framework uses the same approach. In our opinion, validation is not applicable only to the presentation layer so there is no reason to tie it to any particular technology. As such, the Spring.NET Validation Framework is designed in a way that enables data validation in different application layers using the same validation rules.
The goals of the validation framework are the following:
-
Allow for the validation of any object, whether it is a UI control or a domain object.
-
Allow the same validation framework to be used in both Windows Forms and ASP.NET applications, as well as in the service layer (to validate parameters passed to the service, for example).
-
Allow composition of the validation rules so arbitrarily complex validation rule sets can be constructed.
-
Allow validators to be conditional so they only execute if a specific condition is met.
The following sections will describe in more detail how these goals were achieved and show you how to use the Spring.NET Validation Framework in your applications.
Decoupling validation from presentation was the major goal that significantly influenced design of the validation framework. We wanted to be able to define a set of validation rules that are completely independent from the presentation so we can reuse them (or at least have the ability to reuse them) in different application layers. This meant that the approach taken by Microsoft ASP.NET team would not work and custom validation controls were not an option. The approach taken was to configure validation rules just like any other object managed by Spring - within the application context. However, due to possible complexity of the validation rules we decided not to use the standard Spring.NET configuration schema for validator definitions but to instead provide a more specific and easier to use custom configuration schema for validation. Note that the validation framework is not tied to the use of XML, you can use its API Programatically. The following example shows validation rules defined for the Trip object in the SpringAir sample application:
<objects xmlns="http://www.springframework.net" xmlns:v="http://www.springframework.net/validation"> <object type="TripForm.aspx" parent="standardPage"> <property name="TripValidator" ref="tripValidator" /> </object> <v:group id="tripValidator"> <v:required id="departureAirportValidator" test="StartingFrom.AirportCode"> <v:message id="error.departureAirport.required" providers="departureAirportErrors, validationSummary"/> </v:required> <v:group id="destinationAirportValidator"> <v:required test="ReturningFrom.AirportCode"> <v:message id="error.destinationAirport.required" providers="destinationAirportErrors, validationSummary"/> </v:required> <v:condition test="ReturningFrom.AirportCode != StartingFrom.AirportCode" when="ReturningFrom.AirportCode != ''"> <v:message id="error.destinationAirport.sameAsDeparture" providers="destinationAirportErrors, validationSummary"/> </v:condition> </v:group> <v:group id="departureDateValidator"> <v:required test="StartingFrom.Date"> <v:message id="error.departureDate.required" providers="departureDateErrors, validationSummary"/> </v:required> <v:condition test="StartingFrom.Date >= DateTime.Today" when="StartingFrom.Date != DateTime.MinValue"> <v:message id="error.departureDate.inThePast" providers="departureDateErrors, validationSummary"/> </v:condition> </v:group> <v:group id="returnDateValidator" when="Mode == 'RoundTrip'"> <v:required test="ReturningFrom.Date"> <v:message id="error.returnDate.required" providers="returnDateErrors, validationSummary"/> </v:required> <v:condition test="ReturningFrom.Date >= StartingFrom.Date" when="ReturningFrom.Date != DateTime.MinValue"> <v:message id="error.returnDate.beforeDeparture" providers="returnDateErrors, validationSummary"/> </v:condition> </v:group> </v:group> </objects>
There are a few things to note in the example above:
-
You need to reference the validation schema by adding a
xmlns:v="http://www.springframework.net/validation"
namespace declaration to the root element. -
You can mix standard object definitions and validator definitions in the same configuration file as long as both schemas are referenced.
-
The Validator defined in the configuration file is identified by and id attribute and can be referenced in the standard Spring way, i.e. the injection of tripValidator into TripForm.aspx page definition in the first <object> tag above.
-
The validation framework uses Spring's powerful expression evaluation engine to evaluate both validation rules and applicability conditions for the validator. As such, any valid Spring expression can be specified within the test and when attributes of any validator.
The example above shows many of the features of the framework, so let's discuss them one by one in the following sections.
Validators can be grouped together. This is important for many reasons but the most typical usage scenario is to group multiple validation rules that apply to the same value. In the example above there is a validator group for almost every property of the Trip instance. There is also a top-level group for the Trip object itself that groups all other validators.
There are three types of validator groups each with a different behavior:
While the first type (AND) is definitely the most useful, the other two allow you to implement some specific validation scenarios in a very simple way, so you should keep them in mind when designing your validation rules.
Type | XML Tag | Behavior |
---|---|---|
AND | group | Returns true only if all contained validators return true. This is the most commonly used validator group. |
OR | any | Returns true if one or more of the contained validators return true. |
XOR | exclusive | Returns true if only one of the contained validators return true. |
One thing to remember is that a validator group is a validator like any other and can be used anywhere validator is expected. You can nest groups within other groups and reference them using validator reference syntax (described later), so they really allow you to structure your validation rules in the most reusable way.
Ultimately, you will have one or more validator definitions for each piece of data that you want to validate. Spring.NET has several built-in validators that are sufficient for most validations, even fairly complex ones. The framework is extensible so you can write your own custom validators and use them in the same way as the built-in ones.
The condition validator evaluates any logical expression that is supported by Spring's evaluation engine. The syntax is
<v:condition id="id" test="testCondition" when="applicabilityCondition" parent="parentValidator"> actions </v:condition>
An example is shown below
<v:condition test="StartingFrom.Date >= DateTime.Today" when="StartingFrom.Date != DateTime.MinValue"> <v:message id="error.departureDate.inThePast" providers="departureDateErrors, validationSummary"/> </v:condition>
In this example the StartingFrom property of the Trip object is compared to see if it is later than the current date, i.e. DateTime but only when the date has been set (the initial value of StartingFrom.Date was set to DateTime.MinValue).
The condition validator could be considered "the mother of all validators". You can use it to achieve almost anything that can be achieved by using other validator types, but in some cases the test expression might be very complex, which is why you should use more specific validator type if possible. However, condition validator is still your best bet if you need to check whether particular value belongs to a particular range, or perform a similar test, as those conditions are fairly easy to write.
Note | |
---|---|
Keep in mind that Spring.NET Validation Framework typically works with domain objects. This is after data binding from the controls has been performed so that the object being validated is strongly typed. This means that you can easily compare numbers and dates without having to worry if the string representation is comparable. |
This validator ensures that the specified test value is not empty. The syntax is
<v:required id="id" test="requiredValue" when="applicabilityCondition" parent="parentValidator"> actions </v:required>
An example is shown below
<v:required test="ReturningFrom.AirportCode"> <v:message id="error.destinationAirport.required" providers="destinationAirportErrors, validationSummary"/> </v:required>
The specific tests done to determine if the required value is set is listed below
System.Type | Test | |
---|---|---|
System.Type | Type exists | |
System.String | not null or an empty string | |
system.DateTime |
| |
One of the number types. |
| |
System.Char |
| |
Any reference type other than System.String | not null |
Required validator is also one of the most commonly used ones, and
it is much more powerful than the ASP.NET Required validator, because it
works with many other data types other than strings. For example, it
will allow you to validate DateTime
instances (both
MinValue
and MaxValue
return
false
), integer and decimal numbers, as well as any
reference type, in which case it returns true
for a
non-null value and false
for
{{null}}
s.
The test attribute for the required validator will typically specify an expression that resolves to a property of a domain object, but it could be any valid expression that returns a value, including a method call.
The syntax is
<v:regex id="id" test="valueToEvaluate" when="applicabilityCondition" parent="parentValidator"> <v:property name="Expression" value="regularExpressionToMatch"/> <v:property name="Options" value="regexOptions"/> actions </v:regex>
An example is shown below
<v:regex test="ReturningFrom.AirportCode"> <v:property name="Expression" value="[A-Z][A-Z][A-Z]"/> <v:message id="error.destinationAirport.threeCharacters" providers="destinationAirportErrors, validationSummary"/> </v:regex>
Regular expression validator is very useful when validating values that need to conform to some predefined format, such as telephone numbers, email addresses, URLs, etc.
One major difference of the regular expression validator compared
to other built-in validator types is that you need to set a required
Expression
property to a regular expression to match
against.
The syntax is
<v:validator id="id" test="requiredValue" when="applicabilityCondition" type="validatorType" parent="parentValidator"> actions </v:validator>
An example is shown below
<v:validator test="ReturningFrom.AirportCode" type="MyNamespace.MyAirportCodeValidator, MyAssembly"> <v:message id="error.destinationAirport.invalid" providers="destinationAirportErrors, validationSummary"/> </v:required>
Generic validator allows you to plug in your custom validator by
specifying its type name. Custom validators are very simple to
implement, because all you need to do is extend
BaseValidator
class and implement abstract
bool Validate(object objectToValidate)
method. Your
implementation simply needs to return true
if it
determines that object is valid, or false
otherwise
As you can see from the examples above, each validator (and validator group) allows you to define its applicability condition by specifying a logical expression as the value of the when attribute. This feature is very useful and is one of the major deficiencies in the standard ASP.NET validation framework, because in many cases specific validators need to be turned on or off based on the values of the object being validated.
For example, when validating a Trip object we need to validate return date only if the Trip.Mode property is set to the TripMode.RoundTrip enum value. In order to achieve that we created following validator definition:
<v:group id="returnDateValidator" when="Mode == 'RoundTrip'"> // nested validators </v:group>
Validators within this group will only be evaluated for round trips.
Note | |
---|---|
You should also note that you can compare enums using the string value of the enumeration. You can also use fully qualified enum name, such as:
However, in this case you need to make sure that alias for the TripMode enum type is registered using Spring's standard type aliasing mechanism. |
Validation actions are executed every time the containing validator is executed. They allow you to do anything you want based on the result of the validation. By far the most common use of the validation action is to add validation error message to the errors collection, but theoretically you could do anything you want. Because adding validation error messages to the errors collection is such a common scenario, Spring.NET validation schema defines a separate XML tag for this type of validation action.
The syntax is
<v:message id="messageId" providers="errorProviderList" when="messageApplicabilityCondition"> <v:param value="paramExpression"/> </v:message>
An example is shown below
<v:message id="error.departureDate.inThePast" providers="departureDateErrors, validationSummary"> <v:param value="StartingFrom.Date.ToString('D')"/> <v:param value="DateTime.Today.ToString('D')"/> </v:message>
There are several things that you have to be aware of when dealing with error messages:
-
id
is used to look up the error message in the appropriate Spring.NET message source. -
providers
specifies a comma separated list of "error buckets" particular error message should be added to. These "buckets" will later be used by the particular presentation technology in order to display error messages as necessary. -
a message can have zero or more parameters. Each parameter is an expression that will be resolved using current validation context and the resolved values will be passed as parameters to
IMessageSource.GetMessage
method, which will return the fully resolved message.
The syntax is
<v:action type="actionType" when="actionApplicabilityCondition"> properties </v:action>
An example is shown below
<v:action type="Spring.Validation.Actions.ExpressionAction, Spring.Core" when="#page != null"> <v:property name="Valid" value="#page.myPanel.Visible = true"/> <v:property name="Invalid" value="#page.myPanel.Visible = false"/> </v:action>
Generic actions can be used to perform all kinds of validation
actions. In simple cases, such as in the example above where we turn
control's visibility on or off depending on the validation result, you
can use the built-in ExpressionAction
class and
simply specify expressions to be evaluated based on the validator
result.
In other situations you may want to create your own action
implementation, which is fairly simple thing to do – all you need to do
is implement IValidationAction
interface:
public interface IValidationAction { /// <summary> /// Executes the action. /// </summary> /// <param name="isValid">Whether associated validator is valid or not.</param> /// <param name="validationContext">Validation context.</param> /// <param name="contextParams">Additional context parameters.</param> /// <param name="errors">Validation errors container.</param> void Execute(bool isValid, object validationContext, IDictionary contextParams, ValidationErrors errors); }
Sometimes it is not possible (or desirable) to nest all the validation rules within a single top-level validator group. For example, if you have an object graph where both ObjectA and ObjectB have a reference to ObjectC, you might want to set up validation rules for ObjectC only once and reference them from the validation rules for both ObjectA and ObjectB, instead of duplicating them within both definitions.
The syntax is shown below
<v:ref name="referencedValidatorId" context="validationContextForTheReferencedValidator"/>
An example is shown below
<v:group id="objectA.validator"> <v:ref name="objectC.validator" context="MyObjectC"/> // other validators for ObjectA </v:group> <v:group id="objectB.validator"> <v:ref name="objectC.validator" context="ObjectCProperty"/> // other validators for ObjectB </v:group> <v:group id="objectC.Validator"> // validators for ObjectC </v:group>
It is as simple as that — you define validation rules for ObjectC separately and reference them from within other validation groups. Important thing to realize that in most cases you will also want to "narrow" the context for the referenced validator, typically by specifying the name of the property that holds referenced object. In the example above, ObjectA.MyObjectC and ObjectB.ObjectCProperty are both of type ObjectC, which objectC.validator expects to receive as the validation context.
You can also create Validators programmatically using the API. An example is shown below
UserInfo userInfo = new UserInfo(); // has Name and Password props ValidatorGroup userInfoValidator = new ValidatorGroup(); userInfoValidator.Validators .Add(new RequiredValidator("Name", null)); userInfoValidator.Validators .Add(new RequiredValidator("Password", null)); ValidationErrors errors = new ValidationErrors(); bool userInfoIsValid = userInfoValidator.Validate(userInfo, errors);
No matter if you create your validators programmatically or declaratively, you can invoke them in service side code via the 'Validate' method shown above and then handle error conditions. Spring provides AOP parameter validation advice as part of ithe aspect library which may also be useful for performing server-side validation.
Now that you know how to configure validation rules, let's see what it takes to evaluate those rules within your typical ASP.NET application and to display error messages.
The first thing you need to do is inject validators you want to use into your ASP.NET page, as shown in the example below:
<objects xmlns="http://www.springframework.net" xmlns:v="http://www.springframework.net/validation"> <object type="TripForm.aspx" parent="standardPage"> <property name="TripValidator" ref="tripValidator" /> </object> <v:group id="tripValidator"> // our validation rules </v:group> </objects>
Once that's done, you need to perform validation in one or more of the page event handlers, which typically looks similar to this:
public void SearchForFlights(object sender, EventArgs e) { if (Validate(Controller.Trip, tripValidator)) { Process.SetView(Controller.SearchForFlights()); } }
Note | |
---|---|
Keep in mind that your ASP.NET page needs to extend Spring.Web.UI.Page in order for the code above to work. |
Finally, you need to define where validation errors should be
displayed by adding one or more
<spring:validationError/>
and
<spring:validationSummary/>
controls to the
ASP.NET form:
<%@ Page Language="c#" MasterPageFile="~/Web/StandardTemplate.master" Inherits="TripForm" CodeFile="TripForm.aspx.cs" %> <%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %> <asp:Content ID="head" ContentPlaceHolderID="head" runat="server"> <script language="javascript" type="text/javascript"> <!-- function showReturnCalendar(isVisible) { document.getElementById('<%= returningOnDate.ClientID %>').style.visibility = isVisible? '': 'hidden'; document.getElementById('returningOnCalendar').style.visibility = isVisible? '': 'hidden'; } --> </script> </asp:Content> <asp:Content ID="body" ContentPlaceHolderID="body" runat="server"> <div style="text-align: center"> <h4><asp:Label ID="caption" runat="server"></asp:Label></h4> <spring:ValidationSummary ID="validationSummary" runat="server" /> <table> <tr class="formLabel"> <td> </td> <td colspan="3"> <spring:RadioButtonGroup ID="tripMode" runat="server"> <asp:RadioButton ID="OneWay" onclick="showReturnCalendar(false);" runat="server" /> <asp:RadioButton ID="RoundTrip" onclick="showReturnCalendar(true);" runat="server" /> </spring:RadioButtonGroup> </td> </tr> <tr> <td class="formLabel" align="right"> <asp:Label ID="leavingFrom" runat="server" /></td> <td nowrap="nowrap"> <asp:DropDownList ID="leavingFromAirportCode" AutoCallBack="true" runat="server" /> <spring:ValidationError id="departureAirportErrors" runat="server" /> </td> <td class="formLabel" align="right"> <asp:Label ID="goingTo" runat="server" /></td> <td nowrap="nowrap"> <asp:DropDownList ID="goingToAirportCode" AutoCallBack="true" runat="server" /> <spring:ValidationError id="destinationAirportErrors" runat="server" /> </td> </tr> <tr> <td class="formLabel" align="right"> <asp:Label ID="leavingOn" runat="server" /></td> <td nowrap="nowrap"> <spring:Calendar ID="leavingFromDate" runat="server" Width="75px" AllowEditing="true" Skin="system" /> <spring:ValidationError id="departureDateErrors" runat="server" /> </td> <td class="formLabel" align="right"> <asp:Label ID="returningOn" runat="server" /></td> <td nowrap="nowrap"> <div id="returningOnCalendar"> <spring:Calendar ID="returningOnDate" runat="server" Width="75px" AllowEditing="true" Skin="system" /> <spring:ValidationError id="returnDateErrors" runat="server" /> </div> </td> </tr> <tr> <td class="buttonBar" colspan="4"> <br/> <asp:Button ID="findFlights" runat="server"/></td> </tr> </table> </div> <script language="javascript" type="text/javascript"> if (document.getElementById('<%= tripMode.ClientID %>').value == 'OneWay') showReturnCalendar(false); else showReturnCalendar(true); </script> </asp:Content>
Spring.NET allows you to render validation errors within the page
in several different ways, and if none of them suits your needs you can
implement your own validation errors renderer. Implementations of the
Spring.Web.Validation.IValidationErrorsRenderer
that
ship with the framework are:
Name | Class | Description |
---|---|---|
Block | Spring.Web.Validation.DivValidationErrorsRenderer
| Renders validation errors as list items within a
<div> tag. Default renderer for
<spring:validationSummary>
control. |
Inline | Spring.Web.Validation.SpanValidationErrorsRenderer
| Renders validation errors within a
<span> tag. Default renderer for
<spring:validationError>
control. |
Icon | Spring.Web.Validation.IconValidationErrorsRenderer | Renders validation errors as error icon, with error messages displayed in a tooltip. Best option when saving screen real estate is important. |
These three error renderers should be sufficient for most
applications, but in case you want to display errors in some other way
you can write your own renderer by implementing
Spring.Web.Validation.IValidationErrorsRenderer
interface:
namespace Spring.Web.Validation { /// <summary> /// This interface should be implemented by all validation errors renderers. /// </summary> /// <remarks> /// <para> /// Validation errors renderers are used to decouple rendering behavior from the /// validation errors controls such as <see cref="ValidationError"/> and /// <see cref="ValidationSummary"/>. /// </para> /// <para> /// This allows users to change how validation errors are rendered by simply plugging in /// appropriate renderer implementation into the validation errors controls using /// Spring.NET dependency injection. /// </para> /// </remarks> public interface IValidationErrorsRenderer { /// <summary> /// Renders validation errors using specified <see cref="HtmlTextWriter"/>. /// </summary> /// <param name="page">Web form instance.</param> /// <param name="writer">An HTML writer to use.</param> /// <param name="errors">The list of validation errors.</param> void RenderErrors(Page page, HtmlTextWriter writer, IList errors); } }
The best part of the errors renderer mechanism is that you can
easily change it across the application by modifying configuration
templates for <spring:validationSummary>
and
<spring:validationError>
controls:
<!-- Validation errors renderer configuration --> <object id="Spring.Web.UI.Controls.ValidationError" abstract="true"> <property name="Renderer"> <object type="Spring.Web.Validation.IconValidationErrorsRenderer, Spring.Web"> <property name="IconSrc" value="validation-error.gif"/> </object> </property> </object> <object id="Spring.Web.UI.Controls.ValidationSummary" abstract="true"> <property name="Renderer"> <object type="Spring.Web.Validation.DivValidationErrorsRenderer, Spring.Web"> <property name="CssClass" value="validationError"/> </object> </property> </object>
It's as simple as that!