Click here to Skip to main content
13,197,342 members (44,931 online)
Click here to Skip to main content
Add your own
alternative version

Stats

55.4K views
763 downloads
49 bookmarked
Posted 16 Feb 2008

A Scalable AJAX Wizard Control

, 16 Feb 2008
Rate this:
Please Sign up or sign in to vote.
Dynamically creating an AJAX wizard control to facilitate scalability.

Click Here To Go To Demo Site

Introduction

AJAX advertises the ability to quickly develop web applications which act like Windows stand alone applications, by reducing the flicker between post backs. One potential trap, like with all .NET applications, is that if these applications are not well architected, then they will not scale well. The problem is that classical .NET development involves placing controls onto a form and, depending on the situation, setting some of these controls’ visibility to false. The problem is even though a control’s visibility is set to false, most of the code for this control will still run. When there are many complex custom controls on a form with the visibility set to false, the performance suffers. This article will describe a complex custom AJAX-enabled wizard control and how this control can be created dynamically on the page when needed. For example, this would be useful if there are many wizards on a form that could be launched from the sidebar. The sample application discussed in this article involves the following:

  • Extending an ASP 2.0 framework wizard control to create a custom wizard, and nesting it in AJAX Tool Kit’s ModalPopupExtender control to enable it to be shown in a popup.
  • Developing the wizard sheets using custom user controls, and giving the wizard the ability to load them dynamically using the LoadControl method.
  • A ContentPlaceHolder control on the form will be loaded with the wizard when it is to be displayed.
  • JavaScript will be used to load a hidden control on the page with a string value. This will tell the web page’s code-behind which wizard to display or whether to hide the wizard’s popup. This is done to be able to dynamically load the wizard during the OnInit method of the form so that the wizard sheet’s view state will be maintained between page posts. If instead, .NET server events are used, then these events would be handled after the OnInit method and their view state would be lost.
  • UpdatePanel controls are used to give the AJAX effect. Which of these controls are managed by the code-behind is updated using its Update() method.
  • .NET Validator controls and the AJAX Toolbox’s ValidatorCalloutExtender control are used on the wizard sheets to demonstrate that classic validation still works.

To run this sample application, go here. The sample application will only manage a single wizard from the form. However, this solution could be easily extended to have any number of wizard’s launched from a single page, without degrading performance.

Extending the Standard Wizard Control

The wizard control will manage several wizard sheets. Each of these sheets will be a separate custom web user control, and each will implement a common interface. These wizard sheets will be displayed one at a time as the user clicks the Next button. The common interfaces allow the code to use polymorphism so that the sheets’ methods may be called without regard to which particular sheet is being referenced. In the CustomWizard class, we extend the Wizard class and implement the IWizard interface. In the CreateControlHierarchy method, we specify the CSS classes that will give all of the wizards used by the application a similar look and feel. Note that each of the wizards’ sheets will be responsible for loading and saving their own data.

/// <span class="code-SummaryComment"><summary /></span>
/// All wizards need to do these things.
/// <span class="code-SummaryComment"></summary /></span>
interface IWizard
{
    void CreateControls();
    void Save(int contactID);
    void LoadData(int contactID);

}

/// <span class="code-SummaryComment"><summary /></span>
/// Each wizard step needs to save their data.
/// <span class="code-SummaryComment"></summary /></span>
public interface IWizardStepNew
{
    void Save(DataSetContacts.DataTableContactsRow dataTableContactsRow);
}

/// <span class="code-SummaryComment"><summary /></span>
/// In edit mode each wizard will need to load the data on the wizard step first.
/// <span class="code-SummaryComment"></summary /></span>
public interface IWizardLoadData
{
    void Load(DataSetContacts.DataTableContactsRow dataTableContactsRow);
}


/// <span class="code-SummaryComment"><summary /></span>
/// This class will set the look and feel for all the wizards in the application.
/// <span class="code-SummaryComment"></summary /></span>
public class CustomWizard:Wizard, IWizard
{
    public CustomWizard()
    {
        CreateControls();
    }
    protected override void OnUnload(EventArgs e)
    {
        base.OnUnload(e);
    }
    protected override void CreateControlHierarchy()
    {
        base.CreateControlHierarchy();
        this.CssClass = "wizardMaster";

        this.SideBarButtonStyle.ForeColor = System.Drawing.Color.White;

        this.NavigationButtonStyle.CssClass = "navigation";

        this.SideBarStyle.CssClass = "sideBarStyle";
       this.HeaderStyle.CssClass = "headerStyle";
        

    }
    public void AddWizardStep(WizardStep wizardStep)
    {
        this.WizardSteps.Add(wizardStep);
    }
    
}

