了解 ASP.NET 视图状态

 

斯科特·米切尔
4GuysFromRolla.com

2004 年 5 月

适用于:
   Microsoft® ASP.NET
   Microsoft® Visual Studio® .NET

摘要:Scott Mitchell 了解 Microsoft® ASP.NET 中视图状态的好处和混淆。 此外,他还介绍了如何解释 (和保护) 存储在视图状态中的数据。 (25 个打印页)

单击此处下载本文的代码示例。

目录

简介
ASP.NET 页生命周期
视图状态的角色
查看状态和动态添加的控件
ViewState 属性
对视图状态的跟踪进行计时
在页面的 ViewState 属性中存储信息
视图状态的成本
禁用视图状态
指定保存视图状态的位置
分析视图状态
查看状态和安全影响
结论

简介

简而言之,Microsoft® ASP.NET 视图状态是 ASP.NET 网页用来跨回发保留对 Web 窗体状态的更改的技术。 在我作为培训师和顾问的经验中,视图状态在 ASP.NET 开发人员中引起了最大的困惑。 创建自定义服务器控件或执行更高级的页面技术时,如果无法牢牢把握视图状态及其工作原理,则可能会回过头来咬你。 专注于创建低带宽、精简页面的 Web 设计人员也经常发现自己对视图状态感到沮丧。 默认情况下,页面的视图状态放置在名为 的 __VIEWSTATE隐藏窗体字段中。 此隐藏窗体字段很容易变得非常大,大小为几十 KB。 表单字段不仅 __VIEWSTATE 会导致下载速度变慢,而且,每当用户发回网页时,此隐藏的表单字段的内容都必须在 HTTP 请求中发回,从而延长请求时间。

本文旨在深入探讨 ASP.NET 视图状态。 我们将确切地了解存储的视图状态,以及如何将视图状态序列化到隐藏的窗体字段,以及如何在回发时反序列化回。 我们还将讨论降低视图状态所需的带宽的技术。

注意 本文面向 ASP.NET 页开发人员,而不是 ASP.NET 服务器控件开发人员。 因此,本文不包括有关控件开发人员如何实现保存状态的讨论。 有关该问题的深入讨论,请参阅 开发 Microsoft ASP.NET 服务器控件和组件一书。

在深入了解视图状态之前,请务必先花点时间快速讨论 ASP.NET 页生命周期。 也就是说,当请求从浏览器传入 ASP.NET 网页时,会发生什么情况? 我们将在下一部分中逐步完成此过程。

ASP.NET 页生命周期

每次请求到达 web 服务器 ASP.NET 网页时,Web 服务器做的第一件事就是将请求移交给 ASP.NET 引擎。 然后,ASP.NET 引擎通过由多个阶段组成的管道接收请求,其中包括验证 ASP.NET 网页的文件访问权限、恢复用户的会话状态等。 在管道结束时,实例化与请求 ASP.NET 网页对应的类,并 ProcessRequest() 调用 方法, (见图 1) 。

单击此处查看大图。

图 1. ASP.NET 页处理

ASP.NET 页的此生命周期从调用 ProcessRequest() 方法开始。 此方法首先初始化页面的控件层次结构。 接下来,页面及其服务器控件继续执行各个阶段,这些阶段对于执行 ASP.NET 网页至关重要。 这些步骤包括管理视图状态、处理回发事件以及呈现页面的 HTML 标记。 图 2 提供了 ASP.NET 页生命周期的图形表示形式。 生命周期结束时,将网页的 HTML 标记移交给 Web 服务器,后者将其发送回请求该页的客户端。

注意 本文未讨论 ASP.NET 页生命周期前的步骤。 有关详细信息,请阅读 Michele Leroux-Bustamante 的 Inside IIS & ASP.NET。 若要更详细地了解 HTTP 处理程序(即 ASP.NET 管道的终结点),检查我之前关于 HTTP 处理程序的文章。

需要注意的是,每次请求 ASP.NET 网页 ,它都会经历相同的生命周期阶段, (如图 2) 所示。

ms972976.viewstate_fig02 (en-us,MSDN.10) .gif

图 2. 页面生命周期中的事件

阶段 0 - 实例化

ASP.NET 页的生命周期从类的实例化开始,该类表示所请求 ASP.NET 网页,但如何创建此类? 它存储在哪里?

如你所知,ASP.NET 网页由 HTML 部分和代码部分组成,HTML 部分包含 HTML 标记和 Web 控件语法。 ASP.NET 引擎将 HTML 部分从其自由格式文本表示形式转换为一系列以编程方式创建的 Web 控件。

当更改页面中的 HTML 标记或 Web 控件语法 .aspx 后首次访问 ASP.NET 网页时,ASP.NET 引擎会自动生成类。 如果使用代码隐藏技术创建了 ASP.NET 网页,则此自动生成的类派生自页面的关联代码隐藏类 (请注意,代码隐藏类必须直接或间接地从 System.Web.UI.Page 类) 派生;如果使用内联服务器端 <script> 块创建了页面,则类直接派生自 System.Web.UI.Page。 在任一情况下,此自动生成的类以及 类的已编译实例都存储在 WINDOWS``\Microsoft.NET\Framework\``version``\Temporary ASP.NET Files 文件夹中,因此无需为每个页面请求重新创建它。

此自动生成的类的目的是以编程方式创建页面的 控件层次结构。 也就是说, 类负责以编程方式创建页面 HTML 部分中指定的 Web 控件。 这是通过将 Web 控件语法<asp:``WebControlName Prop1="Value1" ...`` />(通常) )转换为类的编程语言 (C# 或 Microsoft® Visual Basic® .NET 来实现的。 除了要转换为相应代码的 Web 控件语法外,ASP.NET 网页的 HTML 部分中存在的 HTML 标记将转换为文本控件。

所有 ASP.NET 服务器控件都可以具有父控件,以及可变数量的子控件。 类 System.Web.UI.Page 派生自基控件类 (System.Web.UI.Control) ,因此也可以具有一组子控件。 在 ASP.NET 网页的 HTML 部分中声明的顶级控件是自动生成 Page 的类的直接子级。 Web 控件也可以嵌套在彼此内部。 例如,大多数 ASP.NET 网页都包含一个服务器端 Web 窗体,在 Web 窗体中具有多个 Web 控件。 Web 窗体是 () System.Web.UI.HtmlControls.HtmlForm 的 HTML 控件。 Web 窗体中的这些 Web 控件是 Web 窗体的子级。

由于服务器控件可以有子级,并且其每个子级都有子级,因此控件及其后代构成了控件树。 此控件树称为控件层次结构。 ASP.NET 网页的控件层次结构的根是 PageASP.NET 引擎自动生成的 派生类。

呼! 最后几段可能有点令人困惑,因为这不是最容易讨论或摘要的主题。 为了清除任何潜在的混淆,让我们看一个快速示例。 假设你有一个 ASP.NET 网页,其中包含以下 HTML 部分:

<html>
<body>
  <h1>Welcome to my Homepage!</h1>
  <form runat="server">
    What is your name?
    <asp:TextBox runat="server" ID="txtName"></asp:TextBox>
    <br />What is your gender?
    <asp:DropDownList runat="server" ID="ddlGender">
      <asp:ListItem Select="True" Value="M">Male</asp:ListItem>
      <asp:ListItem Value="F">Female</asp:ListItem>
      <asp:ListItem Value="U">Undecided</asp:ListItem>
    </asp:DropDownList>
    <br />
    <asp:Button runat="server" Text="Submit!"></asp:Button>
  </form>
</body>
</html>

首次访问此页面时,将自动生成一个类,其中包含用于以编程方式构建控件层次结构的代码。 图 3 中显示了此示例的控件层次结构。

ms972976.viewstate_fig03 (en-us,MSDN.10) .gif

图 3. 示例页的控件层次结构

然后,此控件层次结构将转换为类似于以下内容的代码:

Page.Controls.Add( 
  new LiteralControl(@"<html>\r\n<body>\r\n
    <h1>Welcome to my Homepage!</h1>\r\n"));
HtmlForm Form1 = new HtmlForm();
Form1.ID = "Form1";
Form1.Method = "post";
Form1.Controls.Add(
  new LiteralControl("\r\nWhat is your name?\r\n"));
TextBox TextBox1 = new TextBox();
TextBox1.ID = "txtName";
Form1.Controls.Add(TextBox1);
Form1.Controls.Add(
  new LiteralControl("\r\n<br />What is your gender?\r\n"));
DropDownList DropDownList1 = new DropDownList();
DropDownList1.ID = "ddlGender";
ListItem ListItem1 = new ListItem();
ListItem1.Selected = true;
ListItem1.Value = "M";
ListItem1.Text = "Male";
DropDownList1.Items.Add(ListItem1);
ListItem ListItem2 = new ListItem();
ListItem2.Value = "F";
ListItem2.Text = "Female";
DropDownList1.Items.Add(ListItem2);
ListItem ListItem3 = new ListItem();
ListItem3.Value = "U";
ListItem3.Text = "Undecided";
DropDownList1.Items.Add(ListItem3);
Form1.Controls.Add(
  new LiteralControl("\r\n<br /> \r\n"));
Button Button1 = new Button();
Button1.Text = "Submit!";
Form1.Controls.Add(Button1);
Form1.Controls.Add(
  new LiteralControl("\r\n</body>\r\n</html>"));
Controls.Add(Form1);

注意 上述 C# 源代码不是由 ASP.NET 引擎自动生成的精确代码。 相反,它是一个更简洁且更易于阅读自动生成的代码版本。 若要查看自动生成的完整代码(不会赢得任何可读性分数),请导航到 WINDOWS``\Microsoft.NET\Framework\``Version``\Temporary ASP.NET Files 文件夹并打开或 .vb 文件之.cs一。

需要注意的一点是,构造控件层次结构时,在 Web 控件的声明性语法中显式设置的属性在代码中分配。 (例如,Button Web 控件在声明性语法 Text="Submit!" 中以及自动生成的类Button1.Text = "Submit!";中将其 Text 属性设置为“Submit!”

阶段 1 - 初始化

生成控件层次结构后,将 Page及其控件层次结构中的所有控件一起进入初始化阶段。 此阶段的标志是让 Page 和 控件触发其 Init 事件。 此时,在页面生命周期中,已构造控件层次结构,并且已分配声明性语法中指定的 Web 控件属性。

本文稍后将更详细地介绍初始化阶段。 关于视图状态,这一点很重要,原因有两个:首先,服务器控件直到初始化阶段结束时才会开始跟踪视图状态更改。 其次,在添加需要利用视图状态的动态控件时,需要在 事件而不是 事件Load期间PageInit添加这些控件,我们稍后将看到。

阶段 2 - 加载视图状态

仅当页面已回发时,才会发生加载视图状态阶段。 在此阶段,从上一页访问中保存的视图状态数据将加载并递归填充到 的控件层次结构 Page。 在此阶段,将 验证视图状态。 如本文稍后所述,由于多种原因(例如视图状态篡改和将动态控件注入控件层次结构中间),视图状态可能会变得无效。

阶段 3 - 加载回发数据

加载回发数据阶段也仅在页面已回发时发生。 服务器控件可以指示它有兴趣通过实现 IPostBackDataHandler 接口来检查回发的数据。 在页面生命周期的此阶段中,Page 类枚举回发的窗体字段,并搜索相应的服务器控件。 如果找到控件,它会检查控件是否实现了 接口 IPostBackDataHandler 。 如果存在,则通过调用控件的 LoadPostData() 方法将相应的回发数据移交给服务器控件。 然后,服务器控件将基于此回发数据更新其状态。

为了帮助阐明问题,让我们看一个简单的示例。 ASP.NET 的一个好事是,Web 窗体中的 Web 控件在回发时会记住其值。 也就是说,如果页面上有一个 TextBox Web 控件,并且用户在 TextBox 中输入一些值并发回该页,则 TextBox 的 Text 属性会自动更新为用户输入的值。 发生这种情况是因为 TextBox Web 控件实现 IPostBackDataHandler 接口,而 Page 类将适当的值交给 TextBox 类,然后 TextBox 类会更新其 Text 属性。

若要具体化一些内容,假设我们有一个 ASP.NET 网页,其中 TextBox 的 ID 属性设置为 txtName。 首次访问页面时,将为 TextBox 呈现以下 HTML: <input type="text" id="txtName" name="txtName" />。 当用户在此 TextBox 中输入值时, (例如“Hello, World!”) 并提交表单,浏览器将向同一 ASP.NET 网页发出请求,并将表单字段值传回 HTTP POST 标头中。 其中包括隐藏的窗体字段值 ((如 __VIEWSTATE) )以及 TextBox 中的 txtName 值。

当 ASP.NET 网页在加载回发数据阶段发回时, Page 类会看到其中一个回发窗体字段对应于 接口 IPostBackDataHandler 。 层次结构中存在此类控件,因此调用 TextBox 的 LoadPostData() 方法,将用户输入的值传入 TextBox (“Hello, World!”) 。 TextBox 的 LoadPostData() 方法只是将此传入的值分配给其 Text 属性。

请注意,在关于加载回发数据阶段的讨论中,没有视图状态提及。 因此,你可能会自然而然地想知道,为什么我在有关视图状态的文章中提及加载回发数据阶段。 原因是要注意此阶段 中缺少 视图状态。 开发人员之间常见的误解是,视图状态在某种程度上负责让 TextBoxes、CheckBoxes、DropDownLists 和其他 Web 控件在回发时记住其值。 情况并非如此,因为值是通过回发的表单字段值标识的,并在 方法中 LoadPostData() 为实现 IPostBackDataHandler的控件分配。

阶段 4 - 加载

这是所有 ASP.NET 开发人员都熟悉的阶段,因为我们都为页面的事件 Load (Page_Load) 创建了事件处理程序。 当 Load 事件触发时,视图状态已从阶段 2 加载视图状态) (加载,以及从阶段 3 加载回发数据) (回发数据。 如果页面已发回,则 Load 当事件触发时,我们知道页面已从上一页访问还原到其状态。

阶段 5 - 引发回发事件

某些服务器控件会引发与回发之间发生的更改相关的事件。 例如,DropDownList Web 控件具有 一个 事件,如果 DropDownList 的 SelectedIndex 已从SelectedIndex上一SelectedIndexChanged页加载中的值更改,则会触发该事件。 另一个示例:如果 Web 窗体由于单击按钮 Web 控件而发回,则会在此阶段触发 Button 的 Click 事件。

回发事件有两种风格。 第一个是 已更改 事件。 当某个数据段在回发之间发生更改时,将触发此事件。 例如,DropDownLists SelectedIndexChanged 事件或 TextBox 的事件 TextChanged 。 提供已更改事件的服务器控件必须实现 IPostBackDataHandler 接口。 回发事件的另一种风格是 引发 的事件。 这些事件是由服务器控件引发的,无论控件认为适合什么原因。 例如,按钮 Web 控件在单击时引发 Click 事件,日历控件在用户移动到另一个月时引发 VisibleMonthChanged 事件。 触发引发事件的控件必须实现 IPostBackEventHandler 接口。

由于此阶段检查回发数据以确定是否需要引发任何事件,因此仅当页面已发回时才会发生该阶段。 与加载回发数据阶段一样,引发回发事件阶段根本不使用视图状态信息。 是否引发事件取决于在表单字段中发回的数据。

阶段 6 - 保存视图状态

在保存视图状态阶段, Page 类构造页面的视图状态,该状态表示必须跨回发保留的状态。 页面通过递归调用 SaveViewState() 其控件层次结构中控件的 方法来实现此目的。 然后将此组合保存的状态序列化为 base-64 编码的字符串。 在阶段 7 中,当呈现页面的 Web 窗体时,视图状态将作为隐藏的窗体字段保留在页面中。

阶段 7 - 呈现

在呈现阶段,将生成向请求页面的客户端发出的 HTML。 类 Page 通过递归调用其层次结构中 RenderControl() 每个控件的 方法来实现此目的。

这七个阶段是了解视图状态的最重要阶段。 (请注意,我确实省略了几个阶段,例如 PreRender 和 Unload 阶段。) 继续阅读本文时,请记住,每次请求 ASP.NET 网页时,都会经历这些阶段。

视图状态的角色

查看状态在生活中的目的很简单:它用于跨回发保持状态。 (对于 ASP.NET 网页,其状态是构成其控件层次结构的控件的属性值。) 这引出一个问题:“需要保留哪种状态?”为了回答这个问题,让我们首先看看不需要跨回发保留哪些状态。 回想一下,在页面生命周期的实例化阶段,将创建控件层次结构,并分配声明性语法中指定的那些属性。 由于构造控件层次结构时,这些声明性属性会在每次回发时自动重新分配,因此无需将这些属性值存储在视图状态中。

例如,假设 HTML 部分中有一个具有以下声明性语法的标签 Web 控件:

<asp:Label runat="server" Font-Name="Verdana" 
  Text="Hello, World!"></asp:Label>

在实例化阶段生成控件层次结构时,Label 的 Text 属性将设置为“Hello, World!”,其 Font 属性将 Name 设置为 Verdana。 由于将在实例化阶段期间每次访问页面设置这些属性,因此无需在视图状态中保留此信息。

需要存储在视图状态中的内容是对页面状态的任何编程 更改 。 例如,假设除了此标签 Web 控件之外,该页面还包含两个按钮 Web 控件:一个“更改消息”按钮和一个“空回发”按钮。 “更改消息按钮”有一个 Click 事件处理程序,该处理程序将 Label 的 Text 属性分配给“再见,每个人!”;空回发按钮只会导致回发,但不执行任何代码。 “更改消息按钮”中标签属性的 Text 更改需要保存在视图状态中。 若要了解如何以及何时进行此更改,让我们演练一个快速示例。 假设页面的 HTML 部分包含以下标记:

<asp:Label runat="server" ID="lblMessage" 
  Font-Name="Verdana" Text="Hello, World!"></asp:Label>
<br />
<asp:Button runat="server" 
  Text="Change Message" ID="btnSubmit"></asp:Button>
<br />
<asp:Button runat="server" Text="Empty Postback"></asp:Button>

代码隐藏类包含 Button 的 Click 事件的以下事件处理程序:

private void btnSubmit_Click(object sender, EventArgs e)
{
  lblMessage.Text = "Goodbye, Everyone!";
}

图 4 说明了发生的事件序列,突出显示了为何需要将 Label 属性 Text 的更改存储在视图状态中。

ms972976.viewstate_fig04 (en-us,MSDN.10) .gif

图 4。 事件和视图状态

若要了解在视图状态中保存 Label 的已更改 Text 属性为何至关重要,请考虑在视图状态中未保留此信息时会发生什么情况。 也就是说,假设在步骤 2 的保存视图状态阶段中,没有保留任何视图状态信息。 如果是这种情况,则在步骤 3 中,Label 的 Text 属性将在实例化阶段分配给“Hello, World!”,但在加载视图状态阶段不会重新分配给“Goodbye, Everyone!”。 因此,从最终用户的角度来看,标签的属性 Text 在步骤 2 中为“再见,每个人!”,但似乎会重置为其原始值 (“Hello, World!”单击“空回发”按钮后,在步骤 3 中) 。

视图状态和动态添加的控件

由于所有 ASP.NET 服务器控件都包含通过 Controls 属性公开的子控件集合,因此可以通过将新控件追加到服务器控件的 Controls 集合来动态地将控件添加到控件层次结构中。 对动态控件的深入讨论有点超出了本文的范围,因此我们不会在此处详细介绍该主题;相反,我们将重点介绍如何管理动态添加的控件的视图状态。 (有关使用动态控件的更详细课程,检查 ASP.NET 中的动态控件和使用动态创建的控件。)

回想一下,在页面生命周期中,将创建控件层次结构,并在实例化阶段设置声明性属性。 稍后,在加载视图状态阶段,还原之前的页面访问中已更改的状态。 考虑一下这一点,在使用动态控件时,有三件事变得清晰:

  1. 由于视图状态仅在回发中保留已更改的控件状态,而不是实际控件本身,因此,在初始访问和所有后续回发时,都必须将动态添加的控件添加到 ASP.NET 网页。
  2. 动态控件将添加到代码隐藏类中的控件层次结构中,因此在实例化阶段 之后 的某个时间点添加。
  3. 这些动态添加的控件的视图状态会自动保存在保存视图状态阶段。 (但是,如果在加载视图状态阶段滚动时尚未添加动态控件,则回发时会发生什么情况?)

因此,每次访问时,都必须以编程方式将动态添加的控件添加到网页。 添加这些控件的最佳时间是在页面生命周期的初始化阶段,这发生在加载视图状态阶段之前。 也就是说,我们希望在加载视图状态阶段到达之前完成控件层次结构。 因此,最好为代码隐藏类中的 Page 事件 Init 创建事件处理程序,并在其中添加动态控件。

注意 你可以摆脱在事件处理程序中 Page_Load 加载控件并正确维护视图状态。 这完全取决于你是否以编程方式设置动态加载的控件的任何属性,如果是这样,则何时相对于 Controls.Add(``dynamicControl``) 行进行设置。 对此的深入讨论有点超出了本文的范围,但它可能起作用 Controls 的原因是属性的 Add() 方法以递归方式将父级的视图状态加载到其子级中,即使加载视图状态阶段已过。

当根据某些条件将动态控件 c 添加到某个父控件 p 时, (即,当未在每个页面上加载它们访问) 时,需要确保将 c 添加到 pControls 集合的末尾。 这是因为 p 的视图状态也包含 p 的子级的视图状态,并且,正如我们将在“分析视图状态”部分中讨论的那样, p 的视图状态按索引指定其子级的视图状态。 (图 5 说明了在 Controls 集合末尾以外的位置插入动态控件如何导致视图状态损坏。)

ms972976.viewstate_fig05 (en-us,MSDN.10) .gif

图 5。 插入控件对视图状态的影响

ViewState 属性

每个控件负责存储自己的状态,这是通过将更改的状态添加到其 ViewState 属性来实现的。 属性 ViewState 在 类中 System.Web.UI.Control 定义,这意味着所有 ASP.NET 服务器控件都具有可用的此属性。 (一般情况下,在谈到视图状态时,我会使用小写字母,并在视图和状态之间有空格;在 ViewState 讨论 属性时,我将使用正确的大小写和代码格式的 text.)

如果检查任何 ASP.NET 服务器控件的简单属性,你将看到属性直接读取和写入视图状态。 (可以使用 Reflector.) 等工具查看 .NET 程序集的反编译源代码。例如,请考虑 HyperLink Web 控件的 NavigateUrl 属性。 此属性的代码如下所示:

public string NavigateUrl
{
  get
  {
    string text = (string) ViewState["NavigateUrl"];
    if (text != null)
       return text;
    else
       return string.Empty;
  }
  set
  {
    ViewState["NavigateUrl"] = value;
  }
}

如此代码示例所示,每当读取控件的 属性时,将查阅控件的 ViewState 。 如果 中 ViewState没有条目,则返回属性的默认值。 分配 属性时,分配的值将直接写入 。ViewState

注意 所有 Web 控件都对简单属性使用上述模式。 简单属性是标量值,如字符串、整数、布尔值等。 复杂属性(例如 Label 的 Font 属性(可能是类本身)使用不同的方法。 有关 ASP.NET 服务器控件的状态维护技术的详细信息,请参阅 开发 Microsoft ASP.NET 服务器控件和组件 一书。

ViewState 属性为 System.Web.UI.StateBag 类型。 类 StateBag 提供一种在后台使用 System.Collections.Specialized.HybridDictionary 来存储名称和值对的方法。 NavigateUrl如属性语法所示,可以使用用于从哈希表访问项的相同语法,将项添加到 并从中查询StateBag项。

对视图状态的跟踪计时

回想一下,我之前说过,视图状态只存储需要跨回发保留的状态。 不需要跨回发保留的一位状态是在声明性语法中指定的控件属性,因为它们会在页面的实例化阶段自动恢复。 例如,如果我们在 ASP.NET 网页上有一个 HyperLink Web 控件,并且以声明方式将 NavigateUrl 属性设置为 http://www.ScottOnWriting.NET ,则不需要将此信息存储在视图状态中。

但是,如果看到 HyperLink 控件的属性 NavigateUrl 代码,只要设置了属性值,它就好像将控件 ViewState 的 写入。 因此,在实例化阶段,我们将有类似 HyperLink1.NavigateUrl = http://www.ScottOnWriting.NET;的内容,只有将此信息存储在视图状态中才有意义。

不管看起来是什么,情况并非如此。 这是因为 类 StateBag 仅在调用其 方法 跟踪其 TrackViewState() 成员的更改。 也就是说,如果有 ,StateBag则调用 方法时SaveViewState(),不会保存之前TrackViewState()所做的任何和所有添加或修改。 在 TrackViewState() 初始化阶段结束时调用 方法,这在实例化阶段之后发生。 因此,在保存视图状态阶段的方法调用期间SaveViewState(),实例化阶段的初始属性赋值(写入ViewState属性集访问器中的 时)不会持久保存,因为TrackViewState()尚未调用该方法。

注意使用 TrackViewState() 方法的原因是StateBag尽可能使视图状态保持剪裁状态。 同样,我们不希望将初始属性值存储在视图状态中,因为它们不需要在回发中持久保存。 因此, TrackViewState() 方法允许在实例化和初始化阶段之后开始状态管理。

在页面的 ViewState 属性中存储信息

Page由于 类派生自 System.Web.UI.Control 类,因此它也具有 ViewState 属性。 事实上,可以使用此属性跨回发保留特定于页面和特定于用户的信息。 在 ASP.NET 网页的代码隐藏类中,要使用的语法很简单:

ViewState[keyName] = value

在 中存储信息PageViewState非常有用时,有许多方案。 规范示例是创建可分页、可排序的 DataGrid (或可排序、可编辑的 DataGrid) ,因为必须在回发之间保留排序表达式。 也就是说,如果首先对 DataGrid 的数据进行排序,然后进行分页,则在将数据的下一页绑定到 DataGrid 时,请务必在按用户指定的排序表达式对数据进行排序时获取数据的下一页。 因此,需要以某种方式保留排序表达式。 有各种各样的技术,但在我看来,最简单的是将排序表达式 Page存储在 的 ViewState中。

有关创建可排序的、可分页的 DataGrids (或可分页的、可排序的、可编辑的 DataGrid) 的详细信息,请 ASP.NET Data Web Controls Kick Start Start(数据 Web 控件启动开始)获取我的书籍副本。

视图状态的成本

没有任何内容是免费的,视图状态也不例外。 每当请求 ASP.NET 网页时,ASP.NET 视图状态会施加两个性能命中:

  1. 在所有页面访问中,在保存视图状态阶段期间, Page 类将收集其控件层次结构中所有控件的集体视图状态,并将状态序列化为 base-64 编码的字符串。 (这是在隐藏 __VIEWSTATE 窗体 filed 中发出的字符串。) 同样,在回发时,加载视图状态阶段需要反序列化持久化视图状态数据,并更新控件层次结构中的相关控件。
  2. 隐藏的 __VIEWSTATE 窗体域会为客户端必须下载的网页添加额外的大小。 对于某些视图状态繁重的页面,这可以是数十 KB 的数据,这可能需要多几秒钟 (或几分钟!) 调制解调器用户下载。 此外,在回发时, __VIEWSTATE 必须将表单字段发送回 HTTP POST 标头中的 Web 服务器,从而增加回发请求时间。

如果设计的网站通常由通过调制解调器连接访问的用户访问,则应特别注意视图状态可能会添加到页面的膨胀。 幸运的是,可以使用多种技术来减小视图状态大小。 我们将首先了解如何选择性地指示服务器控件是否应保存其视图状态。 如果控件的状态不需要跨回发保留,我们可以关闭该控件的视图状态跟踪,从而保存该控件本来会添加的额外字节。 接下来,我们将研究如何完全从页面的隐藏窗体字段中删除视图状态,改为将视图状态存储在 Web 服务器的文件系统上。

禁用视图状态

在 ASP.NET 页生命周期的保存视图状态阶段, Page 类以递归方式循环访问其控件层次结构中的控件,调用每个控件的 SaveViewState() 方法。 此集体状态是保留到隐藏 __VIEWSTATE 窗体字段的内容。 默认情况下,控件层次结构中的所有控件都会在调用其 方法时记录其 SaveViewState() 视图状态。 但是,作为页面开发人员,你可以通过将控件的 属性设置为 False 来指定控件不应保存其视图状态或其子控件的 EnableViewState 视图状态, (默认值为 True) 。

属性 EnableViewState 在 类中 System.Web.UI.Control 定义,因此 所有 服务器控件都具有此属性,包括 Page 类。 因此,可以通过将类的 设置为 Page False 来指示不需要保存整个页面的 EnableViewState 视图状态。 (这可以在代码隐藏类 Page.EnableViewState = false; 中使用 或作为 @Page-level 指令完成。<%@Page EnableViewState="False" %>)

并非所有 Web 控件在其视图状态中记录相同的信息量。 例如,标签 Web 控件仅记录对其属性的编程更改,这不会对视图状态的大小产生很大影响。 但是,DataGrid 将其所有内容存储在视图状态中。 对于包含许多列和行的 DataGrid,视图状态大小可以快速相加! 例如,图 6 中显示的 DataGrid (并包含在本文的代码下载中,) HeavyDataGrid.aspx 的视图状态大小约为 2.8 KB,总页面大小为 5,791 字节。 (页面大小的近一半是由于 __VIEWSTATE 隐藏的窗体字段!) 图 7 显示了视图状态的屏幕截图,可以通过访问 ASP.NET 网页、执行 View\Source,然后查找隐藏的窗体域来查看该 __VIEWSTATE 状态。

ms972976.viewstate_fig06 (en-us,MSDN.10) .gif

图 6。 DataGrid 控件

ms972976.viewstate_fig07 (en-us,MSDN.10) .gif

图 7。 DataGrid 控件的视图状态

本文的下载内容还包括名为 LightDataGrid.aspx的 ASP.NET 网页,该网页具有与图 6 中所示相同的 DataGrid,但 EnableViewState 属性设置为 False。 此页面的总视图状态大小? 96 字节。 整个页面大小时钟以 3,014 字节为单位。 LightDataGrid.aspx 视图状态大小约为 大小的 HeavyDataGrid.aspx1/30,总下载大小约为 的一 HeavyDataGrid.aspx半。 使用包含更多行的较宽 DataGrid 时,这种差异将更加明显。 (有关启用视图状态的 DataGrid 和视图状态禁用的 DataGrid 之间的性能比较的详细信息,请参阅 决定何时使用 DataGrid、DataList 或 Repeater.)

希望最后一段能让你相信将属性智能设置为 EnableViewState False 的好处,尤其是对于 DataGrid 等“重”视图状态控件。 现在的问题是,“何时可以安全地将 EnableViewState 属性设置为 False?”若要回答该问题,请考虑何时需要使用视图状态-仅在需要跨回发时记住状态。 DataGrid 将其内容存储在视图状态中,因此页面开发人员无需在每次加载页面时将数据库数据重新绑定到 DataGrid,而只需在第一个页面加载。 优点是不需要经常访问数据库。 但是,如果将 DataGrid 的 EnableViewState 属性设置为 False,则需要在第一页加载和每次后续回发时将数据库数据重新绑定到 DataGrid。

对于具有只读 DataGrid 的网页(如图 6 所示),你肯定要将 DataGrid 的 EnableViewState 属性设置为 False。 你甚至可以创建视图状态禁用的可排序和可分页的 DataGrids, (可以在页面( LightDataGrid-WithFeatures.aspx 包括在下载) 中)进行见证,但同样,你需要确保在第一次访问页面以及所有后续回发时将数据库数据绑定到 DataGrid。

注意 创建具有禁用视图状态的可编辑 DataGrid 需要一些复杂的编程,这涉及到分析可编辑 DataGrid 中回发的表单字段。 之所以需要付出这样的艰苦努力,是因为当可编辑的 DataGrid 盲目重新绑定时,DataGrid 的数据库数据将覆盖用户所做的任何更改, (请参阅 此常见问题解答 以获取) 的详细信息。

指定保存视图状态的位置

在保存视图状态阶段,页面收集其控件层次结构中所有控件的视图状态信息后,它会将其保存到隐藏的 __VIEWSTATE 窗体字段。 当然,此隐藏的窗体字段可以大大增加网页的整体大小。 在保存视图状态阶段,视图状态序列化为类方法SavePageStateToPersistenceMedium()中的Page隐藏窗体字段,并在加载视图状态阶段由 Page 类的 LoadPageStateFromPersistenceMedium() 方法反序列化。 只需一点点工作,就可以将视图状态持久保存到 Web 服务器的文件系统中,而不是作为一个隐藏的窗体字段向下缩放页面。 为此,我们需要创建一个派生自 Page 类的类,并重写 SavePageStateToPersistenceMedium()LoadPageStateFromPersistenceMedium() 方法。

注意 有一个名为 Flesk.ViewStateOptimizer 的第三方产品,它使用类似的技术减少了视图状态膨胀。

视图状态由 System.Web.UI.LosFormatter 类序列化和反序列化(LOS 表示有限的对象序列化),旨在有效地将某些类型的对象序列化为 base-64 编码字符串。 LosFormatter可以序列化可由 类序列化BinaryFormatter的任何类型的对象,但构建的目的是有效地序列化以下类型的对象:

  • 字符串
  • 整数
  • 布尔型
  • 数组
  • ArrayLists
  • Hashtables
  • Pairs
  • Triplets

注意PairTriplet 是在 命名空间中找到的System.Web.UI两个类,提供一个用于存储两个或三个对象的类。 类 Pair 具有 属性 FirstSecond 来访问其两个元素,而 Triplet 具有 FirstSecondThird 作为属性。

方法 SavePageStateToPersistenceMedium()Page 类中调用,并在页面控件层次结构的组合视图状态中传递。 重写此方法时,需要使用 LosFormatter() 将视图状态序列化为 base-64 编码字符串,然后将此字符串存储在 Web 服务器的文件系统上的文件中。 此方法存在两个main挑战:

  1. 提出可接受的文件命名方案。 由于页面的视图状态可能会根据用户与页面的交互而有所不同,因此存储的视图状态对于每个用户和每个页面都必须是唯一的。
  2. 不再需要视图状态文件时,从文件系统中删除它们。

为了解决第一个挑战,我们将根据用户的 SessionID 和页面的 URL 命名持久化视图状态文件。 此方法非常适合浏览器接受会话级 Cookie 的所有用户。 但是,不接受 Cookie 的用户会在每次访问页面时为其生成唯一的会话 ID,因此,这种命名技术对他们来说是行不通的。 在本文中,我只是演示使用 SessionID / URL 文件名方案,尽管它不适用于浏览器配置为不接受 Cookie 的用户。 此外,它不适用于 Web 场,除非所有服务器将视图状态文件存储到集中位置。

注意 一种解决方法是使用全局唯一标识符 (GUID) 作为持久化视图状态的文件名,并将此 GUID 保存在 ASP.NET 网页上的隐藏窗体字段中。 遗憾的是,与使用 SessionID / URL 方案相比,此方法需要花费相当多的精力,因为它涉及到将隐藏的窗体字段注入 Web 窗体。 因此,我将坚持为本文演示更简单的方法。

出现第二个挑战的原因是,每次用户访问其他页面时,都会创建一个新文件来保存该页面的视图状态。 随着时间的推移,这将导致成千上万个文件。 需要执行某种自动任务来定期清理早于特定日期的视图状态文件。 我把这个留作读者的练习。

为了将视图状态信息保存到文件,我们首先创建派生自 类的 Page 类。 然后,此派生类需要重写 SavePageStateToPersistenceMedium()LoadPageStateFromPersistenceMedium() 方法。 以下代码提供此类:

public class PersistViewStateToFileSystem : Page
{
   protected override void 
     SavePageStateToPersistenceMedium(object viewState)
   {
      // serialize the view state into a base-64 encoded string
      LosFormatter los = new LosFormatter();
      StringWriter writer = new StringWriter();
      los.Serialize(writer, viewState);
      // save the string to disk
      StreamWriter sw = File.CreateText(ViewStateFilePath);
      sw.Write(writer.ToString());
      sw.Close();
   }
   protected override object LoadPageStateFromPersistenceMedium()
   {
      // determine the file to access
      if (!File.Exists(ViewStateFilePath))
         return null;
      else
      {
         // open the file
         StreamReader sr = File.OpenText(ViewStateFilePath);
         string viewStateString = sr.ReadToEnd();
         sr.Close();
         // deserialize the string
         LosFormatter los = new LosFormatter();
         return los.Deserialize(viewStateString);
      }
   }
   public string ViewStateFilePath
   {
      get
      {
         string folderName = 
           Path.Combine(Request.PhysicalApplicationPath, 
           "PersistedViewState");
         string fileName = Session.SessionID + "-" + 
           Path.GetFileNameWithoutExtension(Request.Path).Replace("/", 
           "-") + ".vs";
         return Path.Combine(folderName, fileName);
      }
   }
}

类包含公共属性 ,该属性 ViewStateFilePath返回将存储特定视图状态信息的文件的物理路径。 此文件路径取决于用户的 SessionID 和所请求页面的 URL。

请注意, SavePageStateToPersistenceMedium() 方法接受输入 object 参数。 这是 object 从保存视图状态阶段生成的视图状态对象。 的工作 SavePageStateToPersistenceMedium() 是序列化此对象并以某种方式保留它。 方法的代码只是创建 对象的实例 LosFormatter 并调用其 Serialize() 方法,将传入的视图状态信息序列化为 StringWriter writer。 之后,如果指定的文件已存在,则 (创建或覆盖指定的文件) base-64 编码的序列化视图状态字符串的内容。

LoadPageStateFromPersistenceMedium() 加载视图状态阶段开始时调用 方法。 其工作是检索持久化视图状态,并反序列化回可传播到页面控件层次结构中的对象。 这是通过打开上次访问时存储持久化视图状态的同一文件,并通过 Deserialize() 中的 LosFormatter() 方法返回反序列化版本来实现的。

同样,此方法不适用于不接受 Cookie 的用户,但对于接受 Cookie 的用户,视图状态将完全保留在 Web 服务器的文件系统上,从而为整体页面大小增加 0 个字节!

注意 减少视图状态造成的膨胀的另一种方法是在 方法中 SavePageStateToPersistenceMedium() 压缩序列化的视图状态流,然后在 方法中 LoadPageStateFromPersistenceMedium() 将其解压缩回其原始形式。 Scott Galloway 有一 篇博客文章 ,其中他讨论了使用 #ziplib 库压缩视图状态的经验。

分析视图状态

呈现页面时,它会使用 LosFormatter 类将其视图状态序列化为 base-64 编码字符串,默认情况下 () 将其存储在隐藏的窗体字段中。 回发时,将检索隐藏的窗体字段并将其反序列化回视图状态的对象表示形式,然后使用该表示形式还原控件层次结构中控件的状态。 本文中已忽略的一个细节是,类的视图状态对象的结构 Page 到底是什么?

如前所述,的整个视图状态 Page 是控件层次结构中控件视图状态的总和。 换句话说,在控件层次结构中的任意点,该控件的视图状态表示该控件的视图状态及其所有子控件的视图状态。 Page由于 类构成控件层次结构的根,其视图状态表示整个控件层次结构的视图状态。

Page 包含在 SavePageViewState()页面生命周期的保存视图状态阶段调用的 。 方法 SavePageViewState() 首先创建包含以下三个项的 Triplet:

  1. 页面的哈希代码。 此哈希代码用于确保视图状态在回发之间未被篡改。 我们将在“视图状态和安全影响”部分详细介绍视图状态哈希。
  2. 的 控件层次结构的 Page集合视图状态。
  3. ArrayList控件层次结构中需要在生命周期的引发回发事件阶段由页面类显式调用的控件的 。

First中的 TripletThird 项相对简单;Second项是维护 控件层次结构的视图状态Page的位置。 项目 Second 由 Page 通过调用 SaveViewStateRecursive() 方法生成,该方法在 类中 System.Web.UI.Control 定义。 SaveViewStateRecursive() 通过返回包含以下信息的 Triplet 来保存控件及其后代的视图状态:

  1. 的 中ControlViewStateStageBag存在的状态。
  2. 整数 ArrayList 的 。 这会ArrayList维护具有非null视图状态的 子控件的索引Control
  3. ArrayList子控件的视图状态的 。 此ArrayList中的 i视图状态映射到 的 项中的 i项中的ArrayListTripletSecond子控件索引。

Control 计算视图状态,并 Triplet返回 。 的 SecondTriplet 项包含 的后代的 Control视图状态。 最终结果是,视图状态由多个 ArrayList内部TripletTriplet、s 内部、Triplet内部... (视图状态中的精确内容取决于层次结构中的控件。更复杂的控件可能会使用 Pairobject 数组将其自己的状态序列化为视图状态。不过,我们稍后将看到,视图状态由多个 Triplet组成,嵌套ArrayList得很深,就像控件层次结构一样深。)

以编程方式单步执行视图状态

只需一点点工作,我们就可以创建一个类,该类可以分析视图状态并显示其内容。 本文的下载内容包括一个名为 ViewStateParser 的类,该类提供此类功能。 此类包含一个 ParseViewState() 以递归方式逐步执行视图状态的方法。 它采用三个输入:

  1. 当前视图状态对象。
  2. 在视图状态递归中的深度级别。
  3. 要显示的文本标签。

最后两个输入参数仅用于显示目的。 如下所示的此方法的代码确定当前视图状态对象的类型,并通过对当前对象的每个成员以递归方式调用自身来相应地显示视图状态的内容。 (变量 twTextWriter 向其写入输出的实例。)

protected virtual void ParseViewStateGraph(
  object node, int depth, string label)
{
   tw.Write(System.Environment.NewLine);
   if (node == null)
   {
      tw.Write(String.Concat(Indent(depth), label, "NODE IS NULL"));
   } 
   else if (node is Triplet)
   {
      tw.Write(String.Concat(Indent(depth), label, "TRIPLET"));
      ParseViewStateGraph(
        ((Triplet) node).First, depth+1, "First: ");
      ParseViewStateGraph(
        ((Triplet) node).Second, depth+1, "Second: ");
      ParseViewStateGraph(
        ((Triplet) node).Third, depth+1, "Third: ");
   }
   else if (node is Pair)
   {
      tw.Write(String.Concat(Indent(depth), label, "PAIR"));
      ParseViewStateGraph(((Pair) node).First, depth+1, "First: ");
      ParseViewStateGraph(((Pair) node).Second, depth+1, "Second: ");
   }
   else if (node is ArrayList)
   {
      tw.Write(String.Concat(Indent(depth), label, "ARRAYLIST"));
      // display array values
      for (int i = 0; i < ((ArrayList) node).Count; i++)
         ParseViewStateGraph(
           ((ArrayList) node)[i], depth+1, String.Format("({0}) ", i));
   }
   else if (node.GetType().IsArray)
   {
      tw.Write(String.Concat(Indent(depth), label, "ARRAY "));
      tw.Write(String.Concat("(", node.GetType().ToString(), ")"));
IEnumerator e = ((Array) node).GetEnumerator();
      int count = 0;
      while (e.MoveNext())
         ParseViewStateGraph(
           e.Current, depth+1, String.Format("({0}) ", count++));
   }
   else if (node.GetType().IsPrimitive || node is string)
   {
      tw.Write(String.Concat(Indent(depth), label));
      tw.Write(node.ToString() + " (" + 
        node.GetType().ToString() + ")");
   }
   else
   {
      tw.Write(String.Concat(Indent(depth), label, "OTHER - "));
      tw.Write(node.GetType().ToString());
   }
}

如代码所示,ParseViewState()方法循环访问预期的类型-、TripletPairArrayList、数组和基元类型。 对于标量值(整数、字符串等),将显示类型和值;对于聚合类型(数组、配对 Triplet等),则通过递归调用 ParseViewState()来显示构成该类型的成员。

ViewStateParser可以从 ASP.NET 网页使用类 (查看ParseViewState.aspx演示) ,也可以直接从SavePageStateToPersistenceMedium()派生自 Page 类的类中的 方法访问, (查看ShowViewState类) 。 图 8 和图 9 显示了演示 ParseViewState.aspx 的运行情况。 如图 8 所示,用户会看到一个多行文本框,用户可以在其中粘贴某些网页中的隐藏 __VIEWSTATE 窗体字段。 图 9 显示了在 DataGrid 中显示文件系统信息的页面的已分析视图状态的代码片段。

ms972976.viewstate_fig08 (en-us,MSDN.10) .gif

图 8。 解码 ViewState

ms972976.viewstate_fig09 (en-us,MSDN.10) .gif

图 9. ViewState 解码

除了本文下载中提供的视图状态分析程序外, Paul Wilson 还在其网站上提供了 视图状态分析程序Fritz Onion 还有一个 视图状态解码器 WinForms 应用程序 ,可从其网站上的“资源”部分下载。

查看状态和安全影响

默认情况下,ASP.NET 网页的视图状态存储为 base-64 编码的字符串。 正如我们在上一节中看到的,可以轻松解码和分析此字符串,显示视图状态的内容以供所有人查看。 这引发了两个与安全相关的问题:

  1. 由于可以分析视图状态,如何阻止某人更改值、重新序列化它以及使用修改后的视图状态?
  2. 由于可以分析视图状态,这是否意味着我不能在视图状态 (放置任何敏感信息,例如密码、连接字符串等) ?

幸运的是, LosFormatter 类具有解决这两个问题的功能,我们将在接下来的两个部分中看到。 在深入研究这些问题的解决方案之前,请务必首先注意,视图状态应仅用于存储非敏感数据。 视图状态不包含代码,并且绝对不应用于放置连接字符串或密码等敏感信息。

防止视图状态被修改

尽管视图状态应仅存储页面上的 Web 控件的状态和其他非敏感数据,但如果恶意用户能够成功修改页面的视图状态,可能会让你头疼。 例如,假设你运行了一个电子商务网站,该网站使用 DataGrid 显示待售产品的列表及其成本。 除非将 DataGrid 的 EnableViewState 属性设置为 False,否则 DataGrid 的内容(商品的名称和价格)将保留在视图状态中。

恶意用户可以分析视图状态,修改价格,使其全部读取 $0.01,然后将视图状态反序列化回 base-64 编码字符串。 然后,他们可以发送电子邮件或发布链接,单击后,提交表单,将用户发送到产品列表页面,并在 HTTP POST 标头中传递更改后的视图状态。 页面将读取视图状态,并根据此视图状态显示 DataGrid 数据。 最终结果? 你会有很多客户认为他们只需要一分钱就能购买你的产品!

防止此类篡改的一种简单方式是使用计算机身份验证检查或 MAC。 计算机身份验证检查旨在确保计算机接收的数据与它传输出去的数据相同,即它未被篡改。 这正是我们想要对视图状态执行的操作。 使用 ASP.NET 视图状态时, LosFormatter 通过对要序列化的视图状态数据进行哈希处理并将此哈希追加到视图状态的末尾来执行 MAC。 (哈希是一种快速计算摘要,通常用于对称安全方案以确保消息完整性。) 发回网页时, LosFormatter 检查以确保追加的哈希与反序列化视图状态的哈希值匹配。 如果不匹配,则表示视图状态在路由中已更改。

默认情况下, LosFormatter 类应用 MAC。 但是,可以通过设置 Page 类的 EnableViewStateMac 属性来自定义 MAC 是否发生。 默认值为 True,指示 MAC 应发生;值为 False 表示不应。 可以通过指定应采用的哈希算法来进一步自定义 MAC。 在 machine.config 文件中,搜索 <machineKey> 元素的 validation 属性。 使用的默认哈希算法为 SHA1,但如果需要,可以将其更改为 MD5。 (有关 SHA1 的详细信息,请参阅 RFC 3174;有关 MD5 的详细信息,请阅读 RFC 1321.)

注意 使用 Server.Transfer() 时,你可能会发现你收到视图状态身份验证问题。 许多在线文章都提到,唯一的解决方法是将 设置为 EnableViewStateMac False。 虽然这肯定会解决问题,但它打开了一个安全漏洞。 有关详细信息(包括安全解决方法),请参阅 此知识库文章

加密视图状态

理想情况下,视图状态不应需要加密,因为它绝不应包含敏感信息。 但是,如果需要, LosFormatter 确实提供有限的加密支持。 LosFormatter仅允许单一类型的加密:三重 DES。 若要指示应加密视图状态,请将 <machineKey> 文件中元素的 validation 属性 machine.config 设置为 3DES

除了验证 attribute之外, <machineKey> 元素还包含 validationKeydecryptionKey 属性。 属性 validationKey 指定用于 MAC 的密钥; decryptionKey 指示三重 DES 加密中使用的密钥。 默认情况下,这些属性设置为值“AutoGenerate,IsolateApp”,该值唯一地自动生成服务器上每个 Web 应用程序的密钥。 此设置适用于单个 Web 服务器环境,但如果有 Web 场,则所有 Web 服务器必须使用相同的密钥进行 MAC 和/或加密和解密。 在这种情况下,需要在 Web 场中的服务器之间手动输入共享密钥。 有关此过程和 <machineKey> 一般元素的详细信息,请参阅 <machineKey> 技术文档 和 Susan Warren 的文章 从 ASP.NET ViewState 中取出一口

ViewStateUserKey 属性

Microsoft® ASP.NET 版本 1.1 添加了其他Page类属性。ViewStateUserKey 如果使用此属性,则必须在页面生命周期的初始化阶段分配一个字符串值, (Page_Init 事件处理程序) 。 属性的要点是将一些特定于用户的键分配给视图状态,例如用户名。 ( ViewStateUserKey如果提供)在 MAC 期间用作 哈希的盐

属性 ViewStateUserKey 可以防范的情况是,恶意用户访问页面,收集视图状态,然后诱使用户访问同一页面,传入其视图状态 (见图 10) 。 有关此属性及其应用程序的详细信息,请参阅 生成安全 ASP.NET 页和控件

ms972976.viewstate_fig10 (en-us,MSDN.10) .gif

图 10. 使用 ViewStateUserKey 防范攻击

结论

在本文中,我们研究了 ASP.NET 视图状态,不仅研究其用途,还研究了其功能。 为了最好地了解视图状态的工作原理,必须牢牢掌握 ASP.NET 页生命周期,其中包括加载和保存视图状态的阶段。 在有关页面生命周期的讨论中,我们发现某些阶段(例如加载回发数据和引发回发事件)与视图状态没有任何关系。

虽然视图状态使状态能够在回发中毫不费力地持久保存,但代价是页面膨胀。 由于视图状态数据持久保存到隐藏的窗体字段,因此视图状态可以轻松地将数十 KB 的数据添加到网页,从而增加网页的下载和上传时间。 若要减少视图状态施加的页面权重,可以通过将 属性设置为 EnableViewState False 来选择性地指示各种 Web 控件不记录其视图状态。 事实上,通过在 指令中将 属性设置为 EnableViewState false,可以关闭整个页面的 @Page 视图状态。 除了在页面级别或控件级别关闭视图状态外,还可以为视图状态指定备用后备存储,例如 Web 服务器的文件系统。

本文总结了有关视图状态的安全注意事项。 默认情况下,视图状态执行 MAC,以确保视图状态在回发之间未被篡改。 ASP.NET 1.1 提供 ViewStateUserKey 属性以添加额外的安全级别。 视图状态的数据也可以使用三重 DES 加密算法进行加密。

编程愉快!

咨询的工程

有许多很好的资源可用于详细了解 ASP.NET 视图状态。 Paul Wilson 提供了许多资源,例如 “视图状态:你想知道的一切”和 “页面视图状态分析器”。 Dino Esposito 于 2003 年 2 月为 MSDN 杂志 撰写了一篇题为 《ASP.NET 视图状态》的文章,其中讨论了在 Web 服务器的文件系统上存储视图状态的技术。 Susan Warren 撰写的《从 ASP.NET 视图状态中获取咬出》提供了对视图状态的良好高级概述,包括有关加密视图状态的讨论。 斯科特·加洛韦的博客也有一些关于处理 ASP.NET 视图状态的好文章。

特别感谢...

在将我的文章提交到 MSDN 编辑器之前,我让一些志愿者帮助校对文章,并提供有关文章内容、语法和方向的反馈。 本文评审过程的主要贡献者包括 詹姆斯·艾弗里伯纳德·范德·贝肯戴夫·唐纳森斯科特·埃尔金贾斯汀·洛夫尔。 如果你有兴趣加入不断增长的审阅者列表,请在 上给我添加一行 mitchell@4guysfromrolla.com

关于作者

Scott Mitchell 是五本书的作者,4GuysFromRolla.com 的创始人,在过去的五年里一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 可在 或通过其博客(可在 中找到)联系mitchell@4guysfromrolla.comhttp://ScottOnWriting.NET他。

© Microsoft Corporation. 保留所有权利。