5,548,129 members and growing! (20,753 online)
Email Password   helpLost your password?
General Reading » Book Chapters » Addison-Wesley     Beginner

Essential ASP.NET 2.0, 2nd Edition: Chapter 4: State Management

By Addison-Wesley

ASP.NET 2.0 does not offer a penultimate solution for storing client state, but it does introduce three new features that should be considered any time you are looking for a place to store state on behalf of individual users.
C#, Windows, .NET 2.0, .NET, ASP.NET, Visual Studio, VS2005, Dev

Posted: 7 Dec 2006
Updated: 7 Dec 2006
Views: 31,371
Bookmarked: 57 times
Announcements
Want a new Job?



Search    
Advanced Search
Sitemap
12 votes for this Article.
Popularity: 4.58 Rating: 4.24 out of 5
0 votes, 0.0%
1
1 vote, 8.3%
2
0 votes, 0.0%
3
3 votes, 25.0%
4
8 votes, 66.7%
5

1.jpg

Author(s) Fritz Onion, Keith Brown
Title Essential ASP.NET 2.0, 2nd Edition
Publisher Addison-Wesley
Published Oct 30, 2006
ISBN-10 0-321-23770-6
ISBN-13 978-0-321-23770-5
Price US$ 44.99
Pages 384

Introduction

Where do you store per-client state in a Web application? This question is at the root of many heated debates over how to best design Web applications. The disconnected nature of HTTP means that there is no "natural" way to keep state on behalf of individual clients, but that certainly hasn't stopped developers from finding ways of doing it. Today there are many choices for keeping client-specific state in an ASP.NET Web application, including Session state, View state, cookies, the HttpContext.Items collection, and any number of custom solutions. The best choice depends on many things, including the scope (Do you need the state to last for an entire user session or just between two pages?), the size (Are you worried about passing too much data in the response and would prefer to keep it on the server?), and the deployment environment (Is this application deployed on a Web farm so that server state must be somehow shared?), just to name a few.

ASP.NET 2.0 does not offer a penultimate solution for storing client state, but it does introduce three new features that should be considered any time you are looking for a place to store state on behalf of individual users. The first feature, cross-page posting, is actually the resurrection of a common technique used in classic ASP and other Web development environments for propagating state between two pages. This technique was not available in ASP.NET 1.1 because of the way POST requests were parsed and processed by individual pages, but has now been reincorporated into ASP.NET in such a way that it works in conjunction with server-side controls and other ASP.NET features. The second feature is a trio of new server-side controls that implement the common technique of showing and hiding portions of a page as the user interacts with it. The Wizard control gives developers a simple way to construct a multi-step user interface on a single page, and the MultiView and View controls provide a slightly lower-level (and more flexible) way of hiding and displaying panes.

The last feature, Profile, is by far the most intriguing. Profile provides a pre-built implementation that will store per-client state across requests and even sessions of your application in a persistent back-end data store. It ties into the Membership provider of ASP.NET 2.0 for identifying authenticated clients, and generates its own identifier for working with anonymous users as well, storing each client's data in a preconfigured database table. This feature provides a flexible and extensible way of storing client data and should prove quite useful in almost any ASP.NET application.

Cross-Page Posting

This version of ASP.NET reintroduces the ability to perform cross-page posts. Once a common practice in classic ASP applications, ASP.NET 1.x made it nearly impossible to use this technique for state propagation because of server-side forms and view state. This section covers the fundamentals of cross-page posting in general, and then looks at the support added in ASP.NET 2.0.

Fundamentals

One common mechanism for sending state from one page to another in Web applications is to use a form with input elements whose action attribute is set to the URL or the target page. The values of the source page's input elements are passed as name-value pairs to the target page in the body of the POST request (or in the query string if the form's method attribute is set to GET), at which point the target page has access to the values. Listings 4-1 and 4-2 show a pair of sample pages that request a user's name, age, and marital status, and display a customized message on the target page.

Listing 4-1: sourceform.aspx—sample form using a cross-page post
<!-- sourceform.aspx -->
<%@ Page language="C#" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Source Form</title>
</head>
<body>
    <form action="target.aspx" method="post">
        Enter your name:
        <input name="_nameTextBox" type="text" id="_nameTextBox" />
        <br />
        Enter your age:
        <input name="_ageTextBox" type="text" id="_ageTextBox" /><br />
        <input id="_marriedCheckBox" type="checkbox" 
               name="_marriedCheckBox" />
        <label for="_marriedCheckBox">Married?</label><br />
        <input type="submit" name="_nextPageButton" value="Next page" />
    </form>
</body>
</html>
Listing 4-2: target.aspx—sample target page for a cross-page post
<!-- target.aspx -->
<%@ Page language="C#" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <title>Target Page</title>
</head>
<body>
  <h3>
  Hello there 
  <%= Request.Form["_nameTextBox"] %>, you are
  <%= Request.Form["_ageTextBox"] %> years old and are
  <%= (Request.Form["_marriedCheckBox"] == "on") ? "" : "not " %>
  married!
  </h3>
</body>
</html>

This example works fine in both ASP.NET 1.1 and 2.0, and with a few simple modifications would even work in classic ASP. This technique is rarely used in ASP.NET, however, because the form on the source page cannot be marked with runat="server"; thus, many of the advantages of ASP.NET, including server-side controls, cannot be used. ASP.NET builds much of its server-side control infrastructure on the assumption that pages with forms will generate POST requests back to the same page. In fact, if you try and change the action attribute of a form that is also marked with runat="server", it will have no effect, as ASP.NET will replace the attribute when it renders the page with the page's URL itself. As a result, most ASP.NET sites resort to alternative techniques for propagating state between pages (like Session state or using Server.Transfer while caching data in the Context.Items collection).

In the 2.0 release of ASP.NET, cross-page posting is now supported again, even if you are using server-side controls and all of the other ASP.NET features. The usage model is a bit different from the one shown in Listings 4-1 and 4-2, but in the end it achieves the desired goal of issuing a POST request from one page to another, and allowing the secondary page to harvest the contents from the POST body and process them as it desires. To initiate a cross-page post, you use the new PostBackUrl attribute defined by the IButtonControl interface, which is implemented by the Button, LinkButton, and ImageButton controls. When the PostBackUrl property is set to a different page, the OnClick handler of the button is set to call a JavaScript function that changes the default action of the form to the target page's URL. Listing 4-3 shows a sample form that uses cross-page posting to pass name, age, and marital status data entered by the user to a target page.

Listing 4-3: SourcePage1.aspx—using cross-page posting support in ASP.NET 2.0
<!-- SourcePage1.aspx -->
<%@ Page Language="C#" CodeFile="SourcePage1.aspx.cs"
Inherits
="SourcePage1" %> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Source page 1</title> </head> <body> <form id="form1" runat="server"> <div> Enter your name: <asp:TextBox ID="_nameTextBox" runat="server" /><br /> Enter your age: <asp:TextBox ID="_ageTextBox" runat="server" /><br /> <asp:CheckBox ID="_marriedCheckBox" runat="server"
Text="Married?" /><br /> <asp:Button ID="_nextPageButton" runat="server" Text="Next page" PostBackUrl="~/TargetPage.aspx" /> </div> </form> </body> </html>

Once you have set up the source page to post to the target page, the next step is to build the target page to use the values passed by the source page. Because ASP.NET uses POST data to manage the state of its server-side controls, it would not have been sufficient to expect the target page to pull name/value pairs from the POST body, since many of those values (like __VIEWSTATE) need to be parsed by the server-side controls that wrote the values there in the first place. Therefore, ASP.NET will actually create a fresh instance of the source page class and ask it to parse the POST body on behalf of the target page. This page instance is then made available to the target page via the PreviousPage property, which is now defined in the Page class. Listings 4-4 and 4-5 show one example of how you could use this property in a target page to retrieve the values of the controls from the previous page: by calling FindControl on the Form control, you can retrieve individual controls whose state has been initialized with values from the post's body.

Listing 4-4: TargetPage.aspx—target page of a cross-page post
<!-- TargetPage.aspx -->
<%@ Page Language="C#" AutoEventWireup="true" 
         CodeFile="TargetPage.aspx.cs" 
         Inherits="TargetPage" %>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Target Page</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:Label runat="server" ID="_messageLabel" />
        </div>
    </form>
</body>
</html>
Listing 4-5: TargetPage.aspx.cs—target page of a cross-page post codebehind
// TargetPage.aspx.cs

public partial class TargetPage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (PreviousPage != null)
        {
            TextBox nameTextBox = 
(TextBox)PreviousPage.Form.FindControl("_nameTextBox"); TextBox ageTextBox =
(TextBox)PreviousPage.Form.FindControl("_ageTextBox"); CheckBox marriedCheckBox =
(CheckBox)PreviousPage.Form.FindControl("_marriedCheckBox"); _messageLabel.Text = string.Format( "<h3>Hello there {0}, you are {1} years old and {2} married!</h3>", nameTextBox.Text, ageTextBox.Text, marriedCheckBox.Checked ? "" : "not"); } } }

The technique shown in Listing 4-5 for retrieving values from the previous page is somewhat fragile, as it relies on the identifiers of controls on the previous page as well as their hierarchical placement, which could easily be changed. A better approach is to expose any data from the previous page to the target page by writing public property accessors in the code-behind, as shown in Listing 4-6.

Listing 4-6: SourcePage1.aspx.cs—exposing public properties to the target page
// File: SourcePage1.aspx.cs

public partial class SourcePage1 : Page
{    
    public string Name
    {
      get { return _nameTextBox.Text; }
    }

    public int Age
    {
      get { return int.Parse(_ageTextBox.Text); }
    }

    public bool Married
    {
      get { return _marriedCheckBox.Checked; }
    }
}

Once the public properties are defined, the target page can cast the PreviousPage property to the specific type of the previous page and retrieve the values using the exposed properties, as shown in Listing 4-7.

Listing 4-7: TargetPage.aspx.cs—target page using properties to retrieve source page values
// TargetPage.aspx.cs

public partial class TargetPage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        SourcePage1 sp = PreviousPage as SourcePage1;
        if (sp != null)
        {
            _messageLabel.Text = string.Format(
               "<h3>Hello there {0}, you are {1} years" + 
               " old and {2} married!</h3>",
            sp.Name, sp.Age, sp.Married ? "" : "not");
        }
    }
}

Because this last scenario is likely to be the most common use of cross-page posting--that is, a specific source page exposes properties to be consumed by a specific target page--there is a directive called PreviousPageType that will automatically cast the previous page to the correct type for you. When you specify a page in the VirtualPath property of this directive, the PreviousPage property that is generated for that page will be strongly typed to the previous page type, meaning that you no longer have to perform the cast yourself, as shown in Listings 4-8 and 4-9.

Listing 4-8: TargetPage.aspx with strongly typed previous page
<!-- TargetPage.aspx -->
<%@ Page Language="C#" AutoEventWireup="true" 
         CodeFile="TargetPage.aspx.cs" 
         Inherits="TargetPage" %>
<%@ PreviousPageType VirtualPath="~/SourcePage1.aspx" %>
...
Listing 4-9: TargetPage.aspx.cs—using strongly typed PreviousPage accessor
// TargetPage.aspx.cs

public partial class TargetPage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (PreviousPage != null)
        {
            _messageLabel.Text = string.Format(
               "<h3>Hello there {0}, you are {1}" + 
               " years old and {2} married!</h3>",
               PreviousPage.Name, PreviousPage.Age, 
               PreviousPage.Married ? "" : "not");
        }
    }
}

Implementation

