6.1 页面状态概述
两次页面请求之间的数据关联性,ASP.NET是通过视图机制实现的,简单地讲,视图区域信息(ViewState)存储于页面上的一个隐藏字段(名为__VIEWSTATE,只是视图状态中的值经过哈希计算和压缩,并且针对Unicode实现进行编码,其安全性要高于我们自己设置的隐藏域控件),每次需要视图机制保存的一些信息都存储在此字段中,每次提交时,它都会以“客户端到服务端”的形式来回传递一次,当处理完成后,最后会以处理后的新结果作为新的ViewState存储到页面中的隐藏字段,并与页面内容一起返回到客户端。
为了提高性能,通常禁用页面或禁用服务端控件的状态视图,有些控件不需要维护其状态,如Label控件只是显示文本,而标签的文本,值不参与回发,可以设置其属性: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接口,定义了任何类为支持服务器控件的视图状态管理而必须实现的属性和方法,服务器控件的视图状态由控件属性的累计值组成。
该接口包括保存(SaveViewState)并加载(LoadViewState)服务器控件的视图状态值的方法,以及一个指示控件跟踪其视图状态的更改的方法(TrackViewState)。此接口的成员与Control类中的对应方法具有相同的语义。
若要自定义ASP.NET应用程序管理服务器控件视图状态的方式,必须创建一个实现此接口的类。
我们直接使用ViewState对象时其实是隐式用到了IStateManager接口,只不过Control类不是继承IStateManager实现的,而是采用关联对象方式把StateBag类的一个实例作为自己的一个属性保持而已:
//包含服务器控件视图状态信息的 System.Web.UI.StateBag 类的实例
protected virtual StateBag ViewState { get; }
//管理 ASP.NET 服务器控件(包括页)的视图状态。
public sealed class StateBag : IStateManager, IDictionary, ICollection, IEnumerable { ... }
对于自定义的类型,仅实现IStateManager接口的方法是不够的(该方法仅使自定义类具有正反序列化的能力),还需要由主控件的控件生命周期方法来引发调用它们(控件的LoadViewState/SaveViewState方法调用自定义类的同名方法),才能够正确地装载和保存视图数据。
这就要求主控件直接或间接继承Control类,并重载Control类中的LoadViewState和SaveViewState方法,(主控件中的这两个方法与IStateManager中的对应两个同名方法功能类似,但它们只是名称相同而已,这里主控件的两个方法是由页框架在控件生命周期阶段自动调用的。而IStateManager跟页框架没有一点联系,必须与主控件里的这两个方法组合使用,它才有意义,否则就是一堆死代码,永远不会执行。)
这两个方法属于控件生命周期阶段方法,只要是属于控件生命周期的方法,则在控件生成阶段一定会被页框架调用。它们才是视图状态启动的导火线。
6.3 控件状态机制
自ASP.NET 2.0开始支持控件状态机制。控件的状态数据现在能通过控件状态而不是视图状态被保持,控件状态是不能够像视图状态那样被禁用的。由于控件状态的工作方式与视图状态完全一致,并且默认情况下在页面中它们都是存储在同一个隐藏域中。与LoadViewState和SaveViewState类似,控件状态也是提供了一对这样的方法,方法名称分别为LoadControlState和SaveControlState,并且也是在Control基类中提供。
事件顺序:Init - LoadControlState - LoadViewState - LoadPostData - OnLoad - OnPreRender - SaveViewState - SaveControlState - Render
在建立控件状态的控件时,与视图状态有点不同,除了重载SaveControlState和LoadControlState两个方法外,在上面代码中还重写了OnInit方法,并增加如下语句:
Page.RegisterRequiresControlState(this);
该方法的功能是将控件注册为具有持久性控件状态的控件,参数是要注册的控件引用。由于在回发事件的过程中,控件状态的注册无法在请求之间进行传递,因此使用控件状态的自定义服务器控件必须对每个请求调用RegisterRequiresControlState方法。即本方法通知页框架在控件生命周期时调用控件状态的两个方法;反过来讲,如果没有通过上面方法注册当前控件,则控件的LoadControlState和SaveControlState方法不会执行。一般在Init事件中注册控件。
6.4 视图状态和控件状态的关系
6.4.1 在禁用视图状态的情况下仍然使用ViewState对象
原因:在LoadControlState和SaveControlState方法中分别调用base.LoadViewState和base.SaveControlState,我们可以手动调用ViewState属性对象的对象正反序列化过程。
归根到底,也就是说开发人员所谓的禁用视图实际上是禁止LoadViewState和SaveViewState两个方法的执行,
但理论上我们只要启动控件状态,并把这两个方法的逻辑放到LoadControlState和SaveControlState中,仍然可以利用ViewState。
6.4.3 视图状态和控件状态组合使用规则
视图状态的两个方法(LoadViewState和SaveViewState)和控件状态的两个方法(LoadControlState和SaveControlState)可以同时使用,即视图状态和控件状态一起使用。
这样做的好处是把数据量相对比较大的属性或用户数据属性作为ViewState存储,把仅与控件逻辑相关的属性,比如像选项卡控件的当前选中索引CurrentSelectIndex这样的属性,作为控件状态存储。
6.5 加密页面状态
RegisterRequiresViewStateEncryption将控件注册为需要视图状态加密的控件。
该方法必须在页生命周期的PreRender阶段中或该阶段之前调用,比如在控件中重写OnPreRender方法并加入视图加密功能,增加后的代码段如下:
protected override void OnPreRender(EventArgs e)
{
this.Page.ViewStateEncryptionMode = ViewStateEncryptionMode.Auto; //该枚举控制是否加密视图状态信息
this.Page.RegisterRequiresViewStateEncryption();
base.OnPreRender(e);
}
6.7 对动态添加控件的视图状态分析
有时候在Page_Load事件中动态创建控件,而当页面提交后,动态修改的数据就丢失了。如下代码:
protected void Page_Load(object sender, EventArgs e)
{
ListBox lb = new ListBox();
if (!Page.IsPostBack)
{
lb.Items.Add("子项1");
}
this.form1.Controls.Add(lb);
}
这时如果单击页面中事先放置的“提交”按钮,则页面提交后不再看到“子项1”,也就是说在视图状态中没有存储该item项。
为了解决此问题,把代码修改成:
protected void Page_Load(object sender, EventArgs e)
{
ListBox lb = new ListBox();
(lb.Items as IStateManager).TrackViewState(); //ListItemCollection集合对象实现了IStateManager接口
if (!Page.IsPostBack)
{
lb.Items.Add("子项1");
}
this.form1.Controls.Add(lb);
}
以上代码在为ListBox控件lb增加集合子项时之前,先启用Items属性的视图跟踪监控。这样当页面再提交时,则集合项“子项1”仍然能够呈现。
如果没有实现此接口的对象,还有一种方法可以实现视图状态功能:把this.form1.Controls.Add(lb)这句放到lb.Item.Add("子项1")之前。因为Controls集合的Add方法最终调用的是Control控件基类中的方法AddedControl,此方法能够启用视图状态和向页面请求注册控件状态(假如需要的话)。视图状态跟踪被启用后,接下来执行对item“子项1”的增加当然也能够被页框架视图管理器保存。
6.9 页面状态性能优化策略
6.9.1 存储位置优化——把视图状态信息保存在服务端而非客户端
重写Page的PageStatePersister属性:
protected override PageStatePersister PageStatePersister
{
get
{
return new SessionPageStatePersister(this); //替换原来的 base.PageStatePersister;
}
}
除了使用重写基类Page的PageStatePersister属性实现指定持久化类对象外,还可以通过实现System.Web.UI.Adapters.PageAdapter类的页配器实现同样功能。不同的一点是后面这种方式影响到了整个站点下面的所有页面。
6.9.2 体积优化——压缩视图状态数据
另一种优化技术,在序列化对象信息前先压缩数据。其实现机制也是重写基类Page的PageStatePersister属性。
首先定义一个实现System.Web.UI.PageStatePersister基类的类CompressPageStatePersister:
{
private string PageStateKey = "____VIEWSTATE";
public CompressPageStatePersister(Page page) : base(page)
{
}
public override void Load()
{
string postbackState = Page.Request.Form[PageStateKey];
if (!string.IsNullOrEmpty(postbackState))
{
//页面状态包括视图状态和控件状态两部分
Pair statePair = (Pair)CompressHelp.Decompress(postbackState);
if (!Page.EnableViewState)
{
this.ViewState = null;
}
else
{
this.ViewState = statePair.First;
}
this.ControlState = statePair.Second;
}
}
public override void Save()
{
if (!Page.EnableViewState)
{
this.ViewState = null;
}
if (this.ViewState != null || this.ControlState != null)
{
string stateString;
Pair statePair = new Pair(ViewState, ControlState);
//将当前对象的数据压缩,并通过Page.RegisterHiddenField方法把值注册到客户端隐藏控件。
stateString = CompressHelp.Compress(statePair);
Page.ClientScript.RegisterHiddenField(PageStateKey, stateString);
}
}
}
最后,把定义的CompressPageStatePersister应用到页面上:
protected override PageStatePersister PageStatePersister
{
get
{
return new CompressPageStatePersister(this);
}
}
PS:这里的压缩类CompressHelp主要提供了两个静态方法,代码如下
{
//序列化工具,LosFormatter是页面默认的序列器
private static LosFormatter _formatter = new LosFormatter();
/// <summary>
/// 解压并反序列化状态内容
/// </summary>
/// <param name="stateString">从客户端取回的页面状态字符串</param>
/// <returns>还原后的页面状态Pair对象</returns>
public static object Decompress(string stateString)
{
byte[] buffer = Convert.FromBase64String(stateString);
MemoryStream ms = new MemoryStream(buffer);
GZipStream zipStream = new GZipStream(ms, CompressionMode.Decompress);
MemoryStream msReader = new MemoryStream();
buffer = new byte[0x1000];
while (true)
{
int read = zipStream.Read(buffer, 0, buffer.Length);
if (read <= 0)
{
break;
}
msReader.Write(buffer, 0, read);
}
zipStream.Close();
ms.Close();
msReader.Position = 0;
buffer = msReader.ToArray();
stateString = Convert.ToBase64String(buffer);
return _formatter.Deserialize(stateString);
}
/// <summary>
/// 序列化并压缩状态内容
/// </summary>
/// <param name="state">页面状态</param>
/// <returns>结果字符串</returns>
public static string Compress(object state)
{
StringWriter writer = new StringWriter();
_formatter.Serialize(writer, state);
string stateString = writer.ToString();
writer.Close();
byte[] buffer = Convert.FromBase64String(stateString);
MemoryStream ms = new MemoryStream();
GZipStream zipStream=new GZipStream(ms,CompressionMode.Compress,true);
zipStream.Write(buffer, 0, buffer.Length);
zipStream.Close();
buffer = new byte[ms.Length];
ms.Position = 0;
ms.Read(buffer, 0, buffer.Length);
ms.Close();
stateString = Convert.ToBase64String(buffer);
return stateString;
}
}
6.9.3 分块存储视图状态数据
如果隐藏域中的数据量过大,某些代理和防火墙将阻止对包含这些数据的页的访问。
如果您需要存储大量的数据项,可以打开视图状态分块,这样会自动将数据分割到多个隐藏域。
ASP.NET框架提供了MaxPageStateFieldLength属性,用来获取或设置页状态字段的最大长度。
其属性值表示页面状态字段的最大长度,以字节为单位。
在配置文件Web.Config中实现如下:
<system.web>
<pages maxPageStateFieldLength="100" />
<system.web>
注意,如果将MaxPageStateFieldLength设置非常小,会导致性能降低。