The CustomWizard class is extended to give a specific type of wizard as is shown in the code below. Note that in the CreateControls method, individual custom web user controls for our wizard sheets are loaded using the LoadControl method. In the LoadData method, it doesn’t matter which of these individual wizard sheets is being referenced as long as it implements the WizardStep interface. Likewise, the Save method works similarly.

public class Wizard1 : CustomWizard
{

    public override void CreateControls()
    {

        this.ID = "WizardA";

        // ========Sheet 1========
        WizardStep wizardStep = new WizardStep();
        wizardStep.Title = "Personal Information";

        // create an instance of the desired control
        System.Web.UI.UserControl userControl = new UserControl();
        Control lControl = userControl.LoadControl(@"~\Wizard1\Sheet1.ascx");
        wizardStep.Controls.Add(lControl);

        this.AddWizardStep(wizardStep);

        // ========Sheet 2========
        wizardStep = new WizardStep();
        wizardStep.Title = "Comments";


        // create an instance of the desired control
        userControl = new UserControl();
        lControl = userControl.LoadControl(@"~\Wizard1\Sheet2.ascx");
        wizardStep.Controls.Add(lControl);

        this.AddWizardStep(wizardStep);

        // ========Finish===========
        wizardStep = new WizardStep();
        wizardStep.Title = "Confirmation";
        wizardStep.StepType = WizardStepType.Complete;

        // create an instance of the desired control
        userControl = new UserControl();
        lControl = userControl.LoadControl(@"~\Wizard1\Finish.ascx");
        wizardStep.Controls.Add(lControl);

        this.AddWizardStep(wizardStep);


    }
    public override void LoadData(int contactID)
    {
        DataSetContacts dataSetContacts = DataMethods.GetDataSetContacts();
        DataSetContacts.DataTableContactsRow dataTableContactsRow = 
           dataSetContacts.DataTableContacts.FindByContactID(contactID);
        foreach (WizardStep wizardStep in this.WizardSteps)
        {
            foreach (Control control in wizardStep.Controls)
            {
                IWizardLoadData wizardLoadData = control as IWizardLoadData;
                if (wizardLoadData != null)
                {
                    wizardLoadData.Load(dataTableContactsRow);
                }
            }
        }
    }

    public override void Save(int contactID)
    {
        DataSetContacts dataSetContacts = DataMethods.GetDataSetContacts();

        DataSetContacts.DataTableContactsRow dataTableContactsRow;

        if (contactID == -1)
        {
            dataTableContactsRow = 
               dataSetContacts.DataTableContacts.NewDataTableContactsRow();
            dataSetContacts.DataTableContacts.AddDataTableContactsRow(
               dataTableContactsRow);
        }
        else
        {
            dataTableContactsRow = dataSetContacts.DataTableContacts.FindByContactID(
                contactID);
            
        }
        
        foreach(WizardStep wizardStep in this.WizardSteps)
        {
            foreach(Control control in wizardStep.Controls)
            {
                IWizardStepNew wizardStepNew = control as IWizardStepNew;
                if (wizardStepNew != null)
                {
                    wizardStepNew.Save(dataTableContactsRow);
                }
            }
        }

       
        DataMethods.SaveDataSetContacts(dataSetContacts);
        
    }

}

Implementation of a Wizard Sheet

The code below shows one of the custom web user controls. Note that the class implements both the IWizardStepNew and the IWizardLoadData interfaces. The first is used for both inserting a new contact row and editing a row. However, the latter one is used only when editing a contact row. This is because the IWizardLoadData interface is used to load the existing data into the controls that will be edited.

