第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.csdn.net/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 控件生命周期中的视图装载和保存阶段

从图6-1中可以看到LoadViewState和SaveViewState分别在控件生命周期的开始(初始化Init后)和最后(呈现Render之前)。这样我们可以在其间的一些周期阶段操作视图状态数据。而在控件的基类Control中已经提供了对这两个方法的支持。

对于自定义的类型,仅实现IStateManager接口的方法是不够的(该方法仅使自定义类具有正反序列化的能力),还需要由主控件的控件生命周期方法来引发调用它们,才能够正确地装载和保存视图数据。这就要求主控件直接或间接继承Control类,并重载Control类中的LoadViewState和SaveViewState方法,这两个方法属于控件生命周期阶段方法,只要是属于控件生命周期的方法,则在控件生成阶段一定会被页框架调用。它们才是视图状态启动的导火线。

重载这两个方法如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class ViewStatePeriod : WebControl
{
    protected override object SaveViewState()
    {
         //… …
    }
    protected override void LoadViewState(object savedState)
    {
        //… …
    }
}

另外,视图状态监视是在初始化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;
    }
}

在上面代码中有个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;
    }
}

这是一个标准的自定义类型属性。再仔细看一下,该属性的类型为StateBage类,这才是我们要找的关键类,它的代码结构如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/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;

IDictionary集合采用键(string类型)/值(object类型)的格式存储。除了bag对象,还有两个内部变量:

private bool ignoreCase;

private bool marked;

ignoreCase指定在集合中存储的键是否忽略大小写。marked变量就标记是否启用了跟踪监控的变量,只有当该值为true时,才把值保存到视图集合对象中,否则如果集合中有该对象就移除它。在Add方法的核心代码片段中体现了这一点,代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/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;
}

这一段代码比较严谨,除了判断marked是否为true,还判断要增加的对象是否为null如果为null,也不会增加到视图集合列表对象中。另外,在视图集合中,对应的值类型为StateItem,它的代码如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/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; }
}

在这里除了定义了存储数据内容的object对象的value属性外,还有一个Dirty属性,该属性值标志当前集合中的一个对象是否是脏数据(即被改动过了),SaveViewState方法只对脏数据进行保存,以便提高性能。SaveViewState的代码片段如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/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.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:ViewStateControl runat=server></{0}:ViewStateControl>")]
    public class ViewStateControl : WebControl
    {
    }
由于WebControl是继承于Control基类的,因此该控件已经具备控件生命周期在视图阶段执行方法LoadViewState和SaveViewState的能力。在控件中增加三个不同类型的属性,如下所示:
/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>

private string _text;
[Bindable(true)]
[DefaultValue("")]
[Localizable(true)]
[Category("测试视图状态")]
[Description("没有使用视图状态存储")]
public string Text_NoViewState
{
    get
    {
        return _text;
    }
    set
    {
        this._text = value;
    }
}

[Bindable(true)]
[DefaultValue("")]
[Localizable(true)]
[Category("测试视图状态")]
[Description("使用ViewState属性来存储数据此属性")]
public string Text_ViewState
{
    get
    {
        String s = (String)ViewState["Text_ViewState"];
        return ((s == null) ? String.Empty : s);
    }
    set
    {
        ViewState["Text_ViewState"] = value;

    }
}

private FaceStyle _faceStyle;
[PersistenceMode(PersistenceMode.InnerProperty)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[NotifyParentProperty(true)]
[Category("测试视图状态")]
[Description("自定义视图状态(实现IStateManager接口)存储此属性")]
public FaceStyle FaceStyle
{
    get
    {
        if (_faceStyle == null)
        {
            _faceStyle = new FaceStyle();
        }
        if (IsTrackingViewState)
        {
            ((IStateManager)_faceStyle).TrackViewState();
        }
        return _faceStyle;
    }
}

针对视图状态,这三个属性分别表示三种不同类型。第一个属性Text_NoViewState没有使用视图状态机制存储属性值;第二个属性Text_ViewState使用Control基类中的存储简单类型的ViewState对象存储属性值;第三个属性FaceStyle为自定义类型属性,属性类型为FaceStyle。由于FaceStyle为自定义类,要实现其类的视图状态管理,还需要继承前面讲解的IStateManager接口。FaceStyle的类结构如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class FaceStyle : TableItemStyle, IStateManager
{
}

FaceStyle继承了IStateManager接口,并且还继承了TableItemStyle基类,前面讲过TableItemStyle继承了Style,并且Style又继承了IStateManager,也就是说TableItemStyle能够管理其内部的数据,这固然是好的,因为我们不清楚TableItemStyle的成员内容,也无法对基类中的成员进行管理。虽然TableItemStyle能够管理其内部视图数据,但它只能管理其内部数据(类成员)的视图(Style没有完全公开它继承的IStateManager的几个成员,所以需要自己实现IStateManager),而不能够管理FaceStyle类成员的视图,在这里笔者仍然在FaceStyle类中实现了IStateManager类,因为在FaceStyle类中一般还要加入自己的成员,这就需要FaceStyle类自己管理其成员视图状态。

针对FaceStyle这个比较特殊的自定类,我们只要知道下面两点就可以验证它是否真的能够支持和实现视图状态:

  1. TableItemStyle是否能够真的能够保存其内部和基类的数据?
  2. 在FaceStyle类中加入新成员,并继承IStateManager实现其视图状态管理,只要实现FaceStyle中的成员能够完成视图状态的存取即可说明该成员能够支持视图状态。

对于第(1)点,可以直接针对TableItemStyle类内部的属性进行判断。对于第(2)点,专门为FaceStyle类增加一个属性,代码如下:

 

private bool _blnOK;
[Browsable(true)]
[Description("自定义类测试变量")]
public bool OK
{
    get
    {
        return _blnOK;
    }
    set
    {
        _blnOK = value;
    }
}
public FaceStyle()
{
    _blnOK = false;
}

该属性为布尔类型,在这里就不能使用this.ViewState对象存储值了,如果强制使用该对象也无法编译通过,因为只有在继承Control类的派生类中才能够使用ViewState对象。另外,在这里如果继承Control类也没有必要,这仅是一个简单属性的类而已,像这样的样式类还有很多。

下面我们就自己实现FaceStyle类的视图状态管理。在FaceStyle类中增加如下代码:

 

bool IStateManager.IsTrackingViewState
{
    get
    {
        return base.IsTrackingViewState;
    }
}

#endregion

#region 方法


//从当前点开始, 此控件具有保存视图状态功能       
void IStateManager.TrackViewState()
{
    base.TrackViewState();
}

以上代码中包括一个属性和一个方法,这两个成员是对IStateManager接口中成员的实现。IsTrackingViewState属性指示当前FaceStyle属性是否启用视图监控,TrackViewState方法用于启用视图监控功能。这两个属性操作的监控标记布尔变量都是直接使用Style基类中的标记布尔变量,如下代码是marked标记在Style中的实现:

 

private bool marked;
protected internal virtual void TrackViewState()
{
    if (this.ownStateBag)
    {
        this.ViewState.TrackViewState();
    }
    this.marked = true;
}

protected bool IsTrackingViewState
{
    get
    {
        return this.marked;
    }
}

虽然Style类没有公开LoadViewState方法,但对于TrackViewState和IsTrackingViewStatep这两个成员是可以使用的。另外,在FaceStype中使用以上两个成员的好处是可以避免重新定义产生二义性,因为这两个成员性对基类还是FaceStyle类本身来说实现的功能是一样的,也没必要再定义一个marked变量。
IStateManager接口还有两个最重要成员LoadViewState和SaveViewState没有实现,下面是它们的代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>

object IStateManager.SaveViewState()
{
    object[] state = new object[2];
    state[0] = base.SaveViewState();            
    
    state[1] = (object)OK;
    return state;
}
void IStateManager.LoadViewState(object state)
{
    if (state == null)
    {
        return;
    }
    object[] myState = (object[])state;
    base.LoadViewState(myState[0]);
    OK = (bool)myState[1];                
}

以上两个方法是对视图数据操作的核心方法。SaveViewState方法主要将对FaceStyle内部数据成员的视图状态所做的更改保存到对象,如果在主控件中则返回值直接交给页框架序列化处理,最终作为一个散列串存储在页面上的隐藏区域中;在非主控件的自定义类中则由主控件的SaveViewState方法先调用返回值,再由主控件一块把这些自定义类的视图数据一块返回给页框架。在代码中,首先创建一个二维object数组,数组的第一维存储基类TableItemState序列化后的对象(base.SaveViewState即为返回基类序列化对象方法),数组的第二维仅存储我们在FaceStyle中专门增加的那个属性(名称为OK)。最后返回整个数组对象。

LoadViewState恰好与SaveViewState执行相反的过程,主要完成把state二维数据组中的数据赋给控件当前对应的属性。当初始化Init阶段完成后,页框架会马上调用LoadViewState方法,LoadViewState方法带一个参数state,简单地说此参数是从客户端传回的,被页框架反序列化后的对象。

到现在为止,FaceStyle类的视图状态管理已经实现完成。代码比较短,下面是FaceStyle类的完整代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class FaceStyle : TableItemStyle, IStateManager
{
    #region 类变量

    private bool _blnOK;

    #endregion

    #region 构造函数

    public FaceStyle()
    {
        _blnOK = false;
    }

    #endregion

    #region 属性
   
    [Browsable(true)]
    [Description("自定义类测试变量")]
    public bool OK
    {
        get
        {
            return _blnOK;
        }
        set
        {
            _blnOK = value;
        }
    }

    
    bool IStateManager.IsTrackingViewState
    {
        get
        {
            return base.IsTrackingViewState;
        }
    }

    #endregion

    #region 方法


    //从当前点开始, 此控件具有保存视图状态功能       
    void IStateManager.TrackViewState()
    {
        base.TrackViewState();
    }

   
    object IStateManager.SaveViewState()
    {
        object[] state = new object[2];
        state[0] = base.SaveViewState();            
        
        state[1] = (object)OK;

        //状态管理会存储此返回的值; 另外此方法返回值还有个用途: 创建复合控件时取得
        //各个子控件的视图状态
        return state;
    }

   
    void IStateManager.LoadViewState(object state)
    {
        if (state == null)
        {
            return;
        }
        object[] myState = (object[])state;
        base.LoadViewState(myState[0]);

        OK = (bool)myState[1];                
    }
    #endregion
}

之前说过,主控件中的LoadViewState和SaveViewState方法是启用自定义类视图管理的导火线。接下来就实现这两个方法,代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>

protected override object SaveViewState()
{
    Pair p = new Pair();
    p.First = base.SaveViewState();
    p.Second = ((IStateManager)FaceStyle).SaveViewState();
    for (int i = 0; i < 2; i++)
    {
        if (p.First != null || p.Second != null)
        {
            return p;
        }
    }
    return null;
}
protected override void LoadViewState(object savedState)
{
    if (savedState == null)
    {
        base.LoadViewState(null);
        return;
    }
    else
    {
        Pair p = (Pair)savedState;
        if (p == null)
        {
            throw new ArgumentException("无效的View State 数据!");
        }
        base.LoadViewState(p.First);
        if (p.Second != null)
        {
            ((IStateManager)FaceStyle).LoadViewState(p.Second);
        }
    }
}

主控件中的这两个方法与IStateManager中的对应两个同名方法功能类似,但它们只是名称相同而已,这里主控件的两个方法是由页框架在控件生命周期阶段自动调用的。而IStateManager跟页框架没有一点联系,必须与主控件里的这两个方法组合使用,它才有意义,否则就是一堆死代码,永远不会执行。

SaveViewState功能完成对主控件中指定属性的对象序列化。方法体中首先定义了一个Pair类型的对象p,此对象是系统专门为此提供的,能存储一对数据值,类似于对象二维数组(object[2])。其类结构如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public sealed class Pair
{
    // Fields
    public object First;
    public object Second;

    // Methods
    public Pair();
    public Pair(object x, object y);
}

p.First存储基类序列化后对象(base.SaveViewState返回值为Control类成员序列化后的对象),其后面有一句:

p.Second = ((IStateManager)FaceStyle).SaveViewState();

这句功能是取FaceStyle的序列化对象值。指定p.Second存储我们在主控件中自定义的属性FaceStyle序列化后的内容,FaceStyle.SaveViewState方法在FaceStyle内部定义,本节前面已经详细讲解了其实现过程。最后通过条件语句判断,如果Pair对象中First和Second值有一个不为null,就以p作为返回值,否则返回null。

后面的LoadViewState功能与SaveViewState执行逻辑正好相反,即把序列化对象值赋值给控件属性和复杂属性。

对于上面两个方法还要说明一点,LoadViewState方法在第1次请求时不执行,仅执行SaveViewState。通常下LoadViewState在控件周期中是在SaveViewState之前被页框架调用的,但LoadViewState在页面第一次加载时不执行是有特殊原因的,主要由于SaveViewState还未保存过,执行LoadViewState也是无意义的。另外,从功能角度理解,我们只有在第2次提交页面时,才有可能想知道提交之前页面中某个控件的数据是什么,因为第一次请求时想知道第0次请求的数据是无意义的。强烈建议在主控件的这两个方法中设置断点,跟踪调试一下,对理解页面视图机制有极大的帮助。

上面使用了一个Pair对象。在FaceStyle中我们使用的是数组object[2]。除了这两个对象,系统还提供了对象Triplet,它的类结构如下:

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public sealed class Triplet
{
    // Fields
    public object First;
    public object Second;
    public object Third;

    // Methods
    public Triplet();
    public Triplet(object x, object y);
    public Triplet(object x, object y, object z);
}

Pair英文意思是"两份";Triplet英文意思是"三份"。根据不同的应用场景可以选择Pair或Triplet类实现序列化功能。在下一章介绍为控件样式保存视图状态时会用到Triplet,这里就不作示例了。

主控件的核心代码已经讲解完,下面看一下主控件ViewStateControl的完整代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:ViewStateControl runat=server></{0}:ViewStateControl>")]
public class ViewStateControl : WebControl
{
    private string _text;
    [Bindable(true)]
    [DefaultValue("")]
    [Localizable(true)]
    [Category("测试视图状态")]
    [Description("没有使用视图状态存储")]
    public string Text_NoViewState
    {
        get
        {
            return _text;
        }

        set
        {
            this._text = value;
        }
    }

    [Bindable(true)]
    [DefaultValue("")]
    [Localizable(true)]
    [Category("测试视图状态")]
    [Description("使用ViewState属性来存储数据此属性")]
    public string Text_ViewState
    {
        get
        {
            String s = (String)ViewState["Text_ViewState"];
            return ((s == null) ? String.Empty : s);

        }

        set
        {
            ViewState["Text_ViewState"] = value;

        }
    }

    private FaceStyle _faceStyle;
    [PersistenceMode(PersistenceMode.InnerProperty)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    [NotifyParentProperty(true)]
    [Category("测试视图状态")]
    [Description("自定义视图状态(实现IStateManager接口)存储此属性")]
    public FaceStyle FaceStyle
    {
        get
        {
            if (_faceStyle == null)
            {
                _faceStyle = new FaceStyle();
            }

            if (IsTrackingViewState)
            {
                ((IStateManager)_faceStyle).TrackViewState();
            }

            return _faceStyle;
        }
    }

    protected override void RenderContents(HtmlTextWriter output)
    {
        if (DesignMode)
        {
            output.Write("[自定义视图状态存储示例控件]");
        }
    }

    protected override object SaveViewState()
    {
        Pair p = new Pair();
        p.First = base.SaveViewState();
        p.Second = ((IStateManager)FaceStyle).SaveViewState();
        for (int i = 0; i < 2; i++)
        {
            if (p.First != null || p.Second != null)
            {
                return p;
            }
        }
        return null;
    }

    protected override void LoadViewState(object savedState)
    {
        if (savedState == null)
        {
            base.LoadViewState(null);
            return;
        }
        else
        {
            Pair p = (Pair)savedState;
            if (p == null)
            {
                throw new ArgumentException("无效的View State 数据!");
            }
            base.LoadViewState(p.First);
            if (p.Second != null)
            {
                ((IStateManager)FaceStyle).LoadViewState(p.Second);
            }
        }
    }
}

最后,针对该控件是否能够正确实现视图状态的保存,笔者专门做了一个测试示例,对控件的每个属性进行测试。

在页面中放一个ViewStateControl控件,两个Button,一个Label。两个Button中的一个负责服务端设置控件的四个属性(稍后会具体讲解这四个属性);另一个负责提交页面,并检测当前控件属性是否正确保存了视图数据,检测结果通过Label控件显示。Label负责显示状态信息,可以指示某个属性是否真的保存了视图状态数据。设置后的页面中代码如下:

 

<body>
    <form id="form1" runat="server">    
    <div>        
        <cc1:viewstatecontrol ID="ViewStateControl1" runat="server" 
                                Text_NoViewState="" Text_ViewState="">
            <FaceStyle OK="false" BackColor="White" />            
        </cc1:viewstatecontrol>
        <br />
        <br />
        <asp:Button ID="btnSetProperty" runat="server" 
                    OnClick="btnSetProperty_Click" Text="设置控件属性" />
        <asp:Button ID="btnRefresh" runat="server" Text="刷新页面" 
                    OnClick="btnRefresh_Click" />&nbsp;<br />
        <br />
        <asp:Label ID="lbDisplay" runat="server" Height="309px" 
                    Width="307px"></asp:Label></div>          
    </form>
</body>

如上代码主控件中的四个属性:Text_NoViewState,Text_ViewState,FaceStyle.OK,FaceStyle.BackColor各自代表不同的类型成员。在主控件中定义时,第一个属性Text_NoViewState没有使用视图状态机制存储属性值;第二个属性Text_ViewState使用Control基类中的存储简单类型的ViewState对象存储属性值;第三个属性FaceStyle.OK为FaceStyle类中的属性;第四个属性FaceStyle.BackColor为FaceStyle的基类中的属性。如果还不明白请回顾一下主控件的代码。视图状态机制就这么多内容,只要这四个属性能够符合视图状态规则,则就证明我们的控件做到了视图状态管理,也即我们已经掌握了页面视图状态机制。

页面中两个按钮的后台代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>

string strPicXiaoLian = "<Img src='" + "Images\\XL.jpg" + "' /> ";
string strPicJuSang   = "<Img src='" + "Images\\JS.jpg" + "' /> ";
string strPicBG = "<Img src='" + "Images\\BG.jpg" + "' /> ";
private Color c = Color.FromName("#C0C0FE");
protected void btnSetProperty_Click(object sender, EventArgs e)
{
    ///设置没有用视图状态存储的属性值
    this.ViewStateControl1.Text_NoViewState = "我没有用任何视图状态存储!";

    ///设置用ViewState存储的属性值
    this.ViewStateControl1.Text_ViewState = "我是用ViewState容器存储的!";

    //设置用自定义视图状态存储的属性值
    this.ViewStateControl1.FaceStyle.OK = true;
    this.ViewStateControl1.FaceStyle.BackColor = c;
}
protected void btnRefresh_Click(object sender, EventArgs e)
{
    /// <summary>
    /// ViewState容器存储测试--测试存储ViewStateControl1.Text_NoViewState属性
    /// </summary>
    if (this.ViewStateControl1.Text_NoViewState == "我没有用任何视图状态存储!")
    {
        this.lbDisplay.Text = "this.ViewStateControl1.Text_NoViewState属性 已
        经保存了视图状态 " + strPicXiaoLian + " <br><br>";
    }
    else
    {
        this.lbDisplay.Text = "this.ViewStateControl1.Text_NoViewState属性 没
        有保存视图状态 " + strPicJuSang + " <br><br>";
    }


    /// <summary>
    /// ViewState容器存储测试--测试存储ViewStateControl1.Text_ViewState属性
    /// </summary>
    if (this.ViewStateControl1.Text_ViewState == "我是用ViewState容器存储的!")
    {
        this.lbDisplay.Text += "this.ViewStateControl1.Text_ViewState属性 已经
        保存了视图状态 " + strPicXiaoLian + " <br><br>";
    }
    else
    {
        this.lbDisplay.Text += "this.ViewStateControl1.Text_ViewState属性 没有
        保存视图状态 " + strPicJuSang + " <br><br>";
    }


    /// <summary>
    /// 自定义视图状态测试--测试存储类ViewStateControl1.FaceStyle的内部属性OK
    /// </summary>
    if (this.ViewStateControl1.FaceStyle.OK == true)
    {
        this.lbDisplay.Text += "this.ViewStateControl1.FaceStyle属性.OK 已经保
        存了视图状态 " + strPicXiaoLian + " <br><br>";
    }
    else
    {
        this.lbDisplay.Text += "this.ViewStateControl1.FaceStyle属性.OK 没有保
        存视图状态 " + strPicJuSang + " <br><br>";
    }


    /// <summary>
    /// 自定义视图状态测试--测试存储类ViewStateControl1.FaceStyle的基类
         TableItemStyle中的属性BackColor
    /// </summary>
    if (this.ViewStateControl1.FaceStyle.BackColor.Equals(c))
    {
        this.lbDisplay.Text += "this.ViewStateControl1.FaceStyle.BackColor 已
        经保存了视图状态, 瞧,我的颜色就是保存的颜色 " + strPicXiaoLian + " " + strPicBG 
        + "<br><br>";
        //this.lbDisplay.BackColor = c;
    }
    else
    {
        this.lbDisplay.Text += "this.ViewStateControl1.FaceStyle.BackColor 没
        有保存视图状态, 瞧,我的颜色还是白色 " + strPicJuSang + " <br><br>";
    }
}

测试代码看起来有些长,其实逻辑很简单,就是先为属性赋值,然后单击"提交"按钮,看一下页面重新呈现后这些属性值是否还存在。事件体外面定义了几个图片路径和一个颜色变量(测试FaceStyle.BackColor属性时使用)。

btnSetProperty_Click事件方法体主要针对我们要测试的四个属性,为每个属性设置一个值。btnRefresh_Click方法完成提交,并判断控件的每个值是否还等于在btnSetPropertyClick事件方法体中赋的值,如果相同就为当前属性显示 ,表示保存了值;否则显示 ,表示没有正确通过视图存储数据。

好了,现在是心情最轻松最愉快的时刻。开始测试。在浏览器中运行页面,先单击一下"刷新"按钮,页面显示如图6-2所示。

图6-2 页面视图状态示例 -- -- 初始状态

可以看到页面的上的控件还是初始值。顺便查看一下页面中隐藏域的值如下:

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwULLTE3 NjU0NjM4OTEPZBYCAgMPZBYCAgEPD2QUKwACZGhkZNKghO16NX9gdumXeBtdWJZV1bcC" />

内容比较少,仅包括页面自己的一些视图信息。先单击一下"设置控件属性"按钮,为控件的四个属性赋值,然后再单击"刷新页面"按钮,则会看到如图6-3所示的页面。

图6-3 页面视图状态示例 -- -- 存储了视图状态数据

可以看到后面的三个属性已经正确保存了视图状态。但第一个控件仍然没有保存视图状态数据,这是正常的,因为第一个控件我们本来就没有使用任何视图状态对象存储状态数据,如果它能够自动保存视图状态,那真是神了!看一下呈现到客户端源代码隐藏域:

 

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="/wEPDwULLTE3 NjU0NjM4OTEPZBYCAgMPZBYEAgEPDw8WAh4OVGV4dF9WaWV3U3RhdGUFIuaIkeaYr+eUqFZpZXdTdGF0ZeWuueWZqOWtmOWCqOeahCFkFCsAAhYEHglCYWNrQ29sb3IKAB4EXyFTQgIIZ2QCBw8PFgIeBFRleHQF9QN0aGlzLlZpZXdTdGF0ZUNvbnRyb2wxLlRleHRfTm9WaWV3U3RhdGXlsZ7mgKcg5rKh5pyJ5L+d5a2Y6KeG5Zu+54q25oCBIDxJbWcgc3JjPSdJbWFnZXNcSlMuanBnJyAvPiAgPGJyPjxicj50aGlzLlZpZXdTdGF0ZUNvbnRyb2wxLlRleHRfVmlld1N0YXRl5bGe5oCnIOW3sue7j+S/neWtmOS6huinhuWbvueKtuaAgSA8SW1nIHNyYz0nSW1hZ2VzXFhMLmpwZycgLz4gIDxicj48YnI+dGhpcy5WaWV3U3RhdGVDb250cm9sMS5GYWNlU3R5bGXlsZ7mgKcuT0sg5bey57uP5L+d5a2Y5LqG6KeG5Zu+54q25oCBIDxJbWcgc3JjPSdJbWFnZXNcWEwuanBnJyAvPiAgPGJyPjxicj50aGlzLlZpZXdTdGF0ZUNvbnRyb2wxLkZhY2VTdHlsZS5CYWNrQ29sb3Ig5bey57uP5L+d5a2Y5LqG6KeG5Zu+54q25oCBLCDnnqfvvIzmiJHnmoTpopzoibLlsLHmmK/kv53lrZjnmoTpopzoibIgPEltZyBzcmM9J0ltYWdlc1xYTC5qcGcnIC8+ICA8SW1nIHNyYz0nSW1hZ2VzXEJHLmpwZycgLz4gPGJyPjxicj5kZGTFH8U37EhQNJzHTltcDxNaH2mMfQ==" />

内容明显多了许多,再次证明我们控件的属性被存储到视图状态中了。到现在为止,对控件的四个属性类型测试已经成功!

接下来,再进行另一个测试,在Page_Load中加入下面语句:

protected void Page_Load(object sender, EventArgs e)
{
    //禁用页面视图状态
    Page.EnableViewState = false;
    //或禁用控件视图状态
    this.ViewStateControl1.EnableViewState = false;
}

使用这两句中的任何一句即可,表示禁用页面视图或禁用控件视图,然后再按上面的顺序,依次单击"设置控件属性"和"刷新页面"按钮,就会看到如图6-4所示的页面结果。

图6-4 页面视图状态示例 -- -- 禁用了页面或控件的视图

可以看到,当页面或控件的视图被禁用后,任何属性值将不再被保存。这是控件开发人员无能为力的,因为开发人员为了提高性能可以随意禁止页面或控件状态。重要的一点是,一旦开发人员禁用页面或控件视图后,主控件中的LoadViewState和SaveViewState两个方法在控件生命周期不再被页框架自动调用,也就没有了页面状态启动的"导火线",读者可以在控件中设置一下断点,跟踪调试一下。

但有些涉及控件逻辑的一些非业务数据,我们必须使用视图保存,那怎么办?ASP.NET从2.0版本开始已经支持控件状态ControlState,开发人员可以禁用视图状态,但禁用不了控件状态,在本章6.1节对其使用场景作了描述,如果不记得了请回顾一下前面章节。下一节就讲解一下控件状态机制,并介绍如何通过控件状态解决开发人员禁用视图状态而导致的问题。

6.3 控件状态机制

自ASP.NET 2.0开始支持控件状态机制。控件的状态数据现在能通过控件状态而不是视图状态被保持,控件状态是不能够像视图状态那样被禁用的。由于控件状态的工作方式与视图状态完全一致,并且默认情况下在页面中它们都是存储在同一个隐藏域中,这里对它的工作原理就不多介绍了。

与LoadViewState和SaveViewState类似,控件状态也是提供了一对这样的方法,方法名称分别为LoadControlState和SaveControl State,并且也是在Control基类中提供。

图6-5中加灰底部分是控件状态在控件生命周期中的阶段。

如果启用控件状态功能,只需实现以下几个方法:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class ControlStatePeriod : WebControl
{
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        Page.RegisterRequiresControlState(this);
    }
    protected override object SaveControlState()
    {
        //… …
    }
    protected override void LoadControlState(object savedState)
    {
        //… …
    }
}

这里与视图状态有点不同的是,除了重载SaveControlState和LoadControlState两个方法外,在上面代码中还重写了OnInit方法,并增加如下语句:

 

Page.RegisterRequiresControlState(this);

该方法的功能是将控件注册为具有持久性控件状态的控件,参数是要注册的控件引用。由于在回发事件的过程中,控件状态的注册无法在请求之间进行传递,因此使用控件状态的自定义服务器控件必须对每个请求调用RegisterRequiresControlState方法。即本方法通知页框架在控件生命周期时调用控件状态的两个方法;反过来讲,如果没有通过上面方法注册当前控件,则控件的LoadControlState和SaveControlState方法不会执行。一般在Init事件中注册控件。
下面通过示例说明其用法。在上面的ViewStateControl中通过四个不同类型的属性充分验证了ViewState的可行性。这里我们还是使用这四个经典的属性,并使用控件状态机制完成一个新的控件ControlStateControl。直接看一下它的源代码:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[ToolboxData("<{0}:ControlStateControl runat=server></{0}:ControlStateControl>")]
public class ControlStateControl : WebControl
{
    private string _text;
    [Bindable(true)]
    [DefaultValue("")]
    [Localizable(true)]
    [Category("控件状态")]
    [Description("没有使用视图状态存储")]
    public string Text_NoViewState
    {
        get
        {
            return _text;
        }

        set
        {
            this._text = value;
        }
    }

    [Bindable(true)]
    [DefaultValue("")]
    [Localizable(true)]
    [Category("控件状态")]
    [Description("使用ViewState属性来存储数据此属性")]
    public string Text_ViewState
    {
        get
        {
            String s = (String)ViewState["Text_ViewState"];
            return ((s == null) ? String.Empty : s);

        }

        set
        {
            ViewState["Text_ViewState"] = value;

        }
    }

    private FaceStyle _faceStyle;
    [PersistenceMode(PersistenceMode.InnerProperty)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    [NotifyParentProperty(true)]
    [Category("控件状态")]
    [Description("自定义视图状态(实现IStateManager接口)存储此属性")]
    public FaceStyle FaceStyle
    {
        get
        {
            if (_faceStyle == null)
            {
                _faceStyle = new FaceStyle();
            }

            if (IsTrackingViewState)
            {
                ((IStateManager)_faceStyle).TrackViewState();
            }

            return _faceStyle;
        }
    }

    protected override void RenderContents(HtmlTextWriter output)
    {
        if (DesignMode)
        {
            output.Write("[自定义控件状态存储示例控件]");
        }
    }

    /// <summary>
    /// 由于在回发事件的过程中,控件状态的注册无法在请求之间进行传递,因此使用
    /// 控件状态的自定义服务器控件必须对每个请求调用RegisterRequiresControlState
    /// 方法
    /// </summary>
    /// <param name="e"></param>
    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        Page.RegisterRequiresControlState(this);
    }

    protected override object SaveControlState()
    {
        Pair p = new Pair();
        p.First = base.SaveViewState();
        p.Second = ((IStateManager)FaceStyle).SaveViewState();
        for (int i = 0; i < 2; i++)
        {
            if (p.First != null || p.Second != null)
            {
                return p;
            }
        }
        return null;
    }

    protected override void LoadControlState(object savedState)
    {
        if (savedState == null)
        {
            base.LoadViewState(null);
            return;
        }
        else
        {
            Pair p = (Pair)savedState;
            if (p == null)
            {
                throw new ArgumentException("无效的Control State 数据!");
            }
            base.LoadViewState(p.First);
            if (p.Second != null)
            {
                ((IStateManager)FaceStyle).LoadViewState(p.Second);
            }
        }
    }
}

还有FaceStyle类在ControlStateControl主控件中也作为属性类型使用了,在这里的FaceStyle类的代码没有作任何改变,限于篇幅就不重复贴出来了。

主控件中的逻辑与ViewStateControl中的绝大部分相同。不同的是ControlStateControl重载了OnInit方法并增加向页面中注册当前控件的控件状态的请求,还有LoadViewState和SaveViewState对应变为LoadControlState和SaveControlState。除了这几点,其他的代码逻辑完全相同,在ViewStateControl中已经讲解得非常详细了,如果还有不明白的请回顾一下前面章节。

这里的测试页面也与ViewStateControl的测试页面基本相同。唯一不同的是控件名称,这里限于篇幅也不重复贴出源代码,在本书的随书光盘中有完整的控件示例代码,包括测试页面代码。

本节的示例控件名称为ControlStateControl。在运行之前一定要在Page_Load中禁用视图状态,这样才能确定是控件状态在起作用。禁用视图状态的代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected void Page_Load(object sender, EventArgs e)
{
    //禁用页面控件状态测试
    Page.EnableViewState = false;

    //或禁用控件控件状态测试
    this.ControlStateControl1.EnableViewState = false;
}

您应该还记得,控件状态机制被引入的主要目的就是解决视图状态被禁用问题。基于这,我把页面视图状态和控件的视图状态全部禁用了。
运行页面,依次单击"设置控件属性"和"刷新页面"按钮,可以看到如图6-6所示的运行结果。

跟ViewStateControl成功测试结果完全一致,ControlStateControl控件也正确完成了它的功能。还可以进一步测试,把主控件中的以下语句注释掉:

图6-6 成功保存了控件状态

 

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);
    Page.RegisterRequiresControlState(this);
}

