ASP.NET Patterns: The Multi-Part Form using Panels


There are certain subjects that come up over and over. One is how to get a multi-part form, although it is not always asked in that manner. Often times, you see people having a selection, which goes to a form, which goes to another ASP.NET page. This follows the ASP model fairly well, but does not translate well to ASP.NET.

This is the first blog entry on setting up a multi-part form and uses a very simple pattern. I am going to use a series of Panels <asp:Panel> to facilitate the multi-part.

First, let’s understand the mechanics of a multi-part form. In this example, we will have simple nav controls, along with a “menu” of sorts. Step 1 is to set up the page:

<form id="form1" runat="server">
<div>
    <asp:Panel ID="Panel1" runat="server">
    </asp:Panel>
    <asp:Panel ID="Panel2" runat="server">
    </asp:Panel>
    <asp:Panel ID="Panel3" runat="server">
    </asp:Panel>
</div>
</form>

I then add some basic hyperlink buttons to the page so it looks  like this in the browser:

panelNav

The only thing left to do is select all three panels in Visual Studio and then set Visible to false. The rest of our work is done in code. Here is the updated code:

<form id="form1" runat="server">
<div>
    <asp:Panel ID="Panel1" runat="server" Visible="false">
        First panel<br />
        <asp:LinkButton ID="Panel1Forward" runat="server">&gt;
        </asp:LinkButton>
    </asp:Panel>
    <asp:Panel ID="Panel2" runat="server" Visible="false">
        Second panel<br />
        <asp:LinkButton ID="Panel2Backward" runat="server">&lt;</asp:LinkButton>
        &nbsp;<asp:LinkButton ID="Panel2Forward" runat="server">&gt;</asp:LinkButton>
    </asp:Panel>
    <asp:Panel ID="Panel3" runat="server" Visible="false">
        Third Panel<br />
        <asp:LinkButton ID="Panel3Backward" runat="server">&lt;</asp:LinkButton>
    </asp:Panel>
</div>
</form>

If we run this now, the page is empty, so we have to set up the initial state. You might think of doing it like this:

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
        Panel1.Visible = true;
}

The problem here is you end up duplicating code and potentially leaving another panel visible (not in this routine, but the way of thinking leads in that direction). What you need is a state machine that sets only one panel visible. This can be done in a variety of ways, but the following code samples show the thinking best. First, set up an enum.

public enum PanelState
{
    First,
    Second,
    Third
}

The create a routine that sets state of the panels:

private void SetPanelState(PanelState panelState)
{
    Panel1.Visible = false;
    Panel2.Visible = false;
    Panel3.Visible = false;

    switch (panelState)
    {
        case PanelState.First:
            Panel1.Visible = true;
            break;
        case PanelState.Second:
            Panel2.Visible = true;
            break;
        case PanelState.Third:
            Panel3.Visible = true;
            break;
    }
}

The Page_Load then looks like this:

protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)
        SetPanelState(PanelState.First);
}

The benefit here, is I can create the click routines for the link buttons on the page to use this same “state machine”. To do this, just double click each link button on the tagged page. Here is my code for the link buttons:

protected void Panel1Forward_Click(object sender, EventArgs e)
{
    SetPanelState(PanelState.Second);
}
protected void Panel2Backward_Click(object sender, EventArgs e)
{
    SetPanelState(PanelState.First);
}
protected void Panel2Forward_Click(object sender, EventArgs e)
{
    SetPanelState(PanelState.Third);
}
protected void Panel3Backward_Click(object sender, EventArgs e)
{
    SetPanelState(PanelState.Second);
}

If you run this code, you find that the form navigates between the panels. This example is overly simple, but provides a way to navigate through a multi-part form. As an enhancement, you can add a menu (tabbed or otherwise) to the top so the user can go to any step in the process. If the user must fill out sections, a tabbed control works nicely, as you can set the state to all but tab 1 to disabled to start. You then enable the tabs one at a time as the form is filled in (and hopefully validated).

