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.
interface IWizard
{
void CreateControls();
void Save(int contactID);
void LoadData(int contactID);
}
public interface IWizardStepNew
{
void Save(DataSetContacts.DataTableContactsRow dataTableContactsRow);
}
public interface IWizardLoadData
{
void Load(DataSetContacts.DataTableContactsRow dataTableContactsRow);
}
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";
WizardStep wizardStep = new WizardStep();
wizardStep.Title = "Personal Information";
System.Web.UI.UserControl userControl = new UserControl();
Control lControl = userControl.LoadControl(@"~\Wizard1\Sheet1.ascx");
wizardStep.Controls.Add(lControl);
this.AddWizardStep(wizardStep);
wizardStep = new WizardStep();
wizardStep.Title = "Comments";
userControl = new UserControl();
lControl = userControl.LoadControl(@"~\Wizard1\Sheet2.ascx");
wizardStep.Controls.Add(lControl);
this.AddWizardStep(wizardStep);
wizardStep = new WizardStep();
wizardStep.Title = "Confirmation";
wizardStep.StepType = WizardStepType.Complete;
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;
Wizard1 wizard;
protected int RowID
{
get
{
string hRowID = Request.Form[hiddenRowID.ClientID.Replace("_", "$")];
int rowID;
if (!int.TryParse(hRowID, out rowID))
{
rowID = -1;
}
return rowID;
}
}
protected string TargetValue
{
get
{
return Request.Form[hiddenTarget.ClientID.Replace("_", "$")];
}
}
protected override void OnInit(EventArgs e)
{
manageModal();
base.OnInit(e);
}
protected override void OnLoad(EventArgs e)
{
if (!this.IsPostBack)
{
DataMethods.SaveDataSetContacts(DataMethods.GetDataSetContacts());
}
}
protected void updateUserGrid()
{
if (UpdateUserGrid!=null)
{
EventArgs eventArg = new EventArgs();
UpdateUserGrid(this, eventArg);
}
}
protected void manageModal()
{
string targetValue = this.TargetValue;
if (string.IsNullOrEmpty(targetValue) || PlaceHolder1.Controls.Count != 0)
{
return;
}
if (targetValue == "showContact")
{
this.wizard = new Wizard1();
wizard.FinishButtonClick += new WizardNavigationEventHandler(
wizard_FinishButtonClick);
PlaceHolder1.Controls.Add(wizard);
ModalPopupExtender1.Show();
if (this.RowID != -1)
{
this.wizard.LoadData(this.RowID);
}
}
else if(targetValue=="hide")
{
ModalPopupExtender1.Hide();
UpdatePanelPopupPanel.Update();
}
}
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
{
protected override void OnPreInit(EventArgs e)
{
base.OnPreInit(e);
MasterPage masterPage = this.Master as MasterPage;
masterPage.UpdateUserGrid += new EventHandler(masterPage_UpdateUserGrid);
}
void masterPage_UpdateUserGrid(object sender, EventArgs e)
{
this.DataBind();
UpdatePanelUserGrid.Update();
}
protected void Page_Load(object sender, EventArgs e)
{
this.DataBind();
if (!this.IsPostBack && GridView1.Rows.Count==0)
{
PanelInitial.Visible = true;
}
}
public override void DataBind()
{
GridView1.DataSource = DataMethods.GetDataSetContacts().DataTableContacts;
GridView1.DataBind();
}
protected string GetName(string firstName, string lastName)
{
return string.Format("{0} {1}", firstName, lastName);
}
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.