ASP.NET AJAX 对 Web 服务特性为客户端代码访问服务器提供了一个可贵的窗口。但你要做大多数的困难任务,必须精心打造 Web 方法、合适的时机调用它们、适当更新页面、除了 JavaScript 什么都不能用。对于复杂的呈现,这个过程就十分单调。
由于这个原因,ASP.NET 提供了一个更高层的服务器模型,它提供可以直接在 Web 窗体里使用的控件和组件,你可以完全使用服务器端代码工作。
ASP.NET AJAX 控件会自动注入客户端脚本,在幕后使用 ASP.NET AJAX 脚本库。与手动编写 js 代码相比,潜在的缺点是降低了灵活性。
在这篇文章里,我将介绍 ASP.NET 框架中的 3 个 ASP.NET AJAX 控件:UpdatePanel、Timer、UpdateProgress。所有这些控件都支持局部呈现,可以不经过完整回发无缝地更新页面内容,这是 Ajax 的关键概念。
UpdaePanel 的局部呈现
UpdaePanel 能让你使用服务器端逻辑处理普通页面并以无闪动的 Ajax 风格刷新自己。
基本思想是把 Web 页面分解为一到多个独立的区域,其中每个区域都包含在一个不可见的 UpdaePanel 里。当 UpdaePanel 中发生通常会触发一次回发的事件时,UpdaePanel 截取该回发事件代之以执行一次异步回调。
下面是它发生过程的一个示例步骤:
- 用户单击 UpdaePanel 里的一个按钮
- 一些客户端 js 代码(由 ASP.NET AJAX 生成)截取到单击事件并执行一次服务器回调
- 在服务器端,页面周期正常执行,所有常规事件正常发生。
- 最终,页面被呈现为 HTML 返回浏览器
- 客户端 js 得到完整的 HTML 并更新页面上所有的 UpdaePanel (如果变更的内容不在 UpdaePanel 里,就会被忽略)
UpdaePanel 控件和 ScriptManager 控件一起工作。在使用 UpdaePanel 前,需要把 ScriptManager.EnablePartialRendering 属性设置为 true(默认值),然后才可以向页面添加 UpdaePanel 控件(放在 ScriptManager 之后)。
把控件拖到 UpdaePanel 时,内容出现在 <ContentTemplate> 节,如下:
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePartialRendering="true">
</asp:ScriptManager>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
<asp:Button ID="Button1" runat="server" Text="Button" />
</ContentTemplate>
</asp:UpdatePanel>
UpdaePanel 是一个基于模板的控件。呈现自身时,它把自己的 ContentTemplate 节的内容复制到页面上,因此,不能通过 UpdaePanel.Controls 集合动态添加控件,而要通过 UpdaePanel.ContentTemplateContainer.Controls 集合,这点请注意。
UpdaePanel 并非继承自 Panel,而是直接继承 Controls。在其生命周期里只有一个角色:作为异步刷新内容的容器。UpdaePanel 没有可视界面,不支持样式设置。如果要在 UpdaePanel 之外显示边框或者修改背景颜色,那么需要在 UpdaePanel 里放置一个普通的 Panel 或 DIV 标签。
UpdaePanel 把自身呈现为 <div> 标签。但如果设置 RenderMode 属性由 Block 改为 Inline,可以配置 UpdaePanel 自身呈现为嵌入元素。如果要创建的 UpdaePanel 处于段落中或者其他块元素中,就可以进行这样的设置。
下图显示包含 3 个 UpdaePanel 控件的示例页面,每个 UpdaePanel 都包含相同的内容:一个 Label 和一个 Button 控件。每次页面传送到服务器时,Page.Load 事件触发:
protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = DateTime.Now.ToLongTimeString();
Label2.Text = DateTime.Now.ToLongTimeString();
Label3.Text = DateTime.Now.ToLongTimeString();
}
这个页面显示了异步回调无闪动刷新,单击任一按钮,3 个标签都会安静的更新。唯一的例外是浏览器不支持 XMLHttpRequest 对象时,UpdaePanel 会自动降级为使用完整页面回发。
1. 错误处理
UpdaePanel 执行异步回调后,Web 页面代码完全按页面被回发时那样运行。唯一的区别在于:
- 通信方式:页面通过一个 XMLHttpRequest 异步调用获取新数据
- 接收到的数据的处理方式:UpdaePanel 刷新自身内容,页面其余部分保持不变
同步回发中可能发生的问题也完全会在执行异步回发时发生,添加下列代码测试:
if (IsPostBack)
throw new ApplicationException("This operation failed.");
Web 页面抛出一个未处理的异常时,ScriptManager 捕获这个错误并回传给客户端,然后 ASP.NET AJAX 客户端库在页面中抛出一个 JavaScript 错误。下一步发生什么取决于浏览器的设置。如果启用了脚本调试,VS 会在产生错误的那一行中断。如果没有使用脚本调试,浏览器可能通知也可能不通知你发生了一个问题。现在大多数浏览器都被配置为忽略 JavaScript 错误。对于 IE,会在状态栏左下角出现“页面错误”消息。
使用客户端 js 处理错误可以改变这一行为。为此,你需要为 System.Web.PageRequestManager 类的 endRequest 事件注册一个回调。PageRequestManager 是 ASP.NET AJAX 应用程序模型的核心部分,它管理 UpdaePanel 控件的刷新过程并在页面的生命周期各个阶段触发客户端事件。
下面这段客户端脚本块就是这样注册的:
- 定义页面第一次加载时触发的函数
- 不需要使用 onload 事件,只要有 pageLoad() 函数,ASP.NET AJAX 就会自动调用它
- 不需要使用 unload 事件,只要有 pageUnload() 函数,ASP.NET AJAX 也会自动调用它
- 除了上述 2 个函数,其余函数均须手工绑定
下面是 pageLoad() 函数,它得到 PageRequestManager 当前实例的引用,并向 endRequest 事件附加第二个函数:
function pageLoad() {
var pageManager = Sys.WebForms.PageRequestManager.getInstance();
pageManager.add_endRequest(endRequest);
}
endRequest 事件在每次异步回发结束时发生。本例中,endRequest() 函数检查是否有错误发生。如果有,把错误信息显示到另一个控件里,并调用 set_errorHandled() 方法来消除 ASP.NET AJAX 标准错误处理行为(显示错误消息框):
function endRequest(sender, args) {
// Handle the error.
if (args.get_error() != null) {
$get("lblError").innerHTML = args.get_error().message;
// Suppress the message box.
args.set_errorHandled(true);
}
}
下图显示了使用错误处理后的界面效果:
ASP.NET 里有 2 个不能在 UpdatePanel 中使用的控件:FileInput 和 HtmlInputFile 控件。但它们仍可以在含有 UpdatePanel 控件的页面中使用。
2. 条件更新
如果页面上有多个 UpdatePanel 控件且都是完全独立的,那么可以配置 UpdatePanel.UpdateMode 属性由 Always 改为 Conditional 以独立更新它们。即,如果将先前示例中的 3 个 UpdatePanel 控件都设置为 UpdateMode="Conditional" ,那么它们将各自独立工作。
从技术面而言,单击某个按钮时本例中所有的 Label 都会被更新,但是客户端只有部分页面会被刷新以显示相应的效果。多数情况下,这种差别并不重要。但是,它可能会导致异常,因为每个 Label 的值都被保存到了视图状态。这样,下一次页面被送到服务器端时,所有 Label 都将被置为最近的值。
3. 被中断的更新
有一点需要说明,如果执行的是一个很耗时的更新,那么更新可能会被其他更新中断。ASP.NET AJAX 异步回发页面,因此用户能够在回发进行过程中继续单击页面的其他按钮。ASP.NET AJAX 不允许并发更新,因为它需要确保其他信息(如视图状态、会话 cookie 等)保持一致。当一个新的异步回发被启动后,前一个异步回发就被取消了。
大部分情况下,这正是你希望的行为。但为了防止用户打断进行中的异步回发,可以通过 js 代码在异步回发的过程中禁用控件。你需要为 beginRequest 事件附加一个事件处理程序,这和错误处理程序示例中给 endRequest 事件添加处理程序一样。
4. 触发器
如果使用的是条件更新模式,还有其他一些办法来触发更新。其中一个办法就是使用触发器告诉 UpdatePanel 在页面上的某个特定控件的特定事件发生时来呈现自身。
从技术上而言,UpdatePanel 总是使用触发器。UpdatePanel 中所有控件自动成为 UpdatePanel 的触发器。在先前的示例中,当 Button.Click 事件发生时,就会发生一次异步回发。然而,这同时也能和所有 Web 控件的默认事件(控件里用 DefaultEvent 特性标记的事件)一起工作,只要那个事件回发了页面。例如,在 UpdatePanel 里放置一个 TextBox,设置 AutoPostBack 属性为 True,那么 TextBox.TextChanged 事件就会触发一次异步回发,同时 UpdatePanel 也将被更新。
触发器可以按两种方式改变这个行为。首先,它允许把触发器关联到 Panel 之外的某个控件上。例如页面其他位置有一个按钮,这个按钮本将触发一次完整的回发,但把它关联到 UpdatePanel 之后,它将执行一次异步回发。要实现这一设计,只需给 UpdatePanel 增加 AsyncPostBackTrigger,指定要监听的控件的 ID 以及要触发刷新的事件:
<div>
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<br />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<div style="background-color: #FFFFDD; padding: 20px">
<asp:Label ID="Label1" runat="server" Font-Bold="True"></asp:Label>
</div>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="cmdOutsideUpdate" EventName="Click" />
</Triggers>
</asp:UpdatePanel>
<br />
<asp:Button ID="cmdOutsideUpdate" runat="server" Text="Update" />
</div>
EventName 指定要监听的事件,通常不需指定,默认监听控件的默认事件,不过,显式设置是一个好的选择。
现在单击按钮,单击在客户端被拦截,并且 PageRequestManager 执行一次异步回发,会发生如下情况:
- 所有 UpdateMode 设置为 Always 的 UpdatePanel 控件都会被刷新
- 所有 UpdateMode 设置为 Conditional 并且有针对 cmdOutsideUpdate 的 AsyncPostBackTrigger 的 UpdatePanel 控件都会被刷新
- 所有 UpdateMode 设置为 Conditional 但不监听按钮的 UpdatePanel 不会被刷新
注意:
- 为同一 UpdatePanel 添加多个触发器,其中任一事件都将触发更新
- 将同一触发器添加到多个 UpdatePanel,此时一个事件同时更新所有 UpdatePanel
- 在条件更新的 UpdatePanel 里混合匹配触发器和内嵌控件,这样内嵌控件的所有事件及触发器里的事件都将引发更新
还可以按另一种方式使用触发器。假设 UpdatePanel 里有一个按钮,单击将触发一次异步请求和局部更新。如果希望触发一次完整的页面回发,只要添加 PostBackTrigger 即可:
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<br />
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
<div style="background-color: #FFFFDD; padding: 20px">
<asp:Label ID="Label1" runat="server" Font-Bold="True"></asp:Label>
<br />
<br />
<asp:Button ID="cmdOutsideUpdate" runat="server" Text="Update" />
</div>
</ContentTemplate>
<Triggers>
<asp:PostBackTrigger ControlID="cmdOutsideUpdate" />
</Triggers>
</asp:UpdatePanel>
这项技术应用不太常见。但是当 UpdatePanel 中的多个控件只执行很少的更新(因此使用异步回发),而有一个控件执行整个页面的大片更新(因此使用完整回发)时它就很有用了。
5. 优化 UpdatePanel
UpdatePanel 有时会因带宽因素让人有不好的印象。这是因为 UpdatePanel 几乎总是传输超过你所需要的信息量。
假设你创建一个页面用于显示一张表。实现这个页面最有效的办法就是使用 ASP.NET AJAX 和 Web 服务。页面会调用服务器端的一个 Web 服务获取数据,你需要编写客户端的 js 代码并将它转换为 HTML。
与上面的办法相比,类似的解决方案是使用一个带有富数据控件的 UpdatePanel,比如 GridView。这个方案让你可以避免写大部分的代码,但是,UpdatePanel 需要请求更多信息来刷新自身。即 Web 服务器需要发送完整的 GridView 呈现内容,和 UpdatePanel 中其他控件的呈现内容以及页面的完整视图状态。这些信息远多于你采用 Web 服务方式所需的信息量。
为了获得最佳的 UpdatePanel 性能,了解它的本质是必须的。一些最佳的做法如下:
- 尽可能的压缩视图状态数据,使用 EnableViewState 属性关闭那些有变化内容但是不需要用视图状态保存的控件的视图状态。
- 在 UpdatePanel 里只放置最少的内容,如上例中,只因放 GridView,而使用触发器特性将其他可能触发更新的控件放置在 UpdatePanel 之外。
- 如果页面中有多个可更新区域,应放置在独立的 UpdatePanel 中,并设置 Conditional 模式。Web 服务器应答一个回调时,将呈现的标记发送至 UpdatePanel 和页面中所有非 Conditional 的 UpdatePanel。如果你仅仅需要为其中一个面板获取新内容,那就不需要为其他的面板获取内容。
- 在使用了多个更新面板的复杂页面中,考虑用手动的方式刷新它们。要做到这一点,将面板设置为 Conditional 并将 ChildrenAdTriggers 属性设置为 false。现在唯一能引发刷新动作的就是在一个回调请求期间在一个或更多 UpdatePanel 控件上显式调用 Update() 方法。
UpdatePanel 方式永远不会比页面回发的方式差,因为页面回发方式总是将整个页面进行回发。UpdatePanel 方式仅仅在与手工进行 ASP.NET AJAX 编程相比时显得不够好。
使用 Timer 定时刷新
有时候可能希望用户没有动作的情况下强制完成一次完整或部分页面的刷新。例如一个股价的页面,你会希望定期刷新股价区域。ASP.NET AJAX 的 Timer 控件可以实现这种设计。
Timer 控件的使用非常简单,只要把它的 Interval 属性设置为页面更新间隔的最大毫秒数即可。不过要知道,Timer 控件可能会大大增加 Web 应用程序的负载并降低它的可扩展性。
Timer 控件会引发服务器端 Tick 事件,可以在该事件中更新页面。但此事件的使用也不是强制的,因为定时器触发时会执行完整的页面生命周期,也可以响应页面和其他控件事件,比如 Page.Load 。
要在部分呈现中使用 Timer 控件,可把页面的可更新部分包装到 UpdatePanel 控件并设置 UpdateMode 为 Conditional,定时器触发时会强制更新:
<asp:UpdatePanel ID="UpdatePanel1" runat="server" UpdateMode="Conditional">
<ContentTemplate>
...
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="cmdOutsideUpdate" EventName="Tick" />
</Triggers>
</asp:UpdatePanel>
<asp:Timer ID="Timer1" runat="server" Interval="60000" ontick="Unnamed1_Tick"></asp:Timer>
页面的其他部分保持不变,或者如果要响应其他动作而更新它们,可以把它们包装到具有不同触发器的条件更新的 UpdatePanel 控件里。
如果要停止一个定时器,只要在服务器端代码里设置 Enabled 属性为 false 即可。例如下面的代码在 10 次更新后禁用定时器:
protected void Unnamed1_Tick(object sender, EventArgs e)
{
int tickCount = 0;
if (ViewState["TickCount"] != null)
{
tickCount = (int)ViewState["TickCount"];
}
tickCount++;
if (tickCount > 10)
{
Timer1.Enabled = false;
}
}
使用 UpdateProgress 的耗时更新
ASP.NET AJAX 还包括 UpdateProgress 控件,它和 UpdatePanel 的部分呈现一起工作。UpdateProgress 控件的名称不太准确。它并不指示进度,而是提供一条等待信息让用户知道页面还在工作,最后的请求还在继续处理中。
添加 UpdateProgress 控件后,就能够指定异步请求开始后显示某些内容,而这些内容在异步请求结束时又将自动消失。这些内容可以包括固定的消息或图片。通常,会用一个动画 GIF 来模拟进度条。
下面是一个使用了 UpdateProgress 控件的页面在其生命周期 3 个不同阶段的画面:
页面标记中定义了一个 UpdatePanel,随后跟着一个 UpdateProgress 。UpdateProgress 控件包含一个取消按钮,稍后会详细介绍它:
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<div style="background-color: #FFFFE0; padding: 20px">
<asp:Label ID="lblTime" runat="server" Font-Bold="True"></asp:Label>
<br />
<br />
<asp:Button ID="cmdRefreshTime" runat="server" Text="Start the Refresh Process"
OnClick="cmdRefreshTime_Click" />
</div>
</ContentTemplate>
</asp:UpdatePanel>
<br />
<asp:UpdateProgress runat="server" ID="updateProgress1">
<ProgressTemplate>
<div style="font-size: xx-small">
Contacting Server ...<img src="wait.gif" alt="Wait" />
<input id="Button1" onclick="AbortPostBack()" type="button" value="Cancel"
style="font-size: xx-small;" />
</div>
</ProgressTemplate>
</asp:UpdateProgress>
为了模拟一个较慢的过程,可添加一行延时 10 秒的代码:
protected void cmdRefreshTime_Click(object sender, EventArgs e)
{
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(10));
lblTime.Text = DateTime.Now.ToLongTimeString();
}
不需要把 UpdateProgress 控件显式关联到 UpdatePanel 控件。无论哪一个 UpdatePanel 开始回调,UpdateProgress 都会自动显示它的 ProgressTemplate。不过,如果页面很复杂且具有多个 UpdatePanel,可以选择让 UpdateProgress 只关注其中某一个。要这么做的话,只要把 UpdateProgress.AssociatedUpdatePanelID 的属性设为相应的 UpdatePanel 的 ID 即可。甚至可以为同一页面添加多个 UpdateProgress 控件,然后分别关联到不同的 UpdatePanel。
取消
UpdateProgress 控件还支持另一个细节,取消按钮。用户单击取消时,异步回调就会立即被取消,UpdateProgress 的内容将消失,页面重新回到它的原始状态。
添加取消按钮是一个两步过程。首先,要添加一段执行取消的 js 代码。下面的代码执行这一工作(必须放在 ScriptManager 控件之后):
var prm = Sys.WebForms.PageRequestManager.getInstance();
prm.add_initializeRequest(InitializeRequest);
function InitializeRequest(sender, args) {
if (prm.get_isInAsyncPostBack()) {
args.set_cancel(true);
}
}
function AbortPostBack() {
if (prm.get_isInAsyncPostBack()) {
prm.abortPostBack();
}
}
添加这段代码后,就可以随时通过 JavaScript 代码调用 AbortPostBack() 函数取消回调:
<input id="Button1" onclick="AbortPostBack()" type="button" value="Cancel" />
典型情况下,会把这个按钮(或者类似的元素)放到 UpdateProgress 控件的 ProgressTemplate 里,因为它只在回调进行时才被应用。
对那些能够被安全取消的任务提供取消按钮是有意义的,因为它们不影响外部的状态。例如,用户应该能够取消耗时的查询。但是,为更新操作提供取消则不是什么好主意,因为服务器会持续执行直到它结束更新,即使客户端已经停止监听响应时也是如此。
管理浏览器历史
每次页面执行一次全面回发,Web 浏览器就会将其作为一个页面导航来处理,并在历史列表中加入一个新项。然而,当你使用 UpdatePanel 来执行异步回发的操作时,历史列表不会被更新。这一缺点在 ASP.NET AJAX 页面执行一个复杂的多步骤操作的时候变得尤为明显。如果用户稍不注意单击了“后退”按钮回退到前一步的话,浏览器会跳回到前一个页面,并且刚才用户所做的所有工作都会丢失。
如果已经安装了 ASP.NET 4,那么有一个合适的解决方法。通过使用 ScriptManager,你可以控制浏览器的历史列表。你可以在需要的时候新增浏览器列表项,并在用户单击“后退”或“前进”时作出响应,确保页面状态是被正确恢复的。
事实上,与普通 Web 表单的导航相比,这个处理过程工作的相当好,因为不会提示用户重新填写之前页面上的信息。如同大多数 ASP.NET AJAX 特性一样,历史列表支持特性给了你一个很好的方式来获得这些功能,而无需你自己来编写恼人的复杂代码并且无需考虑浏览器的兼容性。
为了看到 ScriptManager 的浏览器历史特性是如何工作的,可以使用 Wizard 控件来创建一个简单的例子。下面是一个分为 3 步向导的简化例子,Wizard 控件包含在 UpdatePanel 中:
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<asp:Wizard ID="Wizard1" runat="server" ...>
<WizardSteps>
<asp:WizardStep runat="server" Title="Step 1">
This is Step 1.
</asp:WizardStep>
<asp:WizardStep runat="server" Title="Step 2">
This is Step 2.
</asp:WizardStep>
<asp:WizardStep runat="server" Title="Step 3">
This is Step 3.
</asp:WizardStep>
</WizardSteps>
...
</asp:Wizard>
</ContentTemplate>
</asp:UpdatePanel>
这个页面充分利用了 ASP.NET AJAX 的优势。现在单击向导中的链接进行导航,页面不会出现山东。然而,历史列表也不会有任何变化。
1. 新增历史点
首先,将 ScriptManager.EnableHistory 属性设为 true,这将允许使用 ScriptManager 来新增历史点并对历史导航作出响应。
接下来,需要在服务器端代码中调用 ScriptManager.AddHistoryPoint() 方法将条目加入到历史列表中。并需要提供下面 3 个参数:
- State:状态值是一个字符串,用来存储与历史点相关的信息。当用户返回到这个历史点时,可以获取到相关的状态。比如在这个例子中,它会存储向导的当前步骤索引。
- Key:键值是一个唯一的字符串名字,用来存储状态信息。允许页面存储多个不冲突的状态值。这对于拥有多个使用历史列表的控件的情况很有用。比如,你有两个 Wizard 控件在同一页面中,只要每一个 Wizard 都使用了一个不同的键值,就可以同时存储各自当前步骤的索引。
- Title:页面标题显示在浏览器窗口顶部并被记录在历史列表中。你也可以省略标题参数,此时历史点将会使用当前的页面标题。
本例中,在控件更改到一个新步骤时新增一个历史点是比较合适的。在向导的 ActiveStepChanged 事件中来做这个动作:
protected void Wizard1_ActiveStepChanged(object sender, EventArgs e)
{
if ((ScriptManager1.IsInAsyncPostBack) && (!ScriptManager1.IsNavigating))
{
string currentStep = Wizard1.ActiveStepIndex.ToString();
ScriptManager1.AddHistoryPoint("Wizard1", currentStep, "Step " + (Wizard1.ActiveStepIndex + 1).ToString());
}
}
在新增历史点之前,代码要检测两个细节。首先检测是不是作为一个异步回调操作的一部分进行变更的。
这可以确保不会在下列情况下新增历史点:
- 页面首次被创建且属性 ActiveStepIndex 第一次被赋值
- 使用“前进”或“后退”按钮时,IsNavigating 属性为 true
上述情况发生的时候,不需要存储状态,相反,需要通过处理 ScriptManager.Navigate 事件来还原它(后面会介绍)。
当新增历史点时,代码使用索引名字 Wizard1(匹配控件的名称)存储当前步骤的索引并将页面标题设置为一个说明性字符串,比如“Step 1”。如果现在运行页面会看到历史表中出现新的项,然而单击一个历史项则什么也不会发生,这是因为还没有编写代码来还原页面状态。
2. 还原页面状态
当用户在历史列表中前后移动时,ScriptManager 执行一个异步回调来刷新页面。这时可以处理 ScriptManager.Navigate 事件。当处理 Navigate 事件时,由你决定使用 HistoryEventArgs.State 集合来获取需要的状态以及当你首次新增历史点的时候所使用的状态值,表明用户可能返回到了页面的第一个书签,这意味着你应当返回到向导控件的初始状态:
protected void ScriptManager1_Navigate(object sender, HistoryEventArgs e)
{
if (e.State["Wizard1"] == null)
{
// Restore default state of page (for exmaple, for first page).
Wizard1.ActiveStepIndex = 0;
}
else
{
Wizard1.ActiveStepIndex = Int32.Parse(e.State["Wizard1"]);
}
Page.Title = "Step " + (Wizard1.ActiveStepIndex + 1).ToString();
}
这段代码同时更新了页面标题以与历史记录相匹配,因为 ScriptManager 不会自己执行这个操作。
现在向导控件已经能够很好的工作了:
3. 状态是如何在 URL 中存储的
事实上,ScriptManager 将状态值放在了页面的 URL 里。它使用 Base64 编码来混淆这些值并在最后使用散列码来做校验。下面是向导例子中的 URL:
状态信息被放置在 URL 符号 # 之后。因此不会打乱任何正在使用的查询字符串参数,这些参数出现在 # 号前。
历史状态使用与视图状态同样的编码机制。这意味着用户能毫不费力的获取到你的状态值,但是用户并不能篡改这些值,因为他们不能在没有 Web 服务器私钥的情况下产生一个正确的散列码。
和视图状态不一样的地方是,你不能加密状态值。然而,如果你想更清晰的查看 URL,可以移除这些编码和散列码。只需设置 ScriptManager.EnableSecureHistoryState 为 false 即可。然后 URL 看起来就像下面这样:
http://localhost:41265/WebSite1/BrowserHistory.aspx#&&Wizard1=2
当然,这就完全使得用户可以通过修改 URL 来更改一个状态值。