可扩展的 AJAX 向导控件






3.40/5 (4投票s)
动态创建 AJAX 向导控件以提高可扩展性。
引言
AJAX 宣传了通过减少回发之间的闪烁来快速开发行为类似于 Windows 独立应用程序的 Web 应用程序的能力。与所有 .NET 应用程序一样,一个潜在的陷阱是,如果这些应用程序的架构不佳,它们将无法很好地扩展。问题在于,传统的 .NET 开发涉及将控件放置在窗体上,并根据情况将其中一些控件的可见性设置为 false。问题是,即使控件的可见性设置为 false,该控件的大部分代码仍然会运行。当窗体上有许多复杂的自定义控件的可见性设置为 false 时,性能就会受到影响。本文将介绍一个复杂的自定义 AJAX 启用的向导控件,以及如何在需要时动态地在页面上创建此控件。例如,如果一个窗体上有许多可以从侧边栏启动的向导,这将很有用。本文讨论的示例应用程序涉及以下内容
- 扩展 ASP 2.0 框架向导控件以创建自定义向导,并将其嵌套在 AJAX Tool Kit 的
ModalPopupExtender
控件中,以便可以在弹出窗口中显示它。 - 使用自定义用户控件开发向导页面,并赋予向导使用
LoadControl
方法动态加载它们的能力。 - 当需要显示向导时,窗体上的
ContentPlaceHolder
控件将被加载向导。 - JavaScript 将用于使用字符串值加载页面上的一个隐藏控件。这将告诉 Web 页面的代码隐藏哪个向导要显示,或者是否要隐藏向导的弹出窗口。这样做是为了能够在窗体的
OnInit
方法期间动态加载向导,以便向导页面的视图状态能够在页面回发之间保持。如果改用 .NET 服务器事件,则这些事件将在OnInit
方法之后处理,并且它们的视图状态将丢失。 UpdatePanel
控件用于实现 AJAX 效果。这些控件中由代码隐藏管理的部分通过其Update()
方法进行更新。- .NET Validator 控件和 AJAX Toolbox 的
ValidatorCalloutExtender
控件用于向导页面,以演示经典的验证仍然有效。
要运行此示例应用程序,请在此处 点击。示例应用程序将仅从窗体管理单个向导。但是,此解决方案可以轻松扩展,以便从单个页面启动任意数量的向导,而不会降低性能。
扩展标准向导控件
向导控件将管理多个向导页面。每个页面都是一个单独的自定义 Web 用户控件,并且每个页面都将实现一个通用接口。当用户单击 Next
按钮时,这些向导页面将一次显示一个。通用接口允许代码使用多态性,以便可以在不考虑引用哪个特定页面(页面)的情况下调用页面(页面)的方法。在 CustomWizard
类中,我们扩展了 Wizard
类并实现了 IWizard
接口。在 CreateControlHierarchy
方法中,我们指定了 CSS 类,这些类将使应用程序使用的所有向导具有相似的外观和感觉。请注意,每个向导页面的加载和保存数据的工作将由它们自己负责。
/// <summary />
/// All wizards need to do these things.
/// </summary />
interface IWizard
{
void CreateControls();
void Save(int contactID);
void LoadData(int contactID);
}
/// <summary />
/// Each wizard step needs to save their data.
/// </summary />
public interface IWizardStepNew
{
void Save(DataSetContacts.DataTableContactsRow dataTableContactsRow);
}
/// <summary />
/// In edit mode each wizard will need to load the data on the wizard step first.
/// </summary />
public interface IWizardLoadData
{
void Load(DataSetContacts.DataTableContactsRow dataTableContactsRow);
}
/// <summary />
/// This class will set the look and feel for all the wizards in the application.
/// </summary />
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);
}
}
CustomWizard
类已扩展,以提供特定类型的向导,如下面的代码所示。请注意,在 CreateControls
方法中,我们使用 LoadControl
方法加载我们向导页面的各个自定义 Web 用户控件。在 LoadData
方法中,只要它实现了 WizardStep
接口,就不需要区分是哪个单独的向导页面。同样,Save
方法的工作方式也类似。
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);
}
}
实现向导页面
下面的代码显示了一个自定义 Web 用户控件。请注意,该类同时实现了 IWizardStepNew
和 IWizardLoadData
接口。第一个接口用于插入新联系人行和编辑行。然而,后者仅在编辑联系人行时使用。这是因为 IWizardLoadData
接口用于将现有数据加载到将被编辑的控件中。
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;
}
母版页 HTML 标记
在此应用程序中,向导的可见性在 MasterPage 中处理。具有此控件层次结构的 HTML 标记如图 1 所示。
图 1 – 控件层次结构
- I.
UpdatePanel
(当我们想要隐藏或显示弹出对话框时使用) - a.
Panel
(由ModalPopupExtender
引用以在弹出对话框中显示向导) - i.
UpdatePanel
(当我们在切换向导页面时使用) - 1.
PlaceHolder
(用于加载向导控件)
Panel
控件由 AJAX Toolbox 的 ModalPopupExtender
引用,提供了将容纳向导控件的模态弹出窗口。PlaceHolder
控件是代码隐藏在需要显示时动态加载向导的地方。UpdatePanel
控件用于消除回发过程中的闪烁。外部 UpdatePanel
仅在我们显示和隐藏弹出窗口时才更新。内部 UpdatePanel
用于我们在向导页面之间切换时,以便仅更新向导页面,而不更新页面上的其他任何内容。另外值得注意的是,在母版页的 HTML 标记中有两个隐藏控件
<input id="hiddenTarget" type="hidden" name="hiddenTarget" runat="server" />
<input id="hiddenRowID" type="hidden" name="hiddenID" runat="server" />
hiddenTarget
控件用于告知代码隐藏要显示或隐藏哪个向导。如果是一个新行,hiddenRowID
将被加载为 -1
,或者如果正在编辑联系人行,则将加载该联系人行的 ID。这些隐藏字段将在代码隐藏的 OnInit
方法中进行检查。在 OnInit
方法中加载向导非常重要,这样各个向导页面的视图状态才能在页面回发之间保持。顶部的 JavaScript 用于加载这些隐藏字段并回发页面。请参阅下文
<script language="javascript" type="text/javascript"></script>
请注意,当回发到服务器时,hiddenTarget.id
作为参数包含在 JavaScript doPostBack
方法中。这样做是因为图 1 中的外部 UpdatePanel
上定义了一个 AsyncPostBackTrigger
,该触发器为 hiddenTarget
控件。通过将其包含为参数,将执行局部页面刷新,只刷新弹出模态窗口。页面的其余部分不会因回发而闪烁。下载代码以查看具体实现。
母版页 HTML 代码隐藏
母版页的代码隐藏如下所示。在页面顶部,有几个属性用于从窗体集合中检索值。这是因为在页面加载其视图状态之前,在 Onit
方法中检索了这些值。然后,Onit
方法调用 manageModal
方法,根据 JavaScript 在隐藏值控件中设置的值来确定显示或隐藏弹出向导。请注意,还有一个事件处理程序会被调用以更新内容页面上的网格。如果内容页面上有需要在向导关闭时更新的网格,则需要订阅此事件。另外值得注意的是,我们通过代码隐藏页面的 UpdatePanel
控件的 update()
方法来控制哪个更新面板进行更新。
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;
///
/// Row ID for the grid that the User is editing.
///
protected int RowID
{
get
{
string hRowID = Request.Form[hiddenRowID.ClientID.Replace("_", "$")];
int rowID;
if (!int.TryParse(hRowID, out rowID))
{
rowID = -1;
}
return rowID;
}
}
/// <summary />
/// Specifies which dialog the user wants to open. Or it will tell us to
/// close the dialog.
/// </summary />
protected string TargetValue
{
get
{
return Request.Form[hiddenTarget.ClientID.Replace("_", "$")];
}
}
/// <summary />
/// Show or hide the modal. Important to load the dialog on OnInit
/// to be able to retain state between posts.
/// </summary />
/// <param name="""""e""""" /></param />
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());
}
}
/// <summary />
/// Raise an event to tell child forms to update their grids.
/// </summary />
protected void updateUserGrid()
{
if (UpdateUserGrid!=null)
{
EventArgs eventArg = new EventArgs();
UpdateUserGrid(this, eventArg);
}
}
/// <summary />
/// Show or hide the modal depending on the targetValue
/// </summary />
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();
}
}
}
Home.aspx 内容页
Home.aspx 内容页的代码隐藏如下所示。此页面非常标准。它包含并管理一个嵌套在 UpdatePanel
中的 GridView
控件。请注意,在 OnPreInit
方法中,此页面订阅了母版页的 UpdateUserGrid
事件,以便在向导关闭时,此页面知道要更新其网格。
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
{
/// <summary />
/// Wire up the masterPage's UpdateUserGrid event.
/// </summary />
/// <param name="""""e""""" /></param />
protected override void OnPreInit(EventArgs e)
{
base.OnPreInit(e);
MasterPage masterPage = this.Master as MasterPage;
masterPage.UpdateUserGrid += new EventHandler(masterPage_UpdateUserGrid);
}
/// <summary />
/// When the master page tell's us to update our grid call this method.
/// </summary />
/// <param name="""""sender""""" /></param />
/// <param name="""""e""""" /></param />
void masterPage_UpdateUserGrid(object sender, EventArgs e)
{
this.DataBind();
UpdatePanelUserGrid.Update();
}
/// <summary />
/// On page load bind the grid.
/// </summary />
/// <param name="""""sender""""" /></param />
/// <param name="""""e""""" /></param />
protected void Page_Load(object sender, EventArgs e)
{
this.DataBind();
if (!this.IsPostBack && GridView1.Rows.Count==0)
{
PanelInitial.Visible = true;
}
}
/// <summary />
/// Bind the grid.
/// </summary />
public override void DataBind()
{
GridView1.DataSource = DataMethods.GetDataSetContacts().DataTableContacts;
GridView1.DataBind();
}
/// <summary />
/// Method is called when binding the grid in the html markup.
/// Will return the full name for the contact.
/// </summary />
/// <param name="""""firstName""""" /></param />
/// <param name="""""lastName""""" /></param />
/// <returns /></returns />
protected string GetName(string firstName, string lastName)
{
return string.Format("{0} {1}", firstName, lastName);
}
/// <summary />
/// When the user wants to delete a name call this event.
/// </summary />
/// <param name="""""sender""""" /></param />
/// <param name="""""e""""" /></param />
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();
}
}
结论
本文的目的是开发一个 AJAX 应用程序,该应用程序可以使用标准的 .NET AJAX 控件扩展成一个更大的应用程序。这并非易事。使用这些控件开发一个无法很好扩展的小型应用程序要容易得多。在 .NET 中使用标准控件开发可扩展应用程序所需的最重要知识是对页面生命周期的理解以及在每个事件中发生的事情。
关注点
上面的代码在 Firefox 2.0 和 IE 7.0 中运行效果大部分是好的。在我第一次将解决方案发布到我的 Web 服务器时,我遇到一个奇怪的错误,这与将 DataSet
对象存储到 session state 有关。对于这个示例应用程序,我没有使用真实数据库,而是将数据集持久化到 session state。这个错误与在部分页面回发之前没有开始使用 session state 有关。那时,当我尝试将数据集存储到 session state 时,它会抛出一个错误。我通过在首次页面加载时将一个空的 DataSet
对象缓存到 session state 中来解决这个问题。我不确定为什么这能解决问题,但它确实有效。
此外,如果您直接运行应用程序,它在两个浏览器中似乎都能正常运行。但是,如果您使用 IFrame
将此应用程序嵌入到另一个应用程序中,则在 IE 7.0 中弹出窗口不会关闭。如果您执行会使 IE 重新绘制屏幕的操作,例如移动滚动条,则页面将刷新,弹出窗口将消失。在使用 IFrame
在 Firefox 中运行应用程序时,不会观察到这种奇怪的行为。
历史
无更改。