再运行时,可以看到控件不能够保存控件状态了,原因是主控件中的LoadConrolState和SaveControlState不再被页框架调用。可见Page.RegisterRequiresControlState(this)方法的重要性。要想使用控件状态,一定不要忘记这句。

另外,要注意一点,使用控件状态时建议只存储少量控件逻辑必需的数据,一般设置少数几个重要属性即可。这里为了演示其功能把所有类型的属性全部使用控件状态实现,在实际开发中不建议这么做。

这一节内容就讲这么多。如果您看得比较细的话,可能会发现在控件ControlStateControl的代码中,有好多代码是视图状态概念中的,在控件状态主控件中几乎没有作任何修改就能使用,比如我们测试的第二个属性仍然使用了基类Control中的ViewState对象,并且还能够保存视图状态,但是页面视图已经被禁用了,这是怎么回事呢?这就涉及视图状态和控件状态的关系了。在下一节专门探讨这个问题。

6.4 视图状态和控件状态的关系

6.4.1 在禁用视图状态的情况下仍然使用ViewState对象

当开发人员禁用了页面或控件视图状态时。控件开发人员一般在无奈情况下会使用控件状态,要重写LoadControlState,SaveControlState,还有一个OnInit方法,这样固然完全可以实现控件重要数据的控件状态数据保存。但一般LoadControlState和SaveControlState方法都要开发人员自定义编程,比较麻烦,更适合对控件中复杂的自定义类型数据进行对象序列化操作,比如一些简单的类型如:string,int,bool,color,datetime,byte,arraylist等如果是控件重要属性的话,也都挤到LoadControlState 和 SaveControlState方法中,则会显得比较啰唆。

