您的位置:首页 > 编程语言 > ASP

ASP.NET 3.5核心编程学习笔记(36):页面的视图状态和控件状态

2011-04-25 13:55 741 查看
  ASP.NET页面提供了ViewState属性,使应用程序能在同一页面的两次连续请求间保存某些数据,并生成调用上下文。视图状态代表页面最近一次接受服务器处理以来的状态。这种状态会被保存(但不一定是在客户端),并在页面请求被处理时恢复。

  默认情况下,视图状态以添加到页面中的隐藏字段的形式来维护。因此,状态信息会与页面一起来回传输。视图状态中存储的信息只与页面和其中的某些子控件有关,浏览器不会以任何形式来使用它。

  视图状态优点:不需占用任何服务器资源,实现容易,使用方法。缺点:对浏览器无用,增加HTML代码中的冗余数据。

StateBag类

  StateBag类是视图状态幕后的类,能够管理ASP.NET页面和控件的信息,以便在同一页面实例的连接回发间保持其状态。该类很像字典,还实现了IStateManager接口。基类Page和Control通过ViewState属性暴露视图状态。因此,我们可以像使用字典一样来对StateBag类添加或移除数据项:

ViewState["FontSize"] = value;


  只有在为页面请求引发Init事件后,才能向视图状态中写入数据。在页面生命周期中,页面进入呈现模式前(在PreRender事件引发前)的任何阶段都能读取数据。

视图状态类的属性

  下表列出了StateBag类的所有属性:



  StateBag类中的元素由StateItem对象表示。向Item索引器属性设置某个值或调用Add方法时,StateItem类的实例会被隐式创建。添加到StateBeg中的数据项会被跟踪,直到视图状态在页面呈现之前被序列化。只有那些IsDirty属性为true的项会被序列化。

  下表列出了StateBeg类的方法:



  IsItemDirty方法是一种对指定的StateItem的IsDirty属性间接的调用方式。

  页面的视图状态是一种积累属性,其值取决于页面ViewState属性的内容和当前页面包容的所有控件的视图状态。

加密与安全

  存储视图状态所用的隐藏字段名称为__VIEWSTATE,它在浏览器端可以被自由访问。在默认情况下,视图状态信息是散列的值,以Base64的方式进行编码。为在客户端解码,攻击者必须完成许多步骤,但还是有望破解。不过,一旦加密视图状态信息被破解,只是暴露其内容。攻击者无法修改视图状态来投递恶意数据,篡改后的视图状态会在服务端被检测到,并抛出异常。

  出于对性能的考虑,可以不加密视图状态。

  在web.config文件中做以下配置可使用3DES加密算法,而不对其内容做散列处理:

<machineKey validation="3DES" />


计算机身份验证检查

  @Page指令包含名为EnableViewStateMac的属性,以检查原始数据是否被篡改,加强视图状态的安全性。若该属性为true,那么在视图状态序列化时,系统会基于配置文件<machineKey>中定义的算法和密钥生成一个验证散列字符串,并将其追加到视图状态中。最终的字节数组(StateBag的二进制序列化结果加上散列值)会按Base64方式编码。默认情况下,生成散列值的加密算法为SHA1,加密密钥与解密密钥是自动生成的,存储在Web服务器的本地安全制授权机构(Local Security Authority,LSA)子系统中。LSA是Windows NT、Windows 2000、Windows Server 2003和Windows XP的受保护组件,提供安全服务,维护着系统中有关本地安全各方面的信息。

  如果EnableViewStateMac为true,在页面被回发时,散列值会被抽取,以便校验返回的视图状态是否在客户端被篡改。如果被篡改,则抛出异常。最终的效果是,视图状态的内容可以被读取,但只有获得加密密钥才能修改它,而该密钥存储在Web服务器的LSA中。默认情况下,EnableViewStateMac的值为true。如果该属性值为false,攻击者可能在客户端更改视图状态信息,并将修改后的版本发往服务器,而ASP.NET会毫不知情的使用这些被篡改的数据。

  为加强视图状态的安全性,ASP.NET 1.1在Page类中添加了ViewStateUserKey属性。可以为该属性设置为用户指定的字符串(一般为会话ID),该值位于服务器端,很难在客户端猜测到。为生成MAC代码,ASP.NET会将该属性的内容作为散列加密算法的输入参数。

页面大小的阈值与页面的呑吐量

  视图状态的体积可能会相当大(以KB来计算),这会增加上传和下载的负担,最终对应用程序整体造成严重的额外开销。

  ASP.NET页面多大才算合理?针对页面的视图状态又应该多大才合适?

  视图状态的理想大小约为7KB,但如果将其保持在3KB以下,则最理想。视图状态的绝对大小,不论在任何情况下,都不应超过页面整体大小的30%。

  如果能够禁用视图状态,那就禁用它。我们至少要避免存储那么不经常更改的、无关紧要的以及更适合缓存到服务器上的元素。