When you set the PostBackUrl property of a button to a different page, it does two things. First, it sets the client-side OnClick handler for that button to point to a JavaScript method called WebForm_DoPostBackWithOptions, which will programmatically set the form's action to the target page. Second, it causes the page to render an additional hidden field, __PREVIOUSPAGE, which contains the path of the source page in an encrypted string along with an accompanying message authentication code for validating the string. Setting the action dynamically like this enables you to have multiple buttons on a page that all potentially post to different pages and keeps the architecture flexible. Storing the path of the previous page in a hidden field means that no matter where you send the POST request, the target page will be able to determine where the request came from, and will know which class to instantiate to parse the body of the message.

Once the POST request is issued to the target page, the path of the previous page is read and decrypted from the __PREVIOUSPAGE hidden field and cached. As you have seen, the PreviousPage property on the target page gives access to the previous page and its data, but for efficiency, this property allocates the previous page class on demand. If you never actually access the PreviousPage property, it will never create the class and ask it to parse the body of the request.

The first time you do access the PreviousPage property in the target page, ASP.NET allocates a new instance of the previous page type, as determined by the cached path to the previous page extracted from the __PREVIOUSPAGE hidden field. Once it is created, it then executes the page much like it would if the request had been issued to it. The page is not executed in its entirety, however, since it only needs to restore the state from the POST body, so it runs through its life cycle up to and including the LoadComplete event. The Response and Trace objects of the previous page instance are also set to null during this execution since there should be no output associated with the process.

It is important to keep in mind that the preceding page will be created and asked to run through LoadComplete. If you have any code that generates side effects, you should make an effort to exclude that code from running when the page is executed during a cross-page postback. You can check to see whether you are being executed for real or for the purpose of evaluating the POST body of a cross-page post by checking the IsCrossPagePostBack property. For example, suppose that the source page wrote to a database in its Load event handler for logging purposes. You would not want this code to execute during a cross-page postback evaluation since the request was not really made to that page. Listing 4-10 shows how you might exclude your logging code from being evaluated during a cross-page postback.

Listing 4-10: Checking for IsCrossPagePostBack before running code with side effects
public partial class SourcePage1 : Page
{    
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsCrossPagePostBack)
        {
            WriteDataToLogFile();
        }
    }
}

Caveats

While this new support for cross-page posting is a welcome addition to ASP.NET, it does have some potential drawbacks you should be aware of before you elect to use it. The first thing to keep in mind is that the entire contents of the source page is going to be posted to the target page. This includes the entire view state field and all input elements on the page. If you are using cross-page posting to send the value of a pair of TextBox controls to a target page, but you have a GridView with view state enabled on the source page, you're going to incur the cost of posting the entire contents of the GridView in addition to the TextBox controls just to send over a pair of strings. If you can't reduce the size of the request on the source page to an acceptable amount, you may want to consider using an alternative technique (like query strings) to propagate the values.

Validation is another potential trouble area with cross-page posting. If you are using validation controls in the client page to validate user input prior to the cross-page post, you should be aware that server-side validation will not take place until you access the PreviousPage property on the target page. Client-side validation will still happen as usual before the page issues the POST, but if you are relying on server-side validation at all, you must take care to check the IsValid property of the previous page before accessing the data exposed by the PreviousPage property.

A common scenario where this may occur is with custom validation controls. If you have set up a custom validation control with a server-side handler for the ServerValidate event, that method will not be called until you access the PreviousPage after the cross-page posting has occurred. Then there is the question of what to do if the previous page contains invalid data, since you can no longer just let the page render back to the client with error messages in place (because the client has already navigated away from the source page). The best option is probably just to place an indicator message that the data is invalid and provide a link back to the previous page to enter the data again. Listings 4-11 and 4-12 show a sample of a source page with a custom validation control and a button set up to use cross-page posting, along with a target page. Note that the code in the target page explicitly checks the validity of the previous page's data before using it and the error handling added if something is wrong.

