Chapter 9. ASP.NET Input Forms

It’s not enough that we do our best; sometimes we have to do what’s required.

Winston Churchill

Although formless pages are still accepted and correctly handled, the typical ASP.NET Web Forms page contains a single <form> tag decorated with the runat attribute set to server. During server-side processing, such a <form> tag is mapped to an instance of the HtmlForm class. The HtmlForm class acts as the outermost container of all server controls and wraps them in a plain HTML <form> element when the page is rendered. The resulting HTML form posts to the same page URL. By design, it doesn’t give you any chance to set the action URL programmatically, and for this reason it is often said to be reentrant. The default method used to submit form data is POST, but GET can be used as well.

In most cases, the server form is the outermost tag of the page and is contained directly in <body>. In general, though, the server <form> tag can be the child of any other server container control, such as <table>, <div>, <body>, and any other HTML generic control. (I covered HTML controls and Web controls in Chapter 6.) If any noncontainer server controls (for example, a TextBox) are placed outside the form tag, an exception is thrown as the page executes—no check is made at compile time. The exception is raised by the control itself when the host page begins to render. Noncontainer Web controls, in fact, check whether they are being rendered within the boundaries of a server form and throw an HttpException if they are not. A call to the Page’s VerifyRenderingInServerForm method does the job. (Be aware of this virtuous behavior when you get to writing custom controls.)

In this chapter, we’ll examine some aspects of form-based programming in ASP.NET, including how to use multiple forms in the same page and post data to a different page. We’ll also touch on input validation and validation controls.

Programming with Forms

One of the most common snags Web developers face when they first approach the ASP.NET lifestyle is the fact that managed Web applications support the single-form interface model. In the single-form interface model, each page always posts to itself and doesn’t supply any hook for developers to set the final destination of the postback. What in HTML programming is the Action property of the form is simply not defined on the ASP.NET HtmlForm class. By default, each ASP.NET page can post only to itself, unless some specific API extensions are used to perform a cross-page post. Unlike the action URL, the HTTP method and the target frame of the post can be programmatically adjusted using ad hoc HtmlForm properties—Method and Target.

The HtmlForm Class

The HtmlForm class inherits from HtmlContainerControl, which provides the form with the capability of containing child controls. This capability is shared with other HTML control classes, such as HtmlTable, characterized by child elements and a closing tag.

Properties of the HtmlForm Class

The HtmlForm class provides programmatic access to the HTML <form> element on the server through the set of properties shown in Table 9-1. Note that the table includes only a few of the properties HtmlForm inherits from the root class Control.

Table 9-1. Form Property

Property

Description

Attributes

Inherited from Control, gets a name/value collection with all the attributes declared on the tag.

ClientID

Inherited from Control, gets the value of UniqueID.

Controls

Inherited from Control, gets a collection object that represents the child controls of the form.

DefaultButton

String property, gets or sets the button control to display as the default button on the form.

DefaultFocus

String property, gets or sets the button control to give input focus when the form is displayed.

Disabled

Gets or sets a value indicating whether the form is disabled. It matches the disabled HTML attribute.

EncType

Gets or sets the encoding type. It matches the enctype HTML attribute.

ID

Inherited from Control, gets or sets the programmatic identifier of the form. The default value is aspnetForm.

InnerHtml

Inherited from HtmlContainerControl, gets or sets the markup content found between the opening and closing tags of the form.

InnerText

Inherited from HtmlContainerControl, gets or sets the text between the opening and closing tags of the form.

Method

Gets or sets a value that indicates how a browser posts form data to the server. The default value is POST. It can be set to GET if needed.

Name

Gets the value of UniqueID.

Style

Gets a collection of all cascading style sheet (CSS) properties applied to the form.

SubmitDisabledControls

Indicates whether to force controls disabled on the client to submit their values, allowing them to preserve their values after the page posts back to the server. False by default.

TagName

Returns “form”.

Target

Gets or sets the name of the frame or window to render the HTML generated for the page.

UniqueID

Inherited from Control, gets the unique, fully qualified name of the form.

Visible

Gets or sets a value that indicates whether the form is rendered. If this property is set to false, the form is not rendered to HTML.

The form must have a unique name. If the programmer doesn’t assign the name, ASP.NET uses a default name—aspnetForm. The programmer can set the form’s identifier by using either the ID or Name property. If both are set, the ID attribute takes precedence. (Note, though, that any reliance on the Name attribute compromises the XHTML compliance of the page.)

The parent object of the form is the outer container control with the runat attribute. If such a control doesn’t exist, the page object is set as the parent. Typical containers for the server form are <table> and <div> if they are marked as server-side objects.

By default, the Method property is set to POST. The value of the property can be modified programmatically. If the form is posted through the GET method, all form data is passed on the URL’s query string. However, if you choose the GET method, make sure the size allowed for a GET request does not affect the integrity of your application or raise security issues.

Methods of the HtmlForm Class

Table 9-2 lists the methods available on the HtmlForm class that you’ll be using more often. All the methods listed in the table are inherited from the base System.Web.UI.Control class.

Table 9-2. Form Methods

Method

Description

ApplyStyleSheetSkin

Applies the style properties defined in the page style sheet.

DataBind

Calls the DataBind method on all child controls.

FindControl

Retrieves and returns the control that matches the specified ID.

Focus

Sets input focus to a control.

HasControls

Indicates whether the form contains any child controls.

RenderControl

Outputs the HTML code for the form. If tracing is enabled, it caches tracing information to be rendered later, at the end of the page.

Note that the FindControl method searches only among the form’s direct children. Controls belonging to an inner naming container, or that are a child of a form’s child control, are not found.

Multiple Forms

As mentioned, the single-form model is the default in ASP.NET and plays a key role in the automatic view state management mechanism I described in Chapter 5. Generally speaking, the ASP.NET’s enforcement of the single-form model does not significantly limit the programming power, and all things considered, doing without multiple forms is not a big sacrifice. Some pages, though, would have a more consistent and natural design if they could define multiple logical forms. In this context a logical form is a logically related group of input controls. For example, think of a page that provides some information to users but also needs to supply an additional form such as a search or login box.

You can incorporate search and login capabilities in ad hoc classes and call those classes from within the page the user has displayed. This might or might not be the right way to factor your code, though. Especially if you’re porting some old code to ASP.NET, you might find it easier to insulate login or search code in a dedicated page. Well, to take advantage of form-based login, how do you post input data to this page?

Using HTML Forms

As mentioned, ASP.NET prevents you from having multiple <form> tags flagged with the runat attribute. However, nothing prevents you from having one server-side <form> tag and multiple client HTML <form> elements in the body of the same Web form. Here’s an example:

<body>
    <table><tr><td>
        <form id="form1" runat="server">
        <h2>Ordinary contents for an ASP.NET page</h2>
        </form>
    </td>
    <td>
        <form method="post" action="search.aspx">
            <table><tr>
                <td>Keyword</td>
                <td><input type="text" id="Keyword" name="Keyword" /></td>
            </tr><tr>
                <td><input type="submit" id="Go" value="Search" /></td>
            </tr></table>
        </form>
    </td>
    </tr></table>
</body>

The page contains two forms, one of which is a classic HTML form devoid of the runat attribute and, as such, completely ignored by ASP.NET. The markup served to the browser simply contains two <form> elements, each pointing to a different action URL.

This code works just fine but has a major drawback: you can’t use the ASP.NET programming model to retrieve posted data in the action page of the client form. When writing search.aspx, in fact, you can’t rely on view state to retrieve posted values. To know what’s been posted, you must resort to the old-fashioned, but still effective, ASP model, as shown in the following code sample:

public partial class Search : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Use the Request object to retrieve posted data
        var textToSearch = Request.Form["Keyword"];
        ...

        // Use standard ASP.NET programming model to populate the page UI
        KeywordBeingUsed.Text = textToSearch;
    }
}

You use the protocol-specific collections of the Request object to retrieve posted data—Form if POST is used, and QueryString in case of GET. In addition, you have to use the name attribute to identify input elements. Overall, this is perhaps not a recommended approach, but it definitely works. Figure 9-1 shows the page in action.

A server form control and a client HTML form working together.

Figure 9-1. A server form control and a client HTML form working together.

When the user clicks the Search button, the search.aspx page is invoked, the page receives only the values posted through the HTML form, and it uses them to proceed.

Nested HTML Forms

In ASP.NET, most real-world pages are based on master pages. And most of the time the master page includes an outermost <form> tag. This means that if you add a client HTML form element at rendering time the two form elements will be nested.

Now nesting forms is possible in theory, but browsers don’t actually render nested forms properly. The HTML 4 standard prevents direct form-to-form dependency. You are beyond the standard if you add a form element as the direct child of another form. Instead, if you embed the child form within a block element (DIV, FIELDSET), it is considered valid from a syntax point of view. As mentioned, though, the fact is that, regardless of what the World Wide Web Consortium (W3C) believes, browsers just glue the content of the two forms together. As a result, the outermost parent form determines where the post is made.

Nicely enough, although browsers actually seem to produce the same final effect—the content of the inner forms merged with the outermost ones—how that happens is slightly different. For example, if you display a page with nested forms in Firefox 3.6.x, you find out that the child <form> tags are just stripped off. The form content, on the other hand, is preserved. With Internet Explorer 8, the child <form> tag is preserved but it’s closed inline, keeping any content out of it and subsequently merging it to the outermost form.

The code that produces the pages shown in Figure 9-1 descends from the standard ASP.NET 4 template for Web Forms pages with a master. However, because the master contains a <form> tag, I had to rework the master template to be able to use side-by-side <form> tags and avoid nesting.

<body>
    <div class="page">
      <form id="Form1" runat="server">
        ...

        <div class="main">
          <asp:ContentPlaceHolder ID="MainContent" runat="server" />
        </div>
      </form>

      <asp:ContentPlaceHolder ID="ExtraFormContent" runat="server" />

      <div class="clear">
      </div>
     <div class="footer">
     </div>
   </div>
</body>

The client HTML form element fits into the ExtraFormContent placeholder.

All in all, nested HTML forms are a nonissue—you just don’t use them. However, a common pitfall in ASP.NET development is that because of master pages you inadvertently end up with nested forms in an attempt to add a second innocent client HTML form.

Multiple Server <form> Tags on a Page

ASP.NET makes it quite clear: you just can’t have multiple server forms in the same Web page. Given the dynamics of page rendering, an exception is thrown if more than one HtmlForm control attempts to render. (See Figure 9-2.)

Using multiple server forms in a page throws a rendering exception.

Figure 9-2. Using multiple server forms in a page throws a rendering exception.

A little-known fact is that a Web form can actually contain as many server-side forms as needed as long as only one at a time is visible. For example, a page with, say, three <form runat=server> tags is allowed, but only one form can be actually rendered. By playing with the Visible property of the HtmlForm class, you can change the active server form during the page lifetime.

This trick doesn’t really let you have multiple active forms at the same time, but it can be helpful sometimes because it allows you to change the active server form over postback events. Let’s consider the following ASP.NET page:

<body>
    <form id="step0" runat="server" visible="true">
        <h1>Welcome</h1>
        <asp:textbox runat="server" id="Textbox1" />
        <asp:button ID="Button1" runat="server" text="Step #1"
            OnClick="Button1_Click" />
    </form>

    <form id="step1" runat="server" visible="false">
        <h1>Step #1</h1>
        <asp:textbox runat="server" id="Textbox2" />
        <asp:button ID="Button2" runat="server" text="Previous step"
            OnClick="Button2_Click" />
        <asp:button ID="Button3" runat="server" text="Step #2"
            OnClick="Button3_Click" />
    </form>

    <form id="step2" runat="server" visible="false">
        <h1>Finalizing</h1>
        <asp:button ID="Button4" runat="server" text="Finish"
            OnClick="Button4_Click" />
    </form>
</body>

As you can see, all <form> tags are marked as runat, but only the first one is visible. Mutually exclusive forms were a cool way of implementing wizards in old versions of ASP.NET, before an official wizard control got introduced. By toggling a form’s visibility in button event handlers, you can obtain a wizard-like behavior, as shown in Figure 9-3.

public partial class MultipleForms : System.Web.UI.Page
{
    protected void Page_Load(Object sender, EventArgs e)
    {
        Title = "Welcome";
    }
    protected void Button1_Click(Object sender, EventArgs e)
    {
        Title = "Step 1";
        step0.Visible = false;
        step1.Visible = true;
    }
    protected void Button2_Click(Object sender, EventArgs e)
    {
        step0.Visible = true;
        step1.Visible = false;
    }
    protected void Button3_Click(Object sender, EventArgs e)
    {
        Title = "Finalizing";
        step1.Visible = false;
        step2.Visible = true;
    }
    protected void Button4_Click(Object sender, EventArgs e)
    {
        Title = "Done";
        step2.Visible = false;
        Response.Write("<h1>Successfully done.</h1>");
    }
}
Mutually exclusive forms.

Figure 9-3. Mutually exclusive forms.

Multiple View and Wizards

If you’re targeting ASP.NET 2.0 or newer, you might not need to resort to the preceding trick to switch between forms. You find two new controls—MultiView and Wizard—ready for the job. The MultiView control employs logic nearly identical to that of multiple exclusive forms, except that it relies on panels rather than full forms.

The MultiView control allows you to define multiple and mutually exclusive HTML panels. The control provides an application programming interface (API) for you to toggle the visibility of the various panels and ensure that exactly one is active and visible at a time. The MultiView control doesn’t provide a built-in user interface. The Wizard control is just that—a MultiView control plus some wizard-like predefined user interface (UI) blocks. I’ll cover the Wizard control in great detail later in the chapter.

Cross-Page Postings

The ASP.NET framework offers a built-in mechanism to override the normal processing cycle and let the page post to another, distinct page. In general, postbacks occur in either of two ways—through a submit button or via script. The client browser usually takes on any post conducted through a button and automatically points to the page that the action attribute of the posting form indicates. A lot more of flexibility is possible when the post occurs via script.

In ASP.NET, however, you can also configure certain page controls—in particular, those that implement the IButtonControl interface—to post to a different target page. This is referred to as cross-page posting.

Posting Data to Another Page

Authoring a Web page that can post data to another page requires only a couple of steps. First, you choose the controls that can cause the postback and set their PostBackUrl property. A page can include one or more button controls and, generally, any combination of button controls and submit buttons. Notice that in this context a button control is any server control that implements IButtonControl. (I fully covered the IButtonControl interface in Chapter 6.) The following code snippet shows how to proceed:

<form id="form1" runat="server">
    <asp:textbox runat="server" id="Keyword" />
    <asp:button runat="server" id="buttonPost"
            Text="Click"
            PostBackUrl="search.aspx" />
</form>

When the PostBackUrl property is set, the ASP.NET runtime binds the corresponding HTML element of the button control to a new JavaScript function. Instead of using our old acquaintance __doPostback, it uses the new WebForm_DoPostBackWithOptions function. The button renders the following markup:

<input type="submit" name="buttonPost" id="buttonPost"
    value="Click"
    onclick="javascript:WebForm_DoPostBackWithOptions(
        new WebForm_PostBackOptions("buttonPost", "",
            false, "", "search.aspx", false, false))" />

As a result, when the user clicks the button, the current form posts its content to the specified target page. What about the view state? When the page contains a control that does cross-page posting, a new hidden field is also created—the __PREVIOUSPAGE field. The field contains the view state information to be used to serve the request. This view state information is transparently used in lieu of the original view state of the page being posted to.

You use the PreviousPage property to reference the posting page and all of its controls. Here’s the code behind a sample target page that retrieves the content of a text box defined in the form:

// This code belongs to doSearch.aspx
protected void Page_Load(Object sender, EventArgs e)
{
    // Ensure this is a cross-page postback
    if (PreviousPage == null)
    {
        Response.Write("Must be a cross-page post.");
        return;
    }

    // Retrieves posted data. This ensures PreviousPage is not null.
    var txt = (TextBox) PreviousPage.FindControl("Keyword");
    ...
}

By using the PreviousPage property on the Page class, you can access any input control defined on the posting page. Access to input controls is weakly typed and occurs indirectly through the services of the FindControl method. The problem here lies in the fact that the target page doesn’t know anything about the type of the posting page. PreviousPage is declared as a property of type Page and, as such, it can’t provide access to members specific to a derived page class.

Furthermore, note that FindControl looks up controls only in the current naming container. If the control you are looking for lives inside another control (say, a template), you must first get a reference to the container, and then search the container to find the control. This happens commonly when you employ master pages. To avoid using FindControl altogether, a different approach is required.

What about using the dynamic type in ASP.NET 4? It might work, but this solution also has a little drawback—the same drawback we encountered in Chapter 8, for master pages. The problem is that you can’t access, say, the Keyword text box control from within the posted page because the Keyword control is mapped as a protected member of the page class. The following code, therefore, throws an exception:

dynamic previousPage = PreviousPage;
var txt = previousPage.Keyword;   // Keyword is inaccessible due to its protection level
if (txt == null)
{
   ...
}

To fix this code, you need to define a public property on the posting page class that exposes as a public member whatever element you want to retrieve from within the posted page. It doesn’t have to be the control reference; it is recommended that you expose just data. Here’s an example:

public partial class Crosspage : System.Web.UI.Page
{
    public String SelectedKeywords
    {
        get { return Keyword.Text; }
    }
}

With this change, the following call will work:

dynamic previousPage = PreviousPage;
var keywords = previousPage.SelectedKeywords;

The dynamic type, though, involves falling down to the Dynamic Language Runtime (DLR) engine and should be used only when you really need dynamically resolved code. In this case, you can get an even more effective (and strongly typed) solution by resorting to a page directive.

The @PreviousPageType Directive

Let’s say it up front. To retrieve values on the posting page, FindControl is your only safe option if you don’t know in advance which page will be invoking your target. However, when you’re using cross-page posting in the context of an application, chances are good that you know exactly who will be calling the page and how. In this case, you can take advantage of the @PreviousPageType directive to cause the target page’s PreviousPage property to be typed to the source page class.

In the target page, you add the following directive:

<%@ PreviousPageType VirtualPath="crosspage.aspx" %>

The directive can accept either of two attributes—VirtualPath or TypeName. The former points to the URL of the posting page; the latter indicates the type of the calling page. The directive just shown makes the PreviousPage property on the target page class be of the same type as the page at the given path (or the specified type). This fact alone, though, is not sufficient to let you access input controls directly. Each page class contains protected members that represent child controls; unfortunately, you can’t call a protected member of a class from an external class. (Only derived classes can access protected members of the parent class.)

To work around the issue, in the caller page you must add public properties that expose any information you want posted pages to access. For example, imagine that crosspostpage.aspx contains a TextBox named Keyword. To make it accessible from within a target page, you add the following code to the code-behind class:

public TextBox KeywordControl
{
    get { return Keyword; }
}

The new KeywordControl property on the page class wraps and exposes the internal text-box control. In light of this code, the target page can now execute the following code:

Response.Write(PreviousPage.KeywordControl.Text);

Although you can directly expose a control reference, it is preferable that you expose just the data the posted page needs to consume. This approach is based on the Law of Demeter, which essentially states that internal details of components should not be made public unless strictly required. Another way of looking at this is in light of the “Tell, don’t ask principle”: your posted page gets what it needs instead of asking for a property on a control.

Detecting Cross-Page Postings

Being the potential target of a cross-page call doesn’t automatically make a target page a different kind of page all of a sudden. There’s always the possibility that the target page is invoked on its own—for example, via hyperlinking. When this happens, the PreviousPage property returns null and other postback-related properties, such as IsPostBack, assume the usual values.

If you have such a dual page, you should insert some extra code to discern the page behavior. The following example shows a page that allows only cross-page access:

if (PreviousPage == null)
{
    Response.Write("Sorry, that's the wrong way to invoke me.");
    Response.End();
    return;
}

The IsCrossPagePostBack property on the Page class deserves a bit of attention. The property returns true if the current page has called another ASP.NET page. It goes without saying that IsCrossPagePostBack on the target page always returns false. Therefore, the following code is not equivalent to the one seen before:

if (!IsCrossPagePostBack)
{
    ...
}

To know whether the current page is being called from another page, you have to test the value of IsCrossPagePostBack on the page object returned by PreviousPage:

// PreviousPage is null in case of a normal request
if (!PreviousPage.IsCrossPagePostBack)
{
    ...
}

However, this code will inevitably throw an exception if the page is invoked in a normal way (that is, from the address bar or via hyperlinking, because PreviousPage is null). In the end, the simplest and most effective way to see whether a page is being invoked through cross-page postbacks is by checking PreviousPage against null.

Redirecting Users to Another Page

In addition to the PostBackUrl property of button controls, ASP.NET provides another mechanism for transferring control and values from one page to another—you can use the Server.Transfer method.

The URL of the new page is not reflected by the browser’s address bar because the transfer takes place entirely on the server. The following code shows how to use the method to direct a user to another page:

protected void Button1_Click(object sender, EventArgs e)
{
    Server.Transfer("target.aspx");
}

Note that all the code that might be following the call to Transfer in the page is never executed. In the end, Transfer is just a page redirect method. However, it is particularly efficient for two reasons. First, no roundtrip to the client is requested as is the case, for example, with Response.Redirect. Second, the same HttpApplication that was serving the caller request is reused, thus limiting the impact on the ASP.NET infrastructure.

How can you retrieve values from within the transferred page?

You can use the same programming model as for cross-page postings and rely on a non-null PreviousPage property, DLR interaction, or the @PreviousPageType directive for strongly typed access to input fields. How can a page detect whether it’s being called through a server transfer or through a cross-page postback? In both cases, PreviousPage is not null, but the IsCrossPagePostBack on the PreviousPage object is true for a cross-page posting and false in the case of a server transfer.

Important

Passing values from one page to another is a task that can be accomplished in a variety of ways—using cross-page posting, server transfer, HTML forms, cookies, or query strings. Which one is the most effective? Cross-page posting and server transfer offer a familiar programming model but potentially move a significant chunk of data through the __PREVIOUSPAGE field. Whether this information is really needed depends on the characteristics of the target page. In many cases, the target page just needs to receive a few parameters to start working. If this is the case, HTML client forms might be more effective in terms of data being moved. HTML forms, though, require an ASP-like programming model.

Validation Controls

The first rule for writing more secure applications is ensuring you get the data right, before you actually start using it. Getting the data right requires you to pass any external input through a validation step. In ASP.NET, validation controls provide an easy-to-use mechanism to perform a variety of validation tasks, including testing for valid types, values within a given range, or required fields.

ASP.NET validation controls work on the server, but they can be configured to filter invalid input already on the client. This is accomplished using some JavaScript code that kicks in and performs validation as soon as the user tabs out of a monitored input field.

All ASP.NET validation controls inherit from the BaseValidator class which, in turn, descends from Label. All validators defined on a page are automatically grouped in the Validators collection of the Page class. You can validate them all in a single shot using the Validate method in the page class or individually by calling the Validate method on each validator. The Validate method sets the IsValid property both on the page and on the individual validator. The IsValid property indicates whether the user’s entries match the requirements of the validators. The user’s entry is validated when the Validate method is called and also whenever the page posts back.

Note

Typical control members involved with input validation have been grouped in the IValidator interface that the BaseValidator class implements. The interface includes the Validate method and the IsValid and ErrorMessage properties.

Generalities of Validation Controls

Each validation control references an input control located elsewhere on the page. When the page is submitted, the content of the monitored server control is passed to the associated validation control for further processing. Each validation control performs a different type of verification. Table 9-3 shows the types of validation supported by the .NET Framework.

Table 9-3. Validation Controls in ASP.NET

Validation Control

Description

CompareValidator

Compares the user’s entry against a fixed value by using a comparison operator such as LessThan, Equal, or GreaterThan. It can also compare against the value of a property in another control on the same page.

CustomValidator

Employs programmatically defined validation logic to check the validity of the user’s entry. You use this validator when the other validators cannot perform the necessary validation and you want to provide custom code that validates the input.

RangeValidator

Ensures that the user’s entry falls within a specified range. Lower and upper boundaries can be expressed as numbers, strings, or dates.

RegularExpressionValidator

Validates the user’s entry only if it matches a pattern defined by a regular expression.

RequiredFieldValidator

Ensures that the user specifies a value for the field.

Multiple validation controls can be used with an individual input control to validate according to different criteria. For example, you can apply multiple validation controls on a text box that is expected to contain an e-mail address. In particular, you can impose that the field is not skipped (RequiredFieldValidator) and that its content matches the typical format of e-mail addresses (RegularExpressionValidator).

Table 9-3 lacks a reference to the ValidationSummary control. The control does not perform validation tasks itself. Instead, it displays a label to summarize all the validation error messages found on a Web page as the effect of other validators. I’ll cover the ValidationSummary control later in the chapter.

The BaseValidator Class

Table 9-4 details the specific properties of validation controls. Some properties—such as ForeColor, Enabled, and Text—are overridden versions of base properties on base classes.

Table 9-4. Basic Properties of Validators

Property

Description

ControlToValidate

Gets or sets the input control to validate. The control is identified by name—that is, by using the value of the ID attribute.

Display

If client-side validation is supported and enabled, gets or sets how the space for the error message should be allocated—either statically or dynamically. In the case of server-side validation, this property is ignored. A Static display is possible only if the browser supports the display CSS style. The default is Dynamic.

EnableClientScript

True by default; gets or sets whether client-side validation is enabled.

Enabled

Gets or sets whether the validation control is enabled.

ErrorMessage

Gets or sets the text for the error message.

ForeColor

Gets or sets the color of the message displayed when validation fails.

IsValid

Gets or sets whether the associated input control passes validation.

SetFocusOnError

Indicates whether the focus is moved to the control where validation failed.

Text

Gets or sets the description displayed for the validator in lieu of the error message. Note, though, this text does not replace the contents of ErrorMessage in the summary text.

ValidationGroup

Gets or sets the validation group that this control belongs to.

All validation controls inherit from the BaseValidator class except for compare validators, for which a further intermediate class—the BaseCompareValidator class—exists. The BaseCompareValidator class serves as the foundation for validators that perform typed comparisons. An ad hoc property, named Type, is used to specify the data type the values are converted to before being compared. The CanConvert static method determines whether the user’s entry can be converted to the specified data type. Supported types include string, integer, double, date, and currency. The classes acting as compare validators are RangeValidator and CompareValidator.

Note

You might want to pay careful attention when using the ForeColor property. Don’t get it wrong—there’s nothing bad with the property, which works as expected and sets the foreground color being used by the validators to show any messages. That’s just the point, however. Today’s applications tend to gain a lot more control over the style of emitted markup and for this reason tend to style through CSS wherever possible. Like many other similar style properties on server controls, the ForeColor property emits inline style information, which is really bad for designers when they get to do their job. Consider that in ASP.NET 4, validation controls no longer use the red color for error messages unless you set the ControlRenderingCompatabilityVersion attribute to “3.5” in the <pages> section of the configuration file. The ForeColor property certainly is not obsolete, but its use should be put aside as much as possible in favor of CSS styles.

Associating Validators with Input Controls

The link between each validator and its associated input control is established through the ControlToValidate property. The property must be set to the ID of the input control. If you do not specify a valid input control, an exception will be thrown when the page is rendered. The associated validator/control is between two controls within the same container—be it a page, user control, or template.

Not all server controls can be validated—only those that specify their validation property through an attribute named [ValidationProperty]. The attribute takes the name of the property that contains the user’s entry to check. For example, the validation property for a TextBox is Text and is indicated as follows:

[ValidationProperty("Text")]
public class TextBox : WebControl, ITextControl
{
    ...
}

The list of controls that support validation includes TextBox, DropDownList, ListBox, RadioButtonList, FileUpload, plus a bunch of HTML controls such as HtmlInputFile, HtmlInputText, HtmlInputPassword, HtmlTextArea, and HtmlSelect. Custom controls can be validated too, as long as they are marked with the aforementioned [ValidationProperty] attribute.

Note

If the validation property of the associated input control is left empty, all validators accept any value and always pass the test. The RequiredFieldValidator control represents a rather natural exception to this rule, because it has been specifically designed to detect fields the user skipped and left blank.

Gallery of Controls

In general, ASP.NET validators are designed to work on a single control and process a single “value” for that control. As mentioned, you use the ValidationProperty attribute on custom controls to specify which property you want to validate. For stock controls, you take what they provide without many chances to modify things. Keep in mind that for validation scenarios that involve multiple controls or multiple properties, you need to create your own custom validation controls.

This said, let’s go ahead and take a closer look at the stock validation controls available in ASP.NET Web Forms.

The CompareValidator Control

The CompareValidator control lets you compare the value entered by the user with a constant value or the value specified in another control in the same naming container. The behavior of the control is characterized by the following additional properties:

  • ControlToCompare Represents the ID of the control to compare with the current user’s entry. You should avoid setting the ControlToCompare and ValueToCompare properties at the same time. They are considered mutually exclusive; if you set both, the ControlToCompare property takes precedence.

  • Operator Specifies the comparison operation to perform. The list of feasible operations is defined in the ValidationCompareOperator enumeration. The default operator is Equal; feasible operators are also LessThan, GreaterThan, and their variations. The DataTypeCheck operator is useful when you want to make sure that certain input data can be converted to a certain type. When the DataTypeCheck operator is specified, both ControlToCompare and ValueToCompare are ignored. In this case, the test is made on the type of the input data and succeeds if the specified data can be converted to the expected type. Supported types are expressed through the following keywords: String, Integer, Double, Date, and Currency (decimal).

  • ValueToCompare Indicates the value to compare the user’s input against. If the Type property is set, the ValueToCompare property must comply with it.

The following code demonstrates the typical markup of the CompareValidator control when the control is called to validate an integer input from a text box representing someone’s age:

<asp:CompareValidator runat="server" id="ageValidator"
    ControlToValidate="ageTextBox"
    ValueToCompare="18"
    Operator="GreaterThanEqual"
    Type="Integer"
    ErrorMessage="Must specify an age greater than 17." />

The CustomValidator Control

The CustomValidator control is a generic and totally user-defined validator that uses custom validation logic to accomplish its task. You typically resort to this control when none of the other validators seems appropriate or, more simply, when you need to execute your own code in addition to that of the standard validators.

To set up a custom validator, you can indicate a client-side function through the ClientValidationFunction property. If client-side validation is disabled or not supported, simply omit this setting. Alternatively, or in addition to client validation, you can define some managed code to execute on the server. You do this by defining a handler for the ServerValidate event. The code will be executed when the page is posted back in response to a click on a button control. The following code snippet shows how to configure a custom validator to check the value of a text box against an array of feasible values:

<asp:CustomValidator runat="server" id="membershipValidator"
    ControlToValidate="membership"
    ClientValidationFunction="CheckMembership"
    OnServerValidate="ServerValidation"
    ErrorMessage="Membership can be Normal, Silver, Gold, or Platinum." />

If specified, the client validation function takes a mandatory signature and looks like this:

function CheckMembership(source, arguments)
{
   ...
}

The source argument references the HTML tag that represents the validator control—usually, a <span> tag. The arguments parameter references an object with two properties, IsValid and Value. The Value property is the value stored in the input control to be validated. The IsValid property must be set to false or true according to the result of the validation.

The CustomValidator control is not associated in all cases with a single input control in the current naming container. For this type of validator, setting the ControlToValidate property is not mandatory. For example, if the control has to validate the contents of multiple input fields, you simply do not set the ControlToValidate property and the arguments.Value variable evaluates to the empty string. In this case, you write the validation logic so that any needed values are dynamically retrieved. With client-side script code, this can be done by accessing the members of the document’s form, as shown in the following code:

function CheckMembership(source, arguments)
{
    // Retrieve the current value of the element
    // with the specified ID
    var membership = document.getElementById("membership").value;
    ...
}

Warning

Setting only a client-side validation code opens a security hole because an attacker could work around the validation logic and manage to have invalid or malicious data sent to the server. By defining a server event handler, you have one more chance to validate data before applying changes to the back-end system.

To define a server-side handler for a custom validator, use the ServerValidate event:

void ServerValidation(object source, ServerValidateEventArgs e)
{
    ...
}

The ServerValidateEventArgs structure contains two properties—IsValid and Value—with the same meaning and goal as in the client validation function. If the control is not bound to a particular input field, the Value property is empty and you retrieve any needed value using the ASP.NET object model. For example, the following code shows how to check the status of a check box on the server:

void ServerValidation (object source, ServerValidateEventArgs e) {
    e.IsValid = (CheckBox1.Checked == true);
}

The CustomValidator control is the only option you have to validate controls that are not marked with the [ValidationProperty] attribute—for example, calendars and check-box controls. Likewise, it is the only option you have to validate multiple values and/or multiple controls linked by some relationship. Finally, CustomValidator is also your starting point for building some remote validation via AJAX. The simplest way of doing that is just by using some JavaScript that, from within the bound client validator, calls into a server method. The jQuery library is perfect for the job.

The RegularExpressionValidator Control

Regular expressions are an effective way to ensure that a predictable and well-known sequence of characters form the user’s entry. For example, using regular expressions you can validate the format of postal codes, Social Security numbers, e-mail addresses, phone numbers, and so on. When using the RegularExpressionValidator control, you set the ValidationExpression property with the regular expression, which will be used to validate the input.

The following code snippet shows a regular expression validator that ensures the user’s entry is an e-mail address:

<asp:RegularExpressionValidator runat="server" id="emailValidator"
    ControlToValidate="email"
    ValidationExpression="[a-zA-Z_0-9.-]+@[a-zA-Z_0-9.-]+.w+"
    ErrorMessage="Must be a valid email address." />

The regular expression just shown specifies that valid e-mail addresses are formed by two nonzero sequences of letters, digits, dashes, and dots separated by an @ symbol and followed by a dot (.) and an alphabetic string. (This might not be the perfect regular expression for e-mail addresses, but it certainly incorporates the majority of e-mail address formats.)

Note

The regular expression validation syntax is slightly different on the client than on the server. The RegularExpressionValidator control uses JavaScript regular expressions on the client and the .NET Framework Regex object on the server. Be aware that the JavaScript regular expression syntax is a subset of the Regex model. Whenever possible, try to use the regular expression syntax supported by JavaScript so that the same result is obtained for both the client and server.

The RangeValidator Control

The RangeValidator control lets you verify that a given value falls within a specified range. The type of the values involved in the check is specified dynamically and picked from a short list that includes strings, numbers, and dates. The following code shows how to use a range validator control:

<asp:RangeValidator runat="server" id="hiredDateValidator"
    ControlToValidate="hired"
    MinimumValue="2000-1-4"
    MaximumValue="9999-12-31"
    Type="Date"
    ErrorMessage="Must be a date after <b>Jan 1, 1999</b>." />

The key properties are MinimumValue and MaximumValue, which together clearly denote the lower and upper boundaries of the interval. Note that an exception is thrown if the strings assigned MinimumValue or MaximumValue cannot be converted to the numbers or dates according to the value of the Type property.

If the type is set to Date, but no specific culture is set for the application, you should specify dates using a culture-neutral format, such as yyyy-MM-dd. If you don’t do so, the chances are good that the values will not be interpreted correctly.

Note

The RangeValidator control extends the capabilities of the more basic CompareValidator control by checking for a value in a fixed interval. In light of this, the RangeValidator control might raise an exception if either MinimumValue or MaximumValue is omitted. Whether the exception is thrown or not depends on the type chosen and its inherent ability to interpret the empty string. For example, an empty string on a Date type causes an exception. If you want to operate on an unbound interval—whether it’s lower or upper unbound—either you resort to the GreaterThan (or LessThan) operator on the CompareValidator control or simply use a virtually infinite value such as the 9999-12-31 value.

The RequiredFieldValidator Control

To catch when a user skips a mandatory field in an input form, you use the RequiredFieldValidator control to show an appropriate error message:

<asp:RequiredFieldValidator runat="server" id="lnameValidator"
    ControlToValidate="lname"
    ErrorMessage="Last name is mandatory" />

As long as you’re using an up-level browser and client-side scripting is enabled for each validator, which is the default, invalid input will display error messages without performing a postback.

Important

Note that just tabbing through the controls is not a condition that raises an error; the validator gets involved only if you type blanks or if the field is blank when the page is posted back.

How can you determine whether a certain field is really empty? In many cases, the empty string is sufficient, but this is not a firm rule. The InitialValue property specifies the initial value of the input control. The validation fails only if the value of the control equals InitialValue upon losing focus. By default, InitialValue is initialized with the empty string.

Special Capabilities

The primary reason why you place validation controls on a Web form is to catch errors and inconsistencies in the user’s input. But how do you display error messages? Are you interested in client-side validation and, if you are, how would you set it up? Finally, what if you want to validate only a subset of controls when a given button is clicked? Some special capabilities of validation controls provide a valid answer to all these issues.

Server-Side Validation

Validation controls are server-side controls; subsequently, they kick in and give a response on the server. All postback controls (for example, buttons, auto-postback controls, and controls that registered as postback controls) validate the state of the page before proceeding with their postback action. For example, here’s how the Button control handles it. The Web Forms page life cycle ends up invoking the RaisePostBackEvent method to force the clicked submit button to execute its click handler:

// Code excerpted from the source code of the System.Web.UI.WebControls.Button
protected virtual void RaisePostBackEvent(string eventArgument)
{
    base.ValidateEvent(this.UniqueID, eventArgument);
    if (this.CausesValidation)
    {
        this.Page.Validate(this.ValidationGroup);
    }
    this.OnClick(EventArgs.Empty);
    this.OnCommand(new CommandEventArgs(this.CommandName, this.CommandArgument));
}

The Validate method on the class Page just loops through the validators registered with the specified validation group and returns a response. The response simply updates the state of validation controls including the validation summary. This response will then be merged into the page response and output to the user.

If you simply need to know whether the state of the page is valid, you call the IsValid Boolean property. Note that Page.IsValid cannot be called before validation has taken place. It should always be queried after a call to Page.Validate—either an explicit call you code yourself or an implicit call that postback controls perform in their event handler. Note that, as the preceding code snippet shows, postback controls don’t do any validation if their CausesValidation property is set to false.

Note

The Validate method on class Page is always invoked during the postback stage, regardless of the features of the postback control—a submit button has different postback mechanics compared to, say, a link button or an auto-postback control. In general, you’ll more likely need to call IsValid in the code-behind class than Validate. After validation has occurred, in fact, you might need to check whether it was successful before you perform some other operations.

Displaying Error Information

The ErrorMessage property determines the static message that each validation control will display if an error occurs. You need to know that if the Text property is also set, it will take precedence over ErrorMessage. Text is designed to display inline where the validation control is located; ErrorMessage is designed to display in the validation summary. (Strategies for using Text and ErrorMessage will be discussed more in the next section, “The ValidationSummary Control.”) Because all validation controls are labels, no other support or helper controls are needed to display any message. The message will be displayed in the body of the validation controls and, subsequently, wherever the validation control is actually placed. The error message is displayed as HTML, so it can contain any HTML formatting attribute.

Validators that work in client mode can create the <span> tag for the message either statically or dynamically. You can control this setting by using the Display property of the validator. When the display mode is set to Static (the default), the <span> element is given the following style:

style="visibility:hidden;"

The CSS visibility style attribute, when set to Hidden, causes the browser not to display the element but reserves space for it. If the Display property contains Dynamic, the style string changes as follows:

style="display:none;"

The CSS display attribute, when set to none, simply hides the element, which will take up space on the page only if displayed. The value of the Display property becomes critical when you have multiple validators associated with the same input control. (See Figure 9-4.)

Input controls in the form are validated on the client.

Figure 9-4. Input controls in the form are validated on the client.

As you can see, the Hire Date text box is first validated to ensure it contains a valid date and then to verify the specified date is later than 1-1-1999. If the Display property is set to Static for the first validator, and the date is outside the specified range, you get a page like the one shown in Figure 9-5.

Static error messages take up space even if they’re not displayed.

Figure 9-5. Static error messages take up space even if they’re not displayed.

Multiple Validators per Control

Note that you can associate multiple validators with a single input control. Here’s an excerpt from the code behind the page in Figure 9-5:

<table>
    <tr>
        <td>Name</td><td>*</td>
        <td><asp:textbox runat="server" id="fname" />
            <asp:RequiredFieldValidator runat="server" id="fnameValidator"
                 ControlToValidate="fname"
            Text="!!!"
                     ErrorMessage="Name is mandatory" /></td></tr>
    <tr>
        <td>Last Name</td><td>*</td>
        <td><asp:textbox runat="server" id="lname" />
                <asp:RequiredFieldValidator runat="server" id="lnameValidator"
                     ControlToValidate="lname"
                 Text="!!!"
                 ErrorMessage="Last name is mandatory" /></td></tr>
    <tr>
        <td>Age</td><td></td>
        <td><asp:textbox runat="server" id="age" />
            <asp:CompareValidator runat="server" id="ageValidator"
                ControlToValidate="age"
                Operator="GreaterThanEqual"
                ValueToCompare="18"
                Type="integer"
                ErrorMessage="Age must be at least 18." /></td></tr>
    <tr>
        <td>Email</td><td></td>
        <td><asp:textbox runat="server" id="email" />
            <asp:RegularExpressionValidator runat="server" id="emailValidator"
                 ControlToValidate="email"
                 ValidationExpression="[a-zA-Z_0-9.-]+@[a-zA-Z_0-9.-]+.w+"
                 ErrorMessage="Must be an email address." /></td></tr>
    <tr>
        <td>Hire Date</td><td></td>
        <td><asp:textbox runat="server" id="hired" />
            <asp:CompareValidator runat="server" id="hiredValidator"
                 ControlToValidate="hired"
                 Display="Static"
                 Operator="DataTypeCheck"
                 Type="date"
                 ErrorMessage="Must enter a date." />
            <asp:RangeValidator runat="server" id="hiredDateValidator"
                 ControlToValidate="hired"
                 Display="Dynamic"
                 MinimumValue="1999-1-1"
                 MaximumValue="9999-12-31"
                 Type="Date"
                 ErrorMessage="Date after 1-1-99." /></td></tr>
    <tr>
        <td>Membership Level</td><td></td>
        <td><asp:textbox runat="server" id="membership" />
            <asp:CustomValidator runat="server" id="membershipValidator"
                     ControlToValidate="membership"
                 ClientValidationFunction="CheckMembership"
                     ErrorMessage="Must be Gold or Platinum." /></td></tr>
</table>

The hired control is being validated by a CompareValidator and a RangeValidator at the same time. Validation takes place in order, and each validation control generates and displays its own error message. The content of the input control is considered valid if all the validators return true. If an input control has multiple valid patterns—for example, an ID field can take the form of a Social Security number or a VAT number—you can either validate by using custom code or regular expressions.

Note

The preceding HTML snippet uses a table element to lay out the input fields around the form. This approach is discouraged and plain block elements should be used (DIV and P tags) that could be lined up via CSS styles. Unfortunately, I’m not a CSS expert.

The ValidationSummary Control

The ValidationSummary control is a label that summarizes and displays all the validation error messages found on a Web page after a postback. The summary is displayed in a single location formatted in a variety of ways. The DisplayMode property sets the output format, which can be a list, bulleted list, or plain text paragraph. By default, it is a bulleted list. The feasible values are grouped in the ValidationSummaryDisplayMode enumeration.

Whatever the format is, the summary can be displayed as text in the page, in a message box, or in both. The Boolean properties ShowSummary and ShowMessageBox let you decide. The output of the ValidationSummary control is not displayed until the page posts back no matter what the value of the EnableClientScript property is. The HeaderText property defines the text that is displayed atop the summary:

<asp:ValidationSummary runat="server"
    ShowMessageBox="true"
    ShowSummary="true"
    HeaderText="The following errors occurred:"
    DisplayMode="BulletList" />

This code snippet originates the screen shown in Figure 9-6.

After the page posts back, the validation summary is updated and a message box pops up to inform the user of the errors.

Figure 9-6. After the page posts back, the validation summary is updated and a message box pops up to inform the user of the errors.

The validation summary is displayed only if there’s at least one pending error. Notice that, in the default case, the labels near the input controls are updated anyway, along with the summary text. In summary, you can control the error information in the following ways:

  • Both in-place and summary information This is the default scenario. Use the ValidationSummary control, and accept all default settings on the validator controls. If you want to leverage both places to display information, a recommended approach consists of minimizing the in-place information by using the Text property rather than ErrorMessage. If you set both, Text is displayed in-place while ErrorMessage shows up in the validation summary. For example, you can set Text with a glyph or an exclamation mark and assign ErrorMessage with more detailed text.

  • Only in-place information Do not use the ValidationSummary control, and set the ErrorMessage property in each validation control you use. The messages appear after the page posts back.

  • Only summary information Use the ValidationSummary control, and set the ErrorMessage property on individual validation controls. Set the Display property of validators to None so that no in-place error message will ever be displayed.

  • Custom error information You don’t use the ValidationSummary control, and you set the Display property of the individual validators to None. In addition, you collect the various error messages through the ErrorMessage property on the validation controls and arrange your own feedback for the user.