无视图状态的Web窗体编程

  默认情况下,所有服务器控件的视图状态都是启用的。但这并不意味着所有控件在任何情况下都需要视图状态。

视图状态的禁用

  把@Page指令的EnableViewState属性设为false,我们可以禁用整个页面的视图状态。对那些不需要回发,也不需要维护视图状态的只读页面,才可考虑禁用整个页面的视图状态。

<%@ Page EnableViewState="false" %>


  更好的办法是只禁用页面中某些控件的视图状态。可以将个别控件的EnableViewState属性设置为false:

<asp:datagrid runat="server" EnableViewState="false">
...
</asp:datagrid>


  在页面的开发阶段,我们可通过启用页面跟踪来观察控件视图状态的大小。跟踪程序不仅会显示整个页面视图状态的大小,而且会针对每个控件逐一列出。

  DataGrid控件不在视图状态中存储其数据源,但绑定到该控件的数据源仍会影响视图状态的大小。整个数据绑定控件中每个子控件的用户界面都会在视图状态中存储其设置,而子控件(如DataGrid中的行和列)的数目显然与数据源有关。

何时需要禁用视图状态

  我们简单地回顾一下视图状态的作用,看看禁用它之后会造成哪些功能的缺失?

  视图状态代表当前页面及其子控件在页面生成HTML时具有的状态。该状态信息会被序列化到隐藏字段,并下载到客户端。当页面回发时,视图状态(作为一种页面请求的调用上下文)会从隐藏字段中恢复,反序列化,并用于对服务器控件和页面本身进行初始化。这是视图状态的前半部分工作。

  加载视图状态后,页面会读取客户端投递的数据,并使用那些值重写服务器控件的大多数设置。而应用投递的值,也会重写视图状态中读取的某些设置。这样便不难理解视图状态造成额外负担的原因了,但只针对由投递的值修改的属性。

  让我们考虑一个典型的例子,假设页面包含一个文本框服务器控件。我们所期望的是,当页面回发时,文本框服务器控件会被自动填充客户端设置的值。如果仅为满足最常见的需求,我们不需要视图状态。示例:

<%@ Page language="C#" %>
<form runat="server">
<asp:textbox runat="server" enableviewstate="false"
id="theInput" readonly="false" text="Type here" />
<asp:checkbox runat="server" enableviewstate="false"
id="theCheck" text="Check me" />
<asp:button runat="server" text="Click" onclick="OnPost" />
</form>


  上例中,即使有两个控件的视图状态被禁用,页面的行为仍是有状态的。因此,TextBox和CheckBox控件的关键属性Text和Checked在回发页面后,会根据用户的设置从回发数据中更新。因此,如果我们只关心这些属性,则根本没必要使用视图状态。

  然而,如果我们为TextBox添加TextChanged事件处理程序,为CheckBox添加CheckedChanged事件处理程序,结果又会怎样呢?

//页面代码
<form id="form1" runat="server">
<div>
<asp:TextBox ID="textBox" runat="server"
Text="type here" ontextchanged="textBox_TextChanged" /><br />
<asp:CheckBox  ID="checkBox" runat="server"
Text="Check me" oncheckedchanged="checkBox_CheckedChanged" /><br />
<asp:Button EnableViewState="false" runat="server" ID="button"  Text="Click me" /><br />
<asp:Literal EnableViewState="false" runat="server" ID="literal" />
</div>
</form>

//后台处理代码
protected void textBox_TextChanged(object sender, EventArgs e)
{
literal.Text += "TextBox被更改<br/>";
}
protected void checkBox_CheckedChanged(object sender, EventArgs e)
{
literal.Text += "CheckBox被更改<br/>";
}


  在上例中,我们会发现,如果TextBox回发的文本值为页面代码中指定的原始值“type here”的话,则不会触发TextChanged事件;同理,如果CheckBox回发的Checked属性值为页面代码中指定的原始值false时,也不会触发CheckedChanged事件。

  但如果我们打开控件的视图状态的话,就会发现TextChanged事件的触发与页面代码中文本的原始值并无多大关系,只要在页面生成后发往客户端与客户端回发的间隙,客户端的文本被更改了,就会触发TextChanged事件;CheckedChanged事件亦是如此。

  为什么会有这样的差别呢?仔细想想,其实不难理解:视图状态被禁用后,服务器端唯有根据页面代码中TextBox控件文本原始值与当前回发的文本值来判断文本是否被更改;而启用视图状态后,服务器可以从视图状态中读出上一次页面中TextBox控件的文本值,所以能根据上一次的文本值与当前回发的文本值判断文本是否被更改。这就是启用与禁用视图状态间,TextChanged事件不同表现的原因了。

  此外,对于在页面中设置的且不会在会话期间发生更改的所有控件属性,我们也不需要视图状态。让我们看看下面的代码:

