[转载]状态管理概述
时间:2010-09-10 来源:JackPeng
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);