Enabling Client Validation

As mentioned earlier, the verification normally takes place on the server as the result of the postback event or after the Validate method is called. If scripting is enabled on the browser, though, you can also activate the validation process on the client, with a significant gain in responsiveness. In fact, there’s no real value in making a roundtrip to the server only to discover that a required field has been left empty. The sooner you can figure it out, the better. On the other hand, you certainly can’t rely exclusively on client-side validation. To run secure code and prevent malicious and underhanded attacks, you should validate any input data on the server too.

When client-side validation is turned on, the page doesn’t post back until all the input fields contain valid data. However, not all types of validation can be accomplished on the client. In fact, if you need to validate against a database, well, there’s no other option than posting back to the server. (AJAX facilities, which we’ll explore in Chapter 20, might provide relief for this problem.)

Client validation can be controlled on a per-validation control basis by using the EnableClientScript Boolean property. By default, the property is set to true, meaning client validation is enabled as long as the browser supports it. By default, the code in the BaseValidator class detects the browser’s capabilities through the Request.Browser property. If the browser is considered up-level, the client validation will be implemented. In ASP.NET 4, browsers and client devices that are considered up-level support at least the following:

  • ECMAScript version 1.2 or newer

  • W3C DOM Level 1 or greater

Today, nearly all browsers available meet these requirements. Generally, an up-level browser matches the capabilities of Internet Explorer 6 and newer. Consider that ASP.NET 4 checks the browser capabilities using the Request.Browser object. The information that this object returns is influenced by the value of the ClientTarget property on the Page class. The property indicates which set of browser capabilities the page assumes from the current browser. Specifying a value for the ClientTarget property overrides the automatic detection of browser capabilities that is normally accomplished. You can set the ClientTarget property via code, using the @Page directive, or in the configuration file.

What are the feasible values for ClientTarget?

In general, ClientTarget gets a string that refers to a user agent string. However, the root web.config configuration file defines a couple of default aliases that you can use as shorthand for common user-agent strings: uplevel and downlevel.

The uplevel alias specifies browser capabilities equivalent to Internet Explorer 6, whereas the downlevel alias refers to the capabilities of older browsers that do not support client script. You can define additional aliases in the clientTarget section of the application-level web.config file. (See Chapter 3.)

Validation Groups

By default, control validation occurs in an all-or-nothing kind of way. For example, if you have a set of input and validation controls and two buttons on the form, clicking either button will always validate all controls. In other words, there’s no way to validate some controls when one button is clicked and some others when the other button is clicked.

The CausesValidation property on button controls allows you to disable validation on a button, but that is not the real point here. What would be desirable is the ability to perform validation on a group of controls. This is exactly what the ValidationGroup property provides. The property is available on validators, input controls, and button controls.

Using the ValidationGroup property is simple; just define it for all the validation controls that you want to group together, and then assign the same name to the ValidationGroup property of the button that you want to fire the validation. Here’s an example:

<asp:textbox runat="server" id="TextBox1"  />
<asp:RequiredFieldValidator runat="server"
    ValidationGroup="Group1"
    ControlToValidate="TextBox1"
    ErrorMessage="TextBox1 is mandatory" />
<asp:textbox runat="server" id="TextBox2"  />
<asp:RequiredFieldValidator runat="server"
    ValidationGroup="Group2"
    ControlToValidate="TextBox2"
    ErrorMessage="TextBox2 is mandatory" />
<asp:Button runat="server" Text="Check Group1"
     ValidationGroup="Group1" />
<asp:Button runat="server" Text="Check Group2"
     ValidationGroup="Group2" />

The two RequiredFieldValidator controls belong to distinct validation groups—Group1 and Group2. The first button validates only the controls defined within Group1; the second button takes care of the input associated with Group2. In this way, the validation process can be made as granular as needed.

Important

The ValidationGroup property can also be defined optionally on input controls. This is required only if you use the CustomValidator control as a way to check whether a given input control belongs to the right validation group. Unlike other validators, the CustomValidator control, in fact, is not strictly bound to a specific control.

Validation groups are well reflected on the server-side, where the Validate method of the Page class features an overload that lets you select the group according to which the page must be validated.

Dealing with Validation in Cross-Page Posts

Validation groups are especially helpful when combined with cross-page postbacks. As you saw earlier in the chapter, a cross-page postback allows a button to post the contents of the current form to another page, in a way overriding the single-form model of ASP.NET. In a cross-page posting scenario, what if the original page contains validators? Imagine a page with a text box whose value is to be posted to another page. You don’t want the post to occur if the text box is empty. To obtain this behavior, you add a RequiredFieldValidator control and bind it to the text box:

<asp:TextBox ID="Keyword" runat="server" />
<asp:RequiredFieldValidator ID="Validator1" runat="server"
     ControlToValidate="Keyword" Text="*" />
<asp:Button ID="Button1" runat="server" Text="Search..."
     OnClick="Button1_Click" PostBackUrl="doSearch.aspx" />

As expected, when you click the button the page won’t post if the text box is empty; and an asterisk (plus an optional message) is displayed to mark the error. This is because RequiredFieldValidator benefits the client-side capabilities of the browser and validates the input controls before proceeding with the post. Hence, in the case of empty text boxes, the button doesn’t even attempt to make the post.

Is that all, or is there more to dig out?

Let’s work with a CustomValidator control, which instead requires that some server-side code be run to check the condition. Can you imagine the scenario? You’re on, say, crosspage.aspx and want to reach doSearch.aspx; to make sure you post only under valid conditions, though, you first need to make a trip to crosspage.aspx to perform some validation. Add this control, write the server validation handler in crosspage.aspx, and put a breakpoint in its code:

<asp:CustomValidator ID="CustomValidator1" runat="server"
     Text="*"
     ControlToValidate="Keyword"
     OnServerValidate="EnsureValidKeywords" />

Debugging this sample page reveals that posting to another page is a two-step operation. First, a classic postback is made to run any server-side code registered with the original page (for example, server-side validation code or code associated with the click of the button). Next, the cross-page call is made to reach the desired page:

void EnsureValidKeywords(Object source, ServerValidateEventArgs args)
{
    args.IsValid = false;
    if (String.Equals(args.Value, "Dino"))
        args.IsValid = true;
}

The preceding code sets the page’s IsValid property to false if the text box contains anything other than “Dino.” However, this fact alone doesn’t prevent the transition to the target page. In other words, you could still have invalid input data posted to the target page.

Fortunately, this issue has an easy workaround, as shown in the following code:

if (!PreviousPage.IsValid)
{
    Response.Write("Sorry, the original page contains invalid input.");
    Response.End();
    return;
}

In the target page, you test the IsValid property on the PreviousPage property and terminate the request in the case of a negative answer. However, to avoid a server request and, worse yet, a page transition, you can add a client check to the CustomValidator control:

<asp:CustomValidator ID="CustomValidator1" runat="server"
     Text="*"
     ControlToValidate="Keyword"
     ClientValidationFunction="ensureValidKeywords"
     OnServerValidate="EnsureValidKeywords" />

Here’s a possible implementation of the JavaScript function:

<script type="text/javascript">
    function ensureValidKeywords(source, arguments) {
        arguments.IsValid = false;
        var buf = arguments.Value;
        if (buf == "Dino")
            arguments.IsValid = true;
    }
</script>

Working with Wizards

An input form is used to collect data from users. However, it is not unusual that the amount of data to be collected is quite large and dispersed. In these cases, a single form is hardly the right solution. A wizard is a sequence of related steps, each associated with an input form and a user interface.

Wizards are typically used to break up large forms to collect user input. Users move through the wizard sequentially, but they are normally given a chance to skip a step or jump back to modify some of the entered values. A wizard is conceptually pretty simple, but implementing it over HTTP connections can be tricky. In ASP.NET, you have a readymade server control—the Wizard control—that automates many of the tasks.

An Overview of the Wizard Control

The Wizard control supports both linear and nonlinear navigation. It allows you to move backward to change values and skip steps that are unnecessary because of previous settings or because users don’t want to fill in those fields. Like many other ASP.NET controls, the Wizard control supports themes, styles, and templates.

Wizard is a composite control and automatically generates some constituent controls, such as navigation buttons and panels. As you’ll see in a moment, the programming interface of the control has multiple templates that provide for in-depth customization of the overall user interface. The control also guarantees that state is maintained no matter where you move—backward, forward, or to a particular page. All the steps of a wizard must be declared within the boundaries of the same Wizard control. In other words, the wizard must be self-contained and not provide page-to-page navigation.

Structure of a Wizard

As shown in Figure 9-7, a wizard has four parts: a header, view, navigation bar, and sidebar.

The four parts of a Wizard control.

Figure 9-7. The four parts of a Wizard control.

The header consists of text you can set through the HeaderText property. You can change the default appearance of the header text by using its style property; you can also change the structure of the header by using the corresponding header template property. If HeaderText is empty and no custom template is specified, no header is shown for the wizard.

The view displays the contents of the currently active step. The wizard requires you to define each step in an <asp:wizardstep> element. An <asp:wizardstep> element corresponds to a WizardStep control. Different types of wizard steps are supported; all wizard step classes inherit from a common base class named WizardStepBase.

All wizard steps must be grouped in a single <wizardsteps> tag, as shown in the following code:

<asp:wizard runat="server" DisplaySideBar="true">
  <wizardsteps>
    <asp:wizardstep runat="server" steptype="auto" id="step1">
      First step
    </asp:wizardstep>
    <asp:wizardstep runat="server" steptype="auto" id="step2">
      Second step
    </asp:wizardstep>
    <asp:wizardstep runat="server" steptype="auto" id="finish">
      Final step
    </asp:wizardstep>
  </wizardsteps>
</asp:wizard>

The navigation bar consists of autogenerated buttons that provide any needed functionality—typically, going to the next or previous step or finishing. You can modify the look and feel of the navigation bar by using styles and templates.

The optional sidebar is used to display content on the left side of the control. It provides an overall view of the steps needed to accomplish the wizard’s task. By default, it displays a description of each step, with the current step displayed in boldface type. You can customize the sidebar using styles and templates. Figure 9-8 shows the default user interface. Each step is labeled using the ID of the corresponding <asp:wizardstep> tag.

A wizard with the default sidebar on the left side.

Figure 9-8. A wizard with the default sidebar on the left side.

Wizard Styles and Templates

You can style all the various parts and buttons of a Wizard control by using the properties listed in Table 9-5.

Table 9-5. The Wizard Control’s Style Properties

Style

Description

CancelButtonStyle

Sets the style properties for the wizard’s Cancel button

FinishCompleteButtonStyle

Sets the style properties for the wizard’s Finish button

FinishPreviousButtonStyle

Sets the style properties for the wizard’s Previous button when at the finish step

HeaderStyle

Sets the style properties for the wizard’s header

NavigationButtonStyle

Sets the style properties for navigation buttons

NavigationStyle

Sets the style properties for the navigation area

SideBarButtonStyle

Sets the style properties for the buttons on the sidebar

SideBarStyle

Sets the style properties for the wizard’s sidebar

StartStepNextButtonStyle

Sets the style properties for the wizard’s Next button when at the start step

StepNextButtonStyle

Sets the style properties for the wizard’s Next button

StepPreviousButtonStyle

Sets the style properties for the wizard’s Previous button

StepStyle

Sets the style properties for the area where steps are displayed

The contents of the header, sidebar, and navigation bar can be further customized with templates. Table 9-6 lists the available templates.

Table 9-6. The Wizard Control’s Template Properties

Style

Description

FinishNavigationTemplate

Specifies the navigation bar shown before the last page of the wizard. By default, the navigation bar contains the Previous and Finish buttons.

HeaderTemplate

Specifies the title bar of the wizard.

SideBarTemplate

Used to display content on the left side of the wizard control.

StartNavigationTemplate

Specifies the navigation bar for the first view in the wizard. By default, it contains only the Next button.

StepNavigationTemplate

Specifies the navigation bar for steps other than first, finish, or complete. By default, it contains Previous and Next buttons.

In addition to using styles and templates, you can control the programming interface of the Wizard control through a few properties.

The Wizard’s Programming Interface

Table 9-7 lists the properties of the Wizard control, excluding style and template properties and properties defined on base classes.

Table 9-7. Main Properties of the Wizard Control

Property

Description

ActiveStep

Returns the current wizard step object. The object is an instance of the WizardStep class.

ActiveStepIndex

Gets and sets the 0-based index of the current wizard step.

DisplayCancelButton

Toggles the visibility of the Cancel button. The default value is false.

DisplaySideBar

Toggles the visibility of the sidebar. The default value is false.

HeaderText

Gets and sets the title of the wizard.

SkipLinkText

The ToolTip string that the control associates with an invisible image, as a hint to screen readers. The default value is “Skip Navigation Links” and is localized based on the server’s current locale.

WizardSteps

Returns a collection containing all the WizardStep objects defined in the control.

A wizard in action is fully represented by its collection of step views and buttons. In particular, you’ll recognize the following buttons: StartNext, StepNext, StepPrevious, FinishComplete, FinishPrevious, and Cancel. Each button is characterized by properties to get and set the button’s image URL, caption, type, and destination URL after a click. The name of a property is the name of the button followed by a suffix. The available suffixes are listed in Table 9-8.

Table 9-8. Suffix of Button Properties

Suffix

Description

ButtonImageUrl

Gets and sets the URL of the image used to render the button

ButtonText

Gets and sets the text for the button

ButtonType

Gets and sets the type of the button: push button, image, or link button

DestinationPageUrl

Gets and sets the URL to jump to once the button is clicked

Note that names in Table 9-8 do not correspond to real property names. You have the four properties in this table for each distinct type of wizard button. The real name is composed by the name of the button followed by any of the suffixes—for example, CancelButtonText, FinishCompleteDestinationPageUrl, and so on.

The Wizard control also supplies a few interesting methods—for example, GetHistory, which is defined as follows:

public ICollection GetHistory()

GetHistory returns a collection of WizardStepBase objects. The order of the items is determined by the order in which the wizard’s pages were accessed by the user. The first object returned—the one with an index of 0—is the currently selected wizard step. The second object represents the view before the current one, and so on.

The second method, MoveTo, is used to move to a particular wizard step. The method’s prototype is described here:

public void MoveTo(WizardStepBase step)

The method requires you to pass a WizardStepBase object, which can be problematic. However, the method is a simple wrapper around the setter of the ActiveStepIndex property. If you want to jump to a particular step and not hold an instance of the corresponding WizardStep object, setting ActiveStepIndex is just as effective.

Table 9-9 lists the key events in the life of a Wizard control in an ASP.NET page.

Table 9-9. Events of the Wizard Control

Event

Description

ActiveViewChanged

Raised when the active step changes

CancelButtonClick

Raised when the Cancel button is clicked

FinishButtonClick

Raised when the Finish Complete button is clicked

NextButtonClick

Raised when any Next button is clicked

PreviousButtonClick

Raised when any Previous button is clicked

SideBarButtonClick

Raised when a button on the sidebar is clicked

As you can see, there’s a common click event for all Next and Previous buttons you can find on your way. A Next button can be found on the Start page as well as on all step pages. Likewise, a Previous button can be located on the Finish page too. Whenever a Next button is clicked, the page receives a NextButtonClick event; whenever a Previous button is clicked, the control raises a PreviousButtonClick event.

Adding Steps to a Wizard

A WizardStep object represents one of the child views that the wizard can display. The WizardStep class ultimately derives from View and adds just a few public properties to it. A View object represents a control that acts as a container for a group of controls. A view is hosted within a MultiView control. To create its output, the wizard makes internal use of a MultiView control. However, the wizard is not derived from the MultiView class.

You define the views of a wizard through distinct instances of the WizardStep class, all grouped under the <WizardSteps> tag. The <WizardSteps> tag corresponds to the WizardSteps collection property exposed by the Wizard control:

<WizardSteps>
    <asp:WizardStep>
        ...
    </asp:WizardStep>
    <asp:WizardStep>
        ...
    </asp:WizardStep>
</WizardSteps>

Each wizard step is characterized by a title and a type. The Title property provides a brief description of the view. This information is not used unless the sidebar is enabled. If the sidebar is enabled, the title of each step is used to create a list of steps. If the sidebar is enabled but no title is provided for the various steps, the ID of the WizardStep objects is used to populate the sidebar, as shown earlier in Figure 9-8.

While defining a step, you can also set the AllowReturn property, which indicates whether the user is allowed to return to the current step from a subsequent step. The default value of the property is true.

Types of Wizard Steps

The StepType property indicates how a particular step should be handled and rendered within a wizard. Acceptable values for the step type come from the WizardStepType enumeration, as listed in Table 9-10.

Table 9-10. Wizard Step Types

Property

Description

Auto

The default setting, which forces the wizard to determine how each contained step should be treated.

Complete

The last page that the wizard displays, usually after the wizard has been completed. The navigation bar and the sidebar aren’t displayed.

Finish

The last page used for collecting user data. It lacks the Next button, and it shows the Previous and Finish buttons.

Start

The first screen displayed, with no Previous button.

Step

All other intermediate pages, in which the Previous and Next buttons are displayed.

When the wizard is in automatic mode—the default type Auto—it determines the type of each step based on the order in which the steps appear in the source code. For example, the first step is considered to be of type Start and the last step is marked as Finish. No Complete step is assumed. If you correctly assign step types to your wizard steps yourself, rather than use the Auto type, the order in which you declare your steps in the .aspx source is not relevant.

Creating an Input Step

The following code shows a sample wizard step used to collect the provider name and the connection string to connect to a database and search for some data. For better graphical results, the content of the step is encapsulated in a fixed-height <div> tag. If all the steps are configured in this way, users navigating through the wizard won’t experience sudden changes in the overall page size and layout:

<asp:wizardstep ID="Wizardstep1" runat="server" title="Connect">
    <div>
        <table>
            <tr><td>Provider</td><td>
                <asp:textbox runat="server" id="ProviderName"
                             text="System.Data.SqlClient" />
            </td></tr>
            <tr><td>Connection String</td><td>
                <asp:textbox runat="server" id="ConnString"
                    text="SERVER=(local);DATABASE=northwind;... " />
            </td></tr>
            <tr><td height="100px"></td></tr>
        </table>
    </div>
</asp:wizardstep>

Figure 9-9 shows a preview of the step. As you could probably guess, the step is recognized as a Start step. As a result, the wizard is added only to the Next button.

A sample Start wizard step.

Figure 9-9. A sample Start wizard step.

A wizard is usually created for collecting input data, so validation becomes a critical issue. You can validate the input data in two nonexclusive ways—using validators and using transition event handlers.

The first option involves placing validator controls in the wizard step. This guarantees that invalid input—empty fields or incompatible data types—is caught quickly and, optionally, already on the client:

<asp:requiredfieldvalidator ID="RequiredField1" runat="server"
    text="*"
    errormessage="Must indicate a connection string"
    setfocusonerror="true"
    controltovalidate="ConnString" />

If you need to access server-side resources to validate the input data, you’re better off using transition event handlers. A transition event is an event the wizard raises when it is about to switch to another view. For example, the NextButtonClick event is raised when the user clicks the Next button to jump to the subsequent step. You can intercept this event, do any required validation, and cancel the transition if necessary. I’ll return to this topic in a moment.

Defining the Sidebar

The sidebar is a left-side panel that lists buttons to quickly and randomly reach any step of the wizard. It’s a sort of quick-launch menu for the various steps that form the wizard. You control the sidebar’s visibility through the Boolean DisplaySideBar attribute and define its contents through the SideBarTemplate property.

Regardless of the template, the internal layout of the sidebar is not left entirely to your imagination. In particular, the <SideBarTemplate> tag must contain a DataList control with a well-known ID—SideBarList. In addition, the <ItemTemplate> block must contain a button object with the name of SideBarButton. The button object must be any object that implements the IButtonControl interface.

Note

For better graphical results, you might want to use explicit heights and widths for all steps and the sidebar as well. Likewise, the push buttons in the navigation bar might look better if they are made the same size. You do this by setting the Width and Height properties on the NavigationButtonStyle object.

Navigating Through the Wizard

When a button is clicked to move to another step, an event is fired to the hosting page. It’s up to you to decide when and how to perform any critical validation, such as deciding whether or not conditions exist to move to the next step.

In most cases, you’ll want to perform server-side validation only when the user clicks the Finish button to complete the wizard. You can be sure that whatever route the user has taken within the wizard, clicking the Finish button will complete it. Any code you bind to the FinishButtonClick event is executed only once, and only when strictly necessary.

By contrast, any code bound to the Previous or Next button executes when the user moves back or forward. The page posts back on both events.

Filtering Page Navigation with Events

You should perform server-side validation if what the user can do next depends on the data she entered in the previous step. This means that in most cases you just need to write a NextButtonClick event handler:

<asp:wizard runat="server" id="QueryWizard"
    OnNextButtonClick="OnNext">
    ...
</asp:wizard>

If the user moves back to a previously visited page, you can usually ignore any data entered in the current step and avoid validation. Because the user is moving back, you can safely assume she is not going to use any fresh data. When a back movement is requested, you can assume that any preconditions needed to visit that previous page are verified. This happens by design if your users take a sequential route.

If the wizard’s sidebar is enabled, users can jump from page to page in any order. If the logic you’re implementing through the wizard requires that preconditions be met before a certain step is reached, you should write a SideBarButtonClick event handler and ensure that the requirements have been met.

A wizard click event requires a WizardNavigationEventHandler delegate (which is defined for you by ASP.NET):

public delegate void WizardNavigationEventHandler(
    object sender,
    WizardNavigationEventArgs e);

The WizardNavigationEventArgs structure contains two useful properties that inform you about the 0-based indexes of the page being left and the page being displayed. The CurrentStepIndex property returns the index of the last page visited; NextStepIndex returns the index of the next page. Note that both properties are read-only.

The following code shows a sample handler for the Next button. The handler prepares a summary message to show when the user is going to the Finish page:

void OnNext(object sender, WizardNavigationEventArgs e)
{
    // Collect the input data if going to the last page
    // -1 because of 0-based indexing, add -1 if you have a Complete page
    if (e.NextStepIndex == QueryWizard.WizardSteps.Count - 2)
        PrepareFinalStep();
}
void PrepareFinalStep()
{
    string cmdText = DetermineCommandText();

    // Show a Ready-to-go message
    var sb = new StringBuilder("");
    sb.AppendFormat("You're about to run: <br><br>{0}<hr>", cmdText);
    sb.Append("<b><br>Ready to go?</b>");
    ReadyMsg.Text = sb.ToString();
}

string DetermineCommandText()
{
    // Generate and return command text here
}

Each page displayed by the wizard is a kind of panel (actually, a view) defined within a parent control—the wizard. This means that all child controls used in all steps must have a unique ID. It also means that you can access any of these controls just by name. For example, if one of the pages contains a text box named, say, ProviderName, you can access it from any event handler by using the ProviderName identifier.

The preceding code snippet is an excerpt from a sample wizard that collects input and runs a database query. The first step picks up connection information, whereas the second step lets users define tables, fields, and optionally a WHERE clause. The composed command is shown in the Finish page, where the wizard asks for final approval. (See Figure 9-10.)

Two successive pages of the sample wizard: query details and the Finish step.

Figure 9-10. Two successive pages of the sample wizard: query details and the Finish step.

Canceling Events

The WizardNavigationEventArgs structure also contains a read/write Boolean property named Cancel. If you set this property to true, you just cancel the ongoing transition to the destination page. The following code shows how to prevent the display of the next step if the user is on the Start page and types in john as the user ID:

void OnNext(object sender, WizardNavigationEventArgs e)
{
    if (e.CurrentStepIndex == 0 &&
        ConnString.Text.IndexOf("UID=john") > -1)
    {
        e.Cancel = true;
        return;
    }
}

You can cancel events from within any transition event handler and not just from the NextButtonClick event handler. This trick is useful to block navigation if the server-side validation of the input data has failed. If you do cause a step to fail, though, you’re responsible for showing some feedback to the user.

Note

You can’t cancel navigation from within the ActiveViewChanged event. This event follows any transition events, such as the NextButtonClick or PreviousButtonClick event, and it occurs when the transition has completed. Unlike transition events, the ActiveViewChanged event requires a simpler, parameterless handler—EventHandler.

Finalizing the Wizard

All wizards have some code to execute to finalize the task. If you use the ASP.NET Wizard control, you place this code in the FinishButtonClick event handler. Figure 9-11 shows the final step of a wizard that completed successfully.

void OnFinish(object sender, WizardNavigationEventArgs e)
{
    string finalMsg = "The operation completed successfully.";
    try
    {
        // Complete the wizard (compose and run the query)
        var command = DetermineCommandText();
        var table = ExecuteCommand(ConnString.Text, command);
        grid.DataSource = table;
        grid.DataBind();

        // OK color
        FinalMsg.ForeColor = Color.Blue;
    }
    catch (Exception ex) {
        FinalMsg.ForeColor = Color.Red;
        finalMsg = String.Format("The operation cannot be completed
                                  due to:<br />{0}", ex.Message);
    }
    finally {
        FinalMsg.Text = finalMsg;
    }
}

string DetermineCommandText()
{
    // Generate and return command text here
}

DataTable ExecuteCommand()
{
    // Execute database query here
}
Final step of a wizard that completed successfully.

Figure 9-11. Final step of a wizard that completed successfully.

If the wizard contains a Complete step, that page should be displayed after the Finish button is clicked and the final task has completed. If something goes wrong with the update, you should either cancel the transition to prevent the Complete page from even appearing or adapt the user interface of the completion page to display an appropriate error message. Which option you choose depends on the expected behavior of the implemented operation. If the wizard’s operation can fail or succeed, you let the wizard complete and display an error message if something went wrong. If the wizard’s operation must complete successfully unless the user quits, you should not make the transition to the Complete page; instead, provide users with feedback on what went wrong and give them a chance to try again.

Summary

Form-based programming is fundamental in Web applications because it’s the only way to have users and applications interact. ASP.NET pages can have only one server-side form with a fixed action property. Subsequently, pages are reentrant and always post to themselves. The behavior of the form can’t be changed because it is crucial to the behavior of ASP.NET, but a different feature—cross-page posting—comes to the rescue to let users post data from one page to another. Cross-page posting is essential when you have legacy pages to integrate in a new application that, for whatever reason, can’t be adapted to a more specific ASP.NET architecture.

Input forms also bring to the table the whole theme of input validation. ASP.NET comes with a stock of native validation controls to cover the basic needs of validation. Validators let you put declarative boundaries around input controls so that any user’s input is filtered and validated both on the client and server. This alone is not sufficient to certify an application as secure, but it is a quantum leap in the right direction.

Finally, in this chapter entirely devoted to getting input data into an ASP.NET server application, we’ve covered wizards—namely, a semi-automatic way of breaking up large forms into smaller pieces served individually to the user, while keeping track of some state. Whether you use the ASP.NET Wizard control or roll your own custom solution, the awareness that splitting large forms into sequential screens gives end users a more pleasant experience is what really matters for ASP.NET developers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.27.29