public partial class Wizard1_Sheet2 : System.Web.UI.UserControl, IWizardStepNew,
    IWizardLoadData
{
    protected void Page_Load(object sender, EventArgs e)
    {

    }

    #region IWizardStepNew Members

    public void Save(DataSetContacts.DataTableContactsRow dataTableContactsRow)
    {
        dataTableContactsRow.Comments = TextBoxComments.Text;
    }

    #endregion

    #region IWizardLoadData Members

    public new void Load(DataSetContacts.DataTableContactsRow dataTableContactsRow)
    {
        TextBoxComments.Text = dataTableContactsRow.Comments;    
    }

Master Page HTML Markup

In this application, the wizard’s visibility is handled in the MasterPage. The HTML mark-up with this control hierarchy is shown in Figure 1 below.

Figure 1 – Control Hierarchy

  • I. UpdatePanel (Used when we want to hide or show the popup dialog)
    • a. Panel (Reference by a ModalPopupExtender to display wizard in popup dialog)
      • i. UpdatePanel (Used when we change wizard sheets)
        • 1. PlaceHolder (Used to load the wizard control)

The Panel control is referenced by AJAX Toolbox’s ModalPopupExtender, providing the modal popup that will house the wizard control. The PlaceHolder control is where the wizard will be dynamically loaded into by the code-behind when it needs to be displayed. The UpdatePanel controls are there to eliminate flicker during post back. The outer UpdatePanel will only be updated when we are showing and hiding the popup. The inner UpdatePanel is used when we go from wizard sheet to wizard sheet so that only the wizard sheet is updated and nothing else on the page. Also worth noting on the Master Page’s HTML markup are two hidden controls:

<input id="hiddenTarget" type="hidden" name="hiddenTarget" runat="server" /> 
<input id="hiddenRowID" type="hidden" name="hiddenID" runat="server" />

The hiddenTarget control is used to tell the code-behind which wizard to display or hide. The hiddenRowID is loaded with -1 if it is a new row or if it will be loaded with the contact row’s ID being edited. These hidden fields will be inspected by the code-behind’s OnInit method. It is important that we load the wizard in the OnInit method so that the view state of the individual wizard sheets are maintained between page posts. The JavaScript at the top of the page is used to load these hidden fields and to post the page. See below:

<script language="javascript" type="text/javascript"></script>

Note that when there is a post back to the server, the hiddenTarget.id is included as a parameter in the JavaScript doPostBack method. This is done because an AsyncPostBackTrigger is defined on the outer UpdatePanel in Figure 1 to be the hiddenTarget control. By including it as a parameter, a partial page refresh is done which only refreshes the popup modal. The rest of the page will not flicker with the post back. Download the code to see how this is done.

Master Page HTML Code-Behind

The master page code-behind is shown below. At the top of the page, there are several properties which retrieve the values from the form’s collection. This is done because the values are retrieved before the page loads its view state during the Onit method. The Onit method then calls the manageModal method to determine whether to show or to hide the popup wizard based on the values that were set in the hidden value controls from the JavaScript. Note that there is also an event handler that is called to update the grid on the content page. If a content page has a grid on it that needs to be updated when the wizard closes, then it will need to subscribe to this event. Also worth noting is that we control which update panel updates through the code-behind page via the UpdatePanel control’s update() method.

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class MasterPage : System.Web.UI.MasterPage
{
    public event EventHandler UpdateUserGrid;

    // holds a reference to the wizard.
    Wizard1 wizard;

    /// <summary />
    /// Row ID for the grid that the User is editing.
    /// </summary />
    protected int RowID
    {
        get
        {
            string hRowID = Request.Form[hiddenRowID.ClientID.Replace("_", "$")];
            int rowID;

            if (!int.TryParse(hRowID, out rowID))
            {
                rowID = -1;
            }
            return rowID;
        }
    }
    /// <span class="code-SummaryComment"><summary /></span>
    /// Specifies which dialog the user wants to open.  Or it will tell us to
    /// close the dialog.
    /// <span class="code-SummaryComment"></summary /></span>
    protected string TargetValue
    {
        get
        {
            return Request.Form[hiddenTarget.ClientID.Replace("_", "$")]; 
        }
    }
    
    /// <span class="code-SummaryComment"><summary /></span>
    /// Show or hide the modal.  Important to load the dialog on OnInit
    /// to be able to retain state between posts.
    /// <span class="code-SummaryComment"></summary /></span>
    /// <span class="code-SummaryComment"><param name="""""e""""" /></param /></span>
    protected override void OnInit(EventArgs e)
    {
        manageModal();
       
        base.OnInit(e);

    }

    protected override void OnLoad(EventArgs e)
    {
        // Do this to prevent an error on first save.
        if (!this.IsPostBack)
        {
           DataMethods.SaveDataSetContacts(DataMethods.GetDataSetContacts());
        }
    }
    
    /// <span class="code-SummaryComment"><summary /></span>
    /// Raise an event to tell child forms to update their grids.
    /// <span class="code-SummaryComment"></summary /></span>
    protected void updateUserGrid()
    {
        if (UpdateUserGrid!=null)
        {
            EventArgs eventArg = new EventArgs();
            UpdateUserGrid(this, eventArg);
        }
        
    }

    /// <span class="code-SummaryComment"><summary /></span>
    /// Show or hide the modal depending on the targetValue
    /// <span class="code-SummaryComment"></summary /></span>
    protected void manageModal()
    {
        string targetValue = this.TargetValue;
        if (string.IsNullOrEmpty(targetValue) || PlaceHolder1.Controls.Count != 0)
        {
            return;
        }
        // show the contact dialog.
        if (targetValue == "showContact")
        {
            this.wizard = new Wizard1();
            wizard.FinishButtonClick += new WizardNavigationEventHandler(
                wizard_FinishButtonClick);
            PlaceHolder1.Controls.Add(wizard);
            ModalPopupExtender1.Show();

            //If the contact is in edit mode then we need to load the old data.
            if (this.RowID != -1)
            {
                this.wizard.LoadData(this.RowID);
            }

        }
        // hide the contact dialog.
        else if(targetValue=="hide")
        {
            ModalPopupExtender1.Hide();
            UpdatePanelPopupPanel.Update();
        }
        
    }
    // when the user clicks the finish button we need to save off the data
    // and close the dialog.
    void wizard_FinishButtonClick(object sender, WizardNavigationEventArgs e)
    {
        if (this.TargetValue == "showContact")
        {
            this.wizard.Save(this.RowID);
            updateUserGrid();
        }
    }
}

The Home.aspx Content Page

The code-behind for the Home.aspx content page is shown below. This page is pretty standard. It contains and manages a GridView control that is nested inside an UpdatePanel. Note that in the OnPreInit method, this page subscribes to the Master page’s UpdateUserGrid event so that when the wizard closes, this page knows to update its grid.

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

public partial class Home : System.Web.UI.Page
{
    /// <span class="code-SummaryComment"><summary /></span>
    /// Wire up the masterPage's UpdateUserGrid event.
    /// <span class="code-SummaryComment"></summary /></span>
    /// <span class="code-SummaryComment"><param name="""""e""""" /></param /></span>
    protected override void OnPreInit(EventArgs e)
    {
        base.OnPreInit(e);
        MasterPage masterPage = this.Master as MasterPage;
        masterPage.UpdateUserGrid += new EventHandler(masterPage_UpdateUserGrid);

    }

    /// <span class="code-SummaryComment"><summary /></span>
    /// When the master page tell's us to update our grid call this method.
    /// <span class="code-SummaryComment"></summary /></span>
    /// <span class="code-SummaryComment"><param name="""""sender""""" /></param /></span>
    /// <span class="code-SummaryComment"><param name="""""e""""" /></param /></span>
    void masterPage_UpdateUserGrid(object sender, EventArgs e)
    {
        this.DataBind();
        UpdatePanelUserGrid.Update();
    }

    /// <span class="code-SummaryComment"><summary /></span>
    /// On page load bind the grid.
    /// <span class="code-SummaryComment"></summary /></span>
    /// <span class="code-SummaryComment"><param name="""""sender""""" /></param /></span>
    /// <span class="code-SummaryComment"><param name="""""e""""" /></param /></span>
    protected void Page_Load(object sender, EventArgs e)
    {
        this.DataBind();
        if (!this.IsPostBack && GridView1.Rows.Count==0)
        {
            PanelInitial.Visible = true;
        }
        
    }

    /// <span class="code-SummaryComment"><summary /></span>
    /// Bind the grid.
    /// <span class="code-SummaryComment"></summary /></span>
    public override void DataBind()
    {
        GridView1.DataSource = DataMethods.GetDataSetContacts().DataTableContacts;
        GridView1.DataBind();

    }
   
   /// <span class="code-SummaryComment"><summary /></span>
   /// Method is called when binding the grid in the html markup.
   /// Will return the full name for the contact.
   /// <span class="code-SummaryComment"></summary /></span>
   /// <span class="code-SummaryComment"><param name="""""firstName""""" /></param /></span>
   /// <span class="code-SummaryComment"><param name="""""lastName""""" /></param /></span>
   /// <span class="code-SummaryComment"><returns /></returns /></span>
    protected string GetName(string firstName, string lastName)
    {
        return string.Format("{0} {1}", firstName, lastName);
    }

    /// <span class="code-SummaryComment"><summary /></span>
    /// When the user wants to delete a name call this event.
    /// <span class="code-SummaryComment"></summary /></span>
    /// <span class="code-SummaryComment"><param name="""""sender""""" /></param /></span>
    /// <span class="code-SummaryComment"><param name="""""e""""" /></param /></span>
    protected void LinkButtonDelete_Click(object sender, EventArgs e)
    {
        LinkButton lLinkButton = (LinkButton)sender;
        int contactID;
        if (!int.TryParse(lLinkButton.CommandArgument, out contactID))
        {
            return;
        }
        DataMethods.DeleteContact(contactID);
        this.DataBind();
        UpdatePanelUserGrid.Update();

    }
}

Conclusion

The objective of this article was to develop an AJAX application that could be extended into a much larger application using the standard .NET AJAX controls. This was not an easy task. It is much easier to develop a small application using these controls that does not scale well. The most significant knowledge required to develop a scalable application in .NET using the standard controls is an understanding of the page’s life cycle and what happens during each event.

Points of Interest

The code above seems to run fine for the most part in Firefox 2.0 and IE 7.0. I did have a strange error when I first published the solution to my web server that had something to do with storing the DataSet object into session state. For this example application, I don’t use a real database but instead persist a dataset in session state. The error had something to do with not beginning using session state until a partial page post. At that point, when I tried to store the dataset in session state, it would throw an error. I got around it by caching an empty DataSet object into session state on first page load. I’m not sure why this fixes the issue, but it works.

Also, the application seems to run fine in both browsers if you run it directly. If you, however, embed this application in another application using an IFrame, then the popup doesn’t close in IE 7.0. If you do something that causes IE to repaint the screen, like moving the scroll bar, then the page will refresh and the popup disappears. No such weirdness is observed when running the application in an IFrame with Firefox.

History

No changes.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mike Clark
Web Developer MC Software
United States United States
Mike is a software consultant with 10+ development experience who works on IT projects mainly for Microsoft and Boeing. Most of these projects have been intranet based web sites connected to a SQL database. In the last 6 years, Mike has focused on C# and SQL Server development.

You may also be interested in...

Pro

Comments and Discussions

 
GeneralAdding a new Wizard Sheet Pin
pym11-Dec-08 14:19
memberpym11-Dec-08 14:19 
GeneralWizard Form Pin
Member 23130973-Oct-08 20:54
memberMember 23130973-Oct-08 20:54 
GeneralRe: Wizard Form Pin
arvind mogha17-Aug-11 7:10
memberarvind mogha17-Aug-11 7:10 
Generalplace ajax enabled contact form on LightWIndow instead of Modal Pin
trickbox9-May-08 9:55
membertrickbox9-May-08 9:55 
GeneralRe: place ajax enabled contact form on LightWIndow instead of Modal Pin
xsoftdev7-Jul-08 6:15
memberxsoftdev7-Jul-08 6:15 
GeneralDemo site Pin
MikeVE20-Feb-08 0:03
memberMikeVE20-Feb-08 0:03 
GeneralRe: Demo site Pin
dsmportal20-Feb-08 7:39
memberdsmportal20-Feb-08 7:39 
GeneralRe: Demo site Pin
Mike Clark21-Feb-08 17:43
memberMike Clark21-Feb-08 17:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171020.1 | Last Updated 16 Feb 2008
Article Copyright 2008 by Mike Clark
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid