第6章 庖丁解牛系列—页面状态机制(视图状态和控件状态)
本章内容
6.1 页面状态概述
6.2 视图状态机制
6.3 控件状态机制
6.4 视图状态和控件状态的关系
6.5 加密页面状态
6.6 清除页面状态
6.7 对动态添加控件的视图状态分析
6.8 自定义类型转换器实现高效率序列化
6.9 页面状态性能优化策略
6.10 视图状态和控件状态的总结
6.1 页面状态概述
在ASP.NET技术的服务器处理机制中,服务器每处理完客户端的一个请求就认为任务结束,当客户端再次请求时,服务器会将其作为一次新的请求处理,即使是相同的客户端也是如此。也就是说服务器不会保存两次请求之间的一些前后相接的数据,这对开发人员经常实现一个前后衔接的操作来说就比较麻烦了,比如输入一些信息到一个文本中,然后提交一个按钮,很多时候我们要在按钮提交的服务端事件中处理提交之前的数据和提交按钮时用户输入的最新数据,即想同时得到文本框的旧值和新值,但是服务端不会保存前一个请求的任何信息,那怎么才能做到这一点呢?
两次页面请求之间的数据关联性,ASP.NET是通过视图机制实现的,简单地讲,视图区域信息(ViewState)存储于页面上的一个隐藏字段(名为__VIEWSTATE,只是视图状态中的值经过哈希计算和压缩,并且针对Unicode实现进行编码,其安全性要高于我们自己设置的隐藏域控件),每次需要视图机制保存的一些信息都存储在此字段中,每次提交时,它都会以“客户端到ó服务端”的形式来回传递一次,当处理完成后,最后会以处理后的新结果作为新的ViewState存储到页面中的隐藏字段,并与页面内容一起返回到客户端。
视图机制支持很多类型的数据存储,其中基本类型的有字符串、数字、布尔值、颜色、日期、字节,各种类型的数组等。视图机制已经对一些如ArrayList和哈希表集合等类型对象进行了优化;除了基本类型视图状态视图机制还支持自定义的类型,由于ViewState数据是作为序列化格式串存储的,因此默认情况下使用.NET Framework提供的二进制序列化功能来序列化对象,对于一些比较复杂的对象,一般都使用专门的类型转换器TypeConvert序列化,要比默认.NET提供的二进制序列化节省资源。关于TypeConvert类的实现在第4章已经讲了很多例子了,在后面会介绍类型转换器应用于视图状态的说明和示例。
为了提高性能,通常禁用页面或禁用服务端控件的状态视图,有些控件不需要维护其状态,如Label控件只是显示文本,而标签的文本,值不参与回发,可以设置其属性:EnableViewState=false;
如果整个页面控件都不需要维持状态视图,则可以设置整个页面的状态视图为false:<%@ Page EnableViewState="false"%>。
由于控件内部使用的视图状态,这样会导致视图状态失效,甚至会产生致命的问题-控件无法使用。说明一点,禁用视图是合法的,一个好的控件应该允许视图状态在适当情况下被开发人员禁用,并且仍然能够正确运行。
为了解决这个问题,ASP.NET 2.0开始支持控件状态机制。控件的状态数据现在能通过控件状态而不是视图状态被保持,控件状态是不能够被禁用的。如果控件中需要保存控件之间的逻辑,比如选项卡控件要记住每次回发时当前已经选中的索引SelectIndex时,就适合使用控件状态。当然ViewState属性完全可以满足此需求,如果视图状态被禁用的话,自定义控件就不能正确运行。控件状态的工作方式与视图状态完全一致,并且默认情况下在页面中它们都是存储在同一个隐藏域中。
总结一下,一般开发人员主要通过以下三种方式使用ASP.NET视图:
1.使用基类提供的ViewState对象
直接访问基类Control中的ViewState对象,类型为StateBag,以键/值对的形式存储数据
2.自定义类型视图状态。
重写控件的默认方法(SaveViewState,LoadViewState),实现自定义类型的视图状态。一般需要与属性对应类类型的视图状态配合使用,类类型视图状态可能通过实现IStateManager接口的几个成员(方法和属性)实现。
3.控件状态
它也提供了可重写的方法(SaveControlState,LoadControlState),实现控件中属性的控件状态。
视图状态数据在每次请求过程中都要在客户端和服务端来回传递,因此在开发过程中要确保数据量不要太大,否则会出现网络传输瓶颈。
从下节开始,详细讲解页面状态(视图状态和控件状态)的内部机制,以及它们在自定义控件中的应用。
6.2 视图状态机制
6.2.1 IStateManager接口
.NET框架为自定义视图状态管理提供了System.Web.UI.IStateManager接口,定义了任何类为支持服务器控件的视图状态管理而必须实现的属性和方法,服务器控件的视图状态由控件属性的累计值组成。该接口包括保存并加载服务器控件的视图状态值的方法,以及一个指示控件跟踪其视图状态的更改的方法。此接口的成员与Control类中的对应方法具有相同的语义。
若要自定义ASP.NET应用程序管理服务器控件视图状态的方式,必须创建一个实现此接口的类。代码如下:
- /// <summary>
- /// 获得本书更多内容,请看:
- /// http://blog.youkuaiyun.com/ChengKing/archive/2008/08/18/2792440.aspx
- /// </summary>
- public interface IStateManager
- {
- // Methods
- object SaveViewState();
- void LoadViewState(object state);
- void TrackViewState();
- // Properties
- bool IsTrackingViewState { get; }
- }
该接口包括以下几个成员:
Ø SaveViewState:保存自从页回发到服务器后发生的所有服务器控件视图状态更改,最后返回最新更改后的视图状态对象。如果没有与控件关联的视图状态,则此方法返回空。保存了视图状态后,页面类会把所有控件的视图状态对象转换为可以通过网络传输的Base64格式字符串形式,最终该字符串对象作为存储在Hidden元素中的变量返回给客户端。使用自定义视图状态时,一般使用SaveViewState和LoadViewState组合完成状态管理。
Ø LoadViewState:把SaveViewState方法保存的上一个页面的视图信息还原到控件复杂属性中。
Ø TrackViewState:在服务器控件的生存期内,将在Init事件结束时自动调用该方法。在开发模板数据绑定控件时调用此方法。此方法提醒ASP.NET监视服务器控件视图状态的更改。如果控件没调用TrackViewState()方法,则本次对控件属性的修改将不会被添加到__VIEWSTATE隐藏域中,下次页面回发时,控件的属性只恢复为之前的旧值。从性能角度讲,为了减少在网络上的传输量,应该只保存“变化”的数据到视图状态中,即仅对需要保存到视图中的数据才调用此方法。其实TrackViewState只是控制一个布尔值作标记,往视图中增加数据时,会判断该值是否为true,如果为true才将其加入视图数据。下节讲解StateBag类时还会说明其内部原理。
Ø IsTrackingViewState:返回当前控件视图是否被ASP.NET框架监视(是否存储该属性到视图中,与TrackViewState方法控制的是同一个标记)。
或许读者会想到,之前在开发控件时使用过视图存储属性值,如ViewState["Text"],而没有使用IStateManager接口控件为什么这样也能够正确保存值呢?在后面的6.2.3小节会说明其原因,事实上它也是使用了IStateManger接口,只是Control提供了更方便的管理而已。
6.2.2 控件生命周期中的装载和保存视图阶段
在第1章中讲过控件周期阶段,其中就包括视图状态的阶段,如图6-1所示。
从图6-1中可以看到LoadViewState和SaveViewState分别在控件生命周期的开始(初始化Init后)和最后(呈现Render之前)。这样我们可以在其间的一些周期阶段操作视图状态数据。而在控件的基类Control中已经提供了对这两个方法的支持。
图6-1 控件生命周期中的视图装载和保存阶段
对于自定义的类型,仅实现IStateManager接口的方法是不够的(该方法仅使自定义类具有正反序列化的能力),还需要由主控件的控件生命周期方法来引发调用它们,才能够正确地装载和保存视图数据。这就要求主控件直接或间接继承Control类,并重载Control类中的LoadViewState和SaveViewState方法,这两个方法属于控件生命周期阶段方法,只要是属于控件生命周期的方法,则在控件生成阶段一定会被页框架调用。它们才是视图状态启动的导火线。
重载这两个方法如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.youkuaiyun.com/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class ViewStatePeriod : WebControl
{
protected override object SaveViewState()
{
//… …
}
protected override void LoadViewState(object savedState)
{
//… …
}
}
u 另外,视图状态监视是在初始化Init阶段完成后启动的,之后就可以监控对视图的操作了。少数情况下,如果在视图状态打开之前想操作视图对象,则要手动启用跟踪:
if (this.IsTrackingViewState == false)
{
this.TrackViewState();
... ...//操作视图
}
6.2.3 简单类型视图状态应用
视图状态默认支持很多类型的数据存储,其中基本类型的有字符串、数字、布尔值、颜色、日期、字节,以及各种类型的数组等。以下是一个最常见的典型用法:
public string Text
{
get
{
String s = (String)ViewState["Text"];
return ((s == null) ? String.Empty : s);
}
set
{
ViewState["Text"] = value;
}
}
u 在上面代码中有个ViewState的对象,此对象没有多么深奥,只是基类Control中定义的一个属性。追溯到它的基类定义,代码如下:
private StateBag _viewState;
[WebSysDescription("Control_State"), Browsable(false), Designer Serializa
tionVisibility(DesignerSerializationVisibility.Hidden)]
protected virtual StateBag ViewState
{
get
{
if (this._viewState == null)
{
this._viewState = new StateBag(this.ViewStateIgnoresCase);
if (this.IsTrackingViewState)
{
this._viewState.TrackViewState();
}
}
return this._viewState;
}
}
u 这是一个标准的自定义类型属性。再仔细看一下,该属性的类型为StateBage类,这才是我们要找的关键类,它的代码结构如下:
- /// <summary>
- /// 获得本书更多内容,请看:
- /// http://blog.youkuaiyun.com/ChengKing/archive/2008/08/18/2792440.aspx
- /// </summary>
- public sealed class StateBag : IStateManager, IDictionary, ICollection, IEnumerable
- {
- // Fields
- private IDictionary bag;
- private bool ignoreCase;
- private bool marked;
- // Methods
- public StateBag()
- : this(false)
- {
- }
- public StateBag(bool ignoreCase)
- {
- this.marked = false;
- this.ignoreCase = ignoreCase;
- this.bag = this.CreateBag();
- }
- public StateItem Add(string key, object value)
- {
- if (string.IsNullOrEmpty(key))
- {
- throw ExceptionUtil.ParameterNullOrEmpty("key");
- }
- StateItem item = this.bag[key] as StateItem;
- if (item == null)
- {
- if ((value != null) || this.marked)
- {
- item = new StateItem(value);
- this.bag.Add(key, item);
- }
- }
- else if ((value == null) && !this.marked)
- {
- this.bag.Remove(key);
- }
- else
- {
- item.Value = value;
- }
- if ((item != null) && this.marked)
- {
- item.IsDirty = true;
- }
- return item;
- }
- public void Clear()
- {
- this.bag.Clear();
- }
- private IDictionary CreateBag()
- {
- return new HybridDictionary(this.ignoreCase);
- }
- public IDictionaryEnumerator GetEnumerator()
- {
- return this.bag.GetEnumerator();
- }
- public bool IsItemDirty(string key)
- {
- StateItem item = this.bag[key] as StateItem;
- return ((item != null) && item.IsDirty);
- }
- internal void LoadViewState(object state)
- {
- if (state != null)
- {
- ArrayList list = (ArrayList)state;
- for (int i = 0; i < list.Count; i += 2)
- {
- string key = ((IndexedString)list[i]).Value;
- object obj2 = list[i + 1];
- this.Add(key, obj2);
- }
- }
- }
- public void Remove(string key)
- {
- this.bag.Remove(key);
- }
- internal object SaveViewState()
- {
- ArrayList list = null;
- if (this.bag.Count != 0)
- {
- IDictionaryEnumerator enumerator = this.bag.GetEnumerator();
- while (enumerator.MoveNext())
- {
- StateItem item = (StateItem)enumerator.Value;
- if (item.IsDirty)
- {
- if (list == null)
- {
- list = new ArrayList();
- }
- list.Add(new IndexedString((string)enumerator.Key));
- list.Add(item.Value);
- }
- }
- }
- return list;
- }
- public void SetDirty(bool dirty)
- {
- if (this.bag.Count != 0)
- {
- foreach (StateItem item in this.bag.Values)
- {
- item.IsDirty = dirty;
- }
- }
- }
- public void SetItemDirty(string key, bool dirty)
- {
- StateItem item = this.bag[key] as StateItem;
- if (item != null)
- {
- item.IsDirty = dirty;
- }
- }
- void ICollection.CopyTo(Array array, int index)
- {
- this.Values.CopyTo(array, index);
- }
- void IDictionary.Add(object key, object value)
- {
- this.Add((string)key, value);
- }
- bool IDictionary.Contains(object key)
- {
- return this.bag.Contains((string)key);
- }
- void IDictionary.Remove(object key)
- {
- this.Remove((string)key);
- }
- IEnumerator IEnumerable.GetEnumerator()
- {
- return this.GetEnumerator();
- }
- void IStateManager.LoadViewState(object state)
- {
- this.LoadViewState(state);
- }
- object IStateManager.SaveViewState()
- {
- return this.SaveViewState();
- }
- void IStateManager.TrackViewState()
- {
- this.TrackViewState();
- }
- internal void TrackViewState()
- {
- this.marked = true;
- }
- // Properties
- public int Count
- {
- get
- {
- return this.bag.Count;
- }
- }
- internal bool IsTrackingViewState
- {
- get
- {
- return this.marked;
- }
- }
- public object this[string key]
- {
- get
- {
- if (string.IsNullOrEmpty(key))
- {
- throw ExceptionUtil.ParameterNullOrEmpty("key");
- }
- StateItem item = this.bag[key] as StateItem;
- if (item != null)
- {
- return item.Value;
- }
- return null;
- }
- set
- {
- this.Add(key, value);
- }
- }
- public ICollection Keys
- {
- get
- {
- return this.bag.Keys;
- }
- }
- bool ICollection.IsSynchronized
- {
- get
- {
- return false;
- }
- }
- object ICollection.SyncRoot
- {
- get
- {
- return this;
- }
- }
- bool IDictionary.IsFixedSize
- {
- get
- {
- return false;
- }
- }
- bool IDictionary.IsReadOnly
- {
- get
- {
- return false;
- }
- }
- object IDictionary.this[object key]
- {
- get
- {
- return this[(string)key];
- }
- set
- {
- this[(string)key] = value;
- }
- }
- bool IStateManager.IsTrackingViewState
- {
- get
- {
- return this.IsTrackingViewState;
- }
- }
- public ICollection Values
- {
- get
- {
- return this.bag.Values;
- }
- }
- }
该类继承了四个接口:IStateManager,IDictionary,ICollection,IEnumerable。IStateManager即.NET Framework为自定义视图状态管理提供的接口,到这里您应该明白我们直接使用ViewState对象时其实是隐式用到了IStateManager接口,只不过Control类不是继承IStateManager实现的,而是采用关联对象方式把StateBag类的一个实例作为自己的一个属性保持而已。这样从技术角度能够把IStateManager接口的几个方法与Control对控件生命周期支持的几个同名方法区别开来(它们命名是相同的)。另外,这几个方法在使用上也非常简便,直接通过属性的方式使用,否则使用时就要重写基类的方法实现,显得比较笨重且缺乏灵活性。
后面三个接口IDictionary,ICollection,IEnumerable主要为视图对象的存储集合以及对集合的快速检索提供支持。在这里可以看到我们使用的ViewState在服务端也存储在一个标准的IDictionary类型中,如下:
private IDictionary bag;
u IDictionary集合采用键(string类型)/值(object类型)的格式存储。除了bag对象,还有两个内部变量:
private bool ignoreCase;
private bool marked;
u ignoreCase指定在集合中存储的键是否忽略大小写。marked变量就标记是否启用了跟踪监控的变量,只有当该值为true时,才把值保存到视图集合对象中,否则如果集合中有该对象就移除它。在Add方法的核心代码片段中体现了这一点,代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.youkuaiyun.com/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public StateItem Add(string key, object value)
{
//… …
StateItem item = this.bag[key] as StateItem;
if (item == null)
{
if ((value != null) || this.marked)
{
item = new StateItem(value);
this.bag.Add(key, item);
}
}
else if ((value == null) && !this.marked)
{
this.bag.Remove(key);
}
else
{
item.Value = value;
}
if ((item != null) && this.marked)
{
item.IsDirty = true;
}
return item;
}
u 这一段代码比较严谨,除了判断marked是否为true,还判断要增加的对象是否为null如果为null,也不会增加到视图集合列表对象中。另外,在视图集合中,对应的值类型为StateItem,它的代码如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.youkuaiyun.com/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public sealed class StateItem
{
// Fields
private bool isDirty;
private object value;
// Methods
internal StateItem(object initialValue);
// Properties
public bool IsDirty { get; set; }
public object Value { get; set; }
}
u 在这里除了定义了存储数据内容的object对象的value属性外,还有一个Dirty属性,该属性值标志当前集合中的一个对象是否是脏数据(即被改动过了),SaveViewState方法只对脏数据进行保存,以便提高性能。SaveViewState的代码片段如下:
- /// <summary>
- /// 获得本书更多内容,请看:
- /// http://blog.youkuaiyun.com/ChengKing/archive/2008/08/18/2792440.aspx
- /// </summary>
- internal object SaveViewState()
- {
- ArrayList list = null;
- if (this.bag.Count != 0)
- {
- IDictionaryEnumerator enumerator = this.bag.GetEnumerator();
- while (enumerator.MoveNext())
- {
- StateItem item = (StateItem)enumerator.Value;
- if (item.IsDirty)
- {
- if (list == null)
- {
- list = new ArrayList();
- }
- list.Add(new IndexedString((string)enumerator.Key));
- list.Add(item.Value);
- }
- }
- }
- return list;
- }
代码体中的语句if(item.IsDirty)就是对要保存的序列化对象进行过滤,最终返回的list集合对象中的item的Dirty属性值都为true。
StateBag类的关键点就介绍这些。StateBag是.NETFramework提供的一个比较实用的类,并且它实现了IStateManager,可以作为自定义类型视图、状态的一个典型例子,在实现自己的视图状态类时完全可以参考它。在实际开发时,很多情况下也并非一定要显示继承 IStateManager接口,系统类有些类型继承了IStateManager,比如Style,这样我们可以直接拿来使用,还有它的一些派生类TableItemStyle,TableStyle,PanelStyle 都可以直接在控件开发中使用。后面会介绍一个使用TableItemStyle作为基类实现自定义类型视图状态的示例。
6.2.4 实现自定义类型视图状态
前面对视图状态的概念和原理已经说得比较清楚了,这一节就以一个实例说明ViewState工作原理。建立一个Web自定义控件ViewStateControl,该控件继承于WebControl或Control,代码如下:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.youkuaiyun.com/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:ViewStateControl runat=server></{0}:ViewStateControl>")]
public class ViewStateControl : WebControl
{
}