想一下,如果页面视图没有禁用时该多好,可以以this.ViewState["Text"]的格式直接使用Control中的ViewState对象,非常方便。既然ViewState对象不管禁用还是不禁用都在Control类中是存在的,那不用它岂不浪费。这一节主要内容就是如何实现在禁用视图状态下仍然可以使用ViewState对象。

该功能在主控件ControlStateControl中已经实现了。回顾一下主控件的代码片段:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class ControlStateControl : WebControl
{
    //… …
    [Description("使用ViewState属性来存储数据此属性")]
    public string Text_ViewState
    {
        get
        {
            String s = (String)ViewState["Text_ViewState"];
            return ((s == null) ? String.Empty : s);

        }

        set
        {
            ViewState["Text_ViewState"] = value;
        }
    }
    //… …
    protected override object SaveControlState()
    {
        Pair p = new Pair();
        p.First = base.SaveViewState();
        p.Second = ((IStateManager)FaceStyle).SaveViewState();
        //… …
        return p;
    }

    protected override void LoadControlState(object savedState)
    {
        if (savedState == null)
        {
            base.LoadViewState(null);
            return;
        }
        else
        {
            //… …
            base.LoadViewState(p.First);
            //… …
        }
    }
    //… …
}

在ControlStateControl中,属性Text_ViewState仍然存储在基类Control的ViewState对象中,且运行页面中同时禁用了页面和控件视图状态,但该属性仍然能够正确地应用视图状态。

在视图状态启用状态下,在LoadViewState方法中会默认调用基类的base.LoadViewState方法,其中就包含对基类中ViewState对象进行对象序列化的代码,如下:

 

protected virtual void LoadViewState(object savedState)
{
    if (savedState != null)
    {
        this.ViewState.LoadViewState(savedState);
        object obj2 = this.ViewState["Visible"];
        if (obj2 != null)
        {
            if (!((bool)obj2))
            {
                this.flags.Set(0x10);
            }
            else
            {
                this.flags.Clear(0x10);
            }
            this.flags.Set(0x20);
        }
    }
}

其中这句this.ViewState.LoadViewState(savedState)为关键语句,还记得ViewState属性实际的类型为StateBag类,它是系统定义的类型视图状态实现类(我们在6.2.3小节探讨过)。

同样在,SaveViewState方法中也有序列化保存ViewState属性对象的代码,如下所示:

 

protected virtual object SaveViewState()
{
    if (this.flags[0x20])
    {
        this.ViewState["Visible"] = !this.flags[0x10];
    }
    if (this._viewState != null)
    {
        return this._viewState.SaveViewState();
    }
    return null;
}

关键语句为this._viewState.SaveViewState(),ViewState是对外属性,其操作的变量就是StateBag类型的_viewState。

在视图状态被禁用的情况下,由于LoadViewState和SaveViewState不再被页框架调用(默认情况下是base.SaveViewState和base.LoadViewState方法不会再被调用),所以ViewState属性功能也就失效。

了解了ViewState对象的来龙去脉,现在就讲解一下在ControlStateControl控件中仍然可以使用ViewState的原因。在LoadControlState和SaveControlState方法中分别调用base.LoadViewState和base.SaveControlState,我们可以手动调用ViewState属性对象的对象正反序列化过程。归根到底,也就是说开发人员所谓的禁用视图实际上是禁止LoadViewState和SaveViewState两个方法的执行,但理论上我们只要启动控件状态,并把这两个方法的逻辑放到LoadControlState和SaveControlState中,仍然可以利用ViewState。

本节内容有些乖张,违反了ASP.NET设计页面状态的规则。既然ASP.NET框架把视图和控件状态已经分开了,建议在实际开发中分开处理,不要滥用。在LoadControlState方法中尽量只写控件状态相关逻辑,在LoadViewState中只写视图状态相关逻辑,毕竟ControlState是专门为存储控件必需的少量数据设计的。不过上面在ControlState中使用ViewState的确是非常方便的,在处理基本类型的属性时能够节省开发时间。

6.4.2 IStateManager接口仍然可以在控件状态中使用

对于IStateManager的功能微软官方文档是这么定义的:定义任何类为支持服务器控件的视图状态管理而必须实现的属性和方法,服务器控件的视图状态由控件属性的累计值组成。该接口包括保存并加载服务器控件的视图状态值的方法,以及一个指示控件跟踪其视图状态的任何更改的方法。其定义为:

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/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,LoadViewState等都是针对视图状态而非控件状态的。笔者个人认为,这里修改成SaveState和LoadState更好些,因为这个接口完全可以应用于控件状态,这也是本节要说明的内容。

看一下ControlStateControl控件的代码片段:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class ControlStateControl : WebControl
{
    //… …
    private FaceStyle _faceStyle;
    [PersistenceMode(PersistenceMode.InnerProperty)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility. Content)]
    [NotifyParentProperty(true)]
    [Category("控件状态")]
    [Description("自定义视图状态(实现IStateManager接口)存储此属性")]
    public FaceStyle FaceStyle
    {
        get
        {
            if (_faceStyle == null)
            {
                _faceStyle = new FaceStyle();
            }

            if (IsTrackingViewState)
            {
                ((IStateManager)_faceStyle).TrackViewState();
            }

            return _faceStyle;
        }
    }
    //… …
    protected override object SaveControlState()
    {
        Pair p = new Pair();
        p.First = base.SaveViewState();
        p.Second = ((IStateManager)FaceStyle).SaveViewState();            
        return p;
    }
    protected override void LoadControlState(object savedState)
    {
            //… …
            Pair p = (Pair)savedState;
            //… …
            if (p.Second != null)
            {
                ((IStateManager)FaceStyle).LoadViewState(p.Second);
            } 
    }
}

以上代码中主控件属性FaceStyle的定义与ViewStateControl定义完全一样;另外,在LoadControlState和SaveControlState中对FaceStyle属性的对象序列化也是与ViewStateControl中的完全一样。FaceStyle属性对应的类型为FaceStyle,ViewStateControl也是使用的同一个类。

这说明IStateManager完全可以用于控件状态。不同的是,这里是由LoadControlState和SaveControlState两个方法驱动,而不是由原来的LoadViewState和SaveViewState方法驱动。同时再一次证明IStateManger只是一个接口,实现它的类默认与页框架没有任何调用关系,必须由主控件中视图状态的两个方法或控件状态的两个方法调用。

6.4.3 视图状态和控件状态组合使用规则

在实际开发时,除了上面的IStateManger接口可以分别用于视图状态和控件状态外,视图状态的两个方法(LoadViewState和SaveViewState)和控件状态的两个方法(LoadControlState和SaveControlState)可以同时使用,即视图状态和控件状态一起使用。

这样做的好处是把数据量相对比较大的属性或用户数据属性作为ViewState存储,把仅与控件逻辑相关的属性,比如像选项卡控件的当前选中索引CurrentSelectIndex这样的属性,作为控件状态存储。

这时就要注意分清楚它们的使用职责了,像类似6.4.1节讲的用法就避免使用,以防出现意想不到的错误,除非我们非常了解基类中的一些方法到底做了些什么事情,但这比较难。

6.5 加密页面状态

本节介绍ASP.NET对视图信息的加密功能。Page.RegisterRequiresViewStateEncryption方法就是将控件注册为需要视图状态加密的控件。如果您要开发用于处理潜在的敏感信息的自定义控件,请调用RegisterRequiresViewStateEncryption方法向页注册控件,并确保该控件的视图状态信息已加密。

RegisterRequiresViewStateEncryption方法必须在页生命周期的PreRender阶段中或该阶段之前调用,下面在前面ViewStateControl控件中重写OnPreRender方法并加入视图加密功能,增加后的代码段如下:

 

protected override void OnPreRender(EventArgs e)
{
    this.Page.RegisterRequiresViewStateEncryption();
    base.OnPreRender(e);
}

编译控件并在浏览器中重新运行,可以看到发送到客户端的隐藏域视图控件中的值由:

 

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value= "/wEPDwULLTE3NjU0NjM4OTEPZBYCAgMPZBYEAgEPDw8WAh4OVGV4dF9WaWV3U3RhdGUFIuaIkeaYr+eUqFZpZXdTdGF0ZeWuueWZqOWtmOWCqOeahCFkFCsAAhYEHglCYWNrQ29sb3IKAB4EXyFTQgIIZ2QCBw8PFgIeBFRleHQF9QN0aGlzLlZpZXdTdGF0ZUNvbnRyb2wxLlRleHRfTm9WaWV3U3RhdGXlsZ7mgKcg5rKh5pyJ5L+d5a2Y6KeG5Zu+54q25oCBIDxJbWcgc3JjPSdJbWFnZXNcSlMuanBnJyAvPiAgPGJyPjxicj50aGlzLlZpZXdTdGF0ZUNvbnRyb2wxLlRleHRfVmlld1N0YXRl5bGe5oCnIOW3sue7j+S/neWtmOS6huinhuWbvueKtuaAgSA8SW1nIHNyYz0nSW1hZ2VzXFhMLmpwZycgLz4gIDxicj48YnI+dGhpcy5WaWV3U3RhdGVDb250cm9sMS5GYWNlU3R5bGXlsZ7mgKcuT0sg5bey57uP5L+d5a2Y5LqG6KeG5Zu+54q25oCBIDxJbWcgc3JjPSdJbWFnZXNcWEwuanBnJyAvPiAgPGJyPjxicj50aGlzLlZpZXdTdGF0ZUNvbnRyb2wxLkZhY2VTdHlsZS5CYWNrQ29sb3Ig5bey57uP5L+d5a2Y5LqG6KeG5Zu+54q25oCBLCDnnqfvvIzmiJHnmoTpopzoibLlsLHmmK/kv53lrZjnmoTpopzoibIgPEltZyBzcmM9J0ltYWdlc1xYTC5qcGcnIC8+ICA8SW1nIHNyYz0nSW1hZ2VzXEJHLmpwZycgLz4gPGJyPjxicj5kZGTFH8U37EhQNJzHTltcDxNaH2mMfQ==" />

变成了已经加密的散列码:

 

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value= "J62G77yDi1rdf8+ZXszW1d3eAPnuzO8h2MCNPEN2HR6daQNjLLamfq4EHwWRRJ16S6kHFp43gwVPkMB9RPQtMaI5Gc+lZ9orQjsZWpvaVDTBI4fF6wFRj7qY1r2hREghEQYM/e1a+JX9oiWk1kUs8vFJ3SNeXCkCdDb7fQtr6DlCKsbCGGWDrawgiIsI0O5pnYMPloa+z74clY6/DYZ23BIAZNNYDLML/e7mVIzSS+V4FBZsXxBBdRx1oWJ60wjrfkScl1g0Dpbn+LZ2NIu7nh2t5Xu8iTW1NmjSfUoH9ymBmQkCNek3jaex18n9wBOdLGsQ8ZxO/hAGVqbcqTQStiuRYezjvm8T9Q0U9cj9SI+FnS2PktVHXqh6qLsjH1Dlwm+qFyjHK2fvbH+WA2NZUq3HzObR4GzNFRlZn8ZliO/FDJEKyH+x9X7qneaJs9dTg6mo/qbvI+S0W1viQ/nj/OrC3PoiLwvwyd1WjBlND4B21rVjnoq/J+jPNQGE+AdQR2fW3kYAtGhKf6PSVmUm9fZ26JY2rkYw2vBahPWmxHqnf9V5yt/0D3LP9Mx7KYay4PAItTeEzIT4G5I0Hp65a8d/QQJbQTCs1Qz798Mei66mqF8QrOBxR2EUsS+uCekxQe+2xDhX2kkxntoqxgRJ4/n8cdKr2Z+K3F3IEmxObo+QvQ5wUtMWrsKIY8Jad562zCNBGjlyU72i+KuORHQCBoXRgt5vPeIBpdjb0Vk3tb6t7g2T5yFyB7PBTqEwV43Ws4/BPxYsdr9YCNOJd/TB6cDI5FUS/dGYTej2EZt2HOPCM758WeEwBQZuG+t1dFEZU6+vicImHDV8ifCwNv+Yhw==" />
</div>

还可以指定视图状态信息的加密模式,通过系统枚举ViewStateEncryptionMode来设置,该枚举可以控制是否加密视图状态信息,它有三个枚举项,如表6-1所示。

表6-1 ViewStateEncryptionMode的枚举项

枚举成员 说明
Auto 如果控件通过调用RegisterRequiresViewStateEncryption方法请求加密,则加密视图状态信息;否则不加密。此值是Page.ViewStateEncryptionMode属性的默认值
Always 强制加密视图状态信息,而不管有没有调用
Never 即使控件请求加密,也不加密视图状态信息

以上枚举项除了在控件中设置外,还可以在配置文件配置应用程序级别,或者在Page页面中配置成页面级别。在控件或页面中应用时仅作如下设置:

this.Page.ViewStateEncryptionMode = ViewStateEncryptionMode.Auto;
this.Page.RegisterRequiresViewStateEncryption();

经过如上设置后,页框架会自动完成加密与解密功能。对该功能再补充两点:

(1)加密视图状态肯定会影响到应用程序的性能,所以使用时要谨慎。

(2)只要页面中有任何一个控件要求对视图状态进行加密,那么页面中所有的视图状态也都将被加密。目前ASP.NET还没有实现针页面中某个控件视图加密功能(在调用时不管是在控件中还是在页面中都是调用页面控件Page的方法RegisterRequiresViewStateEncryption,且该方法没有任何可重载方法)。

6.6 清除页面状态

在控件开发时,有时候需要清除子控件的页面状态,比如在创建子控件时,以下是一个在数据绑定时清除视图并创建子控件的应用场景:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public override void DataBind()
{
    base.OnDataBinding(EventArgs.Empty);
    Controls.Clear();
    ClearChildViewState(); //清理视图状态
    TrackViewState();
    CreateControlHierarchy(true);
    ChildControlsCreated = true;
}

本节介绍几个清理页面状态的方法,主要由Control基类提供。

1.HasChildViewState方法

获取一个值,该值指示当前服务器控件的子控件是否具有任何已保存的视图状态设置。如果有任何子控件具有已保存的视图状态信息,则返回true;否则返回false。

2.IsChildControlStateCleared方法

获取一个值,该值指示当前控件中包含的控件是否具有控件状态。如果该控件的子级不使用控件状态,则返回true;否则返回false。

3.ClearChildState方法

同时清除子控件的视图状态和控件状态。其方法体如下:

 

protected void ClearChildState()
{
    this.ClearChildControlState();
    this.ClearChildViewState();
}
4.ClearChildControlState方法
清除控件状态。其代码如下:
protected void ClearChildControlState()
{
    if (this.ControlState >= ControlState.Initialized)
    {
        this.flags.Set(0x40000);
        if (this.Page != null)
        {
            this.Page.RegisterRequiresClearChildControlState(this);
        }
    }
}

控件状态是通过方法:

Page.RegisterRequiresControlState(this);

将控件注册为具有持久性控件状态的控件。同时也要调用方法:

this.Page.RegisterRequiresClearChildControlState(this);

清除控件状态,不同的是:此方法是内部方法。不供开发人员调用,只能通过ClearChild ControlState方法间接调用。

5.ClearChildViewState方法

此方法主要清除子控件的视图状态,其代码结构如下:

 

protected void ClearChildViewState()
{
    if (this._occasionalFields != null)
    {
        this._occasionalFields.ControlsViewState = null;
    }
}

方法体中设置控件视图状态的IDictionary对象ControlsViewState为null。

6.7 对动态添加控件的视图状态分析

这一节讲点与视图状态相关的有趣的东西。我们在开发ASP.NET页面时,有时候在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);
}

以上代码首先动态创建一个ListBox控件lb,在页面第一次请求时(提交后不再执行此句)为它增加一个集合项"子项1",最后把lb添加到当前form集合中。
这时如果单击页面中事先放置的"提交"按钮,则页面提交后不再看到"子项1",也就是说在视图状态中没有存储该item项。

为了解决此问题,把代码修改成:

 

protected void Page_Load(object sender, EventArgs e)
{
    ListBox lb = new ListBox();
    (lb.Items as IStateManager).TrackViewState();
    if (!Page.IsPostBack)
    {
        lb.Items.Add("子项1");
    }
    this.form1.Controls.Add(lb);
}

以上代码在为ListBox控件lb增加集合子项时之前,先启用Items属性的视图跟踪监控。这样当页面再提交时,则集合项"子项1"仍然能够呈现。扩展一下,多增加几个Item并设置几个为选中状态,再次提交页面,增加的几个item仍然能够被视图状态成功保存,且能够记住当前是否选中状态。这是为什么呢?原来ListBox控件的属性Items的继承基类ListControl中有个ListItemCollection类型的items对象,像"子项1"这些item就是被增加到items对象中进行管理的。关键的一点是,ListItemCollection集合对象实现了IStateManager接口,看一下它的类定义:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public sealed class ListItemCollection : IList, ICollection, IEnumerable, IStateManager
{
    // Fields
    private ArrayList listItems;
    private bool marked;
    private bool saveAll;

    // Methods
    public ListItemCollection();
    public void Add(string item);
    public void Add(ListItem item);
    public void AddRange(ListItem[] items);
    public void Clear();
    public bool Contains(ListItem item);
    public void CopyTo(Array array, int index);
    public ListItem FindByText(string text);
    internal int FindByTextInternal(string text, bool includeDisabled);
    public ListItem FindByValue(string value);
    internal int FindByValueInternal(string value, bool includeDisabled);
    public IEnumerator GetEnumerator();
    public int IndexOf(ListItem item);
    public void Insert(int index, string item);
    public void Insert(int index, ListItem item);
    internal void LoadViewState(object state);
    public void Remove(string item);
    public void Remove(ListItem item);
    public void RemoveAt(int index);
    internal object SaveViewState();
    int IList.Add(object item);
    bool IList.Contains(object item);
    int IList.IndexOf(object item);
    void IList.Insert(int index, object item);
    void IList.Remove(object item);
    void IStateManager.LoadViewState(object state);
    object IStateManager.SaveViewState();
    void IStateManager.TrackViewState();
    internal void TrackViewState();

    // Properties
    public int Capacity { get; set; }
    public int Count { get; }
    public bool IsReadOnly { get; }
    public bool IsSynchronized { get; }
    public ListItem this[int index] { get; }
    public object SyncRoot { get; }
    bool IList.IsFixedSize { get; }
    object IList.this[int index] { get; set; }
    bool IStateManager.IsTrackingViewState { get; }
}

ListItemCollection类实现了IStateManager接口下的几个视图状态相关方法,因为它自身具有视图状态管理能力。基于此,在增加到ListBox对象子项之前,调用这句:

(lb.Items as IStateManager).TrackViewState();

把它的视图跟踪监控功能打开,即从此刻起视图状态将记录它所有的修改,当然包括它增加的"子项1"项。这就是页面提交"子项1"仍然能够显示在页面上的原因。

只有实现了IStateManger接口的集合才能够调用TrackViewState方法打开视图监控。如果没有实现此接口的对象,还有一种方法可以实现视图状态功能,把最初的代码修改成如下格式:

 

protected void Page_Load(object sender, EventArgs e)
{ 
    ListBox lb = new ListBox();
    this.form1.Controls.Add(lb);
    if (!Page.IsPostBack)
    {
        lb.Items.Add("子项1");
    }
}

在上面的代码中对之前不能够保存视图状态的代码,仅调换了一下代码顺序。把this.form1.Controls.Add(lb)这句放到了lb.Item.Add("子项1")之前。运行页面并单击"提交"按钮,就会发现item"子项1"也能够保存视图状态。仅作了代码调整即使lb实现了视图状态保存,这是什么原因呢?这里的关键语句:

 

this.form1.Controls.Add(lb);

中的关键方法Add起了重要作用。Controls集合的Add方法最终调用的是Control控件基类中的方法AddedControl。直接看一下AddedControl的代码实现:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected internal virtual void AddedControl(Control control, int index)
{
    if (control.OwnerControl != null)
    {
        throw new InvalidOperationException(SR.GetString("Substitution_ 
        NotAllowed"));
    }
    if (control._parent != null)
    {
        control._parent.Controls.Remove(control);
    }
    control._parent = this;
    control._page = this.Page;
    control.flags.Clear(0x20000);
    Control namingContainer = this.flags[0x80] ? this : this._namingContainer;
    if (namingContainer != null)
    {
        control.UpdateNamingContainer(namingContainer);
        if ((control._id == null) && !control.flags[0x40])
        {
            control.GenerateAutomaticID();
        }
        else if ((control._id != null) || ((control._occasionalFields != null) 
        && (control._occasionalFields.Controls != null)))
        {
            namingContainer.DirtyNameTable();
        }
    }
    if (this._controlState >= ControlState.ChildrenInitialized)
    {
        control.InitRecursive(namingContainer);
        if (((control._controlState >= ControlState.Initialized) && (control. 
        RareFields != null)) && control.RareFields.RequiredControlState)
        {
            this.Page.RegisterRequiresControlState(control);
        }
        if (this._controlState >= ControlState.ViewStateLoaded)
        {
            object savedState = null;
            if ((this._occasionalFields != null) && (this._occasionalFields. 
            ControlsViewState != null))
            {
                savedState = this._occasionalFields.ControlsViewState[index];
                if (this.LoadViewStateByID)
                {
                    control.EnsureID();
                    savedState =this._occasionalFields.ControlsViewState [control. ID];
                    this._occasionalFields.ControlsViewState.Remove (control.ID);
                }
                else
                {
                    savedState = this._occasionalFields.ControlsViewState[index];
                    this._occasionalFields.ControlsViewState.Remove(index);
                }
            }
            control.LoadViewStateRecursive(savedState);
            if (this._controlState >= ControlState.Loaded)
            {
                control.LoadRecursive();
                if (this._controlState >= ControlState.PreRendered)
                {
                    control.PreRenderRecursiveInternal();
                }
            }
        }
    }
}

这是.NET类库代码,不必全部看懂。但要知道此方法能够启用视图状态和向页面请求注册控件状态(假如需要的话)。

视图状态跟踪被启用后,接下来执行对item"子项1"的增加当然也能够被页框架视图管理器保存。

6.8 自定义类型转换器实现高效率序列化

由于ViewState数据是作为序列化格式串存储的,在视图装载和保存阶段,需要进行正反序列化。这对于复杂对象是比较耗资源的,还好系统已经对一些如ArrayList和哈希表集合等类型对象进行了优化;但对于我们自定义的类对象,在默认情况下使用.NetFramework提供的二进制序列化功能来序列化对象,比较耗资源,解决的办法是为自定义类提供专门的类型转换器(TypeConverter),实现字符串到对象的相互转换,这种方法比默认.Net提供的二进制序列化要节省资源。

类型转换器在第4章的4.6.2节已经讲解过了,这里不再多说,如果还不明白请回顾一下前面章节。之前定义的类型转换器主要用于设计器中的设计与代码视图切换。这里我们把类型转换器应用于视图状态。下面就来看一个例子,示例控件名称为ViewStateTypeConverter,示例中仍然使用在第4章我们自定义的三维坐标类SolidCoordinate和该类的自定义类型转换器SolidCoordinateConverter。这两个类代码比较简单,先回顾一下这两个类的代码。SolidCoordinate代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[TypeConverter(typeof(SolidCoordinateConverter))]
public class SolidCoordinate
{
    private int x;
    private int y;
    private int z;

    public SolidCoordinate()
    {        
    } 

    public SolidCoordinate(int x, int y, int z)
    {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    [NotifyParentProperty(true)]
    public int X
    {
        get
        {
            return this.x;
        }
        set
        {
            this.x = value;
        }
    }

    [NotifyParentProperty(true)]
    public int Y
    {
        get
        {
            return this.y;
        }
        set
        {
            this.y = value;
        }
    }

    [NotifyParentProperty(true)]
    public int Z
    {
        get
        {
            return this.z;
        }
        set
        {
            this.z = value;
        }
    }
}
SolidCoordinateConverter类的代码如下:
/// <summary>
///  SolidCoordinate类的自定义类型转换器
/// </summary>
public class SolidCoordinateConverter : TypeConverter
{
    
    /// <summary>
    /// 判断是否能从字符串转换为SolidCoordinate类
    /// </summary>
    /// <param name="context"></param>
    /// <param name="sourceType"></param>
    /// <returns></returns>
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type 
    sourceType)
    {
        return ((sourceType == typeof(string)) || base.CanConvertFrom(context, 
        sourceType));
    }

    /// <summary>
    /// 判断是否能从SolidCoordinate类转换为string或InstanceDescriptor类型
    /// </summary>
    /// <param name="context"></param>
    /// <param name="destinationType"></param>
    /// <returns></returns>
    public override bool CanConvertTo(ITypeDescriptorContext context, Type 
    destinationType)
    {
        return ((destinationType == typeof(InstanceDescriptor)) || base. 
        CanConvertTo(context, destinationType));
    }
  
    

    /// <summary>
    /// 从字符串转换为SolidCoordinate类型
    /// </summary>
    /// <param name="context"></param>
    /// <param name="culture"></param>
    /// <param name="value"></param>
    /// <returns></returns>
    public override object ConvertFrom(ITypeDescriptorContext context, 
    CultureInfo culture, object value)
    {
        string str = value as string;
        if (str == null)
        {
            return base.ConvertFrom(context, culture, value);
        }
        string str2 = str.Trim();
        if (str2.Length == 0)
        {
            return null;
        }
        if (culture == null)
        {
            culture = CultureInfo.CurrentCulture;
        }
        char ch = culture.TextInfo.ListSeparator[0];
        string[] strArray = str2.Split(new char[] { ch });
        int[] numArray = new int[strArray.Length];
        TypeConverter converter = TypeDescriptor.GetConverter(typeof(int));
        for (int i = 0; i < numArray.Length; i++)
        {
            numArray[i] = (int)converter.ConvertFromString(context, culture, 
            strArray[i]);
        }
        if (numArray.Length != 3)
        {
            throw new Exception("格式不正确!");
        }
        return new SolidCoordinate(numArray[0], numArray[1], numArray[2]);
    }

    /// <summary>
    /// 从SolidCoordinate类转换为string或InstanceDescriptor类型
    /// </summary>
    /// <param name="context"></param>
    /// <param name="culture"></param>
    /// <param name="value"></param>
    /// <param name="destinationType"></param>
    /// <returns></returns>
    public override object ConvertTo(ITypeDescriptorContext context, 
    CultureInfo culture, object value, Type destinationType)
    {
        if (destinationType == null)
        {
            throw new Exception("目标类型不能为空!");
        }
        if (value is SolidCoordinate)
        {
            if (destinationType == typeof(string))
            {
                SolidCoordinate solidCoordinate = (SolidCoordinate)value;
                if (culture == null)
                {
                    culture = CultureInfo.CurrentCulture;
                }
                string separator = culture.TextInfo.ListSeparator + " ";
                TypeConverter converter=TypeDescriptor.GetConverter(typeof(int));
                string[] strArray = new string[3];
                int num = 0;
                strArray[num++] = converter.ConvertToString(context, culture, s
                olidCoordinate.X);
                strArray[num++] = converter.ConvertToString(context, culture, 
                solidCoordinate.Y);
                strArray[num++] = converter.ConvertToString(context, culture, 
                solidCoordinate.Z);
                return string.Join(separator, strArray);
            }
            if (destinationType == typeof(InstanceDescriptor))
            {
                SolidCoordinate solidCoordinate2 = (SolidCoordinate)value;
                ConstructorInfo constructor =typeof(SolidCoordinate). 
                GetConstructor(new Type[]{typeof(int),typeof(int),typeof(int)});
                if (constructor != null)
                {
                    return new InstanceDescriptor(constructor, new object[] 
{ solidCoordinate2.X, solidCoordinate2.Y, solidCoordinate2.Z });
                }
            }
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }

    /// <summary>
    /// 根据上下文和指定的属性字典创建实例
    /// </summary>
    /// <param name="context"></param>
    /// <param name="propertyValues"></param>
    /// <returns></returns>
    public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues)
    {
        if (propertyValues == null)
        {
            throw new Exception("属性值不能为空!");
        }
        object obj2 = propertyValues["X"];
        object obj3 = propertyValues["Y"];
        object obj4 = propertyValues["Z"];
        if (((obj2 == null) || (obj3 == null) || (obj4 == null)) || (!(obj2 is 
        int) || !(obj3 is int) || !(obj4 is int)))
        {
            throw new Exception("格式不正确!");
        }
        return new SolidCoordinate((int)obj2, (int)obj3, (int)obj4);
    }
  

    /// <summary>
    /// 如果更改此对象的属性需要调用CreateInstance 来创建新值,则返回true;否则为false
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
    {
        return true;
    }        
    

    /// <summary>
    /// 使用指定的上下文和attributes集合作为参数,返回由参数value指定的集合数组
    /// </summary>
    /// <param name="context"></param>
    /// <param name="value"></param>
    /// <param name="attributes"></param>
    /// <returns></returns>
    public override PropertyDescriptorCollection GetProperties (ITypeDescriptorContext context, object value, Attribute[] attributes)
    {
        return TypeDescriptor.GetProperties(typeof(SolidCoordinate), attributes).Sort(new string[] { "X", "Y", "Z" });
    }     

    /// <summary>
    /// 使用指定的上下文返回该对象是否支持单属性录入[如果指定, 属性窗口也会提
    /// 供扩展输入模式]
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public override bool GetPropertiesSupported(ITypeDescriptorContext context)
    {
        return true;
    }
}

这两个类在第4章已经讲解过,这里就不再介绍了。如果还不明白,请回顾一下4.6.2.1小节。

接下来讲解我们最关注的主控件。在主控件中增加一个SolidCoordinate类型的属性,代码如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
[DefaultProperty("SolidCoordinate")]
[ToolboxData("<{0}:ViewStateTypeConverter runat=server></{0}:ViewStateTypeConverter>")]
public class ViewStateTypeConverter : WebControl
{
    SolidCoordinate solidCoordinate;
    [Category("转换器")]
    [Description("SolidCoordinate类型属性(具有三个值的扩展类型)")]
    public SolidCoordinate SolidCoordinate
    {
        get
        {
            if (solidCoordinate == null)
            {
                solidCoordinate = new SolidCoordinate();
            }
            return solidCoordinate;
        }
        set
        {
            solidCoordinate = value;
        }
    }
    //… …
}

主控件类代码中就包括一个复杂属性。先不在主控件中增加视图相关的代码,而在页面中测试一下它现在是否能够保存视图状态。编译控件,在页面中放置一个ViewStateType Converter控件,增加一个"提交"按钮,最终页面的前台代码如下:

 

<body>
    <form id="form1" runat="server">
    <div>
        <cc1:viewstatetypeconverter id="ViewStateTypeConverter1" runat= "server"></cc1:viewstatetypeconverter>
        <br />
        <asp:Button ID="Button1" runat="server" Text="提交页面" OnClick= "Button1_Click" />    
    </div>
    </form>
</body>
//然后在Page_Load中增加页面首次请求时的属性赋值
protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack)        
    {
        ViewStateTypeConverter1.SolidCoordinate = new KingControls.Solid Coordinate(1, 2, 3);
    }
}
protected void Button1_Click(object sender, EventArgs e)
{
    this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "OutputResult", "alert('(X:" + ViewStateTypeConverter1.SolidCoordinate. X.ToString() + ",Y:" + ViewStateTypeConverter1.SolidCoordinate.Y.ToString() + ",Z:" + ViewStateTypeConverter1.SolidCoordinate.Z.ToString() + ")');", true);        
}

在上面的代码中还增加了"提交"按钮的事件,主要是通过客户端alert方法输出当前控件的属性值。通过输出结果确认视图状态是否成功保存,如果输出值不等于我们设置的(1,2,3),则说明该属性没有保存到视图状态中。

在浏览器中运行页面。由于这是页面第一次请求,会执行Page_Load中的控件赋值语句,给控件的属性SolidCoordinate赋值(1,2,3),然后单击"提交"按钮,输出结果如图6-7所示。

通过示例证明,现在主控件还不能对属性SolidCoordinate进行视图状态保存,还得我们自己去实现这个功能。OK,继续在主控件中增加视图状态相关的两个方法LoadViewState和SaveViewState,这两个方法最终的代码如下:

图6-7 SolidCoordinate属性值没有保存到ViewState中

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class ViewStateTypeConverter : WebControl
{
    //… …
    protected override object SaveViewState()
    {
        Pair p = new Pair();
        p.First = base.SaveViewState();
        SolidCoordinateConverter convert = new SolidCoordinateConverter();
        p.Second = convert.ConvertTo(null, null, this.SolidCoordinate,typeof (string));
        for (int i = 0; i < 2; i++)
        {
            if (p.First != null || p.Second != null)
            {
                return p;
            }
        }
        return null;
    }

    protected override void LoadViewState(object savedState)
    {
        if (savedState == null)
        {
            base.LoadViewState(null);
            return;
        }
        else
        {
            Pair p = (Pair)savedState;
            if (p == null)
            {
                throw new ArgumentException("无效的View State 数据!");
            }
            base.LoadViewState(p.First);
            if (p.Second != null)
            {
                SolidCoordinateConverter convert = new SolidCoordinate Converter();
                this.SolidCoordinate = convert.ConvertFrom(null, null, p.
                Second) as SolidCoordinate;
            }
        }
    }
    //… …
}

先讲解一下SaveViewState方法中的代码逻辑。首先定义一个Pair对象p,成员p.First负责存储基类中序列化后的对象(其中就包括ViewState属性对象);至于成员p.Second,笔者用它来存储SolidCoordinate属性,主要通过这两句完成序列化功能:

SolidCoordinateConverter convert = new SolidCoordinateConverter();

p.Second=convert.ConvertTo(null,null,this.SolidCoordinate,typeof(string));

第一条语句定义SolidCoordinate类的转换器对象,第二条语句使用转换器对象的ConvertTo方法把SolidCoordinate类型的属性转换为字符串,存储到p.Second属性中。最后返回p作为整个控件的对象序列化结果。

而LoadViewState逻辑正好相反,主要功能是把视图信息对象再赋值给控件属性(包括基类的属性)。不同的是这次调用的是ConvertFrom方法。

增加这两个方法后,主控件已经使SolidCoordinate属性具有了视图状态保存能力。再在浏览器中运行页面验证一下,单击"提交"按钮后输出的结果如图6-8所示。

图6-8 SolidCoordinate属性值成功保存到ViewState中

另外,如果在自定义类内部关联了类型转换器,则还可以把这句:

SolidCoordinateConverter convert = new SolidCoordinateConverter();

修改成如下写法:

TypeConverter convert = TypeDescriptor.GetConverter(this.SolidCoordinate);

这两句功能相同,不同的是后者使用TypeDescriptor的GetConverter静态方法返回指定类的关联转换器。

这一节内容就讲这么多。本节以SolidCoordinate类为典型例子,说明了类型转换器在视图状态中的用法。使用这种方式,为自己的控件属性的增加视图状态时,就比较容易了。噢,忘了一点,还要记住我们是为了提高序列化速度而采用的这种方法。

6.9 页面状态性能优化策略

6.9.1 存储位置优化 -- -- 把视图状态信息保存在服务端而非客户端

视图状态信息默认情况下是存储在客户端的,不占用服务端资源(这里是指持续地占用服务器资源一定时间,视图状态只是在页面呈现时,服务端正反向解析视图状态内容占用一会服务器内存,当页面呈现完成后则会把视图存储到页面上的隐藏控件域中)。当页面结构比较复杂时,会导致视图信息字节数比较大,则会产生带宽瓶颈。为了解决这个问题我们可以选择把视图信息存储到服务器端,或数据库中,或文件等存储介质。这一节我们就以把视图状态存储到服务端Session中为例,说明其实现方法。

新建一个页面SessionPageStatePersister.aspx,页面中的内容仍然使用前面的控件状态控件的测试页面内容,设置好页面内容后,在后台代码中重写基类Page的PageStatePersister属性,如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public partial class Page_SessionPageStatePersister : System.Web.UI.Page
{
    protected override PageStatePersister PageStatePersister
    {
        get
        {
            return new SessionPageStatePersister(this);
        }
    }
}
为了更容易理解,下面是Page对该属性的系统默认实现源代码:
protected virtual PageStatePersister PageStatePersister
{
    get
    {
        if (this._persister == null)
        {
            PageAdapter pageAdapter = this.PageAdapter;
            if (pageAdapter != null)
            {
                this._persister = pageAdapter.GetStatePersister();
            }
            if (this._persister == null)
            {
                this._persister = new HiddenFieldPageStatePersister(this);
            }
        }
        return this._persister;
    }
}

ASP.NET的默认持久性机制是使用HiddenFieldPageStatePersister类将视图状态存储在客户端,即系统默认实现是在属性PageStatePersister中返回HiddenFieldPageStatePersister类实例,表示存储在客户端的隐藏域中。上面我们重写该属性,返回SessionPageStatePersister类实例,SessionPageStatePersister类表示在Web服务器Session会话中存储ASP.NET页视图状态。

OK,运行一下页面,并查看一下它的隐藏域视图信息,会发现__VIEWSTATE由原来的:

 

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value= "/wEPDwUJNzIxMjkzNTM2D2QWAgIDD2QWBAIBDw8WAh4OVGV4dF9WaWV3U3RhdGUFIuaIkeaYr+eUqFZpZXdTdGF0ZeWuueWZqOWtmOWCqOeahCFkZAIHDw8WAh4EVGV4dAWQBHRoaXMuQ29udHJvbFN0YXRlQ29udHJvbDEuVGV4dF9Ob1ZpZXdTdGF0ZeWxnuaApyDmsqHmnInkv53lrZjmjqfku7bnirbmgIEgPEltZyBzcmM9Jy4uXEltYWdlc1xKUy5qcGcnIC8+ICA8YnI+PGJyPnRoaXMuQ29udHJvbFN0YXRlQ29udHJvbDEuVGV4dF9WaWV3U3RhdGXlsZ7mgKcg5bey57uP5L+d5a2Y5LqG5o6n5Lu254q25oCBIDxJbWcgc3JjPScuLlxJbWFnZXNcWEwuanBnJyAvPiAgPGJyPjxicj50aGlzLkNvbnRyb2xTdGF0ZUNvbnRyb2wxLkZhY2VTdHlsZeWxnuaApy5PSyDlt7Lnu4/kv53lrZjkuobmjqfku7bnirbmgIEgPEltZyBzcmM9Jy4uXEltYWdlc1xYTC5qcGcnIC8+ICA8YnI+PGJyPnRoaXMuQ29udHJvbFN0YXRlQ29udHJvbDEuRmFjZVN0eWxlLkJhY2tDb2xvciDlt7Lnu4/kv53lrZjkuobmjqfku7bnirbmgIEsIOeep++8jOaIkeeahOminOiJsuWwseaYr+S/neWtmOeahOminOiJsiA8SW1nIHNyYz0nLi5cSW1hZ2VzXFhMLmpwZycgLz4gIDxJbWcgc3JjPScuLlxJbWFnZXNcQkcuanBnJyAvPiA8YnI+PGJyPmRkGAEFFENvbnRyb2xTdGF0ZUNvbnRyb2wxDw8PFgIfAAUi5oiR5piv55SoVmlld1N0YXRl5a655Zmo5a2Y5YKo55qEIWQUKwACFgQeCUJhY2tDb2xvcgoAHgRfIVNCAghnZHweEc0h/1gIsxczc2fHdmLTQy1x" />
变为现在的:
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value= "/wEPaA8FDzhjYTlhMjI1ZDY2NTQ3MxgBBRRDb250cm9sU3RhdGVDb250cm9sMQ8PDxYCHg5UZXh0X1ZpZXdTdGF0ZQUi5oiR5piv55SoVmlld1N0YXRl5a655Zmo5a2Y5YKo55qEIWQUKwACFgQeCUJhY2tDb2xvcgoAHgRfIVNCAghnZNiSxDj0eWw88GLxQrKC0IucFKmW" />

另外,这里的HiddenFieldPageStatePersister和SessionPageStatePersister都是继承基类 System.Web.UI.PageStatePersister,该基类允许自定义持久化机制,如果既不想存储到客户端隐藏域中,也不想存储到Session中,可以定义该类的派生类,实现自定义持久化机制。在6.8.2小节有一个派生System.Web.UI.PageStatePersister 类的例子。

除了使用重写基类Page的PageStatePersister属性实现指定持久化类对象外,还可以通过实现System.Web.UI.Adapters.PageAdapter类的页配器实现同样功能。PageAdapter类是一个抽象类,可以修改网页以适应特定的浏览器,并提供所有页面适配器可直接或间接继承的基类。下面是一个自定义页适配器的例子:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class SessionPageAdapter : System.Web.UI.Adapters.PageAdapter
{
    public override PageStatePersister GetStatePersister()
    {
        return new SessionPageStatePersister(this.Page);
    }
}
然后在站点下面添加App_Browsers目录,并添加一个文件名称为SessionPageAdapter.browser的文件,内容如下:
<browsers>
    //… …
    <browser refID="Default">
      <controlAdapters>
        <adapter controlType="System.Web.UI.Page" adapterType="KingControls. SessionPageAdapter" />
      </controlAdapters>
    </browser>  
</browsers>

运行一下页面,也会发现得到与之前相同的效果。这两种方法都能够在服务器Session中存储视图信息。不同的一点是后面这种方式影响到了整个站点下面的所有页面。

Session一般用于存储特定于单独会话的短期信息,比存储到客户端隐藏域中方式要安全得多。但这种方式要占用服务器内存,因此在客户端比较多的情况下不要在Session中存储大量的信息。

6.9.2 体积优化 -- -- 压缩视图状态数据

这一节讲解另一种优化技术,在序列化对象信息前先压缩数据。其实现机制也是重写基类Page的PageStatePersister属性,下面就介绍一下它的实现。

首先定义一个实现System.Web.UI.PageStatePersister基类的类CompressPageStatePersister,代码如下所示:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class CompressPageStatePersister : PageStatePersister
{
    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);
            stateString = CompressHelp.Compress(statePair);
            Page.ClientScript.RegisterHiddenField(PageStateKey, stateString);
        }
    }
}

在以上代码中重写了PageStatePersister类的的Load和Save两个方法。PageStateKey变量为页面状态保存在页面中的字段名。

在Load方法中,首先通过语句:

string postbackState = Page.Request.Form[PageStateKey];

取得保存到客户端压缩了的状态信息,然后调用压缩类CompressHelp中的Decompress方法解压缩返回,然后赋值给基类中的this.ViewState和this.ControlState,这两个对象分别标志视图状态和控件状态。

Save的过程正好相反,将当前对象的数据压缩,并通过Page.RegisterHiddenField方法把值注册到客户端隐藏控件。

压缩类CompressHelp主要提供了两个静态方法,代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
public class 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;
    }
}

最后,把定义的KingControls.CompressPageStatePersister应用到页面上(页面名称为CompressPageState.aspx,内容仍然使用前面的控件状态控件的测试页面内容)。代码如下:

 

/// <summary>
/// 获得本书更多内容,请看:
/// http://blog.csdn.net/ChengKing/archive/2008/08/18/2792440.aspx
/// </summary>
protected override PageStatePersister PageStatePersister
{
    get
    {
        return new CompressPageStatePersister(this);
    }
}

OK,在浏览器中运行页面,可以看到,页面中的__VIEWSTATE值由原来的:

 

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value= "/wEPDwUJNzIxMjkzNTM2D2QWAgIDD2QWBAIBDw8WAh4OVGV4dF9WaWV3U3RhdGUFIuaIkeaYr+eUqFZpZXdTdGF0ZeWuueWZqOWtmOWCqOeahCFkZAIHDw8WAh4EVGV4dAWQBHRoaXMuQ29udHJvbFN0YXRlQ29udHJvbDEuVGV4dF9Ob1ZpZXdTdGF0ZeWxnuaApyDmsqHmnInkv53lrZjmjqfku7bnirbmgIEgPEltZyBzcmM9Jy4uXEltYWdlc1xKUy5qcGcnIC8+ICA8YnI+PGJyPnRoaXMuQ29udHJvbFN0YXRlQ29udHJvbDEuVGV4dF9WaWV3U3RhdGXlsZ7mgKcg5bey57uP5L+d5a2Y5LqG5o6n5Lu254q25oCBIDxJbWcgc3JjPScuLlxJbWFnZXNcWEwuanBnJyAvPiAgPGJyPjxicj50aGlzLkNvbnRyb2xTdGF0ZUNvbnRyb2wxLkZhY2VTdHlsZeWxnuaApy5PSyDlt7Lnu4/kv53lrZjkuobmjqfku7bnirbmgIEgPEltZyBzcmM9Jy4uXEltYWdlc1xYTC5qcGcnIC8+ICA8YnI+PGJyPnRoaXMuQ29udHJvbFN0YXRlQ29udHJvbDEuRmFjZVN0eWxlLkJhY2tDb2xvciDlt7Lnu4/kv53lrZjkuobmjqfku7bnirbmgIEsIOeep++8jOaIkeeahOminOiJsuWwseaYr+S/neWtmOeahOminOiJsiA8SW1nIHNyYz0nLi5cSW1hZ2VzXFhMLmpwZycgLz4gIDxJbWcgc3JjPScuLlxJbWFnZXNcQkcuanBnJyAvPiA8YnI+PGJyPmRkGAEFFENvbnRyb2xTdGF0ZUNvbnRyb2wxDw8PFgIfAAUi5oiR5piv55SoVmlld1N0YXRl5a655Zmo5a2Y5YKo55qEIWQUKwACFgQeCUJhY2tDb2xvcgoAHgRfIVNCAghnZMhDeYVwXQ029fkRLFj7HE0Hw8SV" />

变为了现在的:

 

<input type="hidden" name="____VIEWSTATE" id="____VIEWSTATE" value= "H4sIAAAAAAAEAO29B2AcSZYlJi9tynt/SvVK1+B0oQiAYBMk2JBAEOzBiM3mkuwdaUcjKasqgcplVmVdZhZAzO2dvPfee++999577733ujudTif33/8/XGZkAWz2zkrayZ4hgKrIHz9+fB8/Iv7vX/M3/U1/3R97sLe79/De/Xuf/qaz3+bX+rV+bfr31/m16Ivf5tf6Bb/Jm/xd+/v/ZJFfvW6zNv91P/ov/qg/+b/4s/7G//JP+6vsZ//53/B3/+d/9l/1n//1f9Z//of8Vf/ln/OH/a6z2a/16/O7vw7e/XX/pF+nnRfN+KRatnVV8hv6++6Ygb+oHKi/+S/4L/6gvzL9L/6Wv/i/+PP+6P/sH/zzCeh/8Sf8lf/Z3/t3/Jd/zN/xX/xBf3D6+GxxkTb19LOPx+Pf92yRXeTN7/ud1+OfXl18nN49StPHk/oI/7+hx25///nf+bf8l3/vnyj9/Wd/zx9xY5e/9/P36fJZNs1ft9el9jb+8vf6YXU4fpJN355UZVVv6nGU/pd/wV/5P/59fxzNLE3ff/2X/Hn/1R/9t/znf9PfTLMsze2HN6MW+f7J5/Z7g/ls9tv9mr/ubxVD/TcF3/wuv8atuOy3+uTX+LV+m1/nF/yYHWPya/yCX+f3/11fP/m1foOL2f8DRHjCstcCAAA=" />

可以看到明显小了许多。当页面越复杂时压缩比会越大。笔者在自己的项目中测试过,一个50KB的页面,经过压缩后变为40KB,相当于压缩了10KB。另外,它会占用服务器CPU和内存,但相比网络带宽这点代价算不了什么。

6.9.3 分块存储视图状态数据

如果隐藏域中的数据量过大,某些代理和防火墙将阻止对包含这些数据的页的访问。由于最大数量会随所采用的防火墙和代理的不同而不同,较大的隐藏域可能会出现偶发性问题。如果您需要存储大量的数据项,可以打开视图状态分块,这样会自动将数据分割到多个隐藏域。

ASP.NET框架提供了MaxPageStateFieldLength属性,用来获取或设置页状态字段的最大长度。其属性值表示页面状态字段的最大长度,以字节为单位。

微软官方网站以及很多文章介绍说,通过设置Page.MaxPageStateFieldLength属性可以指定块的最大字节数,且MSDN明确说明此属性是公有的,笔者在VS 2005和VS 2008下测试结果是Page下面根本没有这个属性,所以采用在配置文件Web.Config中实现,如下:

 

<system.web>
    <pages maxPageStateFieldLength="100" />    
<system.web>

当MaxPageStateFieldLength属性设置为正数时,发送到客户端浏览器的视图状态将分为多个隐藏字段,并且每个字段的值都小于在MaxPageStateFieldLength属性中指定的大小;而如果MaxPageStateFieldLength属性设置为负数(默认值)则表示不应将视图状态字段分成多个块区。另外,如果将MaxPageStateFieldLength设置非常小,会导致性能降低。

6.10 视图状态和控件状态的总结

前面讲了页面状态的各个方面,从概念到代码演示再到性能。本节做个总结,重新理一下页面状态内容,说明视图状态和控件状态的优缺点和使用场景。

1.视图状态

视图状态是ASP.NET页框架用于保存服务器与客户端往返过程之间的页面和控件值的方法。当呈现页的HTML形式时,需要在回发过程中保留的页的当前状态和值将被序列化为Base64编码的字符串,并输出到视图状态的隐藏字段中,即页面中HTML源代码中的__VIEWSTATE隐藏字段,可以存储:字符串、整数、布尔值、Array对象、ArrayList对象、哈希表等数据类型。

视图状态有如下优点:

(1)节省服务端资源。由于视图状态是存放到隐藏字段(在HTML代码结构中)传送到客户端的,因此不占用服务端资源。

(2)使用方便。默认已经开启视图状态。有些场合,如果控件没有注册服务端事件或者控件没有动态属情况可以将视图状态关闭,节省网络流量,提高页面呈现速度。

(3)视图状态通过散列码校检机制和使用3DES等加密机制来保证数据安全。

(4)自定义存储位置。在Load和Save方法中可以自定义其存储位置。

视图状态有如下缺点:

(1)由于其存储到页面HTML代码结构中,因此传输数据量大时,会严重影响性能。可以用视图状态分块机制,将数据分块存储,设置MaxPageStateFieldLength属性。

(2)如果视图状态被禁用,则无法保存页面状态。

(3)虽然已经通过加密,但由于其是呈现到客户端隐藏字段区域,因此容易被篡改。

2.控件状态

有时,为了让控件正常工作,需要按顺序存储控件状态数据。例如,如果编写了一个自定义控件,其中使用了不同的选项卡来显示不同的信息,如TabTrip,FormView等控件,为了让自定义控件按预期的方式工作,该控件需要知道在往返行程之间选择了哪个选项卡。可以使用ViewState属性来达到这一目的,然而,开发人员可以在页级别关闭视图状态,从而使控件无法正常工作。

为了解决此问题,ASP.NET 2.0增加了一项新的存储功能:控件状态。ControlState属性允许您保持特定于某个控件的属性信息,且不能像ViewState属性那样被关闭。简单地说,当禁用视图状态时,ControlState能够完成ViewState不能够完成的任务。

控件状态有如下优点:

(1)节省服务端资源。跟视图状态一样,控件状态存储在隐藏字段中,也不占用服务器资源。

(2)比视图状态更可靠。控件状态功能推出的一个重大原因就是,当视图状态被禁用时,它依然可以有效。

(3)自定义存储位置。在Load和Save方法中可以自定义其存储位置。

控件状态有如下缺点:

(1)由于其存储到页面HTML代码结构中,因此传输数据量大时,会严重影响性能。

(2)视图状态可以用System.Web.UI.StateBag类型的ViewState来存储,也可以自定义编程。控件状态只能自定义编程。

6.11 本章总结

客户端提交的两次连续的页面请求,其数据可能存在差异。ASP.NET是采用视图机制解决两次页面请求之间的关联性问题的。本章在讲解页面状态存储原理后,对视图状态机制作了详细的介绍,包括基本ViewState工作原理、自定义视图状态的应用、控件状态机制及应用方案、视图状态和控件的关系、对页面状态进行加密以及页面状态的清理机制、动态增加控件使用视图时的注意事项、对页面状态的保存和装载应用高效率的类型转换器,并且讨论了页面状态的性能优化策略,对视图状态和控件状态的优缺点做了比较,分析了它们的及适用场景。在开发过程中,如果能够理解页面状态工作原理,则开发起来就会非常容易。


 

 

下一篇:SharePoint 2007 权限代码分享-马靖