Share via


Cutting Edge

ASP.NET Forms

Dino Esposito

Contents

Rendering Forms
The HtmlForm Class
Managing Multiple Forms
Multiple <form> Tags
Cross-Page Posting
Redirecting Users to Another Page
Conclusion

Forms are an essential piece of ASP.NET—the ASP.NET Web programming model itself wouldn't be possible without forms. The use of forms is not constrained in pure HTML, but it is subject to some restrictions in ASP.NET. In ASP.NET pages, a single form can post to itself, and the model provides for generalized control state management and postback events. Writing ASP.NET apps is easy and effective because of the single form model.

The restrictions on forms in ASP.NET may sound weird and arbitrary at first, but actually the model is quite straightforward to work with. However, there is one real-world scenario that the ASP.NET 1.x form model doesn't address: having multiple, highly specialized forms in the same page. For example, you can't have a search box that posts to a different page.

In the May 2003 issue of MSDN®Magazine, I wrote a column about forms programming in ASP.NET 1.x (see Cutting Edge: Form-based Programming in ASP.NET. With the introduction of ASP.NET 2.0, a few things have changed that specifically address the theme of posting to different pages. This month I'll discuss forms programming in ASP.NET 2.0.

Rendering Forms

Let's start exploring the universe of forms in ASP.NET with some consideration of how forms (and contained controls) are actually rendered. In an ASP.NET page, the <form> tag can be the child of various container controls such as <table>, <div>, or <body>; however, in most pages it is simply the child of <body>. If a non-container control (TextBox, for example) is placed outside the form tag, an HttpException is thrown at run time (no check is made for this at compile time). Take a look at the following code, which is excerpted from the TextBox's AddAttributesToRender method:

protected override void AddAttributesToRender(HtmlTextWriter writer)
{
    if (this.Page != null)  this.Page.VerifyRenderingInServerForm(this);
    ...
}

A call to Page's VerifyRenderingInServerForm method does the job. (Be aware of this behavior when you write your own custom server controls.)

The HtmlForm Class

HtmlForm inherits from HtmlContainerControl, which provides the form with the capability of containing child controls. HtmlForm provides programmatic access to the HTML <form> element on the server through the set of properties listed in Figure 1. As you can see, the interface changes in HtmlForm between ASP.NET 1.x and ASP.NET 2.0 are limited to a few properties.

Figure 1 Properties of the HtmlForm Class

Property ASP.NET 1.x ASP.NET 2.0 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. Matches the disabled HTML attribute.
EncType Gets or sets the encoding type. Matches the enctype HTML attribute.
ID Inherited from Control. Gets or sets the programmatic identifier of the form.
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. 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 false, the form is not rendered to HTML.

A form must have a unique name. One is assigned automatically by ASP.NET if you don't specify one. You can set the form's identifier by using either the ID or Name property. If both are set, the ID attribute takes precedence. It is important to note, though, that any programmatic use of the Name attribute compromises the XHTML compliance of the page. In XHTML, elements are identified by ID and not by Name. So generally speaking, you're better off relying on the ID property.

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

Figure 2 lists some of the methods available on the HtmlForm class that you'll be using most often. All the methods listed in the table are inherited from the base System.Web.UI.Control class. Note that the FindControl method searches only among the form's direct children. Controls belonging to an inner container or a child of a form's child control are not found.

Figure 2 Methods of the HtmlForm Class

Method ASP.NET 1.x ASP.NET 2.0 Description
ApplyStyleSheetSkin   Applies the skin in the defined page theme in the manner defined by StyleSheetTheme.
DataBind Calls the DataBind method on all child controls.
FindControl Retrieves and returns the control that matches the specified ID.
Focus   Sets the 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, caches tracing information to be rendered later, at the end of the page.

Managing Multiple Forms

Generally, embracing the single-form model and renouncing system support for multiple forms is not a big sacrifice. Some pages, though, would have a more consistent and natural design if they could define multiple forms—at least, forms with logically related groups of input controls. For example, think of a page that provides some information to users, but also needs to supply an additional form containing a search or a login box.

You could incorporate search and login functionalities in ad hoc classes and call those classes from within the page displayed to the user. This is not necessarily the best way to factor out your code, however. If you're porting some old code to ASP.NET, you might find it easier to insulate login or search code within a dedicated page. But how do you force the page to post data to this page?

In the single-form model, each page always posts to itself and doesn't supply a hook for developers to set the final destination of the postback. In HTML and ASP programming, the Action property of the form is simply not exposed on the ASP.NET HtmlForm class. The single-form model is so closely integrated with the ASP.NET platform that you can only take or leave it—or, as an additional choice, code the old ASP way without server forms. As you'll see later, in ASP.NET 2.0, posting data to a different page is possible, but the implementation of the feature passes through some new capabilities of button controls. For now, let's see what is involved in using additional HTML non-server forms.

In ASP.NET, an exception is thrown if multiple HtmlForm controls are being rendered. When the first HtmlForm control in the page is rendered, a Boolean flag is flipped to true. The flag thus indicates whether any other HtmlForm control has been rendered. When another HtmlForm attempts to render, the flag will already be true causing an exception to be thrown.

Nothing bad happens if the Web Form contains one server form and any number of <form> tags devoid of the runat attribute. Without the runat attribute, any tags become pure and simple HTML markup and are rendered verbatim (see Figure 3).

Figure 3 Server and Client Forms in the Same Page

<html>
<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>
</html>

The page contains two forms, the second of which is an HTML form and lacks the runat="server" attribute. As such, it is completely ignored by ASP.NET. The markup served to the browser legally contains two <form> elements, each pointing to a different action URL.

Although functional, this code 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, the action page for the HTML client form, you can't rely on page controls that read and update their state from view state and posted values. (The apparent statefulness of ASP.NET server controls is obtained by making pages post to themselves.) To know what's been posted to search.aspx, you must resort to the old-fashioned yet effective ASP model of looking directly into the collection of posted data:

protected void Page_Load(object sender, EventArgs e)
{
    // Use the Request to retrieve posted data
    string textToSearch = Request.Form["Keyword"].ToString();
    ...
    // Use standard ASP.NET to populate the page UI
    KeywordBeingUsed.Text = textToSearch;
}

You use the protocol-specific collections of the HttpRequest object (available from Page.Request as well as HttpContext.Current.Request) to retrieve posted data—Form if POST is used, QueryString if GET is used, or Params if you want combined access to Form, QueryString, ServerVariables, and Cookies. The HttpRequest object is populated with data before the page is created, so any call to Page.Request works from any of the page's events. In self-posting ASP.NET pages, you don't need to use Request because you can rely on a strongly typed programming model, but the old, faithful HttpRequest object is still there for you to use if you need it.

It is also interesting to note that when the user clicks on the Search button, the search.aspx page is invoked, and it receives only the values posted through the HTML form. No view state is posted, and no extra data is passed. If you have to post to another page, the old-fashioned approach is still going to be the most effective performance-wise. As you'll see later in this column, the cross-page posting feature of ASP.NET 2.0 still moves around a fairly large, view-state-like field.

Multiple <form> Tags

If multiple server forms are declared in the same Web Form, an exception is thrown. Not so obvious, and not very well known, is that a Web Form can actually contain as many server-side forms as needed, so long as only one is visible and rendered at a time. For example, a page with three <form> tags marked runat="server" is allowed, but only one form's Visible property can be set to true. By playing with the Visible property of the HtmlForm class, you can change the active server form during the page's lifetime. This little trick doesn't really solve the problem of having multiple active forms, but it can still be helpful at times.

Let's consider the page in Figure 4. As you can see, all <form> tags are marked runat="server", but only the first is visible. Mutually exclusive forms are great for implementing wizards in ASP.NET 1.x. By toggling the form's visibility in button event handlers, you can obtain a wizard-like behavior, as depicted in Figure 5.

Figure 4 Markup for an ASP.NET 1.x Wizard-Like Page

<html>
<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>
</html>

Figure 5 Wizard-Like Page in Action

Figure 5** Wizard-Like Page in Action **

The trick is rather useless in ASP.NET 2.0 because you find two new controls, MultiView and Wizard, ready for the job. The MultiView control employs logic nearly identical to multiple exclusive forms, except that it relies on panels rather than full-fledged forms. MultiView allows you to define multiple and mutually exclusive HTML panels. The control provides an 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 a MultiView plus some wizard-like predefined UI blocks. I covered the Wizard control in the November 2004 issue of MSDN Magazine (see Cutting Edge: The ASP.NET 2.0 Wizard Control).

Cross-Page Posting

ASP.NET 2.0 offers a new built-in mechanism to override the normal processing cycle and to allow a page to post back to another page. In general, postbacks occur in one of two ways: through a Submit button or through a script. Submissions through a button typically point automatically to the address that the posting form indicates. More flexibility is possible when the post occurs through a script. In ASP.NET 2.0, you can configure certain page controls—in particular, those that implement the new IButtonControl interface—to post to a different target page. This is referred to as cross-page posting.

Core controls that implement the IButtonControl interface are Button, ImageButton, and LinkButton. In general, by implementing IButtonControl, any custom control can act like a button on a form. The IButtonControl interface is a clear example of the refactoring process that ASP.NET went through in the transition from version 1.x to 2.0. The IButtonControl interface now groups a few properties that most button controls (including some HTML button controls) support since ASP.NET 1.x. In addition, a few new properties heralding new functionality have been added, like PostBackUrl and ValidationGroup. Figure 6 details the IButtonControl interface. The following code snippet shows how to proceed:

<form runat="server">
    <asp:textbox runat="server" id="Data" />
    <asp:button runat="server" id="buttonPost" Text="Click" 
        PostBackUrl="target.aspx" />
</form>

Figure 6 IButtonControl Interface

Property Description
CausesValidation Boolean value indicating whether validation is performed when the control is clicked.
CommandArgument Gets or sets an optional parameter passed to the button's Command event along with the associated CommandName.
CommandName Gets or sets the command name associated with the button that is passed to the Command event.
PostBackUrl Indicates the URL that will handle the postback triggered through the button control.
Text Gets or sets the caption of the button.
ValidationGroup Gets or sets the name of the validation group to which the button belongs.
Visible Boolean value indicating whether the button control is rendered.

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 the __doPostback function normally used, it uses the new WebForm_DoPostBackWithOptions function. The button defined previously renders the following markup:

<input type="submit" name="buttonPost" id="buttonPost" value="Click"
     onclick="javascript:WebForm_DoPostBackWithOptions(
         new WebForm_PostBackOptions("buttonPost", "", 
         false, "", "target.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 named __PREVIOUSPAGE is created. The field contains information about the posting page. The target page uses this information in order to build a stateful reference to the calling page object.

In the target page, you use the PreviousPage property, a new property on the Page class, to reference the posting page and all of its controls. Here's the codebehind for a sample target page that retrieves the content of a TextBox defined in the form:

protected void Page_Load(object sender, EventArgs e)
{
    // Retrieves some posted data
    TextBox txt = (TextBox) PreviousPage.FindControl("TextBox1");
    ...
}

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 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. 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 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. To avoid using FindControl altogether, a different approach is required.

To retrieve values on the posting page, FindControl represents your only safe option if you don't know in advance which page will be invoking your target. However, when using cross-page posting in the context of an application, there's a good chance 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="crosspostpage.aspx" %>

The directive can accept either of two mutually exclusive attributes—VirtualPath or TypeName. VirtualPath points to the URL of the posting page. TypeName indicates the type of the calling page. The PreviousPageType directive makes the PreviousPage property on the target page return an object of the same type as the page at the given path (or of the specified type if the TypeName attribute is used). This fact alone, though, is not sufficient to let you access input controls directly. In ASP.NET, 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. In fact, only derived classes can access protected members of the parent class.

To work around this 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 _textBox1. To make it accessible from within a target page, you add the following code to the codebehind class:

public TextBox TextBox1
{
    get { return _textBox1; }
}

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

Response.Write(PreviousPage.TextBox1.Text);

Being the potential target of a cross-page call doesn't automatically make a target page a different kind of page. There's always the possibility that the target page is invoked on its own, for example, through a hyperlink. When this happens, the PreviousPage property returns null and other postback-related properties (like IsPostBack) assume the usual values. If you have such a dual page, it's a good idea to add some extra code to discern the page behavior. The following code hosted in the Page_Load event makes the page work only through cross-page calls:

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

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: the Server.Transfer method. When you call this 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—no client redirection ever occurs. The following code shows how to use the method in order to direct a user to another page:

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

Note that any code 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 round-trip 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 to serve the new page request, thus limiting the impact on the ASP.NET infrastructure.

In ASP.NET 1.x, the spawned page can access the page object representing its caller by using the Handler property of the HTTP context object, as shown here:

Page caller = (Page) Context.Handler;

Because the Handler property returns a valid instance of the referrer page object, the spawned page can access all of its public members. It cannot directly access the controls because of the protection level already discussed.

This programming model also works in ASP.NET 2.0. However, in ASP.NET 2.0, things are simplified and using Handler is no longer needed. You can use the same programming model of cross-page postings and rely on a non-null PreviousPage property and 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 of these cases, PreviousPage is not null, but the Page.IsCrossPagePostBack on the PreviousPage object is true for a cross-page posting and false in case of a server transfer.

Conclusion

Passing values from one page to another is a task that can be accomplished in a variety of ways—cross-page posting, server transfer, HTML forms, cookies, session-state, and query strings, among others. Which one is the most effective? In ASP.NET 2.0, cross-page posting and server transfer offer a familiar programming model, but potentially move a significant chunk of data through the view state. 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 so, HTML client forms might be more effective in terms of moving data. HTML forms, though, require an ASP-like programming model.

ASP.NET 2.0 adds a few new properties to the HtmlForm class, but doesn't change its core behavior, so self-posting pages are still the primary method of writing pages in ASP.NET. You can still mix client and server forms and also host multiple server forms provided that only one is visible at a time.

Send your questions and comments for Dino to  cutting@microsoft.com.

Dino Esposito is an instructor and consultant based in Rome, Italy. Author of Programming ASP.NET and the new book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.