2 Cookie
2.1 Cookie概述
Cookie 爲 Web 應用程序保存用戶相關信息提供了一種有用的方法。例如,當用戶訪問站點時,可以利用 Cookie 保存用戶首選項或其他信息,這樣,當用戶下次再訪問站點時,應用程序就可以檢索以前保存的信息。
從技術上講,Cookie是小段保存在客戶端的數據(如果你安裝的是XP,可以看一下<安裝Windows的盤>:/Documents and Settings/<用戶名>/Cookies文件夾)。用戶訪問網站的時候,網站會給用戶一個包含過期時間的Cookie,瀏覽器收到Cookie後就存放在客戶端的文件夾下。以後用戶每次訪問網站頁面的時候,瀏覽器會根據網站的URL在本地Cookie文件夾內查找是否存在當前網站關聯的Cookie,如果有的話就連同頁面請求一起發送到服務器。
關於Cookie的知識還需要了解以下幾點。
· Cookie只是一段字符串,並不能執行。
· 大多數瀏覽器規定Cookie大小不超過4K,每個站點能保存的Cookie不超過20個,所有站點保存的Cookie總和不超過300個。
· 除了Cookie外,幾乎沒有其他的方法在客戶端的機器上寫入數據(就連Cookie的寫入操作也是瀏覽器進行的)。當然,連Cookie都可以通過瀏覽器安全配置來禁止。如果你使用IE瀏覽器,可以看一下“工具”→“Internet”選項→“隱私”一頁。現在的大多數網站都利用Cookie來保存一些數據(比如你的ID),以便你下一次訪問網站時能直接“繼續”以前的配置,所以我還是建議你不要輕易關閉Cookie。
在使用Cookie時,必須意識到其固有的安全弱點。Cookie畢竟是存放於客戶端的。因此,不要在Cookie中保存保密信息,如用戶名、密碼、信用卡號等。在Cookie中不要保存不應該由用戶掌握的內容,也不要保存可能被其他竊取Cookie的人控制的內容。
2.2 Cookie的使用
下面,我們就來討論如何保存、讀取、刪除和修改Cookie。首先在頁面上添加4個按鈕用來完成這4個操作。
<asp:Button ID="btn_SaveCookie" runat="server" OnClick="btn_SaveCookie_Click"
Text="保存Cookie" />
<asp:Button ID="btn_ReadCookie" runat="server" Text="讀取Cookie"
OnClick="btn_ReadCookie_Click" />
<asp:Button ID="btn_ModifyCookie" runat="server" OnClick="btn_ModifyCookie_Click"
Text="修改Cookie" />
<asp:Button ID="btn_DelCookie" runat="server" Text="刪除Cookie"
OnClick="btn_DelCookie_Click" />
保存Cookie的方法如下。
protected void btn_SaveCookie_Click(object sender, EventArgs e)
{
HttpCookie SingleValueCookie = new HttpCookie("test1", "單值Cookie");
SingleValueCookie.Expires = DateTime.Now.AddDays(1);
Response.Cookies.Add(SingleValueCookie);
HttpCookie MultiValueCookie = new HttpCookie("test2");
MultiValueCookie.Values.Add("key1", "value1");
MultiValueCookie.Values.Add("key2", "value2");
MultiValueCookie.Expires = DateTime.Now.AddDays(1);
Response.Cookies.Add(MultiValueCookie);
}
我們可以看到,一個Cookie中允許保存單個值也可以保存多個值。HttpCookie類型表示一個Cookie,Expires屬性用於修改Cookie的過期時間。對於單值Cookie,既可以直接在構造方法中指定值也可以使用Value屬性指定值。對於多值Cookie,既可以使用Values屬性的Add方法添加子鍵和值,也可以直接使用Values屬性的索引設置子鍵和值。上面這段代碼等價於下面這段代碼。
protected void btn_SaveCookie_Click(object sender, EventArgs e)
{
HttpCookie SingleValueCookie = new HttpCookie("test1");
SingleValueCookie.Value = "單值Cookie";
SingleValueCookie.Expires = DateTime.Now.AddDays(1);
Response.Cookies.Add(SingleValueCookie);
HttpCookie MultiValueCookie = new HttpCookie("test2");
MultiValueCookie.Values["key1"] = "value1";
MultiValueCookie.Values["key2"] = "value2";
MultiValueCookie.Expires = DateTime.Now.AddDays(1);
Response.Cookies.Add(MultiValueCookie);
}
在添加完值以後,務必記得使用Response對象把Cookie重新返回給瀏覽器。我們的服務器不能直接在客戶端機器上寫Cookie,而是由瀏覽器完成這一工作,當然用戶也可以設置是否允許瀏覽器讀寫Cookie。
下面是讀取Cookie的操作。
protected void btn_ReadCookie_Click(object sender, EventArgs e)
{
HttpCookie SingleValueCookie = Request.Cookies["test1"];
if (SingleValueCookie != null)
{
Response.Write(string.Format("Key:{0} Value:{1} Expires:{2}<br/>", "test1",
SingleValueCookie.Value, SingleValueCookie.Expires));
}
HttpCookie MultiValueCookie = Request.Cookies["test2"];
if (MultiValueCookie!= null)
{
Response.Write(string.Format("Key:{0} Value:{1}<br/>", "test2", MultiValueCookie.
Value));
foreach (string subkey in MultiValueCookie.Values.AllKeys)
{
Response.Write(string.Format("SubKey:{0} Value:{1} Expires:{2}<br/>",
subkey, MultiValueCookie.Values[subkey], MultiValueCookie.Expires));
}
}
}
對於多值Cookie,我們通過遍歷AllKeys屬性返回的字符串數組獲取所有子鍵Key,從而獲得子鍵的值。要注意的是,在訪問Cookie以前,需要檢測一下Cookie是否存在。打開頁面,先單擊“保存Cookie”按鈕,然後單擊“讀取Cookie”按鈕,得到以下輸出:
Key:test1 Value:單值Cookie Expires:0001-1-1 0:00:00
Key:test2 Value:key1=value1&key2=value2
SubKey:key1 Value:value1 Expires:0001-1-1 0:00:00
SubKey:key2 Value:value2 Expires:0001-1-1 0:00:00
這裏要說明以下幾點。
· 我們發現,所有Cookie的過期時間都不能正常顯示。這是因爲瀏覽器返回給服務器的Cookie是不包含過期時間的,而服務器返回給瀏覽器的Cookie是包含過期時間的。過期時間只對客戶端瀏覽器有意義,對服務器來說沒有什麼意義。
· 直接讀取多值Cookie的Value,它會把所有子鍵和子鍵值都使用key=value方法顯示,多個子鍵使用“&”連接(類似URL的方式)。
下面是刪除Cookie的操作。
protected void btn_DelCookie_Click(object sender, EventArgs e)
{
HttpCookie SingleValueCookie = Request.Cookies["test1"];
SingleValueCookie.Expires = DateTime.MinValue;
Response.Cookies.Add(SingleValueCookie);
}
如果你想刪除所有Cookie,可以遍歷刪除。
protected void btn_DelCookie_Click(object sender, EventArgs e)
{
foreach (string key in Request.Cookies.AllKeys)
{
HttpCookie cookie = Request.Cookies[key];
cookie.Expires = DateTime.MinValue;
Response.Cookies.Add(cookie);
}
}
我們始終要記住,服務器不能直接刪除Cookie,刪除Cookie的操作是瀏覽器進行的。說是刪除,其實是把它的過期時間設置爲過去的時間,讓Cookie過期。因此,對於刪除操作來說有三個步驟。
n 1.從Request對象中獲取Cookie。
n 2.把Cookie的過期時間設置爲過去的時間。
n 3.把Cookie重新寫回Response中。
4.修改Cookie的操作也非常簡單。
protected void btn_ModifyCookie_Click(object sender, EventArgs e)
{
HttpCookie SingleValueCookie = Request.Cookies["test1"];
SingleValueCookie.Value = "修改後的單值Cookie";
Response.Cookies.Add(SingleValueCookie);
}
2.3 Cookie總結
Cookie雖然是一個簡單實用的對象,但是我們也要注意Cookie的工作原理、大小限制以及安全性等,大致可以歸納爲以下幾點。
· 存儲的物理位置。客戶端的Cookies文件夾內。
· 存儲的類型限制。字符串。
· 狀態使用的範圍。當前請求上下文的上下文都能訪問到Cookie,Cookie對每個用戶來說都是獨立的。
· 存儲的大小限制。每個Cookie不超過4K數據。每個網站不超過20個Cookie。所有網站的Cookie總和不超過300個。
· 生命週期。每個Cookie都有自己的過期時間,超過了過期時間後失效。
· 安全與性能。存儲在客戶端,安全性差。對於敏感數據建議加密後存儲。
· 優點缺點與注意事項。可以很方便地關聯網站和用戶,長久保存用戶設置。
InProc
|
StateServer
|
SQLServer
|
|
存儲物理位置
|
IIS進程(內存)
|
Windows服務進程(內存)
|
SQLServer數據庫(磁盤)
|
存儲類型限制
|
無限制
|
可以序列化的類型
|
可以序列化的類型
|
存儲大小限制
|
無限制
|
||
使用範圍
|
當前請求上下文,對於每個用戶獨立
|
||
生命週期
|
第一次訪問網站的時候創建Session超時後銷燬
|
||
優點
|
性能比較高
|
Session不依賴Web服務器,不容易丟失
|
|
缺點
|
容易丟失
|
序列化與反序列化消耗CPU資源
|
序列化與反序列化消耗CPU資源,從磁盤讀取Session比較慢
|
使用原則
|
不要存放大量數據
|
|
|
|
5 Cache
5.1 Cache概述
Cache和Application一樣是整個應用程序共用一份的,而且所有用戶訪問的都是相同的Cache。Cache從字面上說是緩存的意思,我們知道計算機系統本身就是一個多級緩存的結構。CPU的緩存中存放了部分內存中的數據,內存中又存放了部分硬盤中的數據。把最常用的數據放在讀取最快速的硬件中存儲能大大提高效率。對於Web系統來說也一樣,從數據庫(硬盤)中讀取數據的速度肯定比從Cache(內存)中讀取的效率低,基於這個特性,我們通常把改動不大而查詢次數又比較多的數據放到Cache中。
既然緩存中的數據其實是來自數據庫的,那麼緩存中的數據如何和數據庫進行同步呢?一般來說,緩存中應該存放改動不大或者對數據的實時性沒有太多要求的數據。這樣,我們只需要定期更新緩存就可以了。相反,如果緩存的更新頻率過快的話,使用緩存的意義就不是很大了,因此更新緩存的時候需要一次性從數據庫中讀取大量的數據,過於頻繁地更新緩存反而加重了數據庫的負擔。
那麼ASP.NET中的Cache又提供了哪些緩存的過期策略呢?
· 永不過期。和Application一樣,緩存永不過期。
· 絕對時間過期。緩存在某一時間過期,比如5分鐘後。
· 變化時間過期(平滑過期)。緩存在某一時間內未訪問則超時過期,這個和Session有點類似,比如我們可以設定緩存5分鐘沒有人訪問則過期。
· 依賴過期。緩存依賴於數據庫中的數據或者文件中的內容。一旦數據庫中某些表的數據發生變動或者文件內容發生變動,則緩存自動過期。
緩存過期後我們就要更新緩存了,ASP.NET提供了兩種更新策略。
· 被動更新。緩存過期以後手動進行更新。
· 主動更新。緩存過期以後在回調方法中更新。
5.2 Cache性能與過期策略
首先,在頁面上添加兩個按鈕,並雙擊按鈕實現Click事件處理方法。
<asp:Button ID="btn_GetDataFromCache" runat="server" OnClick="btn_GetData_Click"
Text="從緩存中讀取數據" />
<asp:Button ID="btn_GetDataFromDb" runat="server" OnClick="btn_GetDataFromDb_Click"
Text="從數據庫中讀取數據" />
第一個按鈕實現從緩存讀取數據。
注意:本例需要using以下命名空間。
using System.Diagnostics; // 用於精確測定時間間隔
using System.Web.Caching; // 用於緩存的策略
using System.IO; // 用於文件操作
protected void btn_GetData_Click(object sender, EventArgs e)
{
InsertRecord();
Stopwatch sw=new Stopwatch();
sw.Start();
if (Cache["Data"]==null)
{
Response.Write("緩存無效<br/>");
}
else
{
DataSet ds = Cache["Data"] as DataSet;
Response.Write(string.Format("查詢結果:{0}<br/>", ds.Tables[0].Rows[0][0]));
Response.Write(string.Format("耗費時間:{0}<br/>", sw.ElapsedTicks));
}
}
在這裏有幾點需要說明。
· 一開始的InsertRecord()方法是我們自己創建的,用來向數據庫插入一條記錄。這樣,我們就能看出來數據是否是從緩存中讀取的了。
InsertRecord()方法如下:
private void InsertRecord()
{
using (SqlConnection conn = new SqlConnection(@"server=(local)/SQLEXPRESS;
database=Forum;Trusted_Connection=True"))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("Insert into CacheTest (Test) values
('Test')", conn))
{
cmd.ExecuteNonQuery();
}
}
}
· 如果緩存存在則輸出查詢結果和查詢耗費的時間,如果緩存不存在則輸出“緩存無效”。
· Stopwatch類用於精確測定逝去的時間,ElapsedTicks屬性返回了間隔的計數器刻度,所謂計數器刻度就是系統的計數器走過了多少次。當然,Stopwatch還有ElapsedMilliseconds能返回間隔的總毫秒數。之所以使用ElapsedTicks,因爲它是一個更小的時間單位。
第二個按鈕直接從數據庫讀取數據。
protected void btn_GetDataFromDb_Click(object sender, EventArgs e)
{
InsertRecord();
Stopwatch sw = new Stopwatch();
sw.Start();
DataSet ds = GetData();
Response.Write(string.Format("查詢結果:{0}<br/>", ds.Tables[0].Rows[0][0]));
Response.Write(string.Format("耗費時間:{0}<br/>", sw.ElapsedTicks));
}
在這裏,我們把讀取數據的操作使用一個GetData()方法進行了封裝,方法實現如下:
private DataSet GetData()
{
DataSet ds = new DataSet();
using (SqlConnection conn = new SqlConnection(@"server=(local)/SQLEXPRESS;
database=Forum;Trusted_Connection=True"))
{
SqlDataAdapter da = new SqlDataAdapter("select count(*) from CacheTest", conn);
da.Fill(ds);
}
return ds;
}
爲了能體現出緩存的效率,我們在Forum數據庫中又新建立了一個CacheTest數據表,表結構很簡單,如圖12-18所示。
圖12-18 CacheTest表結構
我們在表中插入了10萬條以上的記錄,使得表的大小達到了100MB左右。
運行程序,單擊“從數據庫中讀取數據”按鈕,如圖12-19所示,我們可以看到,這個操作耗費了相當多的時間。
因爲我們直接從數據庫讀取count(*),所以每次單擊按鈕查詢結果顯示的數字都會+1。現在你單擊“從緩存中讀取數據”肯定是顯示“緩存無效”,因爲我們還沒有添加任何緩存。
然後,我們在頁面上添加三個按鈕並雙擊按鈕創建事件處理方法,三個按鈕使用不同的過期策略添加緩存。
<asp:Button ID="btn_InsertNoExpirationCache" runat="server" Text="插入永不過期緩存"
OnClick="btn_InsertNoExpirationCache_Click" />
<asp:Button ID="btn_InsertAbsoluteExpirationCache" runat="server" Text="插入絕對時間
過期緩存" OnClick="btn_InsertAbsoluteExpirationCache_Click" />
<asp:Button ID="btn_InsertSlidingExpirationCache" runat="server" Text="插入變化時間
過期緩存" OnClick="btn_InsertSlidingExpirationCache_Click" />
三個按鈕的Click事件處理方法如下:
protected void btn_InsertNoExpirationCache_Click(object sender, EventArgs e)
{
DataSet ds = GetData();
Cache.Insert("Data", ds);
}
protected void btn_InsertAbsoluteExpirationCache_Click(object sender, EventArgs e)
{
DataSet ds = GetData();
Cache.Insert("Data", ds,null, DateTime.Now.AddSeconds(10), TimeSpan.Zero);
}
protected void btn_InsertSlidingExpirationCache_Click(object sender, EventArgs e)
{
DataSet ds = GetData();
Cache.Insert("Data", ds, null, DateTime.MaxValue, TimeSpan.FromSeconds(10));
}
我們來分析一下這三種過期策略。
· 永不過期。直接賦值緩存的Key和Value即可
· 絕對時間過期。DateTime.Now.AddSeconds(10)表示緩存在10秒後過期,TimeSpan.Zero表示不使用平滑過期策略。
· 變化時間過期(平滑過期)。DateTime.MaxValue表示不使用絕對時間過期策略,TimeSpan.FromSeconds(10)表示緩存連續10秒沒有訪問就過期。
在這裏,我們都使用了Insert()方法來添加緩存。其實,Cache還有一個Add()方法也能向緩存中添加項。不同之處在於Add()方法只能添加緩存中沒有的項,如果添加緩存中已有的項將失敗(但不會拋出異常),而Insert()方法能覆蓋原來的項。
n 注意:和Application不同,這裏不需要使用在插入緩存的時候進行鎖操作,Cache會自己處理 併發。
現在,我們就可以打開頁面對這三種過期策略進行測試了。
n 1.單擊“從緩存中讀取數據”按鈕,提示“緩存無效”。
n 2.單擊“從數據庫中讀取數據”按鈕,查詢結果顯示現在記錄總數爲100646。
n 3.單擊“插入永不過期緩存”按鈕,然後連續單擊“從緩存中讀取數據”按鈕,可以發現,無論過去多久,緩存始終沒有過期,而且觀察記錄查詢結果可以發現值始終沒有發生變化。不同的是,從緩存中讀取數據的效率比從數據庫中讀取數據提高了幾個數量級,如圖12-20所示,你可以和圖12-19進行比較。
n 4.單擊“插入絕對時間過期緩存”,然後連續單擊“從緩存中讀取數據”按鈕,大約10秒過期後,頁面提示“緩存無效”,說明緩存過期了。
n 5.單擊“插入變化時間過期緩存”,然後連續單擊“從緩存中讀取數據”按鈕,緩存始終不過期,如果我們等待10秒後再去單擊按鈕,頁面提示“緩存無效”,說明緩存過期了。
我們再來看一下依賴過期策略。所謂依賴過期就是緩存的依賴項(比如一個文件)的內容改變之後緩存也就失效了。由於篇幅關係,這裏只介紹文件依賴。我們在頁面上再加兩個按鈕並雙擊按鈕添加Click事件處理方法。
<asp:Button ID="btn_ModifyFile" runat="server" Text="修改文件" OnClick="btn_ModifyFile_
Click" />
<asp:Button ID="btn_AddFileDependencyCache" runat="server" Text="插入文件依賴緩存"
OnClick="btn_AddFileDependencyCache_Click" />
在本例中,我們將使緩存依賴一個txt文本文件。因此,首先在項目中添加一個test.txt文本文件。單擊“修改文件”按鈕實現文件的修改。
protected void btn_ModifyFile_Click(object sender, EventArgs e)
{
FileStream fs = new FileStream(Server.MapPath("test.txt"), FileMode.Append,
FileAccess.Write);
StreamWriter sw = new StreamWriter(fs);
sw.WriteLine(DateTime.Now.ToString());
sw.Close();
fs.Close();
}
我們通過在文件的最後寫入當前的時間來修改文件。插入文件依賴緩存按鈕的事件處理方法如下:
protected void btn_AddFileDependencyCache_Click(object sender, EventArgs e)
{
CacheDependency cd = new CacheDependency(Server.MapPath("test.txt"));
DataSet ds = GetData();
Cache.Insert("Data", ds, cd);
}
添加文件依賴緩存同樣簡單,通過CacheDependency關聯了一個文件依賴。
現在就可以打開頁面進行測試了。
n 1.單擊“從緩存中讀取數據”按鈕,提示“緩存無效”。
n 2.單擊“從數據庫中讀取數據”按鈕,查詢結果顯示現在記錄總數爲100710。
n 3.單擊“插入文件依賴緩存”按鈕,然後連續單擊“從緩存中讀取數據”按鈕,可以發現,無論過去多久,緩存始終沒有過期,而且觀察記錄查詢結果可以發現值始終沒有發生變化。
n 4.單擊“修改文件”按鈕,然後單擊“從緩存中讀取數據”按鈕,提示“緩存無效”。由於文件已經修改了,依賴這個文件的緩存立刻失效了。
5.3 Cache的更新策略
最後,我們來討論緩存的更新策略。在Web程序中我們通常會使用被動更新。所謂被動更新,就是在調用數據的時候判斷緩存是否爲空,如果爲空則先更新緩存然後再從緩存中讀取數據,如果不爲空則直接從緩存中讀取數據。可以把“從緩存中讀取數據”按鈕的Click事件處理方法修改成如下,實現被動更新。
protected void btn_GetData_Click(object sender, EventArgs e)
{
InsertRecord();
DataSet ds = new DataSet();
Stopwatch sw = new Stopwatch();
sw.Start();
if (Cache["Data"] == null)
{
ds = GetData();
Cache.Insert("Data", ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero);
}
else
{
ds = Cache["Data"] as DataSet;
}
Response.Write(string.Format("查詢結果:{0}<br/>", ds.Tables[0].Rows[0][0]));
Response.Write(string.Format("耗費時間:{0}<br/>", sw.ElapsedTicks));
}
我們可以看出,如果沒有人訪問數據緩存是不會更新的,只有緩存被訪問的時候發現緩存無效纔會去更新。這樣很明顯的一個缺點就是,如果緩存過期了更新操作將花費很長時間,這個時候的查詢也需要花費很多時間。我們可以利用緩存的回調功能讓緩存過期後自動續建實現自動更新的目的。
protected void btn_InsertActiveUpdateCache_Click(object sender, EventArgs e)
{
DataSet ds = GetData();
Cache.Insert("Data", ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero,
CacheItemPriority.Default, CacheRemovedCallback);
}
最後一個參數表明緩存被移除以後自動調用CacheRemovedCallback()方法,方法實現如下。
private void CacheRemovedCallback(String key, object value, CacheItemRemovedReason
removedReason)
{
DataSet ds = GetData();
Cache.Insert(key, ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero, CacheItemPriority.
Default, CacheRemovedCallback);
}
在回調方法中,我們再次插入一個支持回調的緩存。這樣,緩存被移除以後又能自動更新了。說了這麼多創建緩存的方法,讀者可能會問怎麼手動移除緩存呢?比如我們要移除Key="Data"的緩存只需要:
Cache.Remove("Data");
你可能會馬上想到用Cache.RemoveAll()方法移除所有緩存,可是Cache沒有提供這樣的方法,我們只能通過遍歷來實現移除所有緩存。
IDictionaryEnumerator CacheEnum = HttpRuntime.Cache.GetEnumerator();
while (CacheEnum.MoveNext())
{
Cache.Remove(CacheEnum.Key.ToString());
}
5.4 Cache總結
同樣,我們以第一節中的幾個問題結束對Cache的討論。
· 存儲的物理位置。服務器內存。
· 存儲的類型限制。任意類型。
· 狀態使用的範圍。當前請求上下文,所有用戶共用一份。
· 存儲的大小限制。任意大小。
· 生命週期。有多種過期策略控制緩存的銷燬。
· 安全與性能。數據總是存儲在服務端,安全性比較高,但不易存儲過多數據。
· 優缺點與注意事項。檢索數據速度快,過期策略豐富。注意別把對實時性要求很高的數據放到Cache中,不斷更新Cache會對數據庫造成壓力。
6 隱藏域/ViewState/ControlState——保存數據的另一個場所
6.1 使用隱藏域
Session、Application和Cache都是保存在服務器內存中的。一般來說我們是無權訪問客戶端的機器,把數據直接保存在客戶端的(Cookie是一個例外,不過Cookie只能保存不超過4K的字符串)。我們可以想一下還有哪裏可以讓我們暫時保存數據的?那就是頁面!如果我們在Web頁面中放置一個Label控件,然後設置它隱藏。那麼我們就可以使用這個Label來保存一些臨時數據,供當前頁面的程序使用。
在ASP.NET中,我們還可以使用隱藏域來進行類似的工作,和Label不同的是,在隱藏域中填寫的內容不會直接顯示在IDE的設計視圖中。由於我們保存的這些數據根本不需要顯示給用戶看,所以用隱藏域更合理一些。
<asp:HiddenField ID="HiddenField1" runat="server" Value="編程快樂" />
在代碼中可以直接訪問隱藏域的Value屬性獲得其值。
Response.Write(HiddenField1.Value);
不過,這樣做還有幾個不合理的地方。
· 數據直接暴露給用戶。
· 只能存儲字符串數據。
6.2 使用ViewState
ASP.NET引入了ViewState(視圖狀態)的概念。從這個名字上我們大概可以體會出,ViewState主要是用來存放和視圖有關的一些狀態。比如,在用戶註冊時用戶填寫了一大堆數據,提交頁面後系統返回了一個“用戶名重複”的出錯信息,此時先前用戶在頁面上填寫的一些註冊資料全部沒有了。用戶會是什麼感覺呢?我想大多數用戶會很惱火。ASP.NET通過ViewState自動保存控件的狀態。你可能也發現了,文本框中的數據在頁面提交後還是存在的。
同時,我們也可以利用ViewState來保存一些程序需要的數據。ViewState中的數據默認是使用base64進行編碼的,因此,用戶不能直接看到裏面的數據。我們在代碼中可以這樣添加一個ViewState項:
ViewState["test"] = "編程快樂";
打開頁面,觀察源代碼,ViewState就在這裏:
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="XT+Q3cCGrb+qjUKNB1N7x
CMUgAMjbpmAwtMtwPE+b5Ii8uRFaO42AgKyR+u9T0Be" />
既然ViewState是存在頁面上的,那麼ViewState肯定是不能跨頁面使用的,而且每個用戶訪問到的ViewState都是獨立的。此外,ViewState也沒有什麼聲明週期的概念,頁面在ViewState就在,頁面關閉了ViewState就關閉了。
觀察上面的ViewState,是不是找不到“編程快樂”這幾個字的影子呢?請在頁面上隨便加入一個按鈕,按鈕的Click事件處理方法如下:
Response.Write(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(
Request["__VIEWSTATE"])));
如圖12-21所示,單擊按鈕後頁面顯示如下。
圖12-21 對ViewState數據進行base64解碼
我們對ViewState數據進行base64解碼後就能看到“編程快樂”的字樣了。不過,現在的那串字符串還是很亂。其實,ASP.NET首先對ViewState中的數據進行序列化,然後再使用base64編碼後存儲在頁面的隱藏域中。base64不是什麼加密算法,只是一種編碼算法,任何人都能對base64進行反 編碼。
6.3 ViewState的安全與性能
如果我們需要在ViewState中保存一些相對比較機密的數據(當然,非常機密的數據不建議你保存在ViewState中),又如何保證ViewState的安全性呢?一般來說可以從兩個方面入手。
n 1.保證客戶端提交過來的ViewState沒有被修改。我們做Web應用程序,心中要有這樣一個意識,那就是客戶端的一切都是不可相信的。大家可能以爲只有我們提供了諸如TextBox等控件,用戶才能修改。其實這種觀點是錯誤的,雖然DropDownList中的內容只允許選擇不允許修改,但完全可以僞造一個頁面進行提交。對於ViewState也是同樣道理,爲了進一步的安全,我們需要驗證客戶端發回的ViewState是否已經被修改了。
n 2.保證用戶不能直接看到ViewState中的數據。說白了就是對ViewState進行加密。
在ASP.NET 2.0中,我們只需要進行簡單地配置就能對ViewState進行驗證和加密,在頁面頭部添加EnableViewStateMac(驗證)和ViewStateEncryptionMode(加密)屬性:
<%@ Page Language="C#" … EnableViewStateMac="true" ViewStateEncryptionMode="Always" %>
當然,如果你希望爲所有頁面的ViewState應用驗證和加密,可以在Web.config的system.Web節點中添加:
<pages enableViewStateMac="true" viewStateEncryptionMode="Always"></pages>
既然ViewState中的數據是序列化後加入的,那麼我們就可以把一些複雜的類型也存放到ViewState中。在介紹Session的時候我們曾建立過一個MyUser自定義類,並把它的實例存放到了Session中,後來爲了讓StateServer和SqlServer模式的Session也能保存MyUser類型,我們又爲MyUser標記了[Serializable]。在ViewState中保存自定義類型同樣需要爲類型標記[Serializable],那麼在這裏我們使用ViewState保存MyUser實例的代碼就和使用Session差不多。
MyUser user = new MyUser();
user.sUserName = "小朱";
user.iAage = 24;
ViewState["CustomClass"] = user;
讀取代碼:
MyUser user = ViewState["CustomClass"] as MyUser;
Response.Write(user.ToString());
那麼,ViewState中能保存多少數據呢?暫且不說表單Post的數據是有大小上限的,ViewState是經過序列化和編碼後保存在頁面中的。如果我們在ViewState中保存一個擁有100條記錄的DataSet,恐怕頁面就很難打開了。不信,你可以自己做一個試驗。
DataSet ds = new DataSet();
using (SqlConnection conn = new SqlConnection(@"server=(local)/SQLEXPRESS;database=
Forum;Trusted_Connection=True"))
{
SqlDataAdapter da = new SqlDataAdapter("select top 100 * from CacheTest", conn);
da.Fill(ds);
}
ViewState["Data"] = ds;
僅僅只有100條記錄,在ViewState就成這樣了,如圖12-22所示。
圖12-22 濫用ViewState的結果
而且這些數據還要在瀏覽器和服務器之間往返,佔用的網絡流量很客觀。因此,筆者建議你在ViewState中保存儘量少的數據。如果實在需要在ViewStatge中放置大量數據建議使用maxPageState- FieldLength對ViewState啓用分塊傳輸。
<%@ Page Language="C#" … maxPageStateFieldLength="100"%>
如圖12-23所示就設置了單個ViewState,不超過100字節,ViewState分成了幾個部分。
圖12-23 使用maxPageStateFieldLength控制每個ViewState不超過100字節
我們知道,ViewState不僅僅是我們在使用,ASP.NET會把控件交互相關的一些數據都存放到ViewState中,但是對於一些不實現任何交互的控件(比如顯示10條記錄的GridView),你可以設置控件的EnableViewState屬性爲false來讓控件不使用ViewState,從而減少頁面體積。
6.4 ControlState概述
最後,我們再簡單提一下,ASP.NET 2.0提供了ControlState。它用於保存(自定義)控件的關鍵信息。就算頁面或者控件的ViewState被關閉它還能起作用,彌補了ViewState能被禁止的不足。不過使用ControlState稍顯複雜,我們需要自己序列化複雜對象進行存儲。下面的代碼演示瞭如何在ControlState中保存和讀取簡單字符串:
PageStatePersister.ControlState = "編程快樂";
Response.Write(PageStatePersister.ControlState.ToString());
6.5 總結
其實隱藏域、ViewState和ControlState的原理差不多,我們來總結一下。
· 存儲的物理位置。表單隱藏域。
· 存儲的類型限制。可序列化類型(直接在隱藏域中保存內容需要自己序列化)。
· 狀態使用的範圍。當前頁面(當前控件),對用戶獨立。
· 存儲的大小限制。存儲過大數據會導致頁面不能正常打開,不能正常提交。
· 生命週期。頁面在就在,頁面不在也就不在了。三者始終是依附在頁面的隱藏域中的。
· 安全與性能。在客戶端存儲,安全性低。不過,ViewState提供了驗證和加密。
· 優缺點與注意事項。存儲少量數據非常方便簡單。但需要注意不要存儲敏感數據,不要存儲過大的數據。它們和前面說的Cookie、Session與Application不同。雖然Cookie也是存儲在客戶端,每次提交都附加在HTTP頭中進行提交,但是它的數據量畢竟不大,起了一個標記的作用。Session和Application都是存儲在服務器端的,不會參與頁面往返過程。隱藏域、ViewState和ControlState始終參與往返,而且序列化和反序列化會消耗一定資源,因此,存儲過大的數據會導致網頁加載過慢,浪費服務器帶寬。
7 以人爲本的Profile
7.1 使用Profile製作個性化頁面
一個人性化的網站往往提供給用戶很多個性化選擇。比如讓用戶選擇所喜歡的網站風格,讓用戶選擇是否自動彈出消息提醒等。這些數據需要在用戶把瀏覽器關閉後還能保存下來,因此只能選擇數據庫進行保存。對於登錄過的用戶比較好辦,我們可以根據用戶名和用戶的選擇存放在數據庫中,對於非登錄用戶(匿名用戶)怎麼保存用戶的選擇呢?唯一的方法只能像Session那樣分配給用戶一個ID,把這個ID存放在Cookie中(當然也可以放在URL中),然後在數據庫中保存這個ID相關的一些配置信息。
ASP.NET 2.0提供了Profile機制,能幫助我們完成類似的功能。Profile不僅僅支持登錄用戶還支持匿名用戶,存儲的數據也可以是任何可序列化類型。幾乎無需寫一行代碼就能輕鬆實現用戶個性化數據的保存。我們配置了一個Web.config文件,如下所示。
<?xml version="1.0"?>
<configuration>
<system.web>
<anonymousIdentification enabled="true"/>
<profile automaticSaveEnabled="true">
<properties>
<group name="UI">
<add name="ForeColor" defaultValue="Black" allowAnonymous="true" type="string"/>
<add name="EnableBold" defaultValue="false" allowAnonymous="true" type="bool"/>
</group>
<group name="UserInfo">
<add name="UserName" defaultValue="" allowAnonymous="false" type="string"/>
<add name="UserAge" defaultValue="0" allowAnonymous="false" type="int"/>
</group>
</properties>
</profile>
<compilation debug="true"/>
<authentication mode="Forms"/>
</system.web>
</configuration>
· <anonymousIdentification enabled="true"/>表示對匿名用戶也啓用Profile,系統會給匿名用戶分配一個隨機字符串組成的ID。
· <profile automaticSaveEnabled="true">表示自動在頁面請求結束的時候保存Profile的設置到數據庫中。
· <properties>中就是正式定義Profile的格式了,我們使用<group>標籤把Profile分成了兩組,<group name="UI">和<group name="UserInfo">。
· 在每一個<group>中的纔是真正的Profile。name表示Profile的名字,defaultValue表示默認值,allowAnonymous表示匿名用戶是否可以使用,type表示數據類型。
· <authentication mode="Forms"/>表示爲系統啓用了表單認證(這是ASP.NET認證方式的一種,以後的章節中會詳細介紹),
然後,我們爲頁面添加一些控件來個性化頁面。
n 文字顏色:
<asp:DropDownList ID="ddl_TextColor" runat="server">
<asp:ListItem Selected="True">Black</asp:ListItem>
<asp:ListItem>Blue</asp:ListItem>
<asp:ListItem>Red</asp:ListItem>
</asp:DropDownList>
<asp:CheckBox ID="cb_IsBlod" runat="server" Text="粗體" />
<br />
<asp:Button ID="btn_SaveSettings" runat="server" Text="保存個性化設置" OnClick=
"btn_SaveSettings_Click" />
<asp:Button ID="btn_Login" runat="server" OnClick="btn_
Login_Click" Text="登錄" /><br />
<asp:Label ID="lab_Text" runat="server" Font-Names="黑
體" Font-Size="50pt" Height="77px" Text="編程快樂" Width=
"293px"></asp:Label>
效果如圖12-24所示。
通過下拉框我們可以設置文字的顏色爲黑色、藍色或者紅色,通過複選框我們可以設置文字是否是粗體。單擊“保存個性化設置”按鈕保存設置。
protected void btn_SaveSettings_Click(object sender, EventArgs e)
{
Profile.UI.ForeColor = ddl_TextColor.SelectedValue;
Profile.UI.EnableBold = cb_IsBlod.Checked;
ApplyUISettings();
}
看到這裏讀者會不會很驚訝,我們僅僅在Web.config文件中配置了Profile的信息,怎麼在代碼中就直接能訪問到強類型的Profile了呢?其實,系統會在App_Code下生成臨時的代碼文件,如圖12-25所示。
圖12-25 系統生成的臨時代碼文件
在這裏我們又自定義了一個ApplyUISettings()方法來向頁面中的標籤應用樣式。
private void ApplyUISettings()
{
lab_Text.ForeColor = Color.FromName(Profile.UI.ForeColor);
lab_Text.Font.Bold = Profile.UI.EnableBold;
ddl_TextColor.SelectedValue = Profile.UI.ForeColor;
cb_IsBlod.Checked = Profile.UI.EnableBold;
}
同時爲了保證頁面首次加載的時候也能按照用戶的個性化配置來顯示,我們在Page_Load()的時候也需要應用配置。
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ApplyUISettings();
}
}
現在打開頁面,並把樣式設置爲藍色文字,粗體顯示,如圖12-26所示。
首次操作的時候比較慢,因爲系統正在爲你生成保存信息的數據庫。默認是使用SQL Express數據庫,文件就放在網站的App_Data文件夾下,如圖12-27所示。
圖12-26 保存自己的個性化設置 圖12-27 App_Data下的ASPNETDB數據庫文件
關閉頁面後再打開,可以發現頁面仍然保持了原來的樣式設置。你可能會問,系統怎麼知道我們還是原來的那個用戶呢?其實,系統爲匿名用戶生成了一個ID字符串保存於Cookie中,頁面加載的時候根據這個ID從數據庫中讀出數據填充Profile。在測試了匿名用戶的Profile後,我們來爲登錄按鈕添加Click事件處理方法。
protected void btn_Login_Click(object sender, EventArgs e)
{
FormsAuthentication.SetAuthCookie("test", false);
Response.Redirect(Request.Path);
}
在這裏,我們假設一個名爲test的用戶登錄了系統,並把頁面重定向到本頁。然後,我們在頁面上添加一個PlaceHolder控件,在其中放置一些控件讓用戶輸入Profile的信息和退出登錄。
<asp:PlaceHolder ID="ph_UserInfo" runat="server">
姓名:<asp:TextBox ID="tb_Name" runat="server"></asp:TextBox>
年齡:<asp:TextBox ID="tb_Age" runat="server"></asp:TextBox>
<br />
<asp:Button ID="btn_SaveUserInfo" runat="server" Text="保存用戶信息" OnClick=
"btn_SaveUserInfo_Click" />
<asp:Button ID="btn_Logout" runat="server" OnClick="btn_Logout_Click" Text="退
出" />
</asp:PlaceHolder>
保存用戶信息按鈕Click事件實現如下:
protected void btn_SaveUserInfo_Click(object sender, EventArgs e)
{
Profile.UserInfo.UserName = tb_Name.Text;
Profile.UserInfo.UserAge = int.Parse(tb_Age.Text);
GetUserInfo();
}
這裏的GetUserInfo用於顯示用戶的信息(登錄用戶的信息和非登錄用戶的信息)。
private void GetUserInfo()
{
if (User.Identity.IsAuthenticated)
{
ph_UserInfo.Visible = true;
Response.Write("當前登錄用戶:" + User.Identity.Name + "<br/>");
Response.Write("Profile關聯用戶:" + Profile.UserName + "<br/>");
Response.Write("Profile.UserInfo.UserName:" + Profile.UserInfo.UserName +
"<br/>");
Response.Write("Profile.UserInfo.UserAge:" + Profile.UserInfo.UserAge +
"<br/>");
}
else
{
ph_UserInfo.Visible = false;
Response.Write("Profile關聯用戶:" + Profile.UserName + "<br/>");
}
}
退出登錄按鈕Click事件實現如下:
protected void btn_Logout_Click(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
Profile.UserInfo.UserAge = 0;
Profile.UserInfo.UserName = "";
Response.Redirect(Request.Path);
}
同時,我們修改一下Page_Load,讓頁面首次加載的時候也顯示用戶信息。
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
ApplyUISettings();
GetUserInfo();
}
}
再次打開頁面,如圖12-28所示。
我們看到,對於匿名用戶來說,Profile.UserName屬性爲一個隨機字符串,單擊“登錄”按鈕後如圖12-29所示。
圖12-28 匿名用戶關聯了一個隨機字符串作爲ID 圖12-29 單擊登錄按鈕後顯示用戶信息
現在可以看到Profile關聯的用戶UserName已經爲登錄的test用戶了。現在,我們把姓名設置爲“小朱”,年齡設置爲24,單擊“保存用戶信息”按鈕。然後把樣式修改爲紅色粗體,單擊“保存個性化設置”按鈕。如圖12-30所示。
n 如果你現在關閉窗口再重新打開就會發現個性化設置還是原來在匿名狀態下保存的藍色粗體。因此現在已經不在登錄狀態了,比較一下發現現在的ID和上次的ID還是一樣的。再次單擊“登錄”按鈕後,頁面又加載了test用戶的個性化設置。
7.2 Profile總結
Profile的知識遠遠不止這些,我們這裏僅僅對它進行了簡單的介紹,在結束以前我們以第一節中的幾個問題結束對Profile的討論。
· 存儲的物理位置。客戶端Cookie/URL和服務器數據庫。
· 存儲的類型限制。可序列化類型。
· 狀態使用的範圍。當前請求的上下文,對每一個用戶獨立。
· 存儲的大小限制。任意大小,讀取寫入頻繁的數據不建議存入Profile。
· 生命週期。與關聯的Cookie的生命週期一樣。
· 安全與性能。數據總是存儲在服務端,安全性比較高,但不易存儲過多數據。
· 優缺點與注意事項。可以很方便地保存用戶(匿名用戶和已登錄用戶)的設置。
8 其他
8.1 QueryString
除了Cookie、Session、Application、Cache、HiddenField、ViewState、ControlState、Profile等重要狀態機制外,ASP.NET還提供了一些其他的方法讓我們暫時保存數據。
在很多時候我們希望跨頁面傳輸數據,最常用的一個辦法就是使用GET方式提交數據。也就是在URL中附加一段QUERYSTRING(類似 news.aspx?ID=1這樣的效果),因此,有的時候我們不需要通過程序就能傳輸QUERYSTRING。不過,我們需要注意以下幾點。
· IE瀏覽器對URL長度限制在2083個字符內。由於QUERYSTRING是在URL中傳輸內容,所以也就受到了這個限制。
· 在URL中傳輸的數據都是明文的,而且客戶端隨時能修改,因此千萬別使用QUERYSTRING傳敏感數據。
· 在URL中傳漢字或者一些特殊字符需要進行URL編碼後傳遞,接收的時候再反編碼,否則傳遞的數據可能會出現亂碼或者被截斷。
string s = "編程快樂!@#$%^&*()";
Response.Redirect(Request.Url.AbsolutePath+"?data="+HttpUtility.UrlEncode(s));
接收的時候:
if (Request.QueryString["data"] != null)
Response.Write(HttpUtility.UrlDecode(Request.QueryString["data"]));
8.2 跨頁提交
QUERYSTRING畢竟只能傳輸字符串,如果我們希望在一個頁面中直接訪問另外一個頁面,可以使用ASP.NET 2.0的跨頁提交功能。比如我們建立一個CrossPageSubmitTest.aspx頁面,在任意頁面建立一個按鈕提交到CrossPageSubmitTest.aspx。
<asp:Button ID="btn_CrossPageSubmit" runat="server" OnClick="btn_CrossPageSubmit_
Click" PostBackUrl="CrossPageSubmitTest.aspx" Text="跨頁面提交" />
然後在CrossPageSubmitTest.aspx的Page_Load中加入以下代碼就能輸出前一個頁面上btn_CrossPageSubmit按鈕的Text屬性。
Response.Write((PreviousPage.FindControl("btn_CrossPageSubmit") as Button).Text);