Here is another simple example, where a user picks his meal first, then puts in his name and then gets a confirmation. I have set it up so the user can change his choice, so there is a “tab” at the top. This is very crude compared to some third party controls out there, but it works for illustrating how you use Panels for a multi-part form.

panelNav2

The code looks like this (for the ASPX page):

<form id="form1" runat="server">
<table class="style3">
    <tr>
        <td>
            <asp:LinkButton ID="LinkButton1" runat="server">Dinner</asp:LinkButton>
        </td>
        <td>
            <asp:LinkButton ID="LinkButton2" runat="server" Enabled="false">Your Info</asp:LinkButton>
        </td>
        <td>
            <asp:LinkButton ID="LinkButton3" runat="server" Enabled="false">Confirmation</asp:LinkButton>
        </td>
    </tr>
</table>
<table class="style4">
    <tr>
        <td>
            <asp:Panel ID="Panel1" runat="server" Visible="false">
                Choose a dinner option:<br />
                <br />
                <asp:DropDownList ID="dinnerChoiceDropDownList" runat="server" AutoPostBack="True"
                    OnSelectedIndexChanged="DropDownList1_SelectedIndexChanged">
                    <asp:ListItem>Choose An Item</asp:ListItem>
                    <asp:ListItem>Chicken</asp:ListItem>
                    <asp:ListItem>Beef</asp:ListItem>
                    <asp:ListItem>Fish</asp:ListItem>
                </asp:DropDownList>
                <br />
            </asp:Panel>
            <asp:Panel ID="Panel2" runat="server" Visible="false">
                Enter your name:<br />
                <br />
                <table class="style1">
                    <tr>
                        <td class="style2">
                            Dinner Choice:</td>
                        <td>
                            <asp:Label ID="dinnerLabel" runat="server" Text="Label"></asp:Label>
                        </td>
                    </tr>
                    <tr>
                        <td class="style2">
                            Your Name:
                        </td>
                        <td>
                            <asp:TextBox ID="nameTextBox" runat="server"></asp:TextBox>
                        </td>
                    </tr>
                    <tr>
                        <td class="style2">
                            &nbsp;
                        </td>
                        <td>
                            <asp:Button ID="submitButton" runat="server" Text="SubmitButton" onclick="submitButton_Click"
                                 />
                        </td>
                    </tr>
                </table>
                <br />
            </asp:Panel>
            <asp:Panel ID="Panel3" runat="server" Visible="false">
                Confirmation:<br />
                <table class="style1">
                    <tr>
                        <td class="style2">
                            Dinner Choice:</td>
                        <td>
                            <asp:Label ID="confirmationDinnerLabel" runat="server" Text="Label"></asp:Label>
                        </td>
                    </tr>
                    <tr>
                        <td class="style2">
                            Name:</td>
                        <td>
                            <asp:Label ID="confirmationNameLabel" runat="server" Text="Label"></asp:Label>
                        </td>
                    </tr>
                </table>
                <br />
                <br />
            </asp:Panel>
        </td>
    </tr>
</table>
<asp:Label ID="errorLabel" runat="server" Font-Bold="True" ForeColor="Red"></asp:Label>
</form>