<asp:textbox runat="server" id="textBox" Text="Some Text"
MaxLength="20" ReadOnly="true" />


  如果ReadOnly和MaxLength这两个属性会在页面的生存期中被更改,则需要视图状态保持这些属性的最新状态。如果这两个属性在页面的生存期内是恒定的,我们也不必将它们保存到视图状态中。

  那么,什么时候才真正需要视图状态呢?

  只有需要在页面生存期内“更新”的从属性(而不是那些会被投递的值更改的属性)才有必要保存到视图状态中。“更新”指的是对原始值(要么是默认的值,要么是在设计时赋予的该属性的值)的更新。让我们看下例代码:

<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
theInput.ReadOnly = true;
}
</script>

<form id="form1" runat="server">
<div>
<asp:TextBox runat="server" ID="theInput" EnableViewState="false" Text="Am I read-only?" />
<asp:Button ID="Button1" runat="server" Text="Click" />
</div>
</form>


  当页面首次加载时,该文本框是只读的。随后,用户单击按钮,执行回发。如果视图状态是启用的,页面会按预期工作,文本仍是只读的。如果禁用该文本框的视图状态,那么ReadOnly属性的原始设置会被恢复(这里为false)。

  一般来讲,只要状态可以客户端或运行时环境推断出,则不必使用视图状态。相反,如果状态信息不能动态推出,不能确保所有属性在页面回发后都能正确恢复,那么禁用视图很难实现相应功能。

  为增加代码的灵活性,我们可以动态的设置控件或页面的EnableViewState属性,这样就实现了动态启用或禁用视图状态。

ASP.NET视图状态的新变化

  从ASP.NET 2.0开始,视图状态的实现发生了两个重要变化。

  1. 由于采用了新的序列化格式,视图状态的大小被显著精简。

  2. 视图状态的内容被分为两种状态:传统的视图状态、控件状态。控件状态不能以编程方式禁用,可将其看作控件私有的视图状态。对于构建第三方ASP.NET控件的开发者,该功能十分理想,因为该特性使重要的持久性属性不受最终页面开发者的影响。

  保存到视图状态的内容是对象图序列化过程得到的最终结果。对象图(object graph)是页面中每个对象视图状态按层次串连的结果。保存到视图状态的数据会在一个由特殊容器构成的数组中缓存。最终的流会被散列化处理(或基于配置文件的设置进行加密),按Base64编码,之后保存到隐藏字段中。

控件状态

  在ASP.NET 2.0和更高版本中,控件状态是一种专用的数据容器,它在传统视图状态内创建一种受保护的区域。使用控件状态比视图状态更安全,因为应用程序级和页面级的设置都不会影响它。如果现有的自定义控件需要将私有或受保护的属性存入视图状态,那应将它们都转移到控件状态中。存储在控件状态中的数据会被一直保留,直到显式地将其清除。控件状态会被发送到客户端,并在页面回发时上传。在其中添加的数据越多,需要在浏览器与服务器间来回传输的数据也就越多。所以,我们应该谨慎使用控件状态。

控件状态的编程

  我们需要自行实现控件状态的序列化和反序列化。控件状态与视图状态一同处理,经历同样的序列化和Base64编码过程。控件状态所处的隐藏字段与视图状态相同。序列化到视图状态流的根对象为Pair对象,该对象将控件状态作为第一个成员,将传统的视图状态作为第二个成员。

  .NET中没有现成的字典对象来保存控件状态的数据项。我们不必将对象保存在StateBag这样的固定容器中,我们可以在控件私有或受保护的成员中维护与状态有关的数据。由于这样更直接,无需字典对象,所以数据的访问速度更快。例如,如果要跟踪GridView控件的排序方向和当前分页总数,我们可以定义下面这样的变量来实现:

private int _sortDirection;private int _pageCount;


  定义该变量后,我们可以通过重载Page类的SaveControlState方法将变量保存到控件状态中。同时,通过重载Page类的LoadControlState方法将控件状态中的数据还原给变量:

protected override object SaveControlState()
{
//定义一个数组存储控件状态
object[] stateToSave = new object[2];

//将要保存的变量存入到数组中
stateToSave[0] = _sortDirection;
stateToSave[1] = _pageCount;

return stateToSave;
}

protected override void LoadControlState(object savedState)
{
//从控件状态中获取数组
object[] currentState = (object[])savedState;
if (currentState == null)
return;

//将控件状态中保存的数据还原给对应变量
_sortDirection = currentState[0];
_pageCount = currentState[1];
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: