第6章 状态管理

  (译自 Pro ASP.NET 2.0 IN C# 2005 ,译得不到之处,欢迎批评指正)

6 状态管理

HTTP是无状态的协议,无论采用多先进的应用框架,都无法改变这个事实。每次网页请求后,客户端与服务器断开连接,ASP.NET引擎丢弃页面对象。这个结构保证了Web应用能够扩展,在为成千上万的并发请求提供服务的同时不致耗尽服务器内存。缺点是程序中需要使用其它技术来存储不同请求共享的信息,并且能够在需要时获取这个信息。

本章中,你将看到如何使用多种技术来维护服务器和客户端上的信息。也将学习如何将信息从一个网页传递到另一个网页。

 

.NET2.0里的状态管理有哪些变化(State Management Changes in .NET2.0

.NET2.0里的状态管理保持了与以前一致。这意味着会话(Session)状态、应用(applicaition状态、视图(view)状态和查询字符串(query string)的编程接口没有发生变化。但是,会话状态提供了更多的配置选项,而且也为在页面之间传递信息提供了新的方法。

下面是对这些变化的概览:

l         交叉页回传(Cross-page postbacks):在ASP.NET1.x里,网页只能回传给自己。但在ASP.NET20中,可以从一个网页传递到另一个网页,同时将页面状态一并传递。

l         新的会话状态设置(New session state settings):会话状态具有更多配置选项。这些选项允许你使用自定义SQL Server 数据库(取代ASPState),设置超时(timeout),配置如何使用和命名cookie

l         自定义会话状态提供者(Custom session state provider):微软有开放的会话状态模型,因此你可以开发自定义会话状态提供者(和session ID提供者),将状态存储于其它数据源,或者使用不同的算法产生会话ID

l         特性(Profile):取代编写自己的数据库检索逻辑,可以使用新特征的API来存储用户特定信息到数据库中。最为重要的是,这些信息是强类型的,这与会话状态不同。特性构建于ASP.NET认证模型之上。

在本章,将对这4个主题里的两个进行介绍。特性(Profile)将在第24章中讨论,因为需要使用它们与Windows认证或窗体认证相连接。自定义化会话状态提供者的内容本书尚未涉及。当然,期望看到第三方会话提供者,允许你在使用其它关系数据库时使用会话。

 

ASP.NET状态管理(ASP.NET State Management

ASP.NET包含了很多用于状态管理的选项。它传统的ASP一样描述SessionApplication状态集(有些许增强),但视图状态模型是全新的。ASP.NET甚至包含了缓存系统来保存信息,这样并不影响服务器的扩展性。每个状态管理选择有一个不同的生命周期(lifetime)、范围(scope)、性能负载和支持层级。

6-1,表6-2,表6-3显示了状态管理选项对照的概要。

 

 

显然,ASP.NET中管理状态的选项很多。幸运的是,多数这些状态管理系统展示了相似的基于集(collection)的程序接口。有两个例外是查询字符串(它是真正的传递信息的方法,而不是用于保存)和特性。

本章研究了表6-1,表6-2里的所有状态管理方法,表6-3中的内容没有包含在内。缓存是优化对有限资源(如数据库)的访问的极为重要的技术,将在第11章中介绍。特性是存储用户特定信息的更高层模型,它与ASP.NET认证紧密关联,将在第24章介绍。当然,在研究这些主题之前,需要理解状态管理的基本内容。

另外,你也可以编写自定义的状态管理代码和使用后台的服务器端资源来存储信息。最常见的例子是一个或多个数据库表。使用服务器端资源的缺点是会降低性能,并且制约可扩展性。例如,打开一个到数据库的连接,或者从一个文件中读取信息,都要花费一些时间。在多数情况下,可以使用缓存来作为状态管理系统的一个补充,这样可以弥补上述方法的不足。本书第2部分将研究使用和增强数据库访问的选项。

 

视图状态(View State

在一个单独的页面之内,视图状态是存储信息的首选。视图状态天生就是由ASP.NET Web控件所使用。可以使用视图状态在多个回传(postback)间存储属性。可以使用内置的页属性(ViewState)来添加自己的数据到视图状态集中。能够存储的信息包括简单数据类型和自定义对象。

ASP.NET中的大多数状态管理类型一样,视图状态依赖于字典集。字典集的项目由单一的字符串名索引。例如,考虑下面的代码:

ViewState[“Counter”] = 1;

这段代码将值1放入ViewState集中,并且设置了其描述性的名字Counter。如果ViewState中没有Counter项,就会自动添加一个新项。如果已经存在Counter项,则会替代它。

获取值的时候,使用的是关键名(key name)。与此同时,应该将获取的值转换为正确的数据类型。因为ViewState将所有的项都是存储为普通对象的,它可以让你处理不同的数据类型。

下面的代码从视图状态中获取了Counter,并且将其转化成了整数:

int counter;

if (ViewState["Counter"] != null)

{

counter = (int)ViewState["Counter"];

}

如果在集中找不到相应的值,会抛出一个NullReferenceException异常。为了防止这种异常,应当在获取和转换前检查是否为null值。

注意,ASP.NET提供了许多使用相同的字典语法的集。包括用于会话、应用状态和缓存、cookie的集。本章中会看到这些集中的几个。

 

一个视图状态示例(A View State Example

下面的代码演示了一个使用视图状态的页面。它允许用户保存值集(文本都显示在表中的文本框中),并且稍后将其存储起来。这个示例使用递归逻辑来查找所有子控件,并且使用控件ID作为视图状态的键值,这可以保证在网页中的唯一性。

完整代码如下:

public partial class ViewStateTest : System.Web.UI.Page

{

protected void cmdSave_Click(object sender, System.EventArgs e)

{

// Save the current text.

SaveAllText(Table1.Controls, true);

}

private void SaveAllText(ControlCollection controls, bool saveNested)

{

foreach (Control control in controls)

{

if (control is TextBox)

{

// Store the text using the unique control ID.

ViewState[control.ID] = ((TextBox)control).Text;

}

if ((control.Controls != null) && saveNested)

{

SaveAllText(control.Controls, true);

}

}

}

protected void cmdRestore_Click(object sender, System.EventArgs e)

{

// Retrieve the last saved text.

RestoreAllText(Table1.Controls, true);

}

private void RestoreAllText(ControlCollection controls, bool saveNested)

{

foreach (Control control in controls)

{

if (control is TextBox)

{

if (ViewState[control.ID] != null)

((TextBox)control).Text = (string)ViewState[control.ID];

}

if ((control.Controls != null) && saveNested)

{

RestoreAllText(control.Controls, true);

}

}

}

}

6-1显示了在运行的网页。

 

在视图状态中存储对象(Storing Objects in View State

在视图状态中存储对象与存储数值和字符串类型一样简单。但是,为了在视图状态中存储一个项,ASP.NET应该能够将其转化为字节流,因此可以将其添加到页面中的隐藏输入域。这个过程叫序列化(serialization)。如果对象是不可序列化的(默认),若将其放入视图状态,就会收到一条出错信息。

为了使对象可序例化,在定义类之前应当添加Serializable属性。例如,下面是一个非常简单的Customer类:

[Serializable]

public class Customer

{

public string FirstName;

public string LastName;

public Customer(string firstName, string lastName)

{

FirstName = firstName;

LastName = lastName;

}

}

由于Customer类被标记为可序列化,它就可以被存入视图状态:

// Store a customer in view state.

Customer cust = new Customer(" Marsala ", "Simons");

ViewState["CurrentCustomer"] = cust;

记住,使用自定义对象时,需要对从视图状态获取的数据进行数据转换。

// Retrieve a customer from view state.

Customer cust;

cust = (Customer)ViewState["CurrentCustomer"];

为了使类是可序列化的,还需要满足如下要求:

l         类必须有Serializable属性。

l         继承的任何源类(any class it derives from)也应当具有Serialization属性。

l         类的所有私有变量必须是可序列化数据类型。任何不可序列化数据类型都应当由NonSerialized属性修饰(这样在序列化时就对其忽略)

一旦理解了这些原则,你就能够确定在视图状态中存储什么样的.NET对象。要查找类的相关信息,可以向MSDN寻求帮助。找到你感兴趣的类,检查其文档。如果该类之前有Serializable属性,就可以将其放入视图状态。如果没有Serialization属性,对象是不可序列化的,就不能将其存入视图状态。当然,你仍可以使用其它的状态管理器,如进程内会话状态(in-process session state, 在Session State一节中描述)。

下面的示例中,使用Hashtable类重写了前面的页面。Hashtable类是一个可序列化字典集,它由System.Collections名字空间提供。由于它是可序列化的,可以将其存入视图状态。为了演示这个技术,页面在hashtable中存储了所有控件信息,并且将hashtable添加到页面的视图状态。当用户点击Display按钮时,就获得hashtable,它包含的所有信息都将在label中显示。

public partial class ViewStateObjects : System.Web.UI.Page

{

// This will be created at the beginning of each request.

Hashtable textToSave = new Hashtable();

protected void cmdSave_Click(object sender, System.EventArgs e)

{

// Put the text in the Hashtable.

SaveAllText(Table1.Controls, true);

// Store the entire collection in view state.

ViewState["ControlText"] = textToSave;

}

private void SaveAllText(ControlCollection controls, bool saveNested)

{

foreach (Control control in controls)

{

if (control is TextBox)

{

// Add the text to a collection.

textToSave.Add(control.ID, ((TextBox)control).Text);

}

if ((control.Controls != null) && saveNested)

{

SaveAllText(control.Controls, true);

}

}

}

protected void cmdDisplay_Click(object sender, System.EventArgs e)

{

if (ViewState["ControlText"] != null)

{

// Retrieve the hashtable.

Hashtable savedText = (Hashtable)ViewState["ControlText"];

// Display all the text by looping through the hashtable.

lblResults.Text = "";

foreach (DictionaryEntry item in savedText)

{

lblResults.Text += (string)item.Key + " = " +

(string)item.Value + "<br />";

}

}

}

}

6-2显示了简单的测试结果。输入一些数据,保存,然后获取。

6-2 从视图状态获取对象  

保留成员变量(Retaining Member Variables

与控件属性不同,添加到Web-page类中的成员变量永远都不会存储到视图状态中。有趣的是,你可以使用视图状态绕过这个限制。

有两个基本的方法。第一个是创建一个属性进程来包装视图状态访问。例如,在前面的网页中,你可以为控件指定hashtable文本属性,如下所示:

private Hashtable ControlText

{

get

{

if (ViewState["ControlText"] != null)

return (Hashtable)ViewState["ControlText"];

else

return new Hashtable();

}

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

}

网页的其它代码就可以自由地使用ControlText属性,不需要担心怎样获取它。

另一个方法是,在Page.PreRender事件发生时,保存所有成员变量到视图状态中,而在Page.Load事件发生时,获取成员变量。这样,所有其它事件处理器就可以正常使用这个成员变量。

记住,在使用这些技术时,应该避免存储不必要的信息。如果在视图状态中存储了不必要的信息,将会增大最终输出页面的尺寸,从而延长网页传递时间。  

评估视图状态(Assessing View State

视图状态非常理想,因为它不占用任何内存,并且也没有任意的用法限制(如超时)。那么,是什么可能使你放弃使用视图状态,而选择其它类型的状态管理呢?有三个可能的原因:

l         需要存储关键的、不允许用户篡改的数据(一个水平很高的用户可以在回传请求中修改视图状态)。在这种情况下,考虑使用会话状态。另外,可以考虑使用下一节描述的countmeasure。它们并不是绝对安全的,但是它们确实使攻击者读取或修改视图状态数据更加困难得多。

l         需要存储由多个网页使用的信息。在这种情况下,考虑使用会话状态、cookies或者查询字符串。

l         需要存储大量的信息,但又不想增长网页的传输时间。这种情况下,考虑使用数据库或者会话状态。

视图状态所需要的空间大小取决于控件的数量,控件的复杂性和动态信息的数量。如果想描述页面的视图状态用法,仅仅需要添加Trace属性到Page指令中,以打开跟踪。如下所示:

<%@ Page Language="c#" Trace="true" ... %>

查找Control Tree 项。尽管它不提供页面使用的整个视图状态,但它指明了ViewState Size Bytes列中每个单独的控件所使用的视图状态(图6-3)。不必对Render Size Bytes列考虑过多,它仅仅反映控件渲染的HTML的大小。

6-3 确定页面中使用的视图状态

提示:你也可以使用第2章介绍的ASP.NET Development Helper来检查页面的当前视图状态的内容。

为减少网页的传输时间,尽量在不需要视图状态时禁用视图状态。尽管可以在应用和页面层级禁用视图状态,但基于单个控件禁用视图状态更有意义。下列三种情况,控件不需要视图状态:

l         控件不会发生改变,如有静态文本的按钮不需要视图状态。

l         控件在每次回传时都要重新配置。例如,有一个标签显示当前时间,对当前时间的设置位于Page.Load事件处理器中,它就不需要视图状态。

l         控件是一个输入控件,它仅仅在有用户动作时才发生改变。每次回传之后,ASP.NET使用提交来为输入控件设置值。这意味着文本框中的文本或者列表框中的选项不会丢失,即便你不使用视图状态也不会丢失。

提示,记住,视图状态应用所有的变化,而不仅仅是控件中显示的文本。例如,如果动态改变标签中的颜色,即便动态设置文本,仍然需要使用视图状态。从技术上讲,使用视图状态是控件的基本要求,因此创建一个不保存值的服务器控件也是可能的,即便视图状态启用。但这是为了某种场合下提高性能的做法。

要关闭一个控件的视图状态,将控件的EnableViewState属性设置为false。要关闭整个页面和所有控件的视图状态,设置页面的EnableViewState属性为false,或者在Page指令中使用EnableViewState属性,如下所示:

<%@ Page Language="c#" EnableViewState="false" ... %>

即便禁用了整个页面的视图状态,仍可以看到隐藏的具有少量信息的视图状态的标签。这是因为ASP.NET总是要为页面存储控件的层次结构,这一段数据是不可能被移除的。

 

修剪列表控件中的视图状态(Trimming View State in a List Control

在一些控件中,禁用视图状态会导致部分功能无法使用。尽管通过创建控件状态来改善了这一情况(控件使用的视图状态中一个特权节,第27章将对此进行介绍),但仍然存在问题。这是已经存在的控件中的特殊情况,没有引入导致ASP.NET1.x页面终止的行为改变的情况下,仍可能导致不能更新对控件状态的使用。

一个例子是列表控件,如ListBoxDropDownList,怎样跟踪选择。假设你创建了一个页面,需要将成百的实体填充到下拉列表中,如果创建列表的代价不大(例如,直接从内存或缓存中获取列表),你可能选择禁用列表控件的视图状态,并且在每次回传之初重建列表。下面的示例中,用数值填充了列表:

protected void Page_Load(object sender, EventArgs e)

{

for (int i = 0; i < 1000; i++)

{

lstBig.Items.Add(i.ToString());

}

}

这会导致的问题是,一旦你禁用了视图状态,确信在每次回传时用户的选择都丢失了。这意味着不能从SelectIndex或者SelectItem属性获取信息。相似地,SelectIndexChanged事件不会释放。

有一种方法可以解决这个问题。尽管用户的选择信息丢失了,但是用户的选择仍然保存在Request.Forms集中(这是为了提供向ASP兼容的回传集)。可以使用控件名查找已选择的值,并且可以使用如下代码重置正确的选择索引:

protected void Page_Load(object sender, EventArgs e)

{

for (int i = 0; i < 1000; i++)

{

lstBig.Items.Add(i.ToString());

}

if (Page.IsPostBack)

{

lstBig.SelectedItem.Text = Request.Form["lstBig"];

}

}

注意,显然这代表了某种情形,即需要对用户接口进行重新构想,使其可用性更好。例如,一个更好的设计是询问用户原始的问题来使列表实体数量减少。你甚至可能使用Wizard控件来模型化整个过程。但是,如果你确实需要具有大量实体的列表,你就需要理解怎样优化对视图状态的使用。

 

视图状态安全(View State Security

如在前面章节中介绍的那样,视图状态信息存储在一个单独的Base64编码的字符串中,如下所示:

<input type="hidden" name="__VIEWSTATE" value="dDw3NDg2NTI5MDg 7Oz 4="/>

由于这个值并没有格式化为清楚的文本,多数ASP.NET程序员将视图状态值看成是加密的。但事实并非如此。一个聪明的黑客可以反转这个字符串,并且几秒中内就可以查看视图状态数据。这在第3章中进行了演示。

如果想增强视图状态的安全,有两种方法。一种是使用哈希编码(hash code)使视图状态信息能够防篡改(tamper-proof)。

要实现这一点,可以在.aspx文件的Page指令中添加EnableViewStateMAC属性。如下所示:

<%@ Page EnableViewStateMAC="true" %>

哈希编码是加密的强校验和。本质上,ASP.NET基于当前视图状态内容计算校验和,并在返回页面时将其添加到隐含输入域中。当网页回传时,ASP.NET重计算校验和,并且进行匹配。如果恶意的用户修改了视图状态数据,ASP.NET能够检测到这个改变,然后拒绝回传。

哈希编码在默认状态下是启用的,因此如果想使用这个功能,不需要任何额外的步骤。程序员偶尔会禁用这个功能来防止在不同服务器有不同值时可能出现的问题。(当网页回传而且由一个新的服务器处理时,可能会发生这个问题,因为新的服务器不能确认视图状态信息)。要禁用哈希编码,可以在web.config或者machine.config文件的<pages>元素中使用enableViewStateMac属性,如下所示:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

<system.web>

<pages enableViewStateMac="false" />

...

</system.web>

</configuration>

注意,这样做并不是很好。配置多个服务器时使用相同的值会更好,那样可以解决问题。第5章对如何操作进行了介绍。

尽管使用哈希编码,视图状态数据仍然是可读取的。为了防止用户获得任何视图状态信息,可以启用视图状态加密(encryption)。可以在Page指令中使用ViewStateEncryptionMode属性来为单个网页启用加密。

<%@Page ViewStateEncryptionMode="Always">

也可以在web.config配置文件中设置相同的属性:

<pages viewStateEncryptionMode="Always">

任何一种方法都可以强制加密。视图状态加密设置有3个选项:总是加密(Always)、从不加密(Never)、控件明确请求的加密(Auto)。默认值是Auto,意思就是控件必须调用Page.RegisterRequiresViewStateEncryption()方法来请求加密。如果控件没有调用这个方法来指明它包含了敏感信息,视图状态就不会加密。因此减少了加密带来的负载。另一方面,控件调用加密并不是绝对有效的,如果它调用了Page.RegisterRequiresViewStateEncryption(),但加密选项的选择是Never,视图状态仍不会被加密。

进行哈希编码或者加密数据时,ASP.NET使用计算机特定的键值(key),这个键值在machine.config文件的<machineKey>节中定义(第5章描述过)。默认情况下,你不会在<machinKey>中看到明确的定义,因为这是程序初始化时创建的。当然,你可以在machine.config.comments文件中看到相同的内容,并且想要自定义设置的话,也可以显性地添加<machineKey>元素。

提示,如果不是很必要,就不要对视图状态数据进行加密。因为Web服务器在每次回传时都需要执行加密和解密,这会导致性能降低。

 

传递信息(Transferring Information

视图状态的一个最重要的限制是紧密地绑定到一个特定的页面。如果用户导航到另一个网页,这个信息就会丢失。解决这个问题有几个方法,最好的方法是依赖于请求进行。

 

查询字符串(The Query String

一个通用的方法是在URL中使用查询字符串传递信息。可以在很多搜索引擎中找到这个方法。例如,如果你在Google站点执行搜索,你会重定向一个带了搜索参数的新的URL。示例如下

http://www.google.ca/search?q=organic+gardening

查询字符串是URL的问题标签后的一部分。在这种情况下,它定义了一个单独的变量,命名为ask,包含了“organic gardening”字符串。

查询字符串的优点是它是轻量的,并且不会增大服务器端的负担。相比交叉页面传递,查询字符串能够很容易地将相同信息从一个页面传递到另一个页面。当然,也有一些限制:

l         信息限制为简单字符串,包含的字符必须合乎URL规范。

l         信息对用户是可见的,但对任何从网上进行监听的人也是可见的。

l         一个有企图的用户可能想修改查询字符串,并且提供新值,这有可能使你的程序无法防护。

l         一些浏览器对URL长度有限制(通常在2KB以内)。因此,不能在查询字符串中放置大量信息,并且必须保证与大多数浏览器兼容。

将信息添加到查询字符串是一个有用的技术,它非常适用于数据库应用。在数据库应用中,项的列表对应于数据库的记录,例如产品。可以选择一项,并且将选择项的详细信息转发到另一个页面。实现这个设计的一个简单方法是使第一个页面发送项的ID到第二个页面。第二个页面于是数据库中相应的项,然后显示详细信息。你可以在电子商务站点,如Amazon.com上看到这个技术的应用。

 

使用查询字符串(Using the Query String

要在查询字符串中存储信息,需要你自己来完成这个过程。不幸的是,没有基于集的方法来做这一点。通常情况下,这意味着需要使用特殊的HyperLink控件,或者使用Response.Redirct()语句,如下所示:

// Go to newpage.aspx. Submit a single query string argument

// named recordID and set to 10.

int recordID = 10;

Response.Redirect("newpage.aspx?recordID=" + recordID.ToString());

也可以发送多个参数。参数之间用and 符号(&)分隔:

// Go to newpage.aspx. Submit two query string arguments:

// recordID (10) and mode (full).

Response.Redirect("newpage.aspx?recordID=10&mode=full");

接收页面能够很容易地对查询字符串进行处理。它能够QueryString字典集来接收值。这个字典集位于Request对象内部,如下所示:

string ID = Request.QueryString["recordID"];

注意,信息通常是作为字符串接收的,能够被转换为其它简单数据类型,在QueryString集中的值是根据变量名来索引的。

注意,不幸的是,ASP.NET并没有提供自动确认和加密查询字符的机制。这与对视图状态的保护是一样的,如果没有这些功能,查询字符串的数据极易被篡改。在第25章,你将更深入了解加密类,并且学习如何使用它们来创建可信的安全的查询字符串。

URL编码(URL Encoding

查询字符串中包含了URL不允许的字符时,会出现潜在的问题。允许在URL中使用的字符列表应当比允许HTML文档中使用的字符要短很多。所有字符必须是包含字符和数字的,或者是一些特殊字符,包括$-_.+!*’(),。一些浏览器容许一定数量的其它特殊字符,但是多数是不允许的(包括IE)。

如果担心存储在查询字符串中的数据可能包含了不符合URL规范的字符,那应该使用URL编码。使用URL编码,特定的字符就被百分号(%)开始的转义字符序列(escaped character sequences)所替代。百分号(%)后紧跟着两个十六进制数字。转义字符序列会替换掉所有不合法的字符:

string productName = "Flying Carpet";

Response.Redirect("newpage.aspx?productName=" + Server.UrlEncode(productName));

对信息进行解码示例如下:

string ID = Server.UrlDecode(Request.QueryString["recordID"]);

交叉页面传递(Cross-Page Posting

触发回传到另一个页面是ASP.NET20里的新方法。这个技术的概念看起来就它的名字一样直观,但它其实存在一些潜在的危险。如果不小心,有可能会导致创建与另一个网页紧耦合的页面,这会使功能增强和调试都变得困难。

支持交叉页面回传的基础结构是一个新的属性,名叫PostBackUrl,由按钮控件里的IbuttonControl接口所定义,按钮控件包括ImageButton, LinkButtonButton。为了进行交叉传递,只需要简单地设置PostBackUrl为另一个Web窗体的名字。当用户点击按钮时,页面就传递到新的UrlUrl中带有当前页面的所有输入控件的值。

下面的示例中,定义了一个窗体,两个文本框和一个按钮,交叉传送的接收页面名为CrossPage2.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="CrossPage1.aspx.cs"

Inherits="CrossPage1" %>

<html>

<head runat="server">

<title>CrossPage1</title>

</head>

<body>

<form id="form1" runat="server" >

<div>

<asp:TextBox runat="server" ID="txtFirstName"></asp:TextBox> &nbsp;

<asp:TextBox runat="server" ID="txtLastName"></asp:TextBox>

<asp:Button runat="server" ID="cmdSubmit"

PostBackUrl="CrossPage2.aspx" Text="Submit" />

</div>

</form>

</body>

</html>

CrossPage2.aspx中,网页可以使用Page.PreviousPage属性与CrossPage1.aspx对象交互。示例代码如下:

protected void Page_Load(object sender, EventArgs e)

{

if (PreviousPage != null)

{

lblInfo.Text = "You came from a page titled " +

PreviousPage.Header.Title;

}

}

注意,在访问PrevioursPage对象之前,先检查了其是否为null值。如果没有PreviousPage对象,就没有交叉页面回传。

为了使这个系统工作,ASP.NET使用了一些技巧。第二个页面第一次访问Page.PreviousPage时,ASP.NET需要创建一个先前的页(previous page)对象。这个过程是在ASPNET开始开始处理页面至PreRender阶段前进行的。基于这个方法,创建了一个替代的Response对象来悄悄地抓取或者忽略来自于前一个页面的任何Response.Write()命令。当然,这会带来一些有趣的副作用。例如,来自于前一个页面的所有页面事件释放后,包括Page.Load, Page.Init,甚至是Button.Click事件(它触发回传)。释放这些事件是强制的,因为它们是正确初始化网而的必要条件。跟踪信息不会Response信息一样被忽略,这意味着你可以看到来自于交叉传递的两个页面的信息。

获取页面特定的信息(Getting Page-Specific Information

在前面的示例中,你能够从先前页面获取的信息受限于Page类的成员。如果想获得更多特定的信息,如控件的值,则需要将PreviousPage引用转换为正确的类型。

下面是对这种情况进行正确处理的示例。示例中首先对PreviousPage对象是不是一个期待源(CrossPage1)的一个实例进行了检查:

protected void Page_Load(object sender, EventArgs e)

{

if (PreviousPage != null)

{

CrossPage1 prevPage = PreviousPage as CrossPage1;

if (prevPage != null)

{

// (Read some information from the previous page.)

}

}

}

也可以采用其它方法来解决这个问题。你可以添加PreviousPageType指令到页面中,来指明初始化交叉回传页面时所期望的页面类型,而不用手工对引用进行转换。如下所示:

<%@ PreviousPageType VirtualPath="CrossPage1.aspx" %>

但是,这个方法非常脆弱,因为它将你限制到一个单一的类型。以致于有多个页面可能会触发交叉页面回传时,缺乏必要的灵活性。因此,手工转换的方法更好一些。

提示,由于PostBackUrl属性能够指向一个单一的页面,看起来交叉传递能够提供两个页面的固定关系。但是,可以通过不同的技术来扩展这个关系。例如,你可以通过程序修改PostBackUrl属性来选择不同的目标。反过来,一个交叉传递的目标能够测试PreviousPage属性,检查它是否是几个同类的中的一个。这样你就可以根据页面初始化的哪个交叉传递来执行不同的任务。

一旦将先前的页面(previous page)转换为一个正确的页面类型,你就不能直接访问控件值。因为控件被声明为保护成员。可以通过添加属性到页面类中,包装控件变量来解决这个问题,示例如下:

public TextBox FirstNameTextBox

{

get { return txtFirstName; }

}

public TextBox LastNameTextBox

{

get { return txtLastName; }

}

但是,这并不是最好的方法。其问题在于它暴露了太多的细节,使目标页面能够自由地读取每一个控件属性。如果稍后想使用不同的输入控件来改变页面,维护这些属性就变得困难。这样,你可能必须对两个页面的代码进行重写。

一个更好的方法是定义明确的、限制的方法,仅仅解析所需要的信息,示例如下:

public string GetFullName()

{

get { return txtFirstName.Text + txtLastName.Text; }

}

这样,两个页面的关系就是记录良好的和易于理解的。如果源网页中的控件发生了改变,你仍可能保持了公用方法的相同接口。例如,如果在先前的页面中改变了名字入口来使用不同的控件,你可能仍必须修改GetFullName()方法。但是,一旦你的改变限制于CrossPage1.aspx,你就不需要修改CrossPage2.aspx了。

提示,在多数情况下,对交叉页面传递的更好方法是使用某种类型的控件,来模拟多个页面或者多个步骤,如分离的Panel控件,或者MultiViewWizard控件。这提供了更多的相同的用户体验,并且简化了编码模型。

在事件处事器中执行交叉页面传递(Performing Cross-Page Posting in any Event Handler

正如前一节所了解的那样,仅仅在实现了IbuttonControl接口的控件中才可以进行交叉页面传递。当然,这儿有一个工作区(workaround)。你可以使用方法Server.Transfer()来切换到一个带初始视图状态信息的新的ASP.NET页面。你只需要简单地包含Boolean preserveForm参数,并且将其设置为true,如下所示:

Server.Transfer("CrossPage2.aspx", true);

这使你能够在Web页面代码的任何地方使用交叉页面传递。

有趣的是,存在一个方法来区分由按钮直接初始化的交叉页面和Server.Transfer()方法。尽管在两个类中,你都可以访问Page.PreviousPage,如果使用Server.Transfer(),则Page.PreviousPage.IsCrossPagePostBack属性值为false。下面的伪代码演示了这个区分方法:

if (PreviousPage == null)

{

// The page was requested (or posted back) directly.

}

else if (PreviousPage.IsCrossPagePostBack)

{

// A cross-page postback through a button.

}

else

{

// A stateful transfer through Server.Transfer().

}

 

交叉页面传递和验证(Cross-Page Posting and Validation

当使用交叉页面与验证控件(第4章介绍过)协作时,交叉页面传递引入了几个新方法。如在第4章中了解的那样,当你使用验证控件时,你需要检查Page.IsValid属性来确保用户输入的数据是正确的。尽管用户通常会防止传递无效的页面到服务器(使用精巧的客户端JavaScript),但这并不能保证万无一失。例如,客户端浏览器可能不支持JavaScript,或者恶意用户有预谋地避开客户端有效性校验。

当你在交叉页面传递时使用验证时,存在一些潜在的问题。也就是说,如果你使用交叉页面回传并且源网页具有验证控件时,会发生什么呢?图6-4显示了带有RequireRieldValidator(要求文本框不能为空)的示例。

6-4 在交叉回传的网面中使用验证器

如果点击其中一个按钮来执行交叉回传(它们的CauseValidation都设置为true),则会被客户端的检查所禁止。同时会抛出一个错误信息。但是,当客户端设置RequiredFieldValidator.EnableClientScript属性为false(也可以在修改了代码之后将其设置为false)而禁用了脚本支持后,你需要对会发生什么情况进行检查。现在,点击其中一个按钮,进行页面回传,就会产生一个新的网页。

为防止这个情况发生,在通过检查Page.IsValid来执行别的动作前,你显然需要检查目标网页中的源网页的有效性。这是在使用了验证的任何一个Web网页中进行保护的一个标准线。不同之处是,如果网页无效,也并不是说它就不能做任何事情。相反,你需要采用额外的步骤将用户返回原来的页面。下面是示例代码:

protected void Page_Load(object sender, EventArgs e)

{

if (PreviousPage != null)

{

if (!PreviousPage.IsValid)

{

// Display an error message or just do nothing.

}

else

{ ... }

}

}

还可以对这段代码进行优化。当前,用户返回原始页面时,错误信息不会显示,因为页面正在被重请求(而不是回传)。为了修正这个问题,你可以设置一个标签,使源网页知道页面已经被目标网页重用了。下面的示例添加了一个标签到查询字符串中:

if (!PreviousPage.IsValid)

{

Response.Redirect(Request.UrlReferrer.AbsolutePath + "?err=true");

}

现在,原始原面只需要简单地检查查询字符串的值是否存在,并且相应地执行验证。验证导致了错误消息,以显示任何无效的数据。

protected void Page_Load(object sender, EventArgs e)

{

if (Request.QueryString["err"] != null)

Page.Validate();

}

你还可以做更多的工作来增强页面。例如,如果用户正在填写详细的窗体,重新请求(re-request)网页就不是很好的方法,因为它清除了所有的输入控件,并且强迫用户重新开始。相反,你可能想要写小段JavaScript代码来响应数据流,数据流使用了浏览器的备份功能来返回到源网页。第29章有关于JavaScript的更多内容。

 

自定义Cookies(Custom Cookies)

自定义cookies提供了存储信息以供后续使用的另一种方法。Cookies是一个很小的文件,创建于客户端硬盘上(如果是临时的,就在浏览器缓存中)。Cookies的一个优点是它们的工作是透明的,用户不需要关心其存储的信息。它们极易被应用中的任何其它页面所使用,并且在多次访问间保存下来,这样可以保存较长的时间。它们也有与查询字符串(query strings)相同的缺点,即它们限制于简单的字符串信息,并且如果用户找到并打开了相应文件,对cookies也极易访问和读取。这些因素使复杂的、隐私的或者大规模的数据应用场合无法使用cookies

一些用户在他们的浏览器中禁用了cookies,这会导致要求使用cookies的应用出现问题。对于大部分用户而言,cookies是广泛使用的,因为许多站点都会用到它们。但是,cookies限制了你的潜在用户,如它们对移动设备上的嵌入式浏览器并不适用。

在使用cookies之前,你应当导入System.Net名字空间,以便于你可能对正确的类型进行处理。如下所示:

Using System.Net;

Cookies非常易于使用。无论请求对象还是响应对象(由Page属性提供)都提供了Cookies集。一个重要的技巧是要记住,cookies是通过Request对象获取,通过Response对象设置的。

为了设置cookie,需要创建一个新的System.Net.HttpCookie对象。然后用字符串信息对其进行填充(使用相似的字典模式),并且将其附加到当前的网页响应中。示例如下:

// Create the cookie object.

HttpCookie cookie = new HttpCookie("Preferences");

// Set a value in it.

cookie["LanguagePref"] = "English";

// Add it to the current web response.

Response.Cookies.Add(cookie);

通过这种方式添加的cookie会保存到用户关闭浏览器,并且会在每一次请求中发送。为了创建更长生存时间的cookie,你可以设置失效时间。

Cookies是通过cookie名获取的,这需要使用Request.Cookies集。如下:

HttpCookie cookie = Request.Cookies["Preferences"];

// Check to see if a cookie was found with this name.

// This is a good precaution to take,

// because the user could disable cookies,

// in which case the cookie will not exist.

string language;

if (cookie != null)

{

language = cookie["LanguagePref"];

}

移除cookie的唯一方法是使用一个已经过期的cookie来替换它。下面的代码演示了这个技术:

HttpCookie cookie = new HttpCookie("LanguagePref");

cookie.Expires = DateTime.Now.AddDays(-1);

Response.Cookies.Add(cookie);

注意,你会发现一些其它的ASP.NET功能使用cookies。两个例子是会话状态(session state,允许你在服务器内存中保存用户特定的信息)和窗体安全(forms security,允许你限制站点的入口,并且强制用户通过一个登录页面来访问)。

会话状态(Session State

会话状态是状态管理里的重头戏。它允许信息存储于一个页面中,并且可以在别的网页里访问,它还支持任何对象类型,包括自定义数据类型。最为重要的是,会话状态使用了与视图状态相同的集语法(collection syntax),唯一的不同是内建页属性的名字不同,它叫Session

每一个访问应用的客户都有一个不同的会话和信息的单独集。会话状态是存储信息(如当用户从一个页面浏览到另一个页面时,在当前用户的购物篮中的项目)的理想方式。但会话状态的产生并不是为了自由访问。虽然它处理了许多与其它状态管理的窗体相关的问题,它强制Web服务器存储额外的信息到内存中。这要求额外的内存,即便它很小,能够快速提高性能-----当成百上千客户访问站点时破坏等级。

 

会话结构(Session Architecture

会话管理并不是HTTP标准的一部分。因此,ASP.NET需要做一些额外的工作来跟踪会话信息,并且将其绑定到正确的响应。

ASP.NET使用唯一的120位标识器来跟踪每个会话。ASP.NET使用私有的算法来产生这些值,因此保证(统计意义上)那个数值是唯一的,并且它足够随机,以致于恶意的用户并不能逆转或猜解客户将要使用哪个会话ID。这个ID是信息的小段,在Web服务器与客户端之间传递。当客户端提供了会话IDASP.NET查找对应的会话,从状态服务器获取序列化的数据,转换为活动的对象(live objects),并且将这些对象放置到一个特殊的集中以便于能够通过代码访问它们。这个过程是自动进行的。

记住,每次你发出一个新的请求,ASP.NET产生一个新的会话ID,直到你确实使用了会话状态来存储一些信息。这个行为获得了一些性能的增强。简单地说,如果不需要使用,还保存会话ID干什么呢?

此时,你可能想知道会话信息存储在什么位置,它是怎样序列化和反序列化的。在经典的ASP 中,会话状态是作为一个独立线程的COM对象来实现的,COM对象包含在ASP.DLL库中。而在ASP.NET中,编程接口几乎是完全一样的,但是底层的实现就大为不同了。

正如你在第5章所见到的那样,当ASP.NET处理HTTP请求时,它流过几个不同模型之间的管道,模型对应用事件进行响应。在这个链条上的一个模型就是SessionStateModule(System.Web.SessionState名字空间中)SessionStateModule产生会话ID,从额外的状态提供者那里获取会话数据,并且将数据绑定到一个请求的调用背景上。它也在页面完成处理时,保存会话状态信息。但是,很重要的一点是要认识到SessionStateModule并不是真正地存储会话数据。相反,会话状态保持在外部组件状态提供者中。图6-5显示了交互过程。

6-5 ASP.NET会话状态结构

会话状态是ASP.NET可插入结构的另一个例子。状态提供者是一个实现了IstateClientManager接口的类。这样方便你通过构建(或者购买)一个新的.NET组件来自定义会话状态的工作过程。ASP.NET提供了三个预建的状态提供者,使你能够将信息存储于进程、独立的服务,或者SQL Server数据库中。

有一点始终让人迷惑的是,cookie怎样对一个请求到另一个请求进行跟踪。为了使会话状态正常工作,客户端需要在每个请求中提供正确的会话ID。有两种方法可以做到这一点:

l         使用cookies(Using cookies):在这种情况下,会话状态ID在一个特殊的cookieASP.NET_SessionId)中传递。特殊的cookie在使用会话集时由ASP.NET自动创建。这是默认的。这与以前的ASP里的方法是一样的。

l         使用修改后的URLs(Using modified URLs):在这种情况下,会话状态在一个特殊的修改的URL中传递。这是ASP.NET中的新特征,允许你创建的应用在不支持cookie的客户端能够使用会话状态。

你将在“配置会话状态(Configuring Session State)”节中学到更多关于如何配置无cookie的会话和不同的会话状态提供者的知识。

 

使用会话状态(Using Session State

你可以使用System.Web.SessionState.HttpSessionState类来与会话状态进行交互,该类在ASP.NET网页中作为一个内嵌的Session对象来提供的。添加一项到集(collection)中并且获取它们的语法与添加项到页面的视图状态中是一样的。

例如,你可能用类似如下的代码来存储DataSet到会话内存中:

Session[“ds”] = ds;

你可以使用一个正确的转换操作来获取它:

Ds = (DataSet)Session[“ds”];

会话状态对于当前用户的整个应用来讲是全局的。会话状态在下面几种情况下可能会丢失:

l         如果用户关闭和重启动浏览器;

l         如果用户通过不同的浏览器窗口来访问相同的页面,尽管通过普通的浏览器窗口可以访问网页时,会话仍然存在,浏览器对于会话的处理仍是不同的。

l         如果由于不活动状态超时,默认情况,会话状态的不活动超时为20分钟。

l         如果程序员通过调用Session.Abandon()终止了会话。

在前两种情况,会话仍然保存在内存中,因为Web服务器并不知道客户端是关闭了浏览器还是改变了浏览器窗口。会话会在最终超时前保持在内存中,并且可以访问。

另外,当应用域重建时,会话状态也会丢失。当你更新Web应用或者改变配置时,这个过程就会透明地发生。应用域也会周期性地循环以保证应用完好,第18章对此有描述。如果这个行为导致了问题发生,你可以在进程之外保存会话状态信息(下一节描述)。使用进程外状态存储,会话信息就会在应用域关闭的情况下仍能保存。

6-4描述了HttpSessionState类的方法和属性。

Member  

 Description  

 Count  

 The number of items in the current session collection.  

IsCookielessSession

 Identifies whether this session is tracked with a cookie or with modified URLs. 

 IsNewSession  

 Identifies whether this session was just created for the current request. If there is currently no information in session state, ASP.NET wont bother to track the session or create a session cookie. Instead, the session will be re-created with every request.  

 Mode  

 Provides an enumerated value that explains how ASP.NET stores session state information. This storage mode is determined based on the web.config configuration settings discussed later in this chapter.  

 SessionID  

 Provides a string with the unique session identifier for the current client.  

 StaticObjects  

 Provides a collection of read-only session items that were declared by <object runat=server> tags in the global.asax. Generally, this technique isnt used and is a holdover from ASP programming that is included for backward compatibility.  

 Timeout  

 The current number of minutes that must elapse before the current session will be abandoned, provided that no more requests are received from the client. This value can be changed programmatically, giving you the chance to make the session collection longer term when required for more important operations.  

 Abandon()  

 Cancels the current session immediately and releases all the memory it occupied. This is a useful technique in a logoff page to ensure that server memory is reclaimed as quickly as possible.  

 Clear()  

 Removes all the session items but doesnt change the current session identifier.  

 

6-4 HttpSessionState成员

 

配置会话状态(Configuring Session State

可以在web.config文件的<sessionState>元素中为应用配置会话状态。下面是会话状态的所有可获得的设置:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>

<system.web>

<!-- Other settings omitted. -->

<sessionState

mode="InProc"

stateConnectionString="tcpip=127.0.0.1:42424" stateNetworkTimeout="10"

sqlConnectionString="data source=127.0.0.1;Integrated Security=SSPI"

sqlCommandTimeout="30" allowCustomSqlDatabase="false"

useHostingIdentity="true"

cookieless="UseCookies" cookieName="ASP.NET_SessionId"

regenerateExpiredSessionId="false"

timeout="20"

customProvider=""

/>

</system.web>

</configuration>

会话属性在下面各节中具体描述。

 

Mode

Mode会话状态设置允许你配置使用哪个会话状态提供者来存储多个请求的会话状态信息。下面对这些选项进行解释。

 

Off

禁用了应用中每个页面的会话状态管理。这可以对站点的性能有些许提高。

 

InProc

InProc与经典的ASP中如何存储会话状态是相似的。它指示ASP.NET在当前应用域中存储信息。这提供了最好的性能和最短的持续性。如果重启了服务器,状态信息就会丢失。

InProc是默认选项,这对于大多数小型站点来说非常有意义。在Web庄园(web farm)场合,它就不会工作了。为了使会话状态能在多个服务器间共享,必须使用进程外(out-of-process)或者SQL Server状态服务。应该避免使用InProc模型的另一个原因是,它会造成会话更脆弱。在ASP.NET中,应用域循环以响应不同的动作,包括配置改变、更新页面和达到阀值(无论是不是发生了错误)。如果发现应用频繁重启,并且导致过早丢失会话,你可以通过试验改变一些进程模型的设置来看看效果(详见第18章),也可以改变使用一个或多个更为强健的会话状态提供者。

在使用进程外或SQL Server状态服务前,请记住下面这些注意事项:

l         使用StateServerSqlServer模式时,存储在会话状态中的对象必须可序列化。否则,ASP.NET不能将对象传递到状态服务中,或者存储于数据库中。

l         如果ASP.NET部署于网页庄园(Hosting ASP.NET on a web farm)中,你需要一些额外的配置步骤来确保所有Web服务器同步。否则,会话状态中的编码信息可能会与另一服务器上的不同,如果在会话期间,用户访问从一个服务器跳转到另一服务器,就会出现问题。解决办法就是修改machine.config文件中的<machineKey>节,使所有的服务器保持一致。更多信息,详见第5章。

l         如果没有使用进程内(in-process)状态提供者,SessionStateModule.End事件就不会释放,在globale.asax文件或者HTTP模型中对应的事件处理器会被忽略。

 

StateServer

使用这个设置,ASP.NET状态管理使用独立的Windows服务。即便你在同一Web服务器上运行这个服务,它仍会在ASP.NET主进程外加载,这在ASP.NET进程需要重启时提供了基本的保护。代价是增长了两个进程间传递状态信息时的时延。如果你频繁访问和改变状态信息,就会变得相当慢。

使用StateServer设置时,你需要为stateConnectionString指定值。这个字符串标识了运行StateServer服务的计算机的TCP/IP地址和它的端口号(已经由ASP.NET定义,并且通常不会改变)。这允许你将StateServer部署到另一台计算机。如果不修改这个设置,则使用本地服务器(地址设置为127.0.0.1)。

当然,应该在使用应用之前启用服务。最简单的方法是使用微软管理控制台(Microsoft Management Console),即控制面板里的计算机管理,然后在服务里找到ASP.NET State,如图6-6所示。

6-6 ASP.NET状态服务

一旦在列表中找到了服务,你就可以手动地启动和停止服务。通常情况下,需要将其设置为自动启动。如图6-7所示。

6-7 服务属性

注意,使用StateServer模式时,可以设置可选的stateNetworkTimeout属性来指明在取消请求前,等待服务的最长时间(单位:秒)。默认值为10秒。

 

SqlServer

这个设置指示ASP.NET使用SQL Server数据库来存储会话信息,由sqlConnectionString属性标识。这是能够最大程度地自修复的状态存储,但也是最慢的。要使用这个状态管理方法,需要安装SQL Server数据库。

设置sqlConnectionString时,应当遵循ASP.NET数据访问相同的模式类型(第2章介绍)。一般来说,你需要指定数据源(服务器地址)和一个用户名及密码,否则你应当使用SQL整合安全(SQL integrated security)。

另外,你需要安装特殊的存储过程和临时会话数据库。这些存储过程负责存储和获取会话信息。为此,ASP.NET引入了Transact-SQL脚本(名为InstallSqlState.sql)。可以在c:/[WinDir]/Microsoft.NET/Framework/[Version]目录中找到。可以使用SQL Server工具来运行这些脚本,如OSQL.exe或者查询分析器。它只能被执行一次。如果决定改变状态服务,可以使用UnistallSqlState.sql来移除状态表。

会话状态超时仍要请求SQL Server状态管理。因为InstallSqlState.sql脚本来创建一个新的SQL Server任务,即ASPState_Job_DeleteExpired Sessions。只要SQLServerAgent服务在运行,这个任务就会不停地执行。

另外,状态表在每次SQL Server重启时被移除,无论会话是否超时。这是因为使用InstallSqlState时,状态表都在tempdb数据库(Tempdb是一个临时的存储区域)中创建。如果不希望出现被移除行为,你可以使用InstallPersistSqlState.sqlUninstallPersistSqlState.sql脚本来代替InstallSqlState.sqlUninistallSqlState.sql。这种情况下,状态表就创建在ASPState数据库中,并且永久保存。

通常情况下,状态数据库一般命名为ASPState。因此,在web.config文件中的连接字符串并没有显性地指明数据库名。相反,它简单地反映了服务器的位置和使用的认证类型:

<sessionState sqlConnectionString="data source=127.0.0.1;Integrated Security=SSPI"... />

如果你想使用不同的数据库(具有相同的结构),简单地设置allowCustomSqlDatabasetrue,并且确信连接字符串包含了Initial Catalog设置。Initial Catalog指明了需要使用的数据库名:

<sessionState allowCustomSqlDatabase="false" sqlConnectionString=

"data source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=CustDatabase"

... />

使用SqlServer模式时,可以设置一个可选的sqlCommandTimeout属性来指明取消请求前等待数据库响应的最长时间。默认值是30秒。

 

Custom

使用自定义模式时,应当通过提供customProvider属性来指明要使用哪个会话状态存储提供者(session state store provider)。 customProvider属性指向Web应用的一个类,该类在App_Code目录,或者Bin目录中的编译后的配件中。

创建自定义状态提供者是一个底层的任务,它需要小心地处理,以确保安全、稳定和扩展。自定义状态提供者不在本书的讨论范围之内。可以在下面的地址找到创建自定义状态提供者的示例:http://weblogs.asp.net/ngur/articles/371952.aspx.

 

Cookieless

可以将HttpCookieMode枚举中的某个值设置为cookieless。这些枚举值在表6-5中描述。

Value

Description

UseCookies

 Cookies are always used, even if the browser or device doesnt support cookies or they are disabled. This is the default. If the device does not support cookies, session information will be lost over subsequent requests, because each request will get a new ID.  

UseUri

 Cookies are never used, regardless of the capabilities of the browser or device. Instead, the session ID is stored in the URL.  

UseDeviceProfile

 ASP.NET chooses whether to use cookieless sessions by examining the BrowserCapabilities object. The drawback is that this object indicates what the device should supportit doesnt take into account that the user may have disabled cookies in a browser that supports them. Chapter 27 has more information about how ASP.NET identifies different browsers and decides  whether they support features such as cookies.

AutoDetect

 ASP.NET attempts to determine whether the browser supports cookies by attempting to set and retrieve a cookie (a technique commonly used on the Web). This technique can correctly determine if a browser supports cookies but has them disabled, in which case cookieless mode is used instead.  

6-5 HttpCookieMode

强制cookieless模式(测试时很有用)的示例代码如下:

<sessionState cookieless="UseUri" ... />

cookieless模式中,会话ID将自动插入到URL中。当ASP.NET接收请求时,移除ID,获取会话集,然后将请求转发给到合适的目录。一个原始URL示例:http://localhost/WebApplication/(amfvyc55evojk455cffbq355)/Page1.aspx

由于会话ID被插入当前URL中,相关的链接自动获得会话ID。换句话说,如果用户当前位于Page1.aspx页面上,并且点开了到Page2.aspx的相关链接,则相关链接包含了作为URL一部分的当前会话ID。调用带有相关URLResponise.Redirct()方法,效果也是一样的:

Response.Redirect(“Page2.aspx”);

对于cookieless状态的唯一限制是不能使用绝对链接。因为它们并不包含会话ID。例如,下面这条语句会导致丢失所有会话信息:

Response.Redirect("http://localhost/WebApplication/Page2.aspx");

默认情况下,ASP.NET允许你重用会话标识。例如,如果你发起了一个请求,而查询字符串中包含了一个过期的会话,则ASP.NET会创建一个新的会话,并且使用刚才的会话ID。导致的问题就是会话ID可能会被公开显示,如在一个搜索引擎的结果页面。这也导致多个用户使用相同的会话标识来访问服务器,并且使用相同的会话来连接共享的数据。

为避免这个潜在的安全危,推荐你使用可选的regenerateExpiredSessionId属性,并且在使用cookieless的任何场合都设置为true。这样,如果用户使用过期的会话ID来连接,则会发布一个新的会话ID。这样做也有一个不足,即这个过程强制当前页面丢弃所有视图状态和窗体数据,因为ASP.NET执行了一个转向(redirect)来确保浏览器有一个新的会话标识。

提示,当前是否使用了cookieless会话,可以通过检查Session对象的IsCookielessSession属性来测试。

 

Timeout

web.config文件中的另一个很重要的会话状态设置是超时(timeout)。它表明ASP.NET等待多长时间没有收到请求,就禁止会话。

<sessionState timeout = “ 20” …/>

这个设置体现了会话状态的最重要的权衡。不同的设置对于服务器的负载和应用的性能的影响是很大的。理想情况下,你会选择一个很短的时间帧,在这个时间帧内,服务器能够在客户端停止使用应用后收回宝贵的内存,但又不会长到客户端在不丢失会话的情况下进行暂停和继续会话。

你也可以在程序中改变会话超时。例如,如果你知道一个会话包含了超大量的信息,你需要限制能够被存储的会话的时间限制。然后你要警告用户,并且改变超时属性。下面的代码行将超时设置改变为10秒:

Session.Timeout = 10;

安全会话状态(Securing Session State

在会话状态中的信息是非常安全的,因为它单独存放在服务器上。但是,带有会话IDcookie却是不安全的。这意味着偷听者能够盗取cookie并且在另一台计算机上取得会话。

有几个工作区(workaround)解决这个问题。一个常用的方法是使用自定义会话模型来检查客户端IP的改变(http://msdn.microsoft.com/msdnmag/issues/04/08/WickedCode 上有一个实现的例子)。但真正安全的方法是限制到Web站点的会话cookie使用SSL。那样,会话cookie就被加密了,在另一台计算机上就无法使用。

如果选择使用这个方法,将会话cookie标记为安全cookie是非常有意义的,那样它就只能在SSL连接上发送。这样就防止了用户改变https://URLhttp://,而后者发送cookie并不需要SSL。下面是代码示例:

Request.Cookies["ASP.NET_SessionId"].Secure = true;

通常情况下,你要在用户认证后立即使用这段代码,确保在会话状态中至少有一段信息来保证会话不会被禁止。

另一个安全危存在于cookieless会话中。即便会话ID被加密,聪明的用户仍能够使用社会运作法(social engineering)攻击来欺骗用户加入一个特定的会话。恶意用户所要做的一切就是将一个带有有效会话IDURL提供给用户。当用户点击那个链接,他们就加入了会话。尽管会话ID从这一点是受保护的,攻击者现在就知道了哪个会话ID正在使用并且能够可以在稍后劫持这个会话。

采用适当的步骤可以减少这种攻击的可能性。首先,使用cookieless会话时,总是将regenerateExpiredSessionId设置为true这防止攻击者提供过期的会话ID。其次,在登录一个新用户前,显性地禁止当前会话。

应用状态(Application State

应用状态允许你存储全局的对象,以便于任何客户端都可以访问。应用状态是基于System.Web.HttpApplicationState类的,该类通过内嵌的Application对象在所有网页中提供。

应用状态与会话状态有些相似。它支持相同的对象类型,信息保存在服务器上,并且使用相同的基于字典的语法。一个常见的使用应用状态的例子是全局计数。该计数跟踪来自于任何客户端的某个操作执行的总次数。

例如,你可以创建一个global.asax事件处理器,跟踪创建了多少次会话,或者应用接收了多少次请求。或者使用相似的逻辑,在Page.Load事件处理器中跟踪一个给定的页面被不同的客户端总共请求了多少次。这个示例演示了后一种情况:

protected void Page_Load(Object sender, EventArgs e)

{

int count = (int)Application["HitCounterForOrderPage"];

count++;

Application["HitCounterForOrderPage"] = count;

lblCounter.Text = count.ToString();

}

再次强调,应用状态项是作为对象来存储的,因此从集(collection)中获取后应对它们进行转换。应用状态中的项绝不会超时。它们会一直存在,直到应用或服务器重启,或者直到应用域对自身进行了刷新(由于自动进程循环设置,或者应用中的任何一个页面或组件更新,会出现应用域刷新)。

应用状态并不常用,因为效率不高。在前面的示例中,counter可能并不能保持精确的计数,特别是在重负载的时候。例如,如果两个客户同时请求一个网页,事件序列可能如下:

1、  用户A获取当前计数(432)。

2、  用户B获取当前计数(432)。

3、  用户A设置当前计数为433

4、  用户B设置当前计数为433

换句话说,由于两个客户同时访问计数,导致一个请求并没有被正确计数。为了防止这个问题,你需要使用Lock()Unlock()方法,这两个方法显性地限制在某一个时刻,只能有一个用户访问应用状态集,如下所示:

protected void Page_Load(Object sender, EventArgs e)

{

// Acquire exclusive access.

Application.Lock();

int count = (int)Application["HitCounterForOrderPage"];

count++;

Application["HitCounterForOrderPage"] = count;

// Release exclusive access.

Application.Unlock();

lblCounter.Text = count.ToString();

}

不幸的是,所有其它客户对于页面的请求都将推迟到Application集被释放之后。这会极大地降低性能。通常,频繁地改变值对应用状态不是很好的选择。事实上,应用状态在.NET世界中极少使用,因为大多数的使用方式已经被更加方便和高效的方法替代了:

l         过去,应用状态常用于存储整个应用域的常量,如数据库连接字符串。如第5章见到那样,这种类型的常量可以存储在web.config文件中,这样可以增强灵活性。因为要改变它非常容易,并不需要修改网页代码并且重新编译应用。

l         应用状态也可以用于存储频繁使用但创建很耗时的信息,如全部产品分类,它要求对数据库进行查询。但是,使用应用状态来存储这类型的信息增加了怎样检验数据是否有效性和怎样对其进行替换时的问题。如果产品目录较大,也会降低性能。一个相似但更有意义的方法是存储频繁使用的信息到ASP.NET缓存中。对应用状态的多数使用能够在缓存中被高效地替换。

应用状态信息也常存储于进程之中。这意味着你可以使用任何.NET数据类型。但是,它也引入了相同的两个限制,影响进程内会话状态。也就是说,在Web庄园( web farm)内,你不能在多个服务器间共享应用状态,并且当应用域重启(会产生一个事件,该事件是ASP.NET普通管理者的一部分)时,你将丢失应用状态信息。

注意,应用状态最先是用于与经典ASP后向兼容。在新的应用中,对全局数据依赖于其它机制是更好的方法,如使用数据库连接Cache对象。

静态应用变量(Static Application Variables

可以通过其它方法存储全局应用变量。可以添加静态成员变量到global.asax文件(第5章中介绍)中。这些成员被编译到自定义的HttpApplication类中,所有页面都可以对其访问。示例如下:

public static string[] fileList;

要保证正常工作的关键细节是变量必须是静态的。因为ASP.NET创建了一个HttpApplication类池,因此,每个请求都会由不同的HttpApplication对象提供服务,每个HttpApplication对象都有其自己的实例数据。但是,只有一份静态数据的拷贝,为所有实例所共享。

另一个要求是在Application指令中必须提供ClassName属性。它指明全局应用类的名字。该全局类用于获取创建的静态值。

当然,为了进行最好的封装(而且也是最灵活的封装),你需要使用属性程序:

private static string[] fileList;

public static string[] FileList

{

get { return fileList; }

}

添加成员变量到global.asax文件中时,它本质上与在Application集中的值有相同的特征。换句话说,你可以使用任何.NET数据类型,其值会一直保存到应用域重启,并且状态也不能在多个计算机之间共享。但是,并没有自动锁定。由于多个客户端可能在同一时刻访问和修改值,就需要用C#锁定语句来临时限制变量到单个线程。根据数据被访问的方式,你可能要在Web页中执行锁定(需要同时对锁定的数据执行多个任务时),或者在global.asax文件里的属性过程或方法中(要求锁定保持时间最短时)进行锁定。下面是属性过程的示例,它维护了线程安全的( thread-safe)全局元数据集:

private static Dictionary<string, string> metadata =

new Dictionary<string, string>();

public void AddMetadata(string key, string value)

{

lock (metadata)

{

metadata[key] = value;

}

}

public string GetMetadata(string key)

{

lock (metadata)

{

return metadata[key];

}

}

使用静态成员变量代替Application集有两个好处。首先,它允许你在属性过程中编写自定义代码。你可以使用该代码来记录一个值被访问的次数,或者检查数据是否仍然有效,或者重新创建数据。下面的示例采用了惰性初始化(lazy initialization)模式,并且在其第一次被访问时创建全局的对象:

private static string[] fileList;

public static string[] FileList

{

get

{

if (fileList == null)

{

fileList = Directory.GetFiles(

HttpContext.Current.Request.PhysicalApplicationPath);

}

return fileList;

}

}

这个示例使用了第13章介绍的文件访问类来获取了Web应用中的文件列表。这个方法在Application集中是无法实现的。

使用静态成员变量的另一个好处是代码使用它们是类型安全的。下面的示例使用了FileList属性:

protected void Page_Load(object sender, EventArgs e)

{

StringBuilder builder = new StringBuilder();

foreach (string file in Global.FileList)

{

builder.Append(file + "<br />");

}

lblInfo.Text = builder.ToString();

}

注意,在获取自定义属性值时,没有要求进行转换。

 

总结

状态管理是在多个请求间保留信息的技术。一般情况下,这个信息是基于特定用户的(如购物车中的项目列表,一个用户名,或一个访问层级),但有时它对整个应用是全局的(如跟踪站点活动的统计)。由于ASP.NET使用了非连接的结构,你需要在每个请求中时,显性地存储和获取状态信息。这个存储数据的方法对于应用的性能、可扩展性和安全性有巨大的影响。为了完美地解决状态管理,你很可能会想到使用缓存(Caching)。有关缓存的内容在第11章中介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值