ASP.NET ViewState 初探

ASP.NET ViewState 初探(1)

與剛接觸 ASP.NET 頁面的開發人員交談時,他們通常向我提出的第一個問題就是:“那個 ViewState 到底是什麼?”他們的語氣中流露出的那種感覺,就象我來到一家異國情調的餐館,侍者端上一道我從未見過的菜餚時的那種感覺 - 既疑惑不解,又充滿好奇。但肯定有人認爲它不錯,否則就不會提供了。所以,我會先嚐一嘗,或許會喜歡上它,儘管它看上去的確很古怪!

對於 ViewState 也是如此,但是如果適應了它的風格,您會發現在許多情況下,您將樂於在自己的 ASP.NET 應用程序中使用 ViewState,因爲它可以幫助您使用更少的代碼完成更多的工作。但是,有時也會對 ViewState 完全棄之不用。下面我們就這兩種情況分別進行闡述,不過,讓我們先回答什麼是 ViewState 這個問題。

答案:ViewState 用於維護頁面的 UI 狀態

Web 是沒有狀態的,ASP.NET 頁面也沒有狀態,它們在到服務器的每個往返過程中被實例化、執行、呈現和處理。作爲 Web 開發人員,您可以使用衆所周知的技術(如以會話狀態將狀態存儲在服務器上,或將頁面回傳到自身)來添加狀態。下面我們以圖 1 中的註冊窗體爲例進行論述。

圖 1:恢復回傳的窗體值

從上圖中可以看出,我爲便餐選擇了一個無效的值。此窗體與 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 狀態值。您可以跟蹤 ViewState 中的值,使用的編程語法與會話和高速緩存的語法類似:

[Visual Basic]

' 保存在 ViewStateViewState("SortOrder") = "DESC"

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

[C#]

// 保存在 ViewStateViewState["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">

      ' 在 ViewState 中跟蹤 SortField 屬性
      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

      ' 在 ViewState 中跟蹤 SortAscending 屬性
      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">

      // 在 ViewState 中跟蹤 SortField 屬性
      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;
          }
      }

      // 在 ViewState 中跟蹤 SortAscending 屬性
      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>Muenchen</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 有效負載),同時也增加了回傳的窗體的大小,因此不適合存儲大量數據。
  • 未在 UI 中顯示的安全數據。儘管 ViewState 數據已被編碼,並且可以選擇對其進行加密,但始終不將數據發送到客戶端纔是最安全的。因此,會話是更安全的選擇。(由於數據庫需要額外的憑據進行驗證,因此將數據存儲在數據庫中會更安全。可以添加 SSL 以獲得更安全的鏈接。)但是,如果在 UI 中已經顯示了該專用數據,那麼您應該已經確認了鏈接的安全性。在這種情況下,將同樣的值放入 ViewState 不會降低安全性。
  • 尚未序列化到 ViewState 中的對象,如 DataSet。ViewState 序列化程序只爲一小部分常用的對象類型進行了優化,如下所示。其他可序列化的類型或許可以保留在 ViewState 中,但速度會變慢,並會生成一個非常大的 ViewState
  會話狀態 ViewState
是否使用服務器資源?
是否超時? 是,20 分鐘後(默認)
是否存儲所有 .NET 類型? 否,僅支持:String、Integer、Boolean、Array、ArrayList、Hashtable 和自定義 TypeConverter
是否增加“HTML 有效負載”?
ASP.NET ViewState 初探(2)

使用 ViewState 獲得最佳性能

使用 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 時,這個小網格會給該頁面增加 3000 多字節的 HTML 有效負載!使用 ASP.NET Tracing(英文)或查看發送到瀏覽器的頁面的源代碼(如以下代碼所示),可以清楚地看到這一點。

<HTML>
     <HEAD>
         <title>減少頁面的“HTML 有效負載”</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 有效負載”</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 有效負載”</title>
     </HEAD>
     <body>
         <form runat="server">
             <H3>
                 通過禁用 ViewState 來減少頁面的“HTML 有效負載”
             </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 有效負載”</title>
     </HEAD>
     <body>
         <form runat="server">
             <H3>
                 通過禁用 ViewState 來減少頁面的“HTML 有效負載”
             </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 騙過應用程序的可能性,即防止回傳應用程序通常禁止用戶輸入的值。

可以通過設置 EnableViewStateMAC 屬性來指示 ASP.NET 向 ViewState 字段中追加一個散列代碼:

<%@Page EnableViewStateMAC=true %>

可以在頁面級別上設置 EnableViewStateMAC,也可以在應用程序級別上設置。在回傳時,ASP.NET 將爲 ViewState 數據生成一個散列代碼,並將其與存儲在回傳值中的散列代碼進行比較。如果兩處的散列代碼不匹配,該 ViewState 數據將被丟棄,同時控件將恢復爲原來的設置。

默認情況下,ASP.NET 使用 SHA1 算法來生成 ViewState 散列代碼。此外,也可以通過在 machine.config 文件中設置 <machineKey> 來選擇 MD5 算法,如下所示:

<machineKey validation="MD5" />

加密

可以使用加密來保護 ViewState 字段中的實際數據值。首先,必須如上所述設置 EnableViewStatMAC="true"。然後,將 machineKey validation 類型設置爲 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" οnclick="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 狀態。ViewState 沒有什麼神祕之處,它只是利用了一個老的 Web 編程技巧:在一個隱藏的窗體字段中來回傳遞狀態,並將它直接應用於頁面處理框架中。但效果卻非常好 - 在基於 Web 的窗體中只需編寫並維護很少的代碼。

用戶可能並不總是需要它,但我想您在需要它的時候會發現,ViewState 是提供給頁面開發人員的諸多 ASP.NET 新功能中非常令人滿意的一種功能

ASP.NET ViewState 初探3

刪除 Cookie

刪除 Cookie(即把該 Cookie 從用戶的硬盤上物理刪除)是修改 Cookie 的一種形式。由於 Cookie 位於用戶的計算機中,所以您無法直接將其刪除。但是,您可以讓瀏覽器爲您刪除 Cookie。修改 Cookie 的方法前面已經介紹過(即用相同的名稱創建一個新的 Cookie),不同的是將其有效期設置爲過去的某個日期。當瀏覽器檢查 Cookie 的有效期時,就會刪除這個已過期的 Cookie。

所以,刪除 Cookie 的方法與創建該 Cookie 的方法是相同的,只不過要把其有效期設置爲過去的某個日期。以下示例比刪除單個 Cookie 要稍微有趣一些,它使用的方法可以刪除當前域的所有 Cookie:

Dim i As Integer
Dim cookieName As String
Dim limit As Integer = Request.Cookies.Count - 1
For i = 0 To limit
    aCookie = Request.Cookies(i)

    Response.Cookies.Add(aCookie)
Next

修改或刪除子鍵

修改單個子鍵的方法與最初創建它的方法相同:

Response.Cookies("userInfo")("lastVisit") = DateTime.Now.ToString
Response.Cookies("userInfo").Expires = DateTime.Now.AddDays(1)

比較複雜的問題是如何刪除單個子鍵。您不能只是簡單地重新設置 Cookie 的過期日期,因爲這樣只能刪除整個 Cookie 而不能刪除單個子鍵。實際的解決方案是對包含子鍵的 Cookie 的 Values 集合進行操作。首先,通過從 Request.Cookies 對象中獲取 Cookie 來重新創建 Cookie。然後,您就可以調用 Values 集合的 Remove 方法,將要刪除的子鍵名稱傳遞到 Remove 方法。接下來,您通常可以將修改後的 Cookie 添加到 Response.Cookies 集合,以便將修改後的 Cookie 發送回瀏覽器。

以下代碼顯示瞭如何刪除子鍵。在示例中,要刪除的子鍵的名稱在變量中指定。

Dim subkeyName As String
subkeyName = "userName"
Dim aCookie As HttpCookie = Request.Cookies("userInfo")
aCookie.Values.Remove(subkeyName)
aCookie.Expires = DateTime.Now.AddDays(1)
Response.Cookies.Add(aCookie)

Cookie 與安全性

在使用 Cookie 時,您必須意識到其固有的安全弱點。我所指的安全性並不是隱私問題,正如我在前面的什麼是 Cookie?中所述,隱私在更大程度上是某些用戶面對的問題:這些用戶很關心 Cookie 中的信息是如何被使用的。而 Cookie 的安全性問題與從客戶機獲取數據的安全性問題類似。對於初學者,就應用程序而言,Cookie 是用戶輸入的另一種形式,因而很容易被他人非法獲取和利用。由於 Cookie 保存在用戶自己的計算機上,所以用戶至少可以看到您保存在 Cookie 中的信息。如果用戶願意,還能在瀏覽器向您發送 Cookie 之前修改該 Cookie。

所以,您千萬不要在 Cookie 中保存保密信息 - 用戶名、密碼、信用卡號等等。在 Cookie 中不要保存不應該由用戶掌握的內容,也不要保存可能被其他竊取 Cookie 的人控制的內容。

同樣,要對從 Cookie 中得到的任何信息都持懷疑態度。不要認爲得到的數據就是您當初設想的信息。處理 Cookie 值時採用的安全措施應該與處理 Web 頁面中用戶鍵入的數據時採用的安全措施相同。例如,在頁面中顯示值之前,我會對 Cookie 中的內容進行 HTML 編碼。這是一種標準的方法,可以在顯示之前淨化從用戶處得到的信息,對 Cookie 的處理與此相同。

另一個需要關心的問題是,Cookie 是以純文本的形式在瀏覽器和服務器之間傳送的,任何可以截取 Web 通信的人都可以讀取 Cookie。您可以對 Cookie 的屬性進行設置,使其只能在使用安全套接字層(SSL,又稱 https://)的連接上傳輸。SSL 並不能防止保存在用戶計算機上的 Cookie 被他人讀取或操作,但它能防止 Cookie 在傳輸途中被他人截取。本文不討論 SSL,但您必須清楚,您可以對 Cookie 進行傳輸保護。有關 SSL 的詳細信息,請參閱 Secure Sockets Layer: Protect Your E-Commerce Web Site with SSL and Digital Certificates(英文)。

面對這些安全問題,如何才能安全地使用 Cookie?您可以在 Cookie 中保存一些不重要的數據,如用戶首選項或其他對應用程序沒有重大影響的信息。如果確實需要把某些敏感信息(如用戶 ID)保存在 Cookie 中,就對這些信息進行加密。一種可行的方法是利用 ASP.NET Forms Authentication 實用程序創建一個身份驗證票據,作爲 Cookie 保存。本文不討論有關加密的問題,但是,如果您需要在 Cookie 中保存敏感信息,就應該試着採取措施來隱藏信息,防止被他人盜用。

Mitigating Cross-site Scripting With HTTP-only Cookies(英文)一文中,您可以瞭解到更多有關 Cookie 及其安全弱點的信息。

檢查瀏覽器是否接受 Cookie

我在前面的 Cookie 的限制一節中曾經提到一個潛在問題,即用戶可以設置自己的瀏覽器拒絕接受 Cookie。如何才能知道您是否可以讀寫 Cookie?在不能寫入 Cookie 時不會出現任何錯誤(例如 Response.Cookies 不會拋出異常),因爲服務器並不跟蹤呈現頁面後出現的情況。瀏覽器同樣不會向服務器發送任何有關其當前的 Cookie 設置的信息。(也許您需要了解,但 HttpBrowserCapabilities.Cookies Property [英文] 屬性並不會告訴您 Cookie 是否被啓用,而只能告訴您當前的瀏覽器是否支持 Cookie。)

一種確定瀏覽器是否接受 Cookie 的方法是先編寫一個 Cookie,然後再嘗試讀取這個 Cookie。如果不能讀取這個 Cookie,則可以認爲該瀏覽器不接受 Cookie。

我編寫了一個簡單的示例來說明如何測試 Cookie 是否被接受。該示例包含兩個頁面。在第一個頁面中,我編寫了一個 Cookie,然後把瀏覽器重新定向到第二個頁面。第二個頁面嘗試讀取這個 Cookie,轉而將瀏覽器重新定向到第一個頁面,並向 URL 添加一個帶有測試結果的查詢字符串變量。

第一個頁面的代碼如下:

Sub Page_Load()
    If Not Page.IsPostBack Then
       If Request.QueryString("AcceptsCookies") Is Nothing Then
            Response.Cookies("TestCookie").Value = "ok"
            Response.Cookies("TestCookie").Expires = _
               DateTime.Now.AddMinutes(1)
            Response.Redirect("TestForCookies.aspx?redirect=" & _
               Server.UrlEncode(Request.Url.ToString))
       Else
            labelAcceptsCookies.Text = "接受 Cookie = " & _
               Request.QueryString("AcceptsCookies")
       End If
    End If
End Sub

第一個頁面測試是否有回信,如果沒有,就搜索包含測試結果的查詢字符串變量 (AcceptsCookies)。如果沒有找到查詢字符串變量,則表示測試還沒有完成,代碼就寫出一個名爲“TestCookie”的 Cookie。寫出 Cookie 之後,示例調用 Response.Redirect 來切換到測試頁面 (TestForCookies.aspx)。附加到測試頁面的 URL 的是名爲 redirect 的查詢字符串變量,該變量中包含了當前頁面的 URL,這樣就能在執行測試後把重定向到該頁面。

測試頁面可以完全由代碼組成,不需要包含控件。以下就是我使用的代碼:

Sub Page_Load()
     Dim redirect As String = Request.QueryString("redirect")
     Dim acceptsCookies As String
     ' 是否接受 Cookie?
     If Request.Cookies("TestCookie") Is Nothing Then
         ' 沒有 Cookie,因此不需要接受
         acceptsCookies = 0
     Else
         acceptsCookies = 1
         ' 刪除測試 Cookie
         Response.Cookies("TestCookie").Expires = _
            DateTime.Now.AddDays(-1)
     End If
     Response.Redirect(redirect & "?AcceptsCookies=" & acceptsCookies, _
        True)
End Sub

讀取 redirect 查詢字符串變量後,代碼就嘗試讀取 Cookie。爲了實現日常管理,如果該 Cookie 確實存在,就會被立即刪除。測試完成後,代碼從 redirect 查詢字符串變量傳遞的 URL 構造一個新的 URL。新的 URL 也包括一個包含測試結果的查詢字符串變量。最後一步是使用新的 URL 將瀏覽器重定向到原來的頁面。

這個示例十分簡單,但說明了通過運行程序並查看結果來進行測試的基本原則。其中最需要改進的地方是要永久保存 Cookie 測試結果,這樣用戶就不必在每次瀏覽原始頁面時都重複進行測試。但是,實際上並不能做到這一點。Cookie 不會起作用,原因是顯而易見的。另一種可能是把測試結果保存在會話狀態中,但在默認情況下,會話狀態也依賴於 Cookie,而如果瀏覽器不接受 Cookie,會話狀態也不會起作用。解決後一個問題的辦法是採用無 Cookie 的會話狀態。下一節我將簡要介紹會話狀態如何與 Cookie 協作。

Cookie 和會話狀態

當用戶訪問您的站點時,服務器會爲該用戶創建唯一的會話,會話將一直延續到用戶訪問結束。對於每個會話,ASP.NET 都維護一種基於服務器的結構(會話狀態),在該結構中應用程序可以保存用戶的相關信息。有關詳細信息,請參閱 Session State(英文)。

ASP.NET 需要能跟蹤每個用戶的會話 ID,這樣才能把用戶映射到服務器上的會話狀態信息。默認情況下,ASP.NET 使用一個非永久性的 Cookie 來保存會話狀態。如果您使用讀取 Cookie 一節的“讀取 Cookie 集合”中的示例,您可能就會在 Cookie 中發現一個會話狀態 Cookie。

但是如果用戶禁用了瀏覽器的 Cookie,會話狀態就不能使用 Cookie 來保存會話 ID,會話狀態也不會起作用。這就是爲什麼我在前面的檢查瀏覽器是否接受 Cookie 中說,無法在 Cookie 測試完畢後把測試結果實際保存在會話狀態中,因爲沒有 Cookie 就沒有會話狀態。

ASP.NET 提供了一種解決方案,即利用無 Cookie 的會話。您可以配置自己的應用程序,不在 Cookie 中保存會話 ID,而是在站點頁面的 URL 中保存。會話 ID 保存在 URL 中,也就是 ASP.NET 將 ID 保存在瀏覽器中,從而能夠在用戶請求其他頁面時取回 ID。

無 Cookie 會話可以避免瀏覽器拒絕 Cookie 的問題,使您能夠使用會話狀態。如果您的應用程序依賴於會話狀態,您可能就需要對其進行配置,使它能使用無 Cookie 會話。但是,在某些情況下,如果用戶與其他人共享 URL - 可能是用戶通過電子郵件將 URL 發送給同事,而該用戶的會話仍然處於激活狀態 - 那麼最終這兩個用戶可能共享同一個會話,結果將難以預料

發佈了39 篇原創文章 · 獲贊 0 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章