*
   原稿 (英文)

在 ASP.NET ViewState 中尋寶

作者:Susan Warren
Microsoft Corporation

2001 年 11 月 27 日

當我遇到新的 ASP.NET 網頁開發人員並與其交談時,他們通常第一個問題就問我:「不論從何種角度來看,您認為 ViewState 是什麼?」 而您通常可從他們的音調中聽到令人不安的迷戀,就像在某些奇特的餐館中,侍者將一盤事前不知道的食物端到我面前的感覺一樣。有人必定認為它非常好,否則就不會要求供應它。 因此,我會試試看,或許我會喜歡它,但看起來必定非常奇怪!

ViewState 就像這種樣子。 一旦看過其外觀,就可以發現您樂於將 ViewState 放入 ASP.NET 應用程式的許多情況下,因為它可讓您用較少的程式碼來執行更多的工作。但是當您要明確地留下 ViewState 時,仍然需要花一些時間。我們將觀察兩個案例,但首先要回答何謂 ViewState 的問題。

答:ViewState 維持網頁的 UI 狀態

Web 是沒有狀態 (Stateless) 的,而 ASP.NET 網頁也是如此。 在每個動作中,都會使它們在伺服器個體化、執行、呈現和處置 (Dispose)。身為 Web 開發人員,可以使用知名的技術 (例如將伺服器的狀態儲存至 [工作階段] 狀態中,或將網頁貼回本身) 來新增狀態。使用 [圖 1] 的登記表作為範例。

[圖 1] 還原已張貼的表單值

您可以看到我已經從百樂餐 (Potluck) 的項目中選取無效的值。正如 Web 中的多數表單一樣,這個表單很友善地提供有用的錯誤訊息,並在發生錯誤的欄位旁加上星號。 此外,我在其他文字方塊與下拉式清單中輸入的所有有效值仍然出現在表單中。這在某種程度上是可能的,因為 HTML 表單元素會將其現行值從瀏覽器貼入 HTTP 標題的伺服器中。 您可以使用 ASP.NET 追蹤來查看貼回的表單值,如 [圖 2] 所示。

[圖 2] 貼入 HTTP 表單中的值,如 ASP.NET 追蹤所示

在使用 ASP.NET 之前,透過多重貼回動作將這些值還原至表單欄位中,完全是網頁開發人員的責任。他們必須從 HTTP 表單中逐一選出這些值,再將它們放回欄位中。令人高興的是 ASP.NET 會自動執行這個動作,因此可減少許多例行公事和表單的程式碼。但那並不是 ViewState。

ViewState (英文) 是 ASP.NET 用來追蹤伺服器控制項狀態值的機制,否則這些值就不會貼回作為 HTTP 表單的一部份。 例如,[Label] 控制項所示的文字預設儲存在 ViewState 中。身為開發人員,您可以繫結資料或在首次載入網頁時以程式設定 [Label],在後續貼回時,就會自動從 ViewState 重新填入標籤文字。因此,除了較少的例行公事和程式碼之外,ViewState 的優點是通常很少存取資料庫。

ViewState 的運作方式

ViewState 並沒有任何神奇之處。它是 ASP.NET 網頁架構所管理的隱藏式表單欄位。當 ASP.NET 執行網頁時,會從網頁和所有控制項中收集 ViewState 值並格式化成單一編碼的字串,然後指派至隱藏式表單欄位 (尤其是 <input type=hidden>) 的 [值] 屬性。 由於隱藏式表單欄位是傳送至用戶端的網頁的一部份,所有 ViewState 值會暫時儲存在用戶端的瀏覽器中。如果用戶端選擇將網頁貼回伺服器,則同時會貼回 ViewState 字串。您可在上述 [圖 2] 中實際看到 ViewState 表單欄位及其貼回的值。

在貼回時,ASP.NET 網頁架構會剖析 ViewState 字串,然後填入網頁和各控制項的 ViewState 屬性。這些控制項依序使用 ViewState 資料使它們再化成其先前狀態。

另外,還必須了解有關 ViewState 的其他三項雖小但很有用的事項。

  1. 若要使用 ViewState,在 ASPX 網頁中必須有伺服器端的表單標籤 (<form runat=server>)。需要有表單欄位,才能讓包含 ViewState 資訊的隱藏欄位貼回伺服器。而且它必須是伺服器端的表單,在伺服器執行網頁時,ASP.NET 網頁架構才能新增隱藏欄位。
  2. 網頁本身儲存大約 20 個位元組的資訊至 ViewState,可在貼回時用來將 PostBack 資料和 ViewState 值散發至正確的控制項。因此,即使網頁或應用程式停用 ViewState,仍可在 ViewState 中看到一些殘餘的位元組。
  3. 在不貼回網頁的狀況下,可以藉由忽略伺服器端的 <form> 標籤來去除 ViewState 表單。

從 ViewState 獲得更多

ViewState 是一種藉由貼回來追蹤控制項狀態的神奇方法,因為它不使用伺服器資源、不會逾時,並且可以使用任何瀏覽器。若您是控制項的作者,將會想要明確簽出維持控制項的狀態

網頁作者也可用類似的方法自 ViewState 獲益。 有時候您的網頁將包含並非由控制項儲存的 UI 狀態值。您可以使用類似於 Session 和 Cache 的程式語法來追蹤 ViewState 中的值:

[Visual Basic]

' 存入 ViewState 中
ViewState("SortOrder") = "DESC"

' 從 ViewState 讀取
Dim SortOrder As String = CStr(ViewState("SortOrder"))

[C#]

// 存入 ViewState 中
ViewState["SortOrder"] = "DESC";

// 從 ViewState 讀取
string sortOrder = (string)ViewState["SortOrder"];

考慮這個範例:您要在 Web 網頁中顯示項目清單,而且每一使用者都想用不同方式來排序此清單。 這個項目清單是靜態的,因此這些網頁可以繫結至同一組快取的資料,但排序順序是使用者特有的 UI 狀態。ViewState 是儲存這種值的絕妙位置。下列為其程式碼:

[Visual Basic]

<%@ Import Namespace="System.Data" %>
<HTML>
    <HEAD>
        <title>頁面 UI 狀態值的 ViewState</title>
    </HEAD>
    <body>
        <form runat="server">
            <H3>
               在 ViewState 中儲存非控制項狀態
            </H3>
            <P>
                這個範例將靜態資料清單的現行排序順序存入
                ViewState 中。<br>
                按一下欄標題的連結即可依該欄位來排序資料。<br>
                第二次按下該連結可讓它反向排序。
                <br><br><br>
                <asp:datagrid id="DataGrid1" runat="server" 
OnSortCommand="SortGrid" BorderStyle="None" BorderWidth="1px" 
BorderColor="#CCCCCC" BackColor="White" CellPadding="5" AllowSorting="True">
                    <HeaderStyle Font-Bold="True" ForeColor="White" 
BackColor="#006699">
                    </HeaderStyle>
                </asp:datagrid>
            </P>
        </form>
    </body>
</HTML>
<script runat="server">

    ' SortField 屬性已由 ViewState 追蹤
    Property SortField() As String

        Get
            Dim o As Object = ViewState("SortField")
            If o Is Nothing Then
                Return String.Empty
            End If
            Return CStr(o)
        End Get

        Set(Value As String)
            If Value = SortField Then
                ' 與現行排序檔案相同,切換排序方向
                SortAscending = Not SortAscending
            End If
            ViewState("SortField") = Value
        End Set

    End Property

    ' SortAscending 屬性已由 ViewState 追蹤
    Property SortAscending() As Boolean

        Get
            Dim o As Object = ViewState("SortAscending")
            If o Is Nothing Then
                Return True
            End If
            Return CBool(o)
        End Get

        Set(Value As Boolean)
            ViewState("SortAscending") = Value
        End Set

    End Property

    Private Sub Page_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        If Not Page.IsPostBack Then
            BindGrid()
        End If

    End Sub

    Sub BindGrid()

        ' 取得資料
        Dim ds As New DataSet()
        ds.ReadXml(Server.MapPath("TestData.xml"))
        
        Dim dv As New DataView(ds.Tables(0))

        ' 套用排序篩選器和方向
        dv.Sort = SortField
        If Not SortAscending Then
            dv.Sort += " DESC"
        End If

        ' 繫結格線
        DataGrid1.DataSource = dv
        DataGrid1.DataBind()

    End Sub
    
    Private Sub SortGrid(sender As Object, e As DataGridSortCommandEventArgs)
        DataGrid1.CurrentPageIndex = 0
        SortField = e.SortExpression
        BindGrid()
    End Sub
    
</script>

[C#]

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<HTML>
    <HEAD>
        <title>頁面 UI 狀態值的 ViewState</title>
    </HEAD>
    <body>
        <form runat="server">
            <H3>
               在 ViewState 中儲存非控制項狀態
            </H3>
            <P>
                這個範例將靜態資料清單的現行排序順序存入
                ViewState 中。<br>
                按一下欄標題的連結即可依該欄位來排序資料。<br>
                第二次按下該連結可讓它反向排序。
                <br><br><br>
                <asp:datagrid id="DataGrid1" runat="server" OnSortCommand="SortGrid" 
                BorderStyle="None" BorderWidth="1px" BorderColor="#CCCCCC" 
                BackColor="White" CellPadding="5" AllowSorting="True">
                    <HeaderStyle Font-Bold="True" ForeColor="White" BackColor="#006699">
                    </HeaderStyle>
                </asp:datagrid>
            </P>
        </form>
    </body>
</HTML>
<script runat="server">

    // SortField 屬性已由 ViewState 追蹤
    string SortField {

        get {
            object o = ViewState["SortField"];
            if (o == null) {
                return String.Empty;
            }
            return (string)o;
        }

        set {
            if (value == SortField) {
                // 與現行排序檔案相同,切換排序方向
                SortAscending = !SortAscending;
            }
            ViewState["SortField"] = value;
        }
    }

    // SortAscending 屬性已由 ViewState 追蹤
    bool SortAscending {

        get {
            object o = ViewState["SortAscending"];
            if (o == null) {
                return true;
            }
            return (bool)o;
        }

        set {
            ViewState["SortAscending"] = value;
        }
    }

    void Page_Load(object sender, EventArgs e) {

        if (!Page.IsPostBack) {
            BindGrid();
        }
    }

    void BindGrid() {

        // 取得資料
        DataSet ds = new DataSet();
        ds.ReadXml(Server.MapPath("TestData.xml"));
        
        DataView dv = new DataView(ds.Tables[0]);

        // 套用排序篩選器和方向
        dv.Sort = SortField;
        if (!SortAscending) {
            dv.Sort += " DESC";
        }

        // 繫結格線
        DataGrid1.DataSource = dv;
        DataGrid1.DataBind();
   }

   void SortGrid(object sender, DataGridSortCommandEventArgs e) {

        DataGrid1.CurrentPageIndex = 0;
        SortField = e.SortExpression;
        BindGrid();
    }

</script>

下列為 testdata.xml 的程式碼,參照上述兩個程式碼區段:

<?xml version="1.0" standalone="yes"?>
<NewDataSet>
  <Table>
    <pub_id>0736</pub_id>
    <pub_name>New Moon Books</pub_name>
    <city>Boston</city>
    <state>MA</state>
    <country>USA</country>
  </Table>
  <Table>
    <pub_id>0877</pub_id>
    <pub_name>Binnet &amp; Hardley</pub_name>
    <city>Washington</city>
    <state>DC</state>
    <country>USA</country>
  </Table>
  <Table>
    <pub_id>1389</pub_id>
    <pub_name>Algodata Infosystems</pub_name>
    <city>Berkeley</city>
    <state>CA</state>
    <country>USA</country>
  </Table>
  <Table>
    <pub_id>1622</pub_id>
    <pub_name>Five Lakes Publishing</pub_name>
    <city>Chicago</city>
    <state>IL</state>
    <country>USA</country>
  </Table>
  <Table>
    <pub_id>1756</pub_id>
    <pub_name>Ramona Publishers</pub_name>
    <city>Dallas</city>
    <state>TX</state>
    <country>USA</country>
  </Table>
  <Table>
    <pub_id>9901</pub_id>
    <pub_name>GGG&amp;G</pub_name>
    <city>Muechen</city>
    <country>Germany</country>
  </Table>
  <Table>
    <pub_id>9952</pub_id>
    <pub_name>Scootney Books</pub_name>
    <city>New York</city>
    <state>NY</state>
    <country>USA</country>
  </Table>
  <Table>
    <pub_id>9999</pub_id>
    <pub_name>Lucerne Publishing</pub_name>
    <city>Paris</city>
    <country>France</country>
  </Table>
</NewDataSet>

工作階段狀態或 ViewState?

在某些特定狀況下,將狀態值保存在 ViewState 中並非最佳選擇。最常使用的替代方案就是 [工作階段] 狀態,此狀態一般比較適於:

  • 大量的資料。 因為 ViewState 會增加網頁傳送至瀏覽器的大小 (HTML 負載) 和貼回的表單大小,儲存大量的資料時不該選擇使用 ViewState。
  • 尚未顯示於 UI 中的安全性資料。 雖然 ViewState 資料經過編碼並可選擇性地加密,但若不將資料傳送至用戶端還是最安全的。 因此,[工作階段] 狀態是比較安全的選擇。(將資料儲存在資料庫比較安全,因為需要額外的資料庫憑證。 您可以新增 SSL 使其連結更加安全。) 但是,如果已經在 UI 顯示私密資料,想必您已經安於該連結的安全性。在此狀況下,將同樣的值放入 ViewState 也一樣安全。
  • 尚未序列化成 ViewState 的物件,例如 DataSet。 ViewState 序列化程式最適用於一小組的通用物件類型,如下所示。可序列化的其他類型可能存在 ViewState 中,但會比較慢並產生很大的 ViewState〈痕跡 (footprint)〉。
  工作階段狀態 ViewState
掌握伺服器資源?
逾時? 是,在 20 分鐘之後 (預設)
儲存任何 .NET 類型? 否, 有限支援: 字串, 整數, 布林, 陣列, 陣列清單, 雜湊資料表, 自訂 TypeConverter (strings, integers, Booleans, arrays, ArrayList, hashtable, custom TypeConverters)
增加「HTML 負載」?

取得 ViewState 最佳效能

每一物件都必須序列化才能進入 ViewState,然後在貼回時還原序列化,因此使用 ViewState 的效能成本並非完全免費。 不過,如果遵循一些簡單的指南,使 ViewState 成本維持在控制之下,則通常不會對效能造成重大影響。

  • 在不需要 ViewState 時停用。下一節〈從 ViewState 獲得更少〉將涵蓋這些詳細資料。
  • 使用最佳化的 ViewState 序列化程式。 上面所列的類型具有特殊的序列化程式,可快速而最佳化地產生很小的 ViewState〈痕跡〉。 當您想要序列化未列於上面的類型時,可藉由建立自訂的 TypeConverter 來大為改善其效能。
  • 儘可能使用最少的物件,以減少放入 ViewState 的物件數目。例如,與其使用二維字串陣列的名稱/值 (具有的物件與陣列長度一樣多),不如使用兩個字串陣列 (只有兩個物件)。 不過,在存入 ViewState 之前轉換兩個已知的類型,通常對效能毫無益處,反而基本上付出兩次轉換成本。

從 ViewState 獲得更少

ViewState 預設為啟用,而由控制項 (並非網頁開發人員) 來決定要存入 ViewState 的物件。有時候這個資訊對您的應用程式毫無用處。雖然不會造成傷害,但會大為增加傳送至瀏覽器的網頁大小。如果不使用 ViewState,最好將它關閉,尤其是 ViewState 大小非常重要的場合。

您可以依控制項、依網頁,或依應用程式來關閉 ViewState。在下列狀況下,您不需要 ViewState:

網頁 控制項
  • 網頁不貼回它本身。
  • 您不是正在處理控制項的事件。
  • 控制項沒有動態或資料繫結屬性值 (或在程式碼中對〈每一個〉請求做設定)。

DataGrid 控制項是特別常使用 ViewState 的使用者。在預設狀況下,在格線中顯示的所有資料也會儲存在 ViewState 中,當需要使用昂貴的操作 (例如複雜的搜尋) 來擷取資料時,這是非常棒的功能。 不過,這個行為也會使 DataGrid 成為不需要 ViewState 的頭號目標。

例如,下列簡單的網頁符合上述準則。不需要使用 ViewState,因為網頁並未貼回它本身。

[圖 3] 具有 DataGrid1 的簡單網頁 LessViewState.aspx

<%@ Import Namespace="System.Data" %>
<html>
    <body>
        <form runat="server">
            <asp:DataGrid runat="server" />
        </form>
    </body>
</html>
<script runat="server">

    Private Sub Page_Load(sender As Object, e As EventArgs) 

        Dim ds as New DataSet()
        ds.ReadXml(Server.MapPath("TestData.xml"))

        DataGrid1.DataSource = ds
        DataGrid1.DataBind()

    End Sub

</script>

在啟用 ViewState 時,這個小格線對網頁的 HTML 負載增加超過 3000 個位元組! 使用 ASP.NET 追蹤 (英文) 就可以看到這個事實,或者檢視傳送至瀏覽器的網頁原始碼,如下列程式碼所示。

<HTML>
    <HEAD>
        <title>縮小「HTML Payload」頁面</title>
    </HEAD>
    <body>
    <form name="_ctl0" method="post" action="lessviewstate.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" 
value="dDwxNTgzOTU2ODA7dDw7bDxpPDE+Oz47bDx0PDtsPGk8MT47PjtsPHQ8QDA8cDxw
PGw8UGFnZUNvdW50O18hSXRlbUNvdW50O18hRGF0YVNvdXJjZUl0ZW1Db3VudDtEYXRhS2V
5czs+O2w8aTwxPjtpPDg+O2k8OD47bDw+Oz4+Oz47Ozs7Ozs7OztAMDxAMDxwPGw8SGVhZG
VyVGV4dDtEYXRhRmllbGQ7U29ydEV4cHJlc3Npb247UmVhZE9ubHk7PjtsPHB1Yl9pZDtwd
WJfaWQ7cHViX2lkO288Zj47Pj47Ozs7PjtAMDxwPGw8SGVhZGVyVGV4dDtEYXRhRmllbGQ7
U29ydEV4cHJlc3Npb247UmVhZE9ubHk7PjtsPHB1Yl9uYW1lO3B1Yl9uYW1lO3B1Yl9uYW1
lO288Zj47Pj47Ozs7PjtAMDxwPGw8SGVhZGVyVGV4dDtEYXRhRmllbGQ7U29ydEV4cHJlc3
Npb247UmVhZE9ubHk7PjtsPGNpdHk7Y2l0eTtjaXR5O288Zj47Pj47Ozs7PjtAMDxwPGw8S
GVhZGVyVGV4dDtEYXRhRmllbGQ7U29ydEV4cHJlc3Npb247UmVhZE9ubHk7PjtsPHN0YXRl
O3N0YXRlO3N0YXRlO288Zj47Pj47Ozs7PjtAMDxwPGw8SGVhZGVyVGV4dDtEYXRhRmllbGQ
7U29ydEV4cHJlc3Npb247UmVhZE9ubHk7PjtsPGNvdW50cnk7Y291bnRyeTtjb3VudHJ5O2
88Zj47Pj47Ozs7Pjs+Oz47bDxpPDA+Oz47bDx0PDtsPGk8MT47aTwyPjtpPDM+O2k8ND47a
Tw1PjtpPDY+O2k8Nz47aTw4Pjs+O2w8dDw7bDxpPDA+O2k8MT47aTwyPjtpPDM+O2k8ND47
PjtsPHQ8cDxwPGw8VGV4dDs+O2w8MDczNjs+Pjs+Ozs+O3Q8cDxwPGw8VGV4dDs+O2w8TmV
3IE1vb24gQm9va3M7Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPEJvc3Rvbjs+Pjs+Ozs+O3
Q8cDxwPGw8VGV4dDs+O2w8TUE7Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPFVTQTs+Pjs+O
zs+Oz4+O3Q8O2w8aTwwPjtpPDE+O2k8Mj47aTwzPjtpPDQ+Oz47bDx0PHA8cDxsPFRleHQ7
PjtsPDA4Nzc7Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPEJpbm5ldCAmIEhhcmRsZXk7Pj4
7Pjs7Pjt0PH_u56 ?cDxsPFRleHQ7PjtsPFdhc2hpbmd0b247Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPERDOz
4+Oz47Oz47dDxwPHA8bDxUZXh0Oz47bDxVU0E7Pj47Pjs7Pjs+Pjt0PDtsPGk8MD47aTwxP
jtpPDI+O2k8Mz47aTw0Pjs+O2w8dDxwPHA8bDxUZXh0Oz47bDwxMzg5Oz4+Oz47Oz47dDxw
PHA8bDxUZXh0Oz47bDxBbGdvZGF0YSBJbmZvc3lzdGVtczs+Pjs+Ozs+O3Q8cDxwPGw8VGV
4dDs+O2w8QmVya2VsZXk7Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPENBOz4+Oz47Oz47dD
xwPHA8bDxUZXh0Oz47bDxVU0E7Pj47Pjs7Pjs+Pjt0PDtsPGk8MD47aTwxPjtpPDI+O2k8M
z47aTw0Pjs+O2w8dDxwPHA8bDxUZXh0Oz47bDwxNjIyOz4+Oz47Oz47dDxwPHA8bDxUZXh0
Oz47bDxGaXZlIExha2VzIFB1Ymxpc2hpbmc7Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPEN
oaWNhZ287Pj47Pjs7Pjt0PHA8cDxsPFRleHQ7PjtsPElMOz4+Oz47Oz47dDxwPHA8bDxUZX
h0Oz47bDxVU0E7Pj47Pjs7Pjs+Pjt0PDtsPGk8MD47aTwxPjtpPDI+O2k8Mz47aTw0Pjs+O
2w8dDxwPHA8bDxUZXh0Oz47bDwxNzU2Oz4+Oz47Oz47dDxwPHA8bDxUZXh0Oz47bDxSYW1v
bmEgUHVibGlzaGVyczs+Pjs+Ozs+O3Q8cDxwPGw8VGV4dDs+O2w8RGFsbGFzOz4+Oz47Oz4
7dDxwPHA8bDxUZXh0Oz47bDxUWDs+Pjs+Ozs+O3Q8cDxwPGw8VGV4dDs+O2w8VVNBOz4+Oz
47Oz47Pj47dDw7bDxpPDA+O2k8MT47aTwyPjtpPDM+O2k8ND47PjtsPHQ8cDxwPGw8VGV4d
Ds+O2w8OTkwMTs+Pjs+Ozs+O3Q8cDxwPGw8VGV4dDs+O2w8R0dHJkc7Pj47Pjs7Pjt0PHA8
cDxsPFRleHQ7PjtsPE3DvG5jaGVuOz4+Oz47Oz47dDxwPHA8bDxUZXh0Oz47bDwmbmJzcFw
7Oz4+Oz47Oz47dDxwPHA8bDxUZXh0Oz47bDxHZXJtYW55Oz4+Oz47Oz47Pj47dDw7bDxpPD
A+O2k8MT47aTwyPjtpPDM+O2k8ND47PjtsPHQ8cDxwPGw8VGV4dDs+O2w8OTk1Mjs+Pjs+O
zs+O3Q8cDxwPGw8VGV4dDs+O2w8U2Nvb3RuZXkgQm9va3M7Pj47Pjs7Pjt0PHA8cDxsPFRl
eHQ7PjtsPE5ldyBZb3JrOz4+Oz47Oz47dDxwPHA8bDxUZXh0Oz47bDxOWTs+Pjs+Ozs+O3Q
8cDxwPGw8VGV4dDs+O2w8VVNBOz4+Oz47Oz47Pj47dDw7bDxpPDA+O2k8MT47aTwyPjtpPD
M+O2k8ND47PjtsPHQ8cDxwPGw8VGV4dDs+O2w8OTk5OTs+Pjs+Ozs+O3Q8cDxwPGw8VGV4d
Ds+O2w8THVjZXJuZSBQdWJsaXNoaW5nOz4+Oz47Oz47dDxwPHA8bDxUZXh0Oz47bDxQYXJp
czs+Pjs+Ozs+O3Q8cDxwPGw8VGV4dDs+O2w8Jm5ic3BcOzs+Pjs+Ozs+O3Q8cDxwPGw8VGV
4dDs+O2w8RnJhbmNlOz4+Oz47Oz47Pj47Pj47Pj47Pj47Pj47Pg==" />

哇塞! 只要停用格線的 ViewState,同一網頁的負載大小就明顯變小:

<HTML>
    <HEAD>
        <title>縮小「HTML Payload」頁面</title>
    </HEAD>
    <body>
    <form name="_ctl0" method="post" action="lessviewstate.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwxNTgzOTU2ODA7Oz4=" />

下列是使用 Visual Basic 和 C# 撰寫的完整 LessViewState 程式碼:

[Visual Basic]

<%@ Import Namespace="System.Data" %>
<html>
    <HEAD>
        <title>縮小「HTML Payload」頁面</title>
    </HEAD>
    <body>
        <form runat="server">
            <H3>
                停用 ViewState 來縮小「HTML Payload」頁面
            </H3>
            <P>
                <asp:datagrid id="DataGrid1" runat="server" EnableViewState="false" 
                BorderStyle="None" BorderWidth="1px" BorderColor="#CCCCCC" 
                BackColor="White" CellPadding="5">
                    <HeaderStyle Font-Bold="True" ForeColor="White" BackColor="#006699">
                    </HeaderStyle>
                </asp:datagrid>
            </P>
        </form>
    </body>
</html><script runat="server">

    Private Sub Page_Load(sender As Object, e As EventArgs) 

        Dim ds as New DataSet()
        ds.ReadXml(Server.MapPath("TestData.xml"))

        DataGrid1.DataSource = ds
        DataGrid1.DataBind()

    End Sub

</script>

[C#]

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<html>
    <HEAD>
        <title>縮小「HTML Payload」頁面</title>
    </HEAD>
    <body>
        <form runat="server">
            <H3>
                停用 ViewState 來縮小「HTML Payload」頁面
            </H3>
            <P>
                <asp:datagrid id="DataGrid1" runat="server" EnableViewState="false"
                BorderStyle="None" BorderWidth="1px" BorderColor="#CCCCCC"
                BackColor="White" CellPadding="5">
                    <HeaderStyle Font-Bold="True" ForeColor="White" BackColor="#006699">
                    </HeaderStyle>
                </asp:datagrid>
            </P>
        </form>
    </body>
</html>
<script runat="server">

    void Page_Load(object sender, EventArgs e) {

        DataSet ds = new DataSet();
        ds.ReadXml(Server.MapPath("TestData.xml"));
        
        DataGrid1.DataSource = ds;
        DataGrid1.DataBind();
    }

</script>

停用 ViewState

在上述範例中,我藉由設定格線的 EnableViewState 屬性為 [False] 來停用格線的 ViewState。可以針對單一控制項、整個網頁,或整個應用程式來停用 ViewState,如下所示:

依控制項 (在標記上) <asp:datagrid EnableViewState="false" ?/>
依網頁 (在指示詞中) <%@ Page EnableViewState="False" ?%>
依應用程式 (在 web.config 中) <Pages EnableViewState="false" ?/>

使 ViewState 更加安全

因為並未格式化成純文字,一般人通常假設 ViewState 已加密,但並非如此。 而是 ViewState 只是以 base64 編碼,以確保這些值在來回過程中不會改變,而不管應用程式所用的回應/請求編碼方式。

您可能要在應用程式中加入兩層 ViewState 安全性:

  • 防止竄改
  • 加密

重要的是注意到 ViewState 安全性會直接影響處理和呈現 ASP.NET 網頁所需的時間。簡而言之,更加安全就會變慢,因此如果無此需要,就不要新增安全性至 ViewState。

防止竄改

雜湊程式碼無法確保 ViewState 欄位中實際資料的安全,但可大為降低某人竄改 ViewState 以嘗試欺騙應用程式的可能性,也就是將應用程式通常要避免使用者輸入的值貼回。

您可以指示 ASP.NET 藉由設定 [EnableViewStateMAC] 屬性將雜湊程式碼附加至 ViewState 欄位:

<%@Page EnableViewStateMAC=true %>

可在網頁或應用程式層級設定 EnableViewStateMAC 。在貼回時,ASP.NET 將產生 ViewState 資料的雜湊程式碼,並將它與已張貼的值中所存的雜湊程式碼進行比較。如果它們並不相符,則會捨棄 ViewState 資料,且控制項將回復其原始設定。

在預設狀況下,ASP.NET 會使用 SHA1 演算法來產生 ViewState 雜湊程式碼。或者,您可以藉由設定 machine.config 檔案中的 [machineKey] 來選取 MD5 演算法,如下所示:

<machineKey validation="MD5" />

加密

您可以使用加密來保護 ViewState 欄位中的實際資料。首先,您必須設定EnableViewStatMAC="true",如上所示。然後,將 machineKey [驗證類型] 設定為 [3DES]。 如此將指示 ASP.NET 使用 Triple DES 對稱加密演算法來加密 ViewState 值。

<machineKey validation="3DES" />

Web 伺服陣列上的 ViewState 安全性

在預設狀況下,ASP.NET 會建立隨機驗證機碼,並將它儲存在各伺服器的 [本機安全性授權 (LSA)]。 若要驗證在另一部伺服器所建立的 ViewState 欄位,兩部伺服器的 validationKey 都必須設定為相同的值。如果以上述任何方法對執行於 [Web 伺服陣列] 組態中的應用程式進行 ViewState 保全,則您需要提供所有伺服器單一、共用的驗證機碼。

驗證機碼是 20 至 64 個隨機、〈密碼強化〉位元組的字串,以 40 至 128 個十六進位字元表示。愈長就愈安全,因此建議在支援的機器中使用 128 個字元的機碼。例如:

<machineKey validation="SHA1" validationKey=" 
F3690E7A3143C185AB1089616A8B4D81FD55DD7A69EEAA3B32A6AE813ECEECD28DEA66A
23BEE42193729BD48595EBAFE2C2E765BE77E006330BC3B1392D7C73F" />

System.Security.Cryptography 命名空間包括 RNGCryptoServiceProvider 類別,可用來產生這個字串,如下列 GenerateCryptoKey.aspx 範例所示:

<%@ Page Language="c#" %>
<%@ Import Namespace="System.Security.Cryptography" %>
<HTML>
    <body>
        <form runat="server">
        <H3>產生隨機加密金鑰</H3>
        <P>
            <asp:RadioButtonList id="RadioButtonList1" 
            runat="server" RepeatDirection="Horizontal">
                <asp:ListItem Value="40">40-byte</asp:ListItem>
                <asp:ListItem Value="128" Selected="True">128-byte</asp:ListItem>
            </asp:RadioButtonList>&nbsp;
            <asp:Button id="Button1" runat="server" onclick="GenerateKey"
            Text="產生金鑰">
            </asp:Button></P>
        <P>
            <asp:TextBox id="TextBox1" runat="server" TextMode="MultiLine" 
            Rows="10" Columns="70" BackColor="#EEEEEE" EnableViewState="False">
            複製並貼上產生的結果</asp:TextBox></P>
        </form>
    </body>
</HTML>


<script runat=server>

   void GenerateKey(object sender, System.EventArgs e)
   {
       int keylength = Int32.Parse(RadioButtonList1.SelectedItem.Value);
       
      // 放入使用者程式碼以初始化此網頁
        byte[] buff = new Byte[keylength/2];

        RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

        // 陣列目前填入加密強化的隨機位元組
        rng.GetBytes(buff);

        StringBuilder sb = new StringBuilder(keylength);
        int i;
        for (i = 0; i < buff.Length; i++) {
            sb.Append(String.Format("{0:X2}",buff[i]));
        }
        
        // 貼入文字方塊讓使用者可以複製出去
        TextBox1.Text = sb.ToString();
    }

</script>

總結

ASP.NET ViewState 是一種全新的狀態服務,開發人員可用來依使用者追蹤 UI 狀態。它並沒有任何神奇之處。它只是採用舊有的 Web 程式編輯技巧 (隱藏的表單欄位中的來回狀態),並立即將它處理成網頁處理架構。但其結果出奇得好,在 Web 表單中可以少撰寫和維護許多程式碼。

您並非一直需要它,但在需要時,您將發現 ViewState 是 ASP.NET 提供給網頁開發人員的新功能中令人滿意的附加功能。


Susan Warren 是 .NET Framework 小組之 ASP.NET 的程式管理員。