Listing 4-11: Source page with custom validator
<!-- SourcePageWithValidation.aspx -->
<%@ Page Language="C#" %>

<script runat="server">
    public int Prime
    {
        get { return int.Parse(_primeNumberTextBox.Text); }
    }

    private bool IsPrime(int num)
    {
        // implementation omitted

    }

    protected void _primeValidator_ServerValidate(object source,
                           ServerValidateEventArgs args)
    {
        args.IsValid = IsPrime(Prime);
    }   
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Source page with validation</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            Enter your favorite prime number:
            <asp:TextBox ID="_primeNumberTextBox" runat="server" />
            <asp:CustomValidator ID="_primeValidator" runat="server" 
                 ErrorMessage="Please enter a prime number"
                 OnServerValidate="_primeValidator_ServerValidate">
                                         **</asp:CustomValidator><br />
            <asp:Button ID="_nextPageButton" runat="server" 
                        Text="Next page" 
                        PostBackUrl="~/TargetPageWithValidation.aspx"
                         /><br />
            <br />
            <asp:ValidationSummary ID="_validationSummary" 
                                   runat="server" />
        </div>
    </form>
</body>
</html>
Listing 4-12: Target page checking for validation
<!-- TargetPageWithValidation.aspx -->
<%@ Page Language="C#" %>
<%@ PreviousPageType VirtualPath="~/SourcePageWithValidation.aspx" %>

<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        if (PreviousPage != null && PreviousPage.IsValid)
        {
          _messageLabel.Text = "Thanks for choosing the prime number " +
                               PreviousPage.Prime.ToString();
        }
        else
        {
            _messageLabel.Text = "Error in entering data";
            _messageLabel.ForeColor = Color.Red;
            _previousPageLink.Visible = true;
        }
    }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Target Page With validation</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>            
            <asp:Label runat="server" ID="_messageLabel" /><br />
            <asp:HyperLink runat="server" ID="_previousPageLink" 
                           NavigateUrl="~/SourcePageWithValidation.aspx"
                           visible="false">
                  Return to data entry page...</asp:HyperLink>
        </div>
    </form>
</body>
</html>

Finally, it is important to be aware that the entire cross-page posting mechanism relies on JavaScript to work properly, so if the client either doesn't support or has disabled JavaScript, your source pages will simply post back to themselves as the action on the form will not be changed on the client in response to the button press.

Multi-Source Cross-Page Posting

Cross-page posting can also be used to create a single target page that can be posted to by multiple source pages. Such a scenario may be useful if you have a site that provides several different ways of collecting information from the user but one centralized page for processing it.

If we try and extend our earlier example by introducing a second source page, also with the ability to collect the name, age, and marital status of the client, we run into a problem because each page is a distinct type with its own VirtualPath, and the target page will somehow have to distinguish between a post from source page 1 and one from source page 2. One way to solve this problem is to implement a common interface in each source page's base class; this way, the target page assumes only that the posting page implements a particular interface and is not necessarily of one specific type or another. For example, we could write the IPersonInfo interface to model our cross-page POST data, as shown in Listing 4-13.

Listing 4-13: IPersonInfo interface definition
public interface IPersonInfo
{
  string Name { get; }
  int Age { get; }
  bool Married { get; }
}

In each of the source pages, we then implement the IPersonInfo on the code-behind base class, and our target page can now safely cast the PreviousPage to the IPersonInfo type and extract the data regardless of which page was the source page, as shown in Listing 4-14.

Listing 4-14: Generic target page using interface for previous page
IPersonInfo pi = PreviousPage as IPersonInfo;
if (pi != null)
{
  _messageLabel.Text = string.Format("<h3>Hello there {0}," + 
          " you are {1} years old and {2} married!</h3>",
          pi.Name, pi.Age, pi.Married ? "" : "not");
}

It would be even better if we could use the PreviousPageType directive to strongly type the PreviousPage property to the IPersonInfo interface. In fact, there is a way to associate a type with a previous page instead of using the virtual path, which is to specify the TypeName attribute instead of the VirtualPath attribute in the PreviousPageType directive. Unfortunately, the TypeName attribute of the PreviousPageType directive requires that the specified type inherit from System.Web.UI.Page. You can introduce a workaround to get the strong typing by defining an abstract base class that implements the interface (or just defines abstract methods directly) and inherits from Page, as shown in Listing 4-15.

Listing 4-15: Abstract base class inheriting from Page for strong typing with PreviousPageType
public abstract class PersonInfoPage : Page, IPersonInfo
{
  public abstract string Name { get; }
  public abstract int Age { get; }
  public abstract bool Married { get; }
}

This technique then requires that each of the source pages you author change their base class from Page to this new PersonInfoPage base, and then implement the abstract properties to return the appropriate data. Listing 4-16 shows an example of a code-behind class for a source page using this new base class.

Listing 4-16: Codebehind class for a sample source page inheriting from PersonInfoPage
public partial class SourcePage1 : PersonInfoPage
{ 
  public override string Name
  {
    get { return _nameTextBox.Text; }
  }
  public override int Age
  {
    get { return int.Parse(_ageTextBox.Text); }
  }
  public override bool Married
  {
    get { return _marriedCheckBox.Checked; }
  }
}

Once all source pages are derived from our PersonInfoPage and the three abstract properties are implemented, our target page can be rewritten with a strongly typed PreviousPageType directive, which saves the trouble of casting, as shown in Listing 4-17.

Listing 4-17: Strongly typed target page using TypeName
<%@ PreviousPageType TypeName="PersonInfoPage" %>
 
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
  if (PreviousPage != null)
  {
    _messageLabel.Text = string.Format(
         "<h3>Hello there {0}, you are {1} " + 
         "years old and {2} married!</h3>",
         PreviousPage.Name, PreviousPage.Age, 
         PreviousPage.Married ? "" : "not");
  }
}
</script>
<!-- ... -->

The effort required to get the strong typing to work for multiple source pages hardly seems worth it in the end. You already have to check to see whether the PreviousPage property is null or not, and casting it to the interface using the as operator in C# is about the same amount of work as checking for null. However, both ways are valid approaches, and it is up to you to decide how much effort you want to put into making your previous pages strongly typed.

Wizard and MultiView Controls

This section covers a new collection of controls in ASP.NET 2.0 that simplify the process of collecting data from the user by using a sequence of steps that are all on a single page. The controls include the new Wizard control as well as the View and MultiView controls.

Same Page State Management

Another alternative to storing per-client state across requests is to have the user post back to the same page instead of navigating from one page to another. You can achieve the same sequential set of steps for data collection that you can using multiple pages with this technique by toggling the display of various panels, showing only one of several panels at any given time based on the user's progress. Instead of placing input controls on separate pages, you place them all on the same page, but separate them with Panel (or Placeholder) controls as shown in Figure 4-1. When the user selects the Next button in one panel, the handler for that button sets the visibility of the current panel to false and of the next panel to true.

Figure 4-1:  Multipanel page

This technique works well because all of the state for all the controls is kept on a single page, and even when the controls in a particular panel are not displayed, their state is maintained in view state, so programmatically it is just like working with one giant form. It is also quite efficient, since the contents of invisible panels are not even sent to the client browser; just the state of the controls is sent through view state.

Wizard Control

In the 2.0 release of ASP.NET this technique has been standardized in the form of the Wizard control. Instead of laying out the Panel controls yourself and adding the logic to flip the visibility of each panel in response to button presses, you can use the Wizard control to manage the details for you and focus on laying out the controls for each step. The control itself consists of a collection of WizardSteps which act as containers for any controls you want to add. Listing 4-18 shows a sample Wizard control populated with the input elements described in Figure 4-1 (also included is an adjacent Label control to display the data on completion).

Listing 4-18: Sample Wizard control with three steps
<asp:Wizard ID="_infoWizard" runat="server" ActiveStepIndex="0" 
            OnFinishButtonClick="_infoWizard_FinishButtonClick" 
            DisplaySideBar="False">
    <WizardSteps>
      <asp:WizardStep ID="_step1" runat="server" Title="Name">
      <table>
        <tr>
          <td>First name:</td>
          <td><asp:TextBox ID="_firstNameTextBox" runat="server" /></td>
        </tr>
        <tr>
          <td>Last name:</td>
          <td><asp:TextBox ID="_lastNameTextBox" runat="server" /></td>
        </tr>                        
      </table>
      </asp:WizardStep>
      <asp:WizardStep ID="_step2" runat="server" Title="Address">
        <table>
          <tr>
            <td>Street:</td>
            <td><asp:TextBox ID="_streetTextBox" runat="server" /></td>
          </tr> 
          <tr>
            <td>City:</td>
            <td><asp:TextBox ID="_cityTextBox" runat="server" /></td>
          </tr>        
          <tr>
            <td>State/Province:</td>
            <td><asp:TextBox ID="_stateTextBox" runat="server" /></td>
          </tr>                 
        </table>
      </asp:WizardStep>
      <asp:WizardStep ID="_step3" runat="server" Title="Preferences">
        <table>
          <tr>
            <td>Favorite color:</td>
            <td><asp:TextBox ID="_colorTextBox" runat="server" /></td>
          </tr>        
          <tr>
            <td>Favorite number:</td>
            <td><asp:TextBox ID="_numberTextBox" runat="server" /></td>
          </tr>        
        </table>
      </asp:WizardStep>
    </WizardSteps>
  </asp:Wizard>
  <asp:Label ID="_summaryLabel" runat="server" />

Like most controls in ASP.NET, both the appearance and behavior of the Wizard control are extremely customizable. In the previous example, the control's SideBar portion, which generates a set of navigation hyperlinks on the left side to let the user jump between steps in the wizard without using the Next/Previous buttons, was not displayed. Figure 4-2 shows two different renderings of the Wizard control: the first is exactly how the Wizard control shown in Listing 4-18 would appear, and the second shows the same control with a different formatting applied and with the ShowSideBar attribute set to true. This control also supports templates so that you can completely customize the look and feel of it as desired.

The advantage of working with the Wizard control like this is that it manages all of the details of the sequential interaction with the user, and you can treat all of the input elements in each of the separate steps as if they were all part of a single page. For example, in our OnFinishButtonClick handler for the Wizard control we can easily use all of the data the user has entered. Listing 4-19 shows an example of printing a message back to the user in the form of a label and then hiding the Wizard control as an indicator that the input sequence is complete.

Figure 4-2: Wizard control, unadorned, and with SideBar and formatting

Listing 4-19: Handler for the Wizard's Finish button click event
protected void _infoWizard_FinishButtonClick(object sender,
                                       WizardNavigationEventArgs e)
{
  _summaryLabel.Text = string.Format(
             "<h2>Thank you for submitting your information!</h2>" +
             "Name: {0} {1}<br /><br/>Address: {2}<br/>" +
             "{3}, {4}<br /><br />Prefs: {5} {6}<br />",
                        _firstNameTextBox.Text, 
                        _lastNameTextBox.Text, _streetTextBox.Text,
                        _cityTextBox.Text, _stateTextBox.Text,      
                        _colorTextBox.Text, _numberTextBox.Text);
  _infoWizard.Visible = false;
}

MultiView and View Controls

If you want the ability to toggle among multiple panels on a page but find the Wizard control too constraining, you might instead consider using the MultiView control. A MultiView control consists of several child View controls, and it maintains an active index indicating which of those child views should be visible. In fact, the WizardStep control used by the Wizard control inherits from the View class used by the MultiView, so the similarity is not a coincidence. Unlike the Wizard control, the MultiView renders nothing but the contents of the active view--there are no buttons, links, or titles of any kind. This means that it is up to you to determine how the user switches between the various views. Listings 4-20 and 4-21 show a sample MultiView with three embedded views to collect data from the user. This example uses three link buttons to let the user toggle among the three views by setting the ActiveViewIndex of the MultiView depending on which button was selected.

Listing 4-20: MultiView with LinkButtons
        <asp:LinkButton ID="_view1LinkButton" runat="server" 
                OnClick="_view1LinkButton_Click">
                View 1</asp:LinkButton>  
        <asp:LinkButton ID="_view2LinkButton" runat="server" 
                OnClick="_view2LinkButton_Click">
                View 2</asp:LinkButton>  
        <asp:LinkButton ID="_view3LinkButton" runat="server" 
                OnClick="_view3LinkButton_Click">
                View 3</asp:LinkButton><br />
        <asp:MultiView ID="_infoMultiView" runat="server" 
                       ActiveViewIndex="0">
            <asp:View ID="_view1" runat="server">
             <table>
                <tr>
                  <td>First name:</td>
                  <td><asp:TextBox ID="_firstNameTextBox" 
                                   runat="server" /></td>
                </tr>
                <tr>
                  <td>Last name:</td>
                  <td><asp:TextBox ID="_lastNameTextBox" 
                                   runat="server" /></td>
                </tr>                        
              </table>
            </asp:View>
            <asp:View ID="_view2" runat="server">
                <table>
                  <tr>
                    <td>Street:</td>
                    <td><asp:TextBox ID="_streetTextBox" 
                                     runat="server" /></td>
                  </tr> 
                  <tr>
                    <td>City:</td>
                    <td><asp:TextBox ID="_cityTextBox" 
                                     runat="server" /></td>
                  </tr>        
                  <tr>
                    <td>State/Province:</td>
                    <td><asp:TextBox ID="_stateTextBox" 
                                     runat="server" /></td>
                  </tr>                 
                </table>
            </asp:View>
            <asp:View ID="_view3" runat="server">
                <table>
                  <tr>
                    <td>Favorite color:</td>
                    <td><asp:TextBox ID="_colorTextBox"
                                     runat="server" /></td>
                  </tr>        
                  <tr>
                    <td>Favorite number:</td>
                    <td><asp:TextBox ID="_numberTextBox" 
                                     runat="server" /></td>
                  </tr>        
                </table>
            </asp:View>
        </asp:MultiView>
Listing 4-21: LinkButton handlers for MultiView switching
protected void _view1LinkButton_Click(object sender, EventArgs e)
{
   _infoMultiView.ActiveViewIndex = 0;
}
protected void _view2LinkButton_Click(object sender, EventArgs e)
{
    _infoMultiView.ActiveViewIndex = 1;
}
protected void _view3LinkButton_Click(object sender, EventArgs e)
{
    _infoMultiView.ActiveViewIndex = 2;
}

Profile

Profile provides a simple way of defining database-backed user profile information. With just a few configuration file entries, you can quickly build a site that stores user preferences (or any other data, for that matter) into a database, all with a simple type-safe interface for the developer. In many ways, Profile looks and feels much like Session state, but unlike Session state, Profile is persistent across sessions and is also tied into the Membership provider, so authenticated clients have data stored associated with their real identities instead of some arbitrary identifier. Anonymous clients will have an identifier generated for them, stored as a persistent cookie so that subsequent access from the same machine will retain their preferences as well. In addition, Profile is retrieved on demand and written only when modified, so unlike out-of-process Session state storage, you only incur a trip to the database when you actually use Profile, not implicitly with each request.

Fundamentals

The first step in using Profile is to declare the properties you would like to store on behalf of each user in your web.config file under the <profile> element. Your first decision is whether you want to allow anonymous clients to store profile data or only authenticated clients. If you elect to support anonymous clients, you must enable anonymous identification by adding the anonymousIdentification element in your web.config file with its enabled attribute set to true. This will cause ASP.NET to generate a unique identifier (a GUID) to associate with each anonymous user via a persistent cookie. I