And our code behind is very similar to our last page, except we ae not only showing panels via a “state machine”, but we are also turning on “tab” buttons as the user moves forward through the form ( I have included some comments in the code to make it easier to understand):

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //Set initial state on first hit
        if (!Page.IsPostBack)
            SetPanelState(PanelState.First);

        //Error label should default to invisible on every hit
        //Set it up in the event handlers for controls if otherwise
        errorLabel.Visible = false;

    }

    private void SetPanelState(PanelState panelState)
    {
        //Panel state starts at invisible
        Panel1.Visible = false;
        Panel2.Visible = false;
        Panel3.Visible = false;

        //Based on state of the page, turn on proper panel
        switch (panelState)
        {
            case PanelState.First:
                Panel1.Visible = true;
                break;
            case PanelState.Second:
                Panel2.Visible = true;
                break;
            case PanelState.Third:
                Panel3.Visible = true;
                break;
        }
    }

    protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e)
    {
        string item = dinnerChoiceDropDownList.SelectedValue;

        //If there is no item, we have an error
        //NOTE: In this form it can only happen if you navigate back
        //      and choose the first option "choose an item"
        if (item.Length == 0)
        {
            SetPanelState(PanelState.First);
            errorLabel.Text = "You must choose a dinner item";
            errorLabel.Visible = true;
        }
        else
        {
            SetPanelState(PanelState.Second);
            //Set labels for dinner choice
            dinnerLabel.Text = item;
            confirmationDinnerLabel.Text = item;

            //Enabled second "tab"
            LinkButton2.Enabled = true;
        }
    }

    protected void submitButton_Click(object sender, EventArgs e)
    {
        if (IsFormValid())
        {
            SetPanelState(PanelState.Third);
            //Set up name label on confirmation page
            confirmationNameLabel.Text = nameTextBox.Text;

            //Enable third "tab"
            LinkButton3.Enabled = true;
        }
        else
        {
            SetPanelState(PanelState.Second);
            errorLabel.Text = "The name field must be filled in.";
            errorLabel.Visible = true;
        }
    }

    private bool IsFormValid()
    {
        //Only invalidation here is an empty name textbox
        if (nameTextBox.Text.Trim().Length == 0)
            return false;

        return true;
    }

    protected void LinkButton1_Click(object sender, EventArgs e)
    {
        SetPanelState(PanelState.First);
    }
    protected void LinkButton2_Click(object sender, EventArgs e)
    {
        SetPanelState(PanelState.First);
    }
    protected void LinkButton3_Click(object sender, EventArgs e)
    {
        SetPanelState(PanelState.First);
    }
}

That is the pattern used in a navigation and a form context. If you delve deeper, you will find the MultiView control uses this same basic pattern, although it does not place Panels on the page.

Now, one question I get from this type of example is “what do I have to do to reflect the change in user’s name if he changes it and click the link button?”. Easy solution. This is common on form elements that do not post back. But it is easy to solve. Just make sure the name change portion is reflected in the link button. In this example, I don’t need to pull out this code into its own routine, but I would on a more complex form. It looks like this (only changes shown below).

First add a routine to set the confirmation name.

private void SetConfirmationName()
{
    confirmationNameLabel.Text = nameTextBox.Text;
}

You then call this from the routines where it is important.

protected void submitButton_Click(object sender, EventArgs e)
{
    if (IsFormValid())
    {
        SetPanelState(PanelState.Third);
        //Set up name label on confirmation page
        SetConfirmationName();

        //Enable third "tab"
        LinkButton3.Enabled = true;
    }
    else
    {
        SetPanelState(PanelState.Second);
        errorLabel.Text = "The name field must be filled in.";
        errorLabel.Visible = true;
    }
}

protected void LinkButton3_Click(object sender, EventArgs e)
{
    SetPanelState(PanelState.First);
    SetConfirmationName();
}

The important thing here is that you make sure any changes to state in one Panel are reflected in any “summary” type panels. I assume here that the final submit is from the “confirmation” page. If the button is outside of the Panels, you need a bit more robust checking and setting of state, especially for any controls you are submitting to the database.

Takeaways

The biggest takeaway here is learning about state. Within the ASP.NET model, it is easier to control state in a single page. This is not true for traditional ASP, but it is for ASP.NET (note: ASP.NET MVC is not the same model).

While the examples in this post are not the most real world, they illustrate controlling state through a “state controller” (our SetPanelState routine), which is much easier to maintain than setting up state in various event handlers.

I have also illustrated the idea of moving code to a separate routine when it is used in more than one place (refactoring). This is far more important when the application gets complex, but it is a good skill to hone on simple examples.

Peace and Grace,
Greg

Twitter: @gbworld

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: