Validation In Razor Web Pages 2

The new release of ASP.NET Web Pages - version 2 - doesn't include many obvious changes, but the most significant one is an enhanced Validation system. A couple of new classes have been introduced, and Web Pages validation now works with the MVC Unobtrusive jQuery validation library. This article explores the new validation system and sees what it brings to the party.

To begin with, it's worth noting that Web Pages v 1.0 style validation still works as it always did. You can still use ModelState to log validation errors and test to see if ModelState.IsValid before processing any user input. Version 2.0 introduces two new classes: ValidationHelper and Validator. The Validator class offers a number of different types of validator:

  • Validator.DateTime()
  • Validator.Decimal()
  • Validator.EqualsTo()
  • Validator.Float()
  • Validator.Integer()
  • Validator.Range()
  • Validator.Regex()
  • Validator.Required()
  • Validator.StringLength()
  • Validator.Url()

These methods result in differing validation being applied to values. The DateTime(), Decimal(), Float(), Integer() and Uri() methods all test for data type. The EqualsTo() method enables you to test to see if the value provided in one field is the same as the value provided in another. This is used commonly when you ask someone to enter their password twice on registration to ensure that they got it right first time. The Range() method tests to see if a value is within a specified range of numbers. The Regex() method takes a regular expression pattern and test to see if the supplied value matches the pattern. You might use this to test if a value is in a valid format for an email address, for example. The Required() method is a nice replacement to the old IsEmpty() test, in that it dictates that the field it is applied to is mandatory. Finally, the StringLength() method enables you to specify a minimum and/or maximum number of characters that will be accepted for any value submitted.

The Validation helper class offers some key methods and properties including the following:

  • Validation.Add()
  • Validation.ClassFor()
  • Validation.For()
  • Validation.IsValid()
  • Validation.RequireField()
  • Validation.RequireFields()

There are two ways of specifying that a form field is mandatory. One is to use the Validation.RequireField() method, and the other is to use the Validation.Add() method:

Validation.RequireField("firstname", "You must provide a first name");
Validation.Add("firstname", Validator.Required("You must provide a first name"));

The RequireField method takes the name of the form field and an optional error message. If you do not supply your own error message, the default one provided by the Web Pages framework is used instead. For required fields, that default error message is "This field is required". The Add method is more wordy in that you must specify the type of validator you want to apply through the Validator class. However, it is more flexible in that you can specify any number of different types of validator:

Validation.Add("firstname", 
    Validator.Required("You must provide a first name"), 
    Validator.StringLength(10, 0, "No more than 10 letters")
);

The RequireFields method provides a means for you to specify multiple fields as mandatory, but you cannot provide tailored error messages for each one. You have to reply on the default error message. Lazy, but if you are developing an Intranet where appearances are not important, you might use this:

Validation.RequireFields("firstname", "lastname", "email");

Here's a sample form that shows a variety of validators applied, and the use of the Validation.IsValid() method to test that all validators passed:

@{
    var result = "";
    Validation.Add("firstname", 
        Validator.Required("You must provide a first name"), 
        Validator.StringLength(10, 0, "No more than 10 letters")
        );
    Validation.RequireField("lastname", "Gimme your Last name!");
    Validation.Add("birthdate",
        Validator.Required(),
        Validator.DateTime()
        );
    Validation.Add("webaddress", 
        Validator.Required("URL is required"),
        Validator.Url("Must be a valid web address")
        );
    Validation.Add("email", 
        Validator.Regex(@"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$", 
        "Invalid format for an email address")
        );    
    Validation.Add("number", 
        Validator.Required("Make sure you say how many you want"),
        Validator.Range(1,4, "Must be between 1 and 4")
        );
    Validation.RequireField("password", "Password cannot be empty");
    Validation.Add("password2", 
        Validator.Required("Put your password in here again"),
        Validator.EqualsTo("password", "Must be the same as your password")
        );
    if(IsPost){
        if (Validation.IsValid()) {
            result += "<p>You entered:</p>";
            foreach(string item in Request.Form){
                result += item + ": " + Request[item] + "<br />";
            }
        }
        else{
            ModelState.AddFormError("There are some errors with your submission"); 
        }
    }
}

<!DOCTYPE html>
<html lang="en">
    <head>

        <link href="~/Content/StyleSheet.css" rel="stylesheet" type="text/css" />
    </head>
    <body>
        <fieldset>
            <legend>Web Pages 2 Validation</legend>
            <form method="post" action="">
                <div class="row">
                    @Html.Raw(result)
                    @Html.ValidationSummary(true)
                </div>
                <div class="row">
                    <label class="label" for="firstname">First Name:</label>
                    <span><input name="firstname" type="text" value="@Request["firstname"]" /></span>
                    @Html.ValidationMessage("firstname")
                </div>
                <div class="row">
                    <label class="label" for="lastname">Last Name:</label>
                    <span>@Html.TextBox("lastname", Request["lastname"])</span>
                    @Html.ValidationMessage("lastname")
                </div>
                <div class="row">
                    <label class="label" for="birthdate">Birth Date:</label>
                    <span>@Html.TextBox("birthdate", Request["birthdate"])</span>
                    @Html.ValidationMessage("birthdate")
                </div>
                <div class="row">
                    <label class="label" for="webaddress">Web Address:</label>
                    <span>@Html.TextBox("webaddress", Request["webaddress"])</span>
                    @Html.ValidationMessage("webaddress")
                </div>
                <div class="row">
                    <label class="label" for="email">Email:</label>
                    <span>@Html.TextBox("email", Request["email"])</span>
                    @Html.ValidationMessage("email")
                </div>
                <div class="row">
                    <label class="label" for="number">Number Required:</label>
                    <span>@Html.TextBox("number", Request["number"])</span>
                    @Html.ValidationMessage("number")
                </div>
                <div class="row">
                    <label class="label" for="password">Password:</label>
                    <span>@Html.Password("password")</span>
                    @Html.ValidationMessage("password")
                </div>
                <div class="row">
                    <label class="label" for="password2">Password Again:</label>
                    <span>@Html.Password("password2")</span>
                    @Html.ValidationMessage("password2")
                </div>
                <div>
                    <span class="label">&nbsp;</span>
                    <span><input type="submit" value="Submit" /></span>
                </div>
            </form>
        </fieldset>
    </body>
</html>

The Html.ValidationSummary() method from Web Pages v 1 is still available and will display general messages that have been applied through the ModelState.AddFormError method, and optionally, all individual field validation error messages if the excludeFieldErrors parameter is set to "false" (as is not the case above). You can see that, along with some other interesting things in the image below, which is the result of submitting the form to the server without entering any values at all:

Individual field validation error messages are rendered using the Html.ValidationMessage() helper, which takes the name of the field to be validated as a parameter. This hasn't changed from v 1. Notice in this example that some of the inputs have a pink background, whereas some others don't. Here is the part of the style sheet that controls the appearance of the inputs:

.validation-summary-errors {
    border: 2px solid #990099;
    color: red;
}

.field-validation-error {
    color: #990099;
}

.input-validation-error {
    color: #990099;
    background-color: #ff80ff;
    border-top: 2px solid #990099;
    border-left: 2px solid #990099;
}

The validation-summary-errors rule is applied to the Html.ValidationSummary output. The field-validation-error style is applied to the individual field error messages, and the input-validation-error class is responsible for styling the actual form inputs when they are associated with a failed validation. However, the firstname field is not styled, even though it clearly failed validation, whereas all others have been styled as a result of failed validation. The reason for this is that when you use Html Helpers to render your form fields, the CSS class is applied automagically by the Web Pages framework in the result of failed validation. All fields in the above code except the firstname field are Html helpers. If you want the input-validation-error style to be applied to normal HTML input elements, you need to state that explicitly with the Validation.ClassFor() method:

<input name="firstname" type="text" value="@Request["firstname"]" class="@Validation.ClassFor("firstname")" />

Unobtrusive Client Validation

So far, the example has shown server-side validation in action. And the new validation framework will always ensure that server-side validation takes place. However, as a convenience to your users, you may want to apply client-side validation, which runs in the browser and can give users instant feedback as they move through the form if they attempt to provide invalid values, or if fields are empty when they click the submit button. Since MVC 3, the ASP.NET MVC framework has included a mechnism called Unobtrusive Client Validation. This is a kind of bridge between server-side validators - applied using DataAnnotation attributes on model properties, and the popular jQuery Validate plugin. The same system has now been applied to the Web Pages framework, acting as a bridge between the server-side validators you apply through the Validation.Add or the Validation.RequireField(s) methods, and the jQuery Validate library. In order to get it to work with the form in the example, you need to do two things: first add references to jQuery, jQuery.Validate and jQuery.Validate.Unobtrusive:

    <head>
        <title>Web Pages 2 Validation</title>
        <script src="~/Scripts/jquery-1.7.2.min.js" type="text/javascript"></script>
        <script src="~/Scripts/jquery.validate.min.js" type="text/javascript"></script>
        <script src="~/Scripts/jquery.validate.unobtrusive.min.js" type="text/javascript"></script>
        <link href="~/Content/StyleSheet.css" rel="stylesheet" type="text/css" />
    </head>

You can link to these files through Microsoft's AJAX CDN if you like: http://www.asp.net/ajaxlibrary/cdn.ashx#ASPNET_MVC_Releases_on_the_CDN_11 or get them in the download that accompanies this article. The second thing you need to do - but only where you are not using Html Helpers for form fields - is to explicitly tell jQuery to apply client-side validation through the Validation.For method:

<input name="firstname" type="text" value="@Request["firstname"]" class="@Validation.ClassFor("firstname")" @Validation.For("firstname") />

As with the Validation.ClassFor method, this is not needed when using Html Helpers. When you implement unobtrusive validation, additional attributes are generated by the framework based on the HTML5 data-* attribute. For example, here's the source for the Number Required field:

<input data-val="true" 
       data-val-range="Must be between 1 and 4" 
       data-val-range-max="4" 
	   data-val-range-min="1" 
       data-val-required="Make sure you say how many you want" 
       id="number" 
       name="number" 
       type="text" 
       value="" />

I've broken it over several lines so that it easier to see and to ensure it fits on your screen. The data-val attribute is set to true, and tells jQuery that this field should be validated on the client. The other data-val attributes are pretty self explanatory and ensure that the right type of validation is performed, and that the correct error messages are assoicated with the individual form field. It should be noted at this point that certain types of validation are not available on the client by default. These relate to data type validation: Validator.DateTime(), Validator.Decimal(), Validator.Float(), Validator.Integer(), Validator.Url(). However, these validators are checked on the server.

Validation has certainly taken a step forward in Web Pages 2, and the addition of the Validation helper and Validator class means that it is really easy to ensure that you only get acceptable data posted to your application. In addition, the simple integration with unobtrusive client validation is a nice win too - especially when used with Html helpers.

The sample illustrated above is available as a GitHub repo. It includes the Web Pages 2 assemblies in the bin folder so you can run it from WebMatrix 1 if you haven't upgraded yet.