Wednesday, June 27, 2007

ASP.NET Performance Techniques

By: Xavier Pacheco

Abstract: This session discusses the various measures that developers using Delphi or C#Builder should take to create high-performance ASP.NET applications. We cover components such as caching techniques, state management, etc.�

Xavier Pacheco is the president of Xapware Technologies Inc., which he founded in January 1988 and author of Delphi for .NET Developer's Guide. Xavier has over 16 years of professional experience in developing software solutions such as distributed systems, application architectures, and process and design methodologies. Xavier is an internationally recognized developer, author, consultant, and trainer. He has written several books on Delphi, frequently writes articles, and gives presentations at industry conferences. Xavier can kick John Kaster's butt in a game of pool anyday.

xavier@xapware.com

 

3180 ASP.NET Performance Techniques

Published in Delphi for .NET Developer's Guide, Sam's Publishing
Chapter 33: Caching and Managing State in ASP.NET Applications
by Xavier Pacheco

This chapter discusses two related, but separate, topics on ASP.NET programming—caching and state management. Caching is used to improve performance by persisting commonly accessed data in the Web server. When users request the data, it is retrieved from the Web server rather than the originating data source. State management deals with the issue of Web applications being stateless—knowing nothing about previous requests. State management provides a way for the Web application to maintain information about a user's interaction with the Web application.

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 a GET 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 a POST 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 TextBox so 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 (or ViewState 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




  1. Create the predefined database in SQL Server.



  2. 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.



0 Comments: