To read the article online, visit http://www.4GuysFromRolla.com/articles/070506-1.aspx

Customizing the CreateUserWizard Control

By Erich Peterson


Introduction


In the never ending quest to create web applications that are more functional, responsive, and user-friendly, ASP.NET 2.0 has added a multitude of new server controls to help us achieve those goals. One of these new controls is the CreateUserWizard and is the focus of this article. For those unfamiliar with the control, CreateUserWizard, as its name implies, provides a wizard interface for creating a new user account in the ASP.NET 2.0 membership system. (See Scott Mitchell's articles Creating a Step-by-Step User Interface with the ASP.NET 2.0 Wizard Control and Examining ASP.NET 2.0's Membership, Roles, and Profile for more information on the Wizard control and on the membership system.)

In this article we will be stepping through the process of customizing the CreateUserWizard into a multi-step process. Some believe breaking up a large data entry form into a multi-step process is less off-putting to the user and encourages them to see the process to its end. The process steps we will be creating will include:

  • A step which will gather billing address information,
  • Another for shipping information, and finally,
  • One to collect user information
Read on to learn more!

Preparations


To understand this article fully and to be able to work through the code yourself, we assume that you know at least the basics of setting up your website to use the built-in membership provider (SqlMembershipProvider) for use with a SQL Express database in your App_Data folder. For more information on this process, please check out the Examining ASP.NET 2.0's Membership, Roles, and Profile article series. Also, reading Scott's Creating a Step-by-Step User Interface with the ASP.NET 2.0 Wizard Control will help greatly in your overall understanding of the Wizard control, which is shown in our examples here as well.

Basics of the CreateUserWizard


The CreateUserWizard control extends the Wizard control, and as such lends itself very nicely in creating a multi-step user registration process. If you simply place the CreateUserWizard control on a page, the default behavior of it can seem quite unrealistic for use in real-world web applications (as can be seen in the following figure).

That is, it only has inputs for the: User Name, Password, E-mail, and Secret Question and Answer. But alas, the control can be "templated" to change the layout and to require more information to be gathered, and can be turned into a multi-step process. Best of all, the control will still provide us seamless integration with our membership provider (more on that later). The following shows the markup that is created when the control is first placed on a page:

<asp:CreateUserWizard ID="CreateUserWizard1" runat="server">
    <WizardSteps>
        <asp:CreateUserWizardStep ID="CreateUserWizardStep1" runat="server">
        </asp:CreateUserWizardStep>
        <asp:CompleteWizardStep ID="CompleteWizardStep1" runat="server">
        </asp:CompleteWizardStep>
    </WizardSteps>
</asp:CreateUserWizard>

From this declaration, we can already guess a bit as to how we might customize the control. First we see the <WizardSteps> tags, in which all the steps of our user registration process must be enclosed. Next, we see the first step of our process declared by:

<asp:CreateUserWizardStep ID="CreateWizardStep1" runat="server"></asp:CreateUserWizardStep>

This is the step that by default, displays the inputs we talked about earlier (i.e. User Name, Password, etc�), and after the "Create User" button is pressed, will then create the appropriate records in the SQL Express database (which will represent that user). Lastly, we see the step declared by:

<asp:CompleteWizardStep ID="CompleteWizardStep1" runat="server"></asp:CompleteWizardStep>

This step by default, displays the message: "Your account has been successfully created" and provides a button labeled "Continue", which when pressed, will take the user to the page defined by the ContinueDestinationPageUrl property of the CreateUserWizard control.

Customizing the CreateUserWizard Control


Let us now take a look at how we might customize the steps in our wizard. We can add new steps to the user registration process by adding new <asp:WizardStep> elements. For example, to start the user registration process by collecting billing information, we can add the following step before all current steps:

<asp:WizardStep ID="CreateUserWizardStep0" runat="server">
    <table>
        <tr>
            <th>Billing Information</th>
        </tr>
        <tr>
            <td>Billing Address:</td>
            <td>
                <asp:TextBox runat="server" ID="BillingAddress" MaxLength="50" />
                <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator1" ControlToValidate="BillingAddress"
                     ErrorMessage="Billing Address is required." />
            </td>
        </tr>
        <tr>
            <td>Billing City:</td>
            <td>
                <asp:TextBox runat="server" ID="BillingCity" MaxLength="50" Columns="15" />
                <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator2" ControlToValidate="BillingCity"
                     ErrorMessage="Billing City is required." />
            </td>
        </tr>  
        <tr>
            <td>Billing State:</td>
            <td>
                <asp:TextBox runat="server" ID="BillingState" MaxLength="25" Columns="10" />
                <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator3" ControlToValidate="BillingState"
                     ErrorMessage="Billing State is required."  />
            </td>
        </tr>  
        <tr>
            <td>Billing Zip:</td>
            <td>
                <asp:TextBox runat="server" ID="BillingZip" MaxLength="10" Columns="10" />
                <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator4" ControlToValidate="BillingZip"
                     ErrorMessage="Billing Zip is required." />
            </td>
        </tr>
    </table>
</asp:WizardStep>

Inside this WizardStep (whose ID property is set to CreateUserWizardStep0), we have placed all of the input controls and RequiredFieldValidator's that we would like the first step of our multi-step process to contain. Specifically we are asking the user to input their billing address, city, state, and zip code; along with requiring all of those fields.

To collect the user's shipping information we can add an almost identical <asp:WizardStep> immediately after the previously mentioned one. This shipping information step will prompt the user shipping address, city, state, and other shipping-related information. Let's omit this step's markup for brevity; the entire source code for this sample can be downloaded at the end of the article. Give this new WizardStep an ID of CreateUserWizardStep1. This ID will conflict with the CreateUserWizardStep's ID which is on the page as well; just rename the CreateUserWizardStep's ID to CreateUserWizardStep2. With that we are done defining our first two custom CreateUserWizard steps!

Although we could leave the CreateUserWizardStep alone and let it display its default inputs, let us customize the layout to match the other two steps. In doing so, it will provide you with the knowledge needed to later customize this step further as well, if you so chose to. To start the customization process of this step, we must add the <ContentTemplate> tag within this step and then place all of our code inside it. Let us take a look at the markup that is required to customize this step and then explain.

<asp:CreateUserWizardStep ID="CreateUserWizardStep2" runat="server">
    <ContentTemplate>
        <table>
            <tr>
                <th>User Information</th>
            </tr>
            <tr>
                <td>Username:</td>
                <td>
                    <asp:TextBox runat="server" ID="UserName" />
                    <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator9" ControlToValidate="UserName"
                        ErrorMessage="Username is required." />
                </td>
            </tr>
            <tr>
                <td>Password:</td>
                <td>
                    <asp:TextBox runat="server" ID="Password" TextMode="Password" />
                    <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator10" ControlToValidate="Password"
                        ErrorMessage="Password is required." />
                </td>
            </tr>
            <tr>
                <td>Confirm Password:</td>
                <td>
                    <asp:TextBox runat="server" ID="ConfirmPassword" TextMode="Password" />
                    <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator13" ControlToValidate="ConfirmPassword"
                        ErrorMessage="Confirm Password is required." />
                </td>
            </tr>
            <tr>
                <td>Email:</td>
                <td>
                    <asp:TextBox runat="server" ID="Email" />
                    <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator11" ControlToValidate="Email"
                        ErrorMessage="Email is required." />
                </td>
            </tr>
            <tr>
                <td>Question:</td>
                <td>
                    <asp:TextBox runat="server" ID="Question" />
                    <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator12" ControlToValidate="Question"
                        ErrorMessage="Question is required." />
                </td>
            </tr>
            <tr>
                <td>Answer:</td>
                <td>
                    <asp:TextBox runat="server" ID="Answer" />
                    <asp:RequiredFieldValidator runat="server" ID="RequiredFieldValidator14" ControlToValidate="Answer"
                        ErrorMessage="Answer is required." />
                </td>
            </tr>
            <tr>
                <td colspan="2">
                     <asp:CompareValidator ID="PasswordCompare" runat="server" ControlToCompare="Password"
                            ControlToValidate="ConfirmPassword" Display="Dynamic" ErrorMessage="The Password and Confirmation Password must match."></asp:CompareValidator>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <asp:Literal ID="ErrorMessage" runat="server" EnableViewState="False"></asp:Literal>
                </td>
            </tr>
        </table>
    </ContentTemplate>
</asp:CreateUserWizardStep>

As mentioned, minus the layout HTML (i.e. <table>), the code listed above is what is needed to give ASP.NET 2.0's SqlMembershipProvider the proper information to create a new user and to provide feedback to the user on errors that may occur. What is the most important concept to understand in the code shown above, is that we must name our controls properly (using the correct IDs), so ASP.NET knows which input is for which user attribute. Take the TextBox above with an ID="Username" as an example. By giving it that ID, ASP.NET knows that, that TextBox will contain the user's username. The same is true about the TextBox with ID="Password". ASP.NET knows to use that TextBox to create the user's password. So, now looking at the rest of the code, you can tell by the server controls ID's, what they are used for. If you now wanted to customize this step further (with say a CheckBox, asking if the user would like to receive your newsletter), you could do so by adding the applicable server controls and markup, just like we did in the first two customized steps.

Storing Custom User Data


At this point we have customized the CreateUserWizard control to prompt the user for billing and shipping information, but, by default, the CreateUserWizard control will only add a new user to the membership system with the default user attributes (username, email, password, security question and answer, and so on). The SqlMembershipProvider we are using already has database tables to store the core user-related settings. In order to store the additional user attributes we either need to use ASP.NET 2.0's profile system or create our own database tables to hold this additional data. For this article we will use the latter approach.

To help facilitate our storage of the extra information we are gathering in our CreateUserWizard, I have created an extra table in the SQL Server Express database (ASPNETDB.MDF, located in the App_Data folder of the project) named UserAddresses, with the following schema:

The UserId column in UserAddresses acts as a both a primary key for the UserAddresses table and as a foreign key that points to the user in the aspnet_Users table. To insert this additional data, we can use a SqlDataSource server control somewhere inside of the CreateUserWizardStep that will insert the custom user attributes after the user account has been created in the membership system. (For more information on the SqlDataSource control see Scott Mitchell's Accessing and Updating Data in ASP.NET 2.0 article series.)

The following markup creates a SqlDataSource control whose InsertCommand will add a new record to the UserAddresses table:

<asp:SqlDataSource ID="InsertExtraInfo" runat="server" ConnectionString="<%$ ConnectionStrings:ASPNETDBConnectionString1 %>"
    InsertCommand="INSERT INTO [UserAddresses] ([UserId], [BillingAddress], [BillingCity], [BillingState], [BillingZip], [ShippingAddress], [ShippingCity], [ShippingState], [ShippingZip]) VALUES (@UserId, @BillingAddress, @BillingCity, @BillingState, @BillingZip, @ShippingAddress, @ShippingCity, @ShippingState, @ShippingZip)"
    ProviderName="<%$ ConnectionStrings:ASPNETDBConnectionString1.ProviderName %>">
    <InsertParameters>
        <asp:ControlParameter Name="BillingAddress" Type="String" ControlID="BillingAddress" PropertyName="Text" />
        <asp:ControlParameter Name="BillingCity" Type="String" ControlID="BillingCity" PropertyName="Text" />
        <asp:ControlParameter Name="BillingState" Type="String" ControlID="BillingState" PropertyName="Text" />
        <asp:ControlParameter Name="BillingZip" Type="String" ControlID="BillingZip" PropertyName="Text" />
        <asp:ControlParameter Name="ShippingAddress" Type="String" ControlID="ShippingAddress" PropertyName="Text" />
        <asp:ControlParameter Name="ShippingCity" Type="String" ControlID="ShippingCity" PropertyName="Text" />
        <asp:ControlParameter Name="ShippingState" Type="String" ControlID="ShippingState" PropertyName="Text" />
        <asp:ControlParameter Name="ShippingZip" Type="String" ControlID="ShippingZip" PropertyName="Text" />
    </InsertParameters>
</asp:SqlDataSource>

The ShippingAddress, ShippingCity, ShippingState, and ShippingZip InsertParameter values are coming from controls in the second step of our CreateUserWizard, which we left out for brevity.

After the user account has been added to the membership system, the CreateUserWizard fires its OnCreatedUser event. Therefore, create an event handler for this event and invoke the SqlDataSource's Insert() method, thereby causing the additional user attributes to be added to the UserAddresses table after the user account has been added to the membership system:

protected void CreateUserWizard1_CreatedUser(object sender, EventArgs e)
{
    TextBox UserNameTextBox =  (TextBox)CreateUserWizardStep2.ContentTemplateContainer.FindControl("UserName");
    SqlDataSource DataSource = (SqlDataSource)CreateUserWizardStep2.ContentTemplateContainer.FindControl("InsertExtraInfo");

    MembershipUser User = Membership.GetUser(UserNameTextBox.Text);
    object UserGUID = User.ProviderUserKey;

    DataSource.InsertParameters.Add("UserId", UserGUID.ToString());
    DataSource.Insert();
}

In this event handler, we simply grab the new user's username from the CreateUserWizardStep2 step so that we may pass it to the Membership.GetUser(userName) method. The Membership.GetUser(userName) method returns information about the selected user, including their UserId. We then assign the membership user's UserId the UserId parameter of the SqlDataSource control and initiate the insertion of the record into our UserAddresses table.

Once you add colors to your liking and run the project, you should see output similar to the following:

You can now go through each step and even go back to a previous step, filling out the fields, and finally creating the user. After you are finished creating the user, be sure and look in your database table to see that a new record has been added to the UserAddresses table.

Conclusion


In this article, we stepped through the process of customizing ASP.NET 2.0's CreateUserWizard server control. We were able to create two separate steps for the collection of billing and shipping information. In addition, we were able to gather the inputs from the user and store them in a database table, which contained a foreign key to ASP.NET's users table.

Happy Programming!

  • By Erich Peterson


    Attachments


  • Download the code samples examined in this article
  • Suggested Readings


  • Creating a Step-by-Step User Interface with the ASP.NET 2.0 Wizard Control
  • Examining ASP.NET 2.0's Membership, Roles, and Profile
  • HowTo: Customize the ASP.NET CreateUserWizard Control
  • CreateUserWizard QuickStart Tutorials
  • Article Information
    Article Title: ASP.NET.Customizing the CreateUserWizard Control
    Article Author: Erich Peterson
    Published Date: July 5, 2006
    Article URL: http://www.4GuysFromRolla.com/articles/070506-1.aspx


    Copyright 2014 QuinStreet Inc. All Rights Reserved.
    Legal Notices, Licensing, Permissions, Privacy Policy.
    Advertise | Newsletters | E-mail Offers