個性化
??? 另一個新增的服務是個性化,它提供了一種現成的解決方案,用于解決存儲站點用戶的個性化設置問題。目前,此類設置通常存儲在Cookie、后端數據庫或這兩者中。無論這些設置存儲在何處,ASP.NET 1.x都不能提供什么幫助。這需要由您來設置和管理后端數據存儲,以及使用經過身份驗證的用戶名、Cookie或其他某種機制來關聯個性化數據。
??? ASP.NET 2.0個性化服務使得存儲各個用戶的設置以及隨意檢索這些設置變得非常容易。該服務基于用戶配置文件—您可以使用新的元素在Web.config中予以定義。下面的代碼節選自Web.config:
<profile>
??? <properties>
??????? <add name="Theme" />
??????? <add name="Birthday" Type="System.DateTime" />
??????? <add name="LoginCount" Type="System.Int32" defaultValue="0" />
??? </properties>
</profile>
??? 它定義了一個包含三個屬性的配置文件:一個名為Theme的字符串,一個名為Birthday的DateTime值,以及一個名為LoginCount的整數。后面這個屬性被賦予默認值0。
??? 在運行時,您可以使用頁面的Profile屬性(該屬性引用包含該配置文件中定義的屬性的動態編譯類的實例)來訪問當前用戶的這些屬性。例如,下列語句可從當前用戶的配置文件中讀取屬性值:
string theme = Profile.Theme;
DateTime birthday = Profile.Birthday;
int logins = Profile.LoginCount;
??? 還可以將值賦予配置文件屬性:
Profile.Theme = "SmokeAndGlass";
Profile.Birthday = new DateTime (1959, 9, 30);
Profile.LoginCount = Profile.LoginCount + 1;
??? 個性化服務的一個明顯優勢是強類型化。另一個優勢在于個性化數據是按需讀寫的。請將此與會話狀態(無論是否使用,都會將其加載并保存到每個請求中)進行對比。但是,個性化服務的最大優勢可能在于您不必顯式地將數據存儲在任何位置;系統會替您完成該工作,并且它會永久性地存儲數據,以便數據在您需要時隨時可用。配置文件不會像會話那樣超時。
??? 那么,個性化數據存儲在哪里呢?這要依具體情況而定。個性化服務基于提供程序,因此您可以將其配置為使用任何可用的提供程序。ASP.NET 2.0將至少附帶兩個個性化提供程序:一個用于Access,另一個用于SQL Server。如果您不另行指定,則個性化服務將使用Access提供程序,默認情況下,該提供程序會將個性化數據存儲在本地Data\AspNetDB.mdb中。您可以通過修改Web.config(手動或使用Webadmin.axd)來改用SQL Server數據庫。如果您不希望將個性化數據存儲在Access數據庫或SQL Server數據庫中,則可以編寫自己的提供程序。
??? 默認情況下,ASP.NET使用經過身份驗證的用戶名作為所存儲的個性化數據的鍵,但您也可以將其配置為支持匿名用戶。首先,通過將以下語句添加到Web.config中來啟用匿名標識:
<anonymousIdentification enabled="true" />
??? 然后,將allowAnonymous="true"添加到您要為匿名用戶存儲的配置文件屬性中:
<name="Theme" allowAnonymous="true" />
??? 現在,Theme屬性可以作為個性化設置使用,而無論站點的調用方是否經過了身份驗證。
??? 默認情況下,匿名標識使用Cookie來標識回返用戶。由支持的屬性可以用各種方式來配置這些Cookie。例如,您可以指定Cookie名稱,并指明是否應該將該Cookie的內容加密。您還可以將個性化服務配置為使用無Cookie的匿名標識,因此它將依靠URL Munging來標識回返用戶。甚至還存在一個自動檢測選項:如果請求瀏覽器支持Cookie,則使用Cookie;如果不支持,則使用URL Munging。
??? 要查看個性化的工作方式,請運行本文隨附的下載資料中的Personalize.aspx示例。它會讓站點的每個訪問者選擇一個主題,然后記錄該主題,并且每當該訪問者返回時都將應用該主題。請注意,該主題是在頁面的PreInit事件(它是一個新事件,它的激發時間甚至早于Init)中以編程方式應用于該頁面的。
??? 在您運行該示例之前,需要啟用匿名標識,并定義一個包含名為Theme的字符串屬性的配置文件。以下代碼行顯示了執行上述兩項任務的Web.config文件:
<configuration>
??? <system.web>
??????? <anonymousIdentification enabled="true" />
??????? <profile>
??????????? <properties>
??????????????? <property name="Theme" allowAnonymous="true" />
??????????? </properties>
??????? </profile>
??? </system.web>
</configuration>
SQL緩存依賴性
??? ASP.NET 1.x中令人遺憾地缺少的另一項功能是數據庫緩存依賴性。可以將ASP.NET應用程序緩存中放置的項目與其他緩存項目聯系起來,或者與文件系統中的對象聯系起來,但不能與數據庫實體聯系起來。ASP.NET 2.0通過引入SQL緩存依賴性來糾正這一由于疏忽而造成的錯誤。
??? SQL緩存依賴性由新的SQLCacheDependency類的實例表示。它們的用法非常簡單。下面的語句將一個名為ds的數據集插入到應用程序緩存中,并且在該數據集和Northwind數據庫的Products表之間創建依賴性:
Cache.Insert ("ProductsDataSet", ds,
??? new SqlCacheDependency ("Northwind", "Products");
??? 如果Products表的內容改變,則ASP.NET會自動刪除該數據集。
??? SQL緩存依賴性還可以與ASP.NET輸出緩存配合使用。下面的指令指示ASP.NET緩存來自包含頁面的輸出,直至Products表的內容改變或者滿60秒為止(滿足任一條件即可):
<%@ OutputCache Duration="60" VaryByParam="None"
??? SqlDependency="Northwind:Products"
%>
??? SQL緩存依賴性適用于SQL Server 7.0、SQL Server 2000和即將問世的SQL Server 2005。對于SQL Server 2005,無需進行任何準備;但必須將SQL Server 7.0和SQL Server 2000數據庫配置為支持SQL緩存依賴性。準備工作涉及到創建數據庫觸發器,以及創建一個特殊的表,以供ASP.NET在確定是否已經發生更改時參考。該表由一個后臺線程使用可配置的輪詢間隔(默認為5秒鐘)來定期輪詢。在SQL Server 2005中,要檢測更改,既不需要特殊的表,也不需要輪詢。此外,SQL Server 2005緩存依賴性可以在行級應用,而SQL Server 7.0和SQL Server 2000緩存依賴性在表級工作。您可以使用Aspnet_regsqlcache.exe工具或Webadmin.axd來準備數據庫,以使其支持SQL緩存依賴性。
新的動態編譯模型
??? ASP.NET 1.x中引入的眾多創新之一是:系統能夠在首次訪問您的代碼時對其進行編譯。但是,只有頁面能夠被自動編譯,并且輔助類(如數據訪問組件)必須單獨編譯。
??? ASP.NET 2.0擴展了動態編譯模型,以便能夠自動編譯幾乎所有的組件。bin目錄仍然保留以便實現向后兼容性,但它現在添加了名為Code和Resources的目錄。Code目錄中的C#和Visual Basic文件以及Resources目錄中的RESX和RESOURCE文件被ASP.NET自動編譯并緩存在系統子目錄中。此外,落入Code目錄中的Web服務描述語言(WSDL)文件被編譯為Web服務代理,而XML架構定義語言(XSD)文件被編譯為類型化數據集。通過Web.config,還可以擴展這些目錄以支持其他文件類型。
預編譯并且在不帶源代碼的情況下進行部署
??? 提到動態編譯,與ASP.NET 1.x有關的最常見問題之一是:是否可以預編譯頁面,以避免在首次訪問頁面時發生的編譯延遲?盡管該問題本身在某種程度上無關緊要(延遲非常小,并且延遲的開銷被成千上萬甚至數以百萬的后續請求所分攤),但Microsoft仍然感到有必要采取相應的措施來減輕開發人員的擔憂。這一“措施”就是能夠通過提交對名為precompile.axd的幻像資源的請求,來預編譯應用程序中的所有頁面。
??? 但預編譯并不僅限于此。另一個經常被請求的功能是:能夠將整個應用程序預編譯為可以在不帶源代碼的情況下進行部署的托管程序集(該功能在宿主方案中尤其有用)。ASP.NET 2.0包含一個名為Aspnet_compiler.exe的新的命令行工具,它能夠執行預編譯并且在不帶源代碼的情況下進行部署;Visual Studio 2005將包含類似的功能。下面的命令將預編譯Web1目錄中的應用程序,并且在不帶源代碼的情況下將其部署到Web2:
Aspnet_compiler -v /web1 -p c:\web1 c:\web2
??? 之后,目標目錄將包含空的ASP.NET文件(ASPX、ASCX、ASIX等等)以及源目錄中存在的所有靜態內容(如HTML文件、.config文件和圖像文件)的副本。在不帶源代碼的情況下進行部署并不會為您的知識產權提供牢不可破的保護,因為聰明的ISP仍然可以通過反編譯生成的程序集來弄清楚應用程序的來龍去脈,但是,它確實對一般的代碼竊取者設置了更大的阻礙。
新的代碼分隔模型
??? ASP.NET 1.x支持兩種編程模型:內聯模型—HTML和代碼共存于同一個ASPX文件中;代碼隱藏模型—它將HTML分隔到ASPX文件中,并將代碼分隔到源代碼文件(例如,C#文件)中。ASP.NET 2.0引入了第三個模型:一種新的代碼隱藏形式,它依賴于Visual C#和Visual Basic .NET編譯器中的不完全類支持。新的代碼隱藏解決了原來的代碼隱藏中存在的一個惱人的問題:傳統的代碼隱藏類必須包含受保護的字段,這些字段的類型和名稱需要映射到ASPX文件中聲明的相應控件。
??? 圖10 顯示了新的代碼隱藏模型的工作方式。Hello.aspx包含頁面的聲明部分,Hello.aspx.cs包含代碼。您應該注意@ Page指令中的CompileWith屬性。此外,請注意MyPage類中缺少的字段(它們提供到ASPX文件中聲明的控件的映射)。舊樣式的代碼隱藏仍然受支持,但新樣式將是今后的首選編程模型。一點都不奇怪,Visual Studio 2005天生就支持新的代碼分隔模型。
Hello.aspx
<%@ Page CompileWith="Hello.aspx.cs" ClassName="MyPage" %>
<html>
??? <body>
??????? <form runat="server">
??????????? <asp:TextBox ID="Input" RunAt="server" />
??????????? <asp:Button Text="Test" OnClick="OnTest" RunAt="server" />
??????????? <asp:Label ID="Output" RunAt="server" />
??????? </form>
??? </body>
</html>
Hello.aspx.cs
using System;
partial class MyPage
{
??? void OnTest (Object sender, EventArgs e)
??? {
??????? Output.Text = "Hello, " + Input.Text;
??? }
}
圖10 Codebehind模型
客戶端回調管理器
??? ASP.NET 2.0中我最喜歡的功能之一就是由新的客戶端回調管理器提供的“輕量級回發”功能。在過去,ASP.NET頁面必須回發給服務器才能調用服務器端代碼。回發是低效的,因為它們將包含由頁面控件生成的所有回發數據。它們還強制頁面刷新,從而導致不雅觀的閃爍。
??? ASP.NET 2.0引入了客戶端回調管理器,它使頁面無需完全回發就可以回調到服務器。回調是異步的,并且通過XML-HTTP來完成。它們不包含回發數據,并且不會強制頁面刷新。(在服務器端,頁面像平常一樣執行至PreRender事件,但在即將呈現任何HTML之前停止。)它們確實需要支持XML-HTTP協議的瀏覽器(這通常意味著Microsoft Internet Explorer 5.0或更高版本)。
??? 使用客戶端回調管理器涉及三個步驟。首先,調用Page.GetCallbackEventReference以獲取對某個特定函數(可以從客戶端腳本中調用該函數,以執行到服務器的XML-HTTP回調)的引用。ASP.NET提供了該函數的名稱和實現。其次,在客戶端腳本中編寫一個將在回調返回時調用的方法。方法名稱是傳遞給GetCallbackEventReference的參數之一。第三,在頁面中實現ICallbackEventHandler接口。該接口包含一個方法—RaiseCallbackEvent,當回調發生時,該方法將在服務器端調用。RaiseCallbackEvent所返回的字符串將被返回到第二步所述的方法。
??? 圖11 中的代碼顯示了客戶端回調的工作方式,并且演示了它們的一個非常實際的用途。該頁面顯示了一個請求姓名和地址的窗體。在Zip Code字段中鍵入378xx或379xx郵政編碼,然后單擊Autofill按鈕,City字段中將顯示一個名稱。值得注意的是,該頁面會返回到服務器以獲取城市名稱,但它使用客戶端回調而不是完全回發來完成此工作。在實際操作中,它可以找到某個數據庫以將郵政編碼轉換為城市名稱。請注意,該頁面并不像頁面在回發到服務器時通常所做的那樣進行重新繪制。相反,更新是快速且簡潔的!
<%@ Implements Interface="System.Web.UI.ICallbackEventHandler" %>
<html>
??? <body>
??????? <h1>Please Register</h1>
??????? <hr>
??????? <form runat="server">
??????????? <table>
??????????????? <tr>
??????????????????? <td>First Name</td>
??????????????????? <td><asp:TextBox ID="FirstName" RunAt="server" /></td>
??????????????????? <td></td>
??????????????? </tr>
??????????????? <tr>
??????????????????? <td>Last Name</td>
??????????????????? <td><asp:TextBox ID="LastName" RunAt="server" /></td>
??????????????????? <td></td>
??????????????? </tr>
??????????????? <tr>
??????????????????? <td>Address 1</td>
??????????????????? <td><asp:TextBox ID="Address1" RunAt="server" /></td>
??????????????????? <td></td>
??????????????? </tr>
??????????????? <tr>
??????????????????? <td>Address 2</td>
??????????????????? <td><asp:TextBox ID="Address2" RunAt="server" /></td>
??????????????????? <td></td>
??????????????? </tr>
??????????????? <tr>
??????????????????? <td>City</td>
??????????????????? <td><asp:TextBox ID="City" RunAt="server" /></td>
??????????????????? <td></td>
??????????????? </tr>
??????????????? <tr>
??????????????????? <td>State</td>
?&n,bsp;????????????????? <td><asp:TextBox ID="State" RunAt="server" /></td>
??????????????????? <td></td>
??????????????? </tr>
??????????????? <tr>
??????????????????? <td>Zip Code</td>
??????????????????? <td><asp:TextBox ID="Zip" RunAt="server" /></td>
??????????????????? <td><asp:Button ID="AutofillButton" Text="Autofill"
??????????????????????? RunAt="server" /></td>
??????????????? </tr>
??????????? </table>
??????? </form>
??? </body>
</html>
<script language="javascript">
// Function called when callback returns
function __onCallbackCompleted (result, context)
{
??? // Display the string returned by the server's RaiseCallbackEvent
??? // method in the input field named "City"
??? document.getElementById ('City').value = result;
}
</script>
<script language="C#" runat="server">
void Page_Load (Object sender, EventArgs e)
{
??? // Get callback event reference (e.g., "__doCallback (...)")
??? string cbref = GetCallbackEventReference (this,
??????? "document.getElementById ('Zip').value",
??????? "__onCallbackCompleted", "null", "null");
??? // Wire the callback event reference to the Autofill button with
??? // an onclick attribute (and add "return false" to event reference
??? // to prevent a postback from occurring)
??? AutofillButton.Attributes.Add ("onclick",
??????? cbref + "; return false;");
}
// Server-side callback event handler
string ICallbackEventHandler.RaiseCallbackEvent (string arg)
{
??? if (arg.StartsWith ("378"))
??????? return "Oak Ridge";
??? else if (arg.StartsWith ("379"))
??????? return "Knoxville";
??? else
??????? return "Unknown";
}
</script>