Caching ASP.NET Applications
This section discusses how caching works in ASP.NET applications. There are primarily three forms of caching. These are page caching, page fragment caching, and data caching.
Page Caching
Page caching is the process of persisting an entire page on the server, proxy server, or the client browser so that the next time it is retrieved, it does not have to be generated by ASP.NET.
Using the @ OutputCache Directive
Page caching is enabled by including the directive in bold (line 2) in the .aspx file shown in Listing 33.1.
Listing 33.1 .aspx File Containing Page Caching Directive
1: <%@ Page language="c#" Debug="true" Codebehind="WebForm1.pas" AutoEventWireup="false" Inherits="WebForm1.TWebForm1" %> 2: <%@ OutputCache Duration="20" Location="Any" VaryByParam="none" %> 3: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> 4: 5: <html> 6: <head> 7: <title></title> 8: <meta name="GENERATOR" content="Borland Package Library 7.1"> 9: </head> 10: 11: <body ms_positioning="GridLayout"> 12: <form runat="server"> 13: <asp:label id=Label1 14: style="Z-INDEX: 1; LEFT: 62px; POSITION: absolute; 15: TOP: 14px" runat="server">Label</asp:label> 16: <asp:button id=Button1 17: style="Z-INDEX: 2; LEFT: 70px; POSITION: absolute; 18: TOP: 46px" runat="server" text="Button"> 19: </asp:button> 20: </form> 21: </body> 22: </html>
Find the code on the CD: CodeChapter 33Ex01.
In this example, the @ OutputCache directive includes three attributes. The first, Duration, specifies how long (in seconds) the cache will retain the page before regenerating its HTML. The second attribute, Location, determines where the cache is stored. The third allows you to create a different version of the resulting page based on values provided in the comma-separated list following the VaryByParam attribute. These and other attributes are more fully explained in Table 33.1. Some of these attributes pertain to page or control caching or both.
Table 33.1 @ OutputCache Attributes
Attribute | Description |
Duration | Specifies the time in seconds that the page is cached. By specifying a value, an expiration policy is established for the page or control being cached. This is a required attribute. |
Location | Location allows you to specify where the page is cached. It can be one of the following values. Any—The item can be cached on any of the following locations. This is the default setting. Client—The item is cached in the client's browser. Downstream—The item is cached on a downstream server. None—There is not page caching performed. Server—The item is cached on the server. |
Shared | Shares deals with user controls and determines whether the control's cache can be shared with other pages. |
VaryByCustom | Semicolon separated strings that allow varying pages based on browser type or custom strings. |
VaryByHeader | Semicolon separated list of headers that can be used for serving different pages based on header information. |
VaryByParam | Semicolon separated list of strings representing parameters that are used in determining varying page output. These strings correspond to attributes sent with aGET method or parameters sent with the POST method. This attribute is required and might contain an empty string. |
VaryByControl | Semicolon separated list of user-control property names. This is only valid for control caching (fragment caching). |
The example in Listing 33.1 illustrates how page caching works. It is a page that contains a Button and a Label control. The code-behind for the Button's Click event performs the following:
procedure TWebForm1.Button1_Click(sender: System.Object; e: System.EventArgs); begin Label1.Text := System.String.Format('Time on the Server is: {0}', [System.DateTime.Now.ToLongTimeString]); end;
When running the application, clicking the button will reveal that the page is being cached. The time that is written to the page does not change until the cache has expired, as determined by the Duration attribute of the @ OutputCache directive.
Varying by Parameters
I will illustrate one of the varying attributes, specifically the VaryByParam attribute. This attribute can have one of three possible values, including none, an asterisk *, and a valid string that represents a GET method attribute or aPOST parameter name. VaryByParam results in a different page being cached for each distinct request (as determined by the parameters being passed). When using the * as shown next, all parameters are taken into account.
<%@ OutputCache Duration="20" Location="Any" VaryByParam="*" %>
You can also spell out a specific parameter by name, causing only the specified parameters to be taken into account by distinguishing a separate request needing to be cached. This is illustrated here:
<%@ OutputCache Duration="20" Location="Any" VaryByParam="FirstName" %>
To illustrate this, Listing 33.2 is the .aspx file for an example similar to that shown in Listing 33.1. Notice that the VaryByParam attribute now contains an asterisk.
Listing 33.2 Varying by Parameter Example
1: <%@ Page language="c#" Debug="true" Codebehind="WebForm1.pas" AutoEventWireup="false" Inherits="WebForm1.TWebForm1" %> 2: <%@ OutputCache Duration="120" Location="Any" VaryByParam="*" %> 3: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> 4: 5: <html> 6: <head> 7: <title></title> 8: <meta name="GENERATOR" content="Borland Package Library 7.1"> 9: </head> 10: 11: <body ms_positioning="GridLayout"> 12: <form runat="server"> 13: <asp:label id=Label1 14: style="Z-INDEX: 1; LEFT: 38px; POSITION: absolute; 15: TOP: 14px" runat="server">Label</asp:label> 16: </form> 17: </body> 18: </html>
Find the code on the CD: CodeChapter 33Ex02.
When entering a URL such as
http://localhost/PgCshByParam/WebForm1.aspx?FirstName=Bob
the output written will be
"Bob, the time on the Server is: 8:46:04 a.m."
Changing the URL to
http://localhost/PgCshByParam/WebForm1.aspx?FirstName=Sam
results in the output
"Sam, the time on the Server is: 8:46:17 a.m."
Assuming that VaryByParam="FirstName" and using Bob as the parameter, you will see that the original output is returned with the time appearing to move backward at 8:46:04 a.m. What's happening here is that there are two versions of this page being cached—one for when the FirstName parameter equals Bob, and the other for when FirstName is Sam. As another interesting point, consider the following two URLs:
http://localhost/PgCshByParam/WebForm1.aspx?FirstName=Bob&LastName=Jones http://localhost/PgCshByParam/WebForm1.aspx?FirstName=Bob&LastName=Archer
Both would result in the same cached page being returned. However, changing the @ OutputCache directive to the following would have different results:
<%@ OutputCache Duration="120" Location="Any" VaryByParam="FirstName" %>
With this directive, only requests in which the FirstName parameter is different will result in different cached pages. The previous two URLs containing the same FirstName but different LastName parameters will be served the same cached page.
Varying by Headers
Incertain situations, you'll want your ASP.NET applications to take advantage of browser capabilities. However, you won't want to serve pages to browsers that use capabilities not supported by the target browser. Therefore, when using caching, it doesn't make any sense to be caching a page that won't be supported by the client's browser. The following @ OutputCache directive shows how you can create a different page depending on HTTP header information—specifically the User-Agent header by including the VaryByHeader attribute.
<%@ OutputCache Duration="120" Location="Any" VaryByParam="*" VaryByHeader="User-Agent" %>
The client's browser can be identified by the User-Agent HTTP header. You can specify other HTTP headers in the VaryByHeader or multiple headers separated by semicolons.
Varying by Custom Strings
You can get very specific about the requirements that determine cached page variations by using the VaryByCustom attribute of the @ OutputCache directive.
There are two ways to use this attribute. The first and simplest is to specify a value of "Browser" as shown here:
<%@ OutputCache Duration="120" Location="Any" VaryByParam="*" VaryByCustom="Browser" %>
This causes behavior similar to the VaryByHeader example previously explained. It differs in that VaryByCustom="Browser" only uses the browser type and major version rather than the additional information that might be included in the User-Agent header.
Another way to use the VaryByCustom attribute is to specify a user-defined string. In doing so, you must override the GetVaryByCustomString() method of the HttpApplication class in Globals.pas. This would look like the following code:
function TGlobal.GetVaryByCustomString(Context: HTTPContext; custom: &String): System.String; begin if custom = 'Country' then Result := GetCountry(Context) else Result := GetVaryByCustomString(Context, custom); end;
This illustrates a way that you might cache pages based on the country of the user originating the request. This assumes that the GetCountry() method returns a string that would be used as the string by which to vary cached pages.
Page Fragment Caching
Page fragment caching is similar to page caching but instead of caching the entire page, you are caching specific elements of the page. This can be accomplished by caching user controls, which are covered in Chapter 34, "Developing Custom ASP.NET Server Controls." User controls can be used with the @ OutputCache directive just like pages can. Some attributes aren't supported because they make no sense since user controls exist in the context of the page. These are Location and VaryByHeader. The following .ascx file defines a user control that uses such caching:
<%@ Control Language="c#" AutoEventWireup="false" Codebehind="WebUserControl1.pas" Inherits="WebUserControl1.TWebUserControl1"%> <%@ OutputCache Duration="20" VaryByParam="*" %> <asp:label id=Label1 style="Z-INDEX: 101; LEFT: 38px; POSITION: absolute; TOP: 38px" runat="server">Label</asp:label>
Find the code on the CD: CodeChapter 33Ex03.
This control simply displays the system time. To illustrate the partial page caching, it is included on a page that also displays the system time but is not cached. Figure 33.1 shows the output after refreshing the browser several times. You can see that that user control retains its original time and will do so until the cache has expired.
Figure 33.1
Caching a user control.
Data Caching
Data caching is a way to cache data of any type. This is particularly useful for obtaining a performance boost by not having to request data from a data source. Instead, this can be done once to fill a DataSet that you then cache. Subsequent requests for this DataSet will then retrieve it from the cache. To illustrate this, we'll need to examine the Cache class.The Cache Class
The Cache class is defined in the System.Web.Caching namespace and provides the capability to store information to memory at the application level, which can be retrieved upon different requests. This works similarly to the Application class, which I discuss later in this chapter.There are two properties and four methods of interest regarding the Cache class that are described in Table 33.2.
Table 33.2 Cache Class Properties and Methods
Property/Method | Description |
Count | This property returns the number of items that are currently stored in the Cache. |
Item | This property is an indexer array that returns the item by a specified key. |
Add() | This method allows you to add an item to the Cache. You can specify as parameters dependencies, an expiration policy, a priority policy, and a remove callback method. Add() fails if an item already exists in the Cache for a given key. |
Get() | This method returns an item from the Cache by the specified key. |
Insert() | This method inserts an item into the Cache, replacing any item that exists with the same key. You can include the same parameters as with the Add() method. |
Remove() | This method removes an item from the Cache with the specified key. |
As indicated for the Cache.Add() and Cache.Insert() methods, there are several parameters that pertain to the item being placed in the Cache. These are discussed in Table 33.3 in the order that they appear as parameters.
Table 33.3 Cache Class Properties and Methods
Property/Method | Description |
Key | The string key used to refer to the cached item. |
Value | The item (a System.Object parameter) that is added to the Cache. |
Dependencies | A single or multiple files, directories, or the keys to another cached item on which this new item depends. When either the file or cached item changes, this cached item is removed from the Cache. |
AbsoluteExpiration | The time at which the item is removed from the Cache. |
SlidingExperation | The time interval at which the item is removed from the Cache if it has not been accessed during this time. If the item is accessed, the expiration is set to be the access time plus the time specified by this value. |
Priority | A CacheItemPriority enumeration value that is used by the Cache when evicting objects. |
OnRemoveCallback | A delegate (event handler) that gets invoked whenever a cached item is removed. |
The following code illustrates the use of the Cache class:
if Cache['DateToday'] <> nil then Response.Write('From Cache: '+ DateTime(Cache['DateToday']).Today.ToLongDateString) else begin Response.Write('From System: '+ System.DateTime.Today.ToLongDateString); Cache.Add('DateToday', System.DateTime.Today, nil, GetMidnight, Cache.NoSlidingExpiration, CacheItemPriority.Default, nil) end;This code displays today's date retrieved from the system or the Cache if today's date exists in the Cache. The call to Cache.Add() illustrates using the AbsoluteExpiration parameter, which is set to midnight through the helper function GetMidnight(). In case you're wondering, the GetMidnight() function simply returns a DateTime value for tomorrow by adding 1 day to today's date:
function GetMidnight: System.DateTime; begin Result := System.DateTime.Today.AddDays(1); end;
Data Caching Example
Caching simple data types can be useful. The real value in performance is gained when you Cache data that would otherwise be retrieved from another resource such as a database. Listing 33.3 shows an excerpt from an example that illustrates this technique.Listing 33.3 Caching Data
1: const 2: c_cnstr = 'server=XWING;database=Northwind;Trusted_Connection=Yes'; 3: c_sel = 'select * from products'; 4: 5: procedure TWebForm1.Page_Load(sender: System.Object; 6: e: System.EventArgs); 7: begin 8: if not IsPostBack then 9: GetData; 10: end; 11: 12: procedure TWebForm1.GetData; 13: var 14: sqlcn: SqlConnection; 15: sqlDa: SqlDataAdapter; 16: Ds: DataSet; 17: dtView: DataView; 18: begin 19: 20: dtView := Cache['dvProducts'] as DataView; 21: if dtView = nil then 22: begin 23: sqlcn := SqlConnection.Create(c_cnstr); 24: sqlDA := SqlDataAdapter.Create(c_sel, sqlcn); 25: Ds := DataSet.Create; 26: sqlDA.Fill(Ds); 27: try 28: dtView := DataView.Create(Ds.Tables['Table']); 29: Cache['dvProducts'] := dtView; 30: Label1.Text := 'From Database'; 31: finally 32: sqlcn.Close; 33: end; 34: end 35: else 36: Label1.Text := 'From Cache'; 37: 38: DataGrid1.DataSource := dtView; 39: DataBind; 40: end; 41: 42: procedure TWebForm1.DataGrid1_PageIndexChanged(source: System.Object; 43: e: System.Web.UI.WebControls.DataGridPageChangedEventArgs); 44: begin 45: GetData; 46: DataGrid1.CurrentPageIndex := e.NewPageIndex; 47: DataGrid1.DataBind; 48: end;
Find the code on the CD: CodeChapter 33Ex04.
The GetData() procedure (lines 12–40) is the one we'll want to examine closely. Line 20 attempts to retrieve a DataView from the Cache. If it does not exist, it will be nil. That being the case, the data is obtained from the Northwind database. In addition to extracting this data from the database, it gets added to the Cache (line 29). Upon subsequent requests for this page, line 20 should not return nil but instead the cached DataView.
This technique works well for data that will not change, such as lookup information. It will also work for data that does change—in which case, you will need to develop code to synchronize data stored in the Cache and the database. This could be as simple as refreshing the entire DataView when a change is made. It can also increase in complexity, such as updating both the Cache and database with only the modified data. Another useful solution is to cache the information until some specific time (for instance, midnight). This works nicely for rarely updated data. Several factors will determine approaches you should take, such as scalability and system requirements to name a few. For instance, you wouldn't want to expend system memory by caching huge DataSets, which would defeat any performance benefits you intended to gain.
Cache File Dependencies
It is possible to make a cached item dependent on single or multiple associated files, directories, or other cached items. When the associated entity is modified, the dependant item is removed from the Cache. Listing 33.4 shows how to establish such a dependency using the Cache.Insert() method.
Listing 33.4 Establishing a Dependency on a Cached File
1: procedure TWebForm1.Button1_Click(sender: System.Object; 2: e: System.EventArgs); 3: var 4: str: String; 5: begin 6: str := Cache['MyData'] as System.String; 7: if str = nil then 8: begin 9: Label1.Text := 'Not in Cache'; 10: Str := 'Now in Cache'; 11: Cache.Insert('MyData', Str, 12: CacheDependency.Create(MapPath('cache.txt'))); 13: end 14: else 15: Label1.Text := Str; 16: end;
Find the code on the CD: CodeChapter 33Ex05.
Lines 11–12 associate the file cache.txt as the dependency for the cached 'MyData' item. When cache.txt is modified, 'MyItem' will be removed from the Cache. This allows you to establish an external mechanism by which you can invoke a refresh of the cached information. The following section illustrates a practical example of this technique.
Tip - Use the MapPath() method to translate virtual paths to physical paths as used in Listing 33.4.
In this example, when the file, 'cache.txt' is modified or deleted, the item keyed by 'MyData' is also removed from the Cache.
You can also establish a key dependency. A key dependency is one in which a cached item is dependent on another cached item. This is done by using the INSERT statement as
keyAry[0] := 'Item1'; keyAry[1] := 'Item2'; Cache.Insert('MyData', Str, CacheDependency.Create(nil, keyAry));
In this example, the item with the key 'MyData' becomes dependant on the items keyed as 'Item1' and 'Item2'.
The technique of creating an array of items can also be used in establishing a dependency on multiple files or directories by creating a string array of file or directory names.
Extending File Dependencies for Use in SQL Server
This section illustrates a realistic example of using cache dependencies.
Listing 33.5 shows a GetData() method similar to that seen in Listing 33.3.
Listing 33.5 GetData() with a Cached Dependency
1: procedure TWebForm1.GetData; 2: var 3: sqlcn: SqlConnection; 4: sqlDa: SqlDataAdapter; 5: Ds: DataSet; 6: dtView: DataView; 7: begin 8: dtView := Cache['dvEmp'] as DataView; 9: if dtView = nil then 10: begin 11: sqlcn := SqlConnection.Create(c_cnstr); 12: sqlDA := SqlDataAdapter.Create(c_sel, sqlcn); 13: Ds := DataSet.Create; 14: sqlDA.Fill(Ds); 15: try 16: dtView := DataView.Create(Ds.Tables['Table']); 17: Cache.Insert('dvEmp', dtView, 18: CacheDependency.Create(MapPath('cache.txt'))); 19: Label1.Text := 'From Database'; 20: finally 21: sqlcn.Close; 22: end; 23: end 24: else 25: Label1.Text := 'From Cache'; 26: DataGrid1.DataSource := dtView; 27: DataBind; 28: end;
Find the code on the CD: CodeChapter 33Ex06.
Lines 17 and 18 are where the dependency is established. When the file 'cache.txt' is modified or deleted, the DataView is removed from the Cache. You would want to do this when the data in the database is modified, making the cached information out of sync. The question this raises is how to modify cache.txt. If the underlying database is modified through an external program, this external program can modify the file. If the database is modified by this same ASP.NET application, it too can modify the file; however, one must wonder why we wouldn't just refresh the data without dealing with the file at all.
The idea here is to invoke a refresh of the cached data whenever the data stored in the table that the DataView represents gets changed. It really doesn't matter where the data was modified. The way to do this is to create a trigger on the SQL table that modified the file. An example is shown here:
CREATE TRIGGER EMP_UPD_CACHE ON Employees FOR UPDATE, DELETE, INSERT AS DECLARE @ShCmd VARCHAR(100) SELECT @ShCmd = 'echo '+ Cast(GetDate() as VARCHAR(25))+' > "C:Datacache.txt"' EXEC master..xp_cmdshell @ShCmd, no_output
This trigger will write information to the cache.txt file when a record is updated deleted or added to the Employees table, which will effectively invoke the refresh we desire.
Note - The preceding example assumes that the SQL Server runs on the same machine as the Web Server, or at least that the two servers see the same NTFS share.
Cache-Callback Methods
This section illustrates how you can associate a callback method with an item added to the Cache. This callback method gets invoked whenever the items with which it is associated gets removed from the Cache. The callback method takes the three parameters listed here:
- key—String index for the cached item.
- value—Value of the item removed from the Cache.
- reason—Reason item was removed from the Cache. This value is one of the CacheItemRemovedReason enumeration values.
Valid values for the reason parameter are
- DependencyChanged—A dependency on the item was modified.
- Expired—The item reached its expiration period.
- Removed—The item was removed from the Cache through the Remove() or Insert() method.
- Underused—The item was removed by the system to free up memory.
Listing 33.6 demonstrates how to use the callback method with cached items.
Listing 33.6 Cache-callback Example
1: procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs); 2: var 3: keyAry: array[0..0] of String; 4: begin 5: if not IsPostBack then 6: begin 7: Cache.Insert('Item1', 'Item 1', nil, 8: System.DateTime.Now.AddSeconds(10), Cache.NoSlidingExpiration, 9: CacheItemPriority.Default, CacheItemRemoved); 10: Cache.Insert('Item2', 'Item 2', nil, 11: System.DateTime.Now.AddSeconds(10), Cache.NoSlidingExpiration, 12: CacheItemPriority.Default, CacheItemRemoved); 13: keyAry[0] := 'Item2'; 14: Cache.Insert('Item3', 'Item 3', CacheDependency.Create(nil, keyAry), 15: System.DateTime.Now.AddSeconds(10), Cache.NoSlidingExpiration, 16: CacheItemPriority.Default, CacheItemRemoved); 17: end; 18: end; 19: 20: procedure TWebForm1.CacheItemRemoved(Key: System.String; Value: TObject; 21: Reason: CacheItemRemovedReason); 22: begin 23: FItemArray := Application['Log'] as ArrayList; 24: if FItemArray = nil then 25: begin 26: FItemArray := ArrayList.Create; 27: Application['Log'] := FItemArray; 28: end; 29: FItemArray.Add(Key+': '+Enum(Reason).ToString()); 30: end; 31: 32: procedure TWebForm1.btnRemove_Click(sender: System.Object; 33: e: System.EventArgs); 34: begin 35: Cache.Remove('Item2'); 36: btnGetLog_Click(nil, nil); 37: end; 38: 39: procedure TWebForm1.btnGetLog_Click(sender: System.Object; 40: e: System.EventArgs); 41: begin 42: if Application['Log'] <> nil then 43: begin 44: DataGrid1.DataSource := Application['Log']; 45: DataGrid1.DataBind; 46: DataBind; 47: end; 48: Label1.Text := 'Items cached: '+Cache.Count.ToString 49: end;
Find the code on the CD: CodeChapter 33Ex07.
In this example, lines 7–16 inserts three items to the Cache. The item inserted at line 14 (Item3), is also associated with Item2 through a dependency. This means that when Item2 is changed or removed, Item3 will be removed from the Cache.
Lines 20–30 show the callback method that is used when items are removed from the Cache. This method adds the item name and reason to an ArrayList that is stored in the HttpApplicationState object. This list is used by the btnGetLog_Click() event handler, which binds the ArrayList to a DataGrid. The btnRemove_Click() event handler removes Item2 from the Cache and calls the btnGetLog_Click() method to display the results. Because Item3 is dependent on Item2, removing Item2 should also result in Item3 being removed. This is verified in Figure 33.2.
Figure 33.2
Results of the CacheItemRemoveCallback.
State Management in ASP.NET Applications
State management is related to caching—whereas caching keeps global state for multiple clients, session state keeps state for a single client. Web applications are stateless; therefore, they don't inherently track user information between requests. Each request is viewed as a distinct request entirely unrelated to previous requests.
ASP.NET provides several levels at which state can be managed. These are cookies, ViewState, Session, and Application.
The following sections cover each of these different state management mechanisms.
Managing State with Cookies
Cookies are basically text that the Web server can place in the client's browser. They are transferred via HTTP headers. As the user hits various pages within a Web site or application, the server can examine the content of these cookies. A cookie is associated with the domain of the server that initiated its creation. Therefore, a cookie can never be transferred to other domains. A cookie can be temporary in that it lasts for the current session only. It can also be persistent across multiple sessions. This is one mechanism that the server can use to maintain state information.
Creating a Cookie
Creating a cookie is simple. The cookie itself is encapsulated by the HTTPCookie class defined in the System.Web.HttpCookie namespace. The following code shows how to create a cookie whose value contains the text entered from a TextBox control:
var MyCookie: HttpCookie; begin MyCookie := HttpCookie.Create('MyName', TextBox1.Text); Response.Cookies.Add(MyCookie); Response.Redirect('WebForm2.aspx') end;
Find the code on the CD: CodeChapter 33Ex08.
This code creates a cookie with the name 'MyName' and adds it to the collection of cookies in the HttpResponse object. This is the server's way of telling the browser to maintain the cookie specified. To illustrate how the cookie is available in a separate request, the user is redirected to another page that will retrieve the cookie.
Retrieving Cookie Values
When the browser makes a request to a Web server, it sends along its collection of cookies for that domain. They can be retrieved through the HttpRequest.Cookies collection as shown here:
procedure TWebForm2.Page_Load(sender: System.Object; e: System.EventArgs); begin if Request.Cookies['MyName'] <> nil then Label1.Text := System.String.Format('Hello {0}, Welcome to the site', Request.Cookies['MyName'].Value) else Label1.Text := 'I don''t know you'; end;
Find the code on the CD: CodeChapter 33Ex08.
This code demonstrates how the cookie has been transferred by the browser as part of the request. The server can then obtain the cookie's value and, in this example, use it as part of a string displayed to the user.
Creating Persistent Cookies
A persistent cookie is one that the browser places on the user's hard drive in a directory that the browser knows about. The server can initiate this by adding an expiration date to the HTTPCookie.Expires property. The following code illustrates this procedure:
var MyCookie: HttpCookie; begin MyCookie := HttpCookie.Create('MyName', TextBox1.Text); if cbxRemember.Checked then MyCookie.Expires := System.DateTime.Today.AddDays(30); Response.Cookies.Add(MyCookie); Response.Redirect('WebForm2.aspx') end;
In this example, a CheckBox on the Web From determines whether the server will tell the browser to create a persistent cookie. If it does, the cookie is set to expire 30 days from today.
Now suppose that a user were to revisit a site with this code after having added a persistent cookie. The code in Listing 33.7 demonstrates how to use the cookie value to present a welcome message and to pre-populate TextBoxso the user wouldn't have to reenter her name.
Listing 33.7 Using a Cookie to Pre-populate Controls
procedure TWebForm1.Page_Load(sender: System.Object; e: System.EventArgs); begin if not IsPostBack then if not Request.Browser.Cookies then lblNoCookie.Text := 'Your browser does not support cookies.' else begin if Request.Cookies['MyName'] <> nil then begin lblWelcome.Text := System.String.Format('Welcome back {0}', Request.Cookies['MyName'].Value); TextBox1.Text := Request.Cookies['MyName'].Value; end; end; end;
Find the code on the CD: CodeChapter 33Ex08.
To delete a cookie from the user's machine, you can do one of two things:
- You can add another cookie of the same name that is session based; it has no assignment to the Expires property.
- You can add a cookie with System.DateTime.Now assigned to the Expires property, causing the cookie to be removed from the user's machine.
Cookie Drawbacks
Although convenient and easily implemented, cookies do have their drawbacks. They can only contain a small amount of data, 4KB specifically. Additionally, they can only store string data. Therefore, you would have to convert data such as dates, integers, floats, and so on to strings prior to storing them in cookies. Cookies are also browser dependant, and some browsers do not support them. Last, they require that the user permits your application to store files on her machine. Cookies have gotten a bad rep because they consume space on users' machines without them knowing about it. Therefore, many users opt for disabling cookie support in their browsers.
Cookies are great to use for some circumstances. However, when it comes to managing state in more complex scenarios, ASP.NET has other means for doing this, which I discuss next.
Working with ViewState
ViewState is the component of ASP.NET pages that keeps track of information such as server control properties. It is handled automatically; you typically do nothing with it other than to disable it if you don't need it. It merits discussion here because there are things you can do to improve performance of your Web applications by making adjustments to ViewState. Plus, you can use that state bag to store custom information for these round-trips.
Here's how ViewState works. When you fill out a Web form, the data entered into the form is sent to the server as part of the POST/GET commands. The server, in turn, packages this information into a hidden field, ViewState, and sends them back to the client along with the rest of the response. The client doesn't do anything with this information. ViewState is supplied to the client so that it is sent back to the server to use upon subsequent requests. This is referred to as round-tripping. The simple case is that the server uses the information to set controls properties upon a post-back of the page.
Note - The actual name of the ViewState hidden field is __ViewState. I refer to it as simply ViewState in this section.
In many examples that I have seen to demonstrate ViewState, you're asked to create an ASP.NET application with a few form fields and a Submit button. You're told to fill out the fields and click the Submit button. The page will post-back, and the fields will be populated with the information you originally entered. If you click the Back button on your browser, the fields will retain their values. This is credited to ViewState.
It is true that the properties of controls, such as the Text property of the TextBox control, are added to the ViewState. However, control properties that are sent to the server as part of the POST command are used to generate the HTML in the response instead of those values contained in the ViewState. You can see this by disabling ViewState for the entire page of an application similar to that just described. You will see that the controls retain their values. In other words, the page is still stateful, even without ViewState.
So why bother with ViewState at all? Remember that the server uses values passed as part of the POST command to initialize controls with data in generating the HTML. The only properties that get initialized as such are those that were included in the POST command. Other properties rely on ViewState. For example, consider the page shown in Figure 33.3.
Figure 33.3
Example customer lookup form.
Find the code on the CD: CodeChapter 33Ex09.
Imagine that this is a customer lookup form. The Lookup Customer button is disabled by default. The Page_Load() event handler contains the following code:
if not IsPostBack then btnLookup.Enabled := LoggedIn;
LoggedIn() simply returns True. It simulates some form of test for user authentication.
With ViewState enabled for the page, the page works as you would expect. When the user first goes to the page, btnLookup.Enabled is set to True. When clicking the Lookup Customer button, it retains its enabled state. However, when disabling ViewState for the page, clicking the Lookup Customer button reveals that it will be disabled on the post-back. This is because the LoggedIn() method is not invoked since this is a post-back. The server generates the HTML for the button based on its design-time property values. The server has no information in the POST command properties, nor is there any state information in ViewState from which it can determine a different property value for the button's Enabled property.
You can see where ViewState serves a good purpose. However, more often than not, you really do not need ViewState. It is generally recommended that you disable ViewState on all pages that do not need it. This will prevent performance loss resulting in the extra bytes being tagged along in your HTML documents.
Disabling ViewState on the Page
You can disable ViewState for an entire page by adding it to the @ Page directive. This is shown here:
<%@ Page language="c#" EnableViewState="False" Codebehind="WebForm1.pas" AutoEventWireup="True" Inherits="WebForm1.TWebForm1" %>
Disabling ViewState for a Control
You can disable ViewState for a specific control by simply setting its EnableViewState property to False in the Object Inspector. You can also edit the .aspx file directly as shown here:
<asp:button id=btnLookup style="Z-INDEX: 9; LEFT: 222px; POSITION: absolute; TOP: 206px" runat="server" enableviewstate="False" enabled="False" text="Lookup Customer"> </asp:button>
Adding Values to the State Bag
If you recall from the section on cookies, some browsers do not support cookies or the feature has been disabled by the user. You can store the same type of information in the ViewState by adding them to the state bag (orViewState property of each control). Adding a value to the state bag is simple, as shown in the following line:
ViewState.Add('MyData', 'MyDataText');
This adds the item 'MyDataText' keyed off the string 'MyData'. To reference an item in the state bag, simply index it by its string key:
Response.Write(ViewState['MyData']);
Find the code on the CD: CodeChapter 33Ex10.
Note - Unlike cookies, which are restricted to storing only strings, ViewState supports storing arbitrary objects.
Session State Management
Session state management occurs during the course of a user's visit to a site. It typically begins once the user visits the site and ends when the user leaves the site.
When a user first enters a site, ASP.NET creates a unique session for that user. This session is an instance of the HttpSessionState class. The session consumes a certain amount of memory for this user. Additionally, the user is given a unique ID, which is passed to the user's browser and returned to the server on each request during the run of the session. By default, this is all done via a cookie.
The session remains in memory until the user leaves the site or until the session has timed out, which, by default, occurs after 20 minutes of inactivity.
You can store information in the HttpSessionState class instance that can be retrieved upon subsequent requests.
Session information is maintained by a session state provider. This provider is run in one of three modes: InProc, StateService, or SQLServer.
- InProc—Session data is maintained within the same domain as the ASP.NET application. It is within the context of aspnet_wp.exe. This is the default setting.
- StateServer—Session data is maintained within the context of a Windows NT Service aspnet_state.exe. This service can be run on the same or on a different machine.
- SQLServer—Session data is maintained in a predefined SQL Server database.
- Off—Session state is disabled.
Storing and Retrieving Information Using the Session Object
The following code demonstrates how to add data to the Session object:
Session.Add('UserName', TextBox1.Text);
To retrieve this same information, you would issue a statement such as
Response.Write('Welcome '+Session['UserName'].ToString);
You can add any object to the Session class. For instance, the following code adds a DataSet to the Session class:
Session.Add('MyData', DataSet1);
To remove an item from the Session object, simply call its Remove() method as
Session.Remove('MyData');
Changing the Default Session Timeout
You can change the session's default timeout by modifying the web.config file. The default timeout is 20 minutes. The following modification to web.config sets the timeout to 60 minutes:
<configuration> <system.web> <sessionState timeout="60"/> </system.web> </configuration>
Making Sessions Cookieless
Earlier, I stated how the SessionID is passed between the server and browser via a cookie. When discussing cookies in this chapter, I pointed out some drawbacks to cookies, such as the user disabling cookie support in her browser. This would render the Session cookie unusable for an ASP.NET site. Therefore, you can change how ASP.NET transfers the SessionID. It entails modifying the web.config file as shown here:
<configuration> <system.web> <sessionState cookieless="true" /> </system.web> </configuration>
When this is done, the SessionID is passed as part of the URL. This is called cookie munging. A URL with the SessionID would look something like the one here:
http://www.xapware.com/SessionEx/(kxn1f555r4xgbe45jlr4wcyf)/WebForm1.aspx
The SessionID is the portion in bold.
Using this technique has a few drawbacks. First, you cannot place absolute links to pages within your site. All links must be relative to the current page. If you can live with that, this is a great way to get around cookie limitations on the client side. Second, it reduces the usefulness of client and proxy-side caching of complete HTML pages. URLs change for each session, so yesterday's cached pages will not be used today, for instance.
Storing Session Data in a Session State Server
By default, the ASP.NET applications use an in-process Session State Server. This ties session information directly to the ASP.NET application in that they are both running in the same process. If the ASP.NET application were to be shut down, all session information would be lost. This is the disadvantage of the InProc mode. The advantage is one of performance. With the session information existing within the same process and machine for that matter, data retrieval is faster. The following web.config setting shows the Session's default InProc setting:
<configuration> <system.web> <sessionState mode="InProc"/> </system.web> </configuration>
To configure for an Out-of-Proc Session State Server, the web.config file would contain something similar to
<configuration> <system.web> <sessionState mode="StateServer" stateConnectionString="tcpip=192.168.0.20:42424"/> </system.web> </configuration>
tcpip refers to the IP address of the machine hosting the session state server. In this example, the port used is 42424. You can change this and be sure to make it a port unused by other processes on the machine.
To start the session state server on the machine that will be hosting it, you simply have to issue net start aspnet_state on the command line as shown here:
C:>net start aspnet_state The ASP.NET State Service service is starting. The ASP.NET State Service service was started successfully. C:>
By storing session information out of process, you gain the benefit of reducing the chances of session data being lost. If the ASP.NET application or if the Web server were to be shut down, the session information would be retained by the session state server on another machine most likely. Again, the performance here is reduced and not only because of network transfer of data, but also because of the serialization/deserialization operations that must take place.
Storing Session Data in SQL Server
It is possible to store session information in SQL Server using a set of predefined tables. This approach comes with the greatest robustness, but also with the least performance. However, for applications needing robust failover, this is the best option because you can take advantage of database clustering to deal with database failures.
To get set up for storing state information in SQL Server, you must
- Create the predefined database in SQL Server.
- Configure the web.config file to point to that SQL Server.
Creating the SQL Server State Database
This first step involves running Enterprise Manager and running a ready-made script to create the database and tables needed. There are two sets of script pairs:
- InstallSqlState.sql—Creates the ASPState and TempDB databases. State information is maintained in the TempDB database, which only holds this information temporarily. If SQL Server is shut down, the data is lost.
- UninstallSqlState.sql—Uninstalls the database created with InstallSqlState.sql.
- InstallPersistSqlState.sql—Installs the ASPState database. This version of the database stores state information in the same database, and therefore state data is persistent.
- UninstallPersistSqlState.sql—Uninstalls the database created with InstallPersistSqlState.sql.
You will find these scripts located in the following directory:
%SystemRoot%Microsoft.NETFramework[Framework Version]
Depending on which install script you chose to run, you should find the ASPState and possibly the TempDB databases in SQL Server through the Enterprise Manager.
Modifying web.config for SQLServer State Management
Once your database is set up, you need to modify the web.config file to point the ASP.NET application to the database for state management. The web.config should look similar to that shown here:
<configuration> <system.web> <sessionState mode="SQLServer" sqlConnectionString="data source=localhost;user id=sa;pwd=somepwd" /> </system.web> </configuration>
Note the setting of the mode attribute to SQLServer. Additionally, you'll see the connection information provided so that a connection can be made to the database.
Session Events
Two events related to Sessions exist that you can handle. These are the Session_Start and Session_End events. The Session_Start event occurs when a user first visits the site. The Session_End event occurs when the user leaves the site or when the session times out. Both events are declared under the TGlobal class. This class is found in the Global.pas file included with your project.
One way to use these events is to maintain a running user count on your site. When a user visits, you up the count. When a user leaves, you decrement the count. Listing 33.8 shows how you might do this.
Listing 33.8 Storing a User Count in Session_Start
procedure TGlobal.Session_Start(sender: System.Object; e: EventArgs); begin Application.Lock; try if Application['NumUsers'] = nil then Application['NumUsers'] := System.Object(Integer(1)) else Application['NumUsers'] := System.Object(Integer(Application['NumUsers'])+1); finally Application.UnLock; end; end;
Find the code on the CD: CodeChapter 33Ex12.
Conversely, you would decrement the user count in the Session_End event as shown in Listing 33.9.
Listing 33.9 Storing a User Count in Session_End
1: procedure TGlobal.Session_End(sender: System.Object; e: EventArgs); 2: begin 3: Application.Lock; 4: try 5: if Application['NumUsers'] <> nil then 6: Application['NumUsers'] := 7: System.Object(Integer(Application['NumUsers'])-1) 8: else 9: Application['NumUsers'] := System.Object(Integer(0)); 10: finally 11: Application.UnLock; 12: end; 13: end;
Find the code on the CD: CodeChapter 33Ex12.
You might have noticed that both these event handlers make use of the Application object, which I discuss in the next section.
Application State Management
Application state is different from session state in that data stored at the application level is available to all users of the application. In session state, session data is stored for the user of the session only. Figure 33.4 depicts this difference.
Figure 33.4
Difference between application and session state.
Application state is maintained by the HttpApplicationState class. This class is a dictionary, and it is created upon the first request to the application. This is unlike the HttpSessionState class, which is created upon each user's visit to the site.
The HttpApplicationState class works very much like the HttpSessionState class.
Information you would store in the HttpApplicationState class needs to be available to all users of the applications. For instance, connection strings to the database and number of users signed on are examples of application-wide information.
Storing Information Using the Application Object
You can add, access, and remove data similarly to how you do so with the HttpSessionState class.
To add an item, simply do the following:
Application['NumUsers'] := System.Object(Integer(1));
To remove an item, call the HttpApplicationState.Remove() function like this:
Application.Remove('MyItem');
Accessing an item is equally as simple:
MyItem := Application['MyItem'];
You can clear the contents of the Application object by calling its RemoveAll() method:
Application.RemoveAll;
Synchronizing Access to State Data in the Application Object
The operations of the HttpApplicationState class are thread-safe, but if you intend to perform a group of operations, you might want to lock writing access to the application state until you are finished with your set of operations. Listings 33.8 and 33.9 illustrate using the Application.Lock() and Application.UnLock() methods for locking and unlocking write access to the data maintained by the Application object.
Note that it is necessary to match every Lock() call with an UnLock() call that is protected by a try-finally clause.
By doing this, you can prevent concurrent access, which can cause deadlocks, race conditions, and other problems.
Using Cache Versus Application
It might appear that there are close likenesses between the Cache and HttpApplicationState classes. Both have the capability to store data in an application-wide context, and the syntax for dealing with them is basically identical. The differences, however, are great.
Both the Cache and HttpApplicationState classes provide a mechanism for storing application-wide data and can be used for managing state because of this. This is where the likenesses end.
The Cache class takes management of this data further than that of the HttpApplicationClass.
First, accessing data in the Cache class is fully thread-safe. This is unlike the HttpApplicationState class, which requires you to surround data access with synchronization methods Lock() and UnLock().
Second, the Cache class, based on a prioritization scheme, can free data from the Cache when it has not been used in order to free up memory when resources are low.
Also, you get more control over the items added to the Cache by setting absolute and sliding expiration policies.
Last, you can associate items with the Cache to other cached items or to a file, which will result in the cached items being removed from the Cache.
The HttpApplicationState class serves well as a general state store for information needing to be available application-wide and needing to exist during the life of the application.
The Cache class is better suited for complex state management in which greater control over the cached data is required.
No comments:
Post a Comment