Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

.pdf
Скачиваний:
111
Добавлен:
16.08.2013
Размер:
10.35 Mб
Скачать

894 CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

Because the System.Web.UI.StateBag type has been designed to operate on any type-derived System.Object, when you wish to access the value of a given key, you will need to explicitly cast it into the correct underlying data type (in this case, a System.String). Be aware, however, that values placed within the __VIEWSTATE field cannot literally be any object. Specifically, the only valid types are strings, integers, Booleans, ArrayLists, Hashtables, or an array of these types.

So, given that *.aspx pages may insert custom bits of information into the __VIEWSTATE string, the next logical question is when you would want to do so. Most of the time, custom view state data is best suited for user-specific preferences. For example, you may establish a point of view-state data that specifies how a user wishes to view the UI of a GridView (such as a sort order). View state data is not well suited for full-blown user data, such as items in a shopping cart, cached DataSets, or whatnot. When you need to store this sort of complex information, you are required to work with session data. Before we get to that point, you need to understand the role of the Global.asax file.

Source Code The ViewStateApp files are included under the Chapter 24 subdirectory.

A Brief Word Regarding Control State

As of ASP.NET 2.0, a control’s state data can now be persisted via control state rather than view state. This technique is most helpful if you have written a custom ASP.NET web control that must remember data between round-trips. While the ViewState property can be used for this purpose, if view state is disabled at a page level, the custom control is effectively broken. For this very reason, web controls now support a ControlState property.

Control state works identically to view state; however, it will not be disabled if view state is disabled at the page level. As mentioned, this feature is most useful for those who are developing custom web controls (a topic not covered in this text). Consult the .NET Framework 2.0 SDK documentation for further details.

The Role of the Global.asax File

At this point, an ASP.NET application may seem to be little more than a set of *.aspx files and their respective web controls. While you could build a web application by simply linking a set of related web pages, you will most likely need a way to interact with the web application as a whole. To this end, your ASP.NET web applications may choose to include an optional Global.asax file via the WebSite Add New Item menu option (see Figure 24-2).

CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

895

Figure 24-2. The Global.asax file

Simply put, Global.asax is just about as close to a traditional double-clickable *.exe that we can get in the world of ASP.NET, meaning this type represents the runtime behavior of the website itself. Once you insert a Global.asax file into a web project, you will notice it is little more than

a <script> block containing a set of event handlers:

<%@ Application Language="C#" %> <script runat="server">

void Application_Start(Object sender, EventArgs e)

{

// Code that runs on application startup

}

void Application_End(Object sender, EventArgs e)

{

//Code that runs on application shutdown

}

void Application_Error(Object sender, EventArgs e)

{

// Code that runs when an unhandled error occurs

}

void Session_Start(Object sender, EventArgs e)

{

// Code that runs when a new session is started

}

void Session_End(Object sender, EventArgs e)

{

// Code that runs when a session ends

}

</script>

896 CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

Looks can be deceiving, however. At runtime, the code within this <script> block is assembled into a class type deriving from System.Web.HttpApplication. If you have a background in ASP.NET 1.x, you may recall that the Global.asax code-behind file literally did define a class deriving from

HttpApplication.

As mentioned, the members defined inside Global.asax are in event handlers that allow you to interact with application-level (and session-level) events. Table 24-1 documents the role of each member.

Table 24-1. Core Types of the System.Web Namespace

Event Handler

Meaning in Life

Application_Start()

This event handler is called the very first time the web application is

 

launched. Thus, this event will fire exactly once over the lifetime of

 

a web application. This is an ideal place to define application-level data

 

used throughout your web application.

Application_End()

This event handler is called when the application is shutting down. This

 

will occur when the last user times out or if you manually shut down

 

the application via IIS.

Session_Start()

This event handler is fired when a new user logs on to your application.

 

Here you may establish any user-specific data points.

Session_End()

This event handler is fired when a user’s session has terminated

 

(typically through a predefined timeout).

Application_Error()

This is a global error handler that will be called when an unhandled

 

exception is thrown by the web application.

 

 

The Global Last Chance Exception Event Handler

First, let me point out the role of the Application_Error() event handler. Recall that a specific page may handle the Error event to process any unhandled exception that occurred within the scope of the page itself. In a similar light, the Application_Error() event handler is the final place to handle an exception that was not handled by a specific page. As with the page-level Error event, you are able to access the specific System.Exception using the inherited Server property:

void Application_Error(Object sender, EventArgs e)

{

Exception ex = Server.GetLastError(); Response.Write(ex.Message); Server.ClearError();

}

Given that the Application_Error() event handler is the last chance exception handler for your web application, odds are that you would rather not report the error to the user, but you would like to log this information to the web server’s event log, for example:

<%@ Import Namespace = "System.Diagnostics"%>

...

void Application_Error(Object sender, EventArgs e)

{

// Log last error to event log.

Exception ex = Server.GetLastError(); EventLog ev = new EventLog("Application");

ev.WriteEntry(ex.Message, EventLogEntryType.Error); Server.ClearError();

Response.Write("This app has bombed. Sorry!");

}

CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

897

The HttpApplication Base Class

As mentioned, the Global.asax script is dynamically generated into a class deriving from the System.Web.HttpApplication base class, which supplies the same sort of functionality as the System.Web.UI.Page type. Table 24-2 documents the key members of interest.

Table 24-2. Key Members Defined by the System.Web.HttpApplication Type

Property

Meaning in Life

Application

This property allows you to interact with application-level variables, using the

 

exposed HttpApplicationState type.

Request

This property allows you to interact with the incoming HTTP request (via

 

HttpRequest).

Response

This property allows you to interact with the incoming HTTP response (via

 

HttpResponse).

Server

This property gets the intrinsic server object for the current request (via

 

HttpServerUtility).

Session

This property allows you to interact with session-level variables, using the

 

exposed HttpSessionState type.

 

 

Understanding the Application/Session Distinction

Under ASP.NET, application state is maintained by an instance of the HttpApplicationState type. This class enables you to share global information across all users (and all pages) who are logged on to your ASP.NET application. Not only can application data be shared by all users on your site, but also if one user changes the value of an application-level data point, the change is seen by all others on their next postback.

On the other hand, session state is used to remember information for a specific user (again, such as items in a shopping cart). Physically, a user’s session state is represented by the HttpSessionState class type. When a new user logs on to an ASP.NET web application, the runtime will automatically assign that user a new session ID, which by default will expire after 20 minutes of inactivity. Thus, if 20,000 users are logged on to your site, you have 20,000 distinct HttpSessionState objects, each of which is assigned a unique session ID. The relationship between a web application and web sessions is shown in Figure 24-3.

898 CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

As you may know, under classic ASP, applicationand session-state data is represented using distinct COM objects (e.g., Application and Session). Under ASP.NET, Page-derived types as well as the HttpApplication type make use of identically named properties (i.e., Application and Session), which expose the underlying HttpApplicationState and HttpSessionState types.

Maintaining Application-Level State Data

The HttpApplicationState type enables developers to share global information across multiple sessions in an ASP.NET application. For example, you may wish to maintain an applicationwide connection string that can be used by all pages, a common DataSet used by multiple pages, or any other piece of data that needs to be accessed on an applicationwide scale. Table 24-3 describes some core members of this type.

Table 24-3. Members of the HttpApplicationState Type

Members

Meaning in Life

AllKeys

This property returns an array of System.String types that represent all the

 

names in the HttpApplicationState type.

Count

This property gets the number of item objects in the HttpApplicationState type.

Add()

This method allows you to add a new name/value pair into the

 

HttpApplicationState type. Do note that this method is typically not used in

 

favor of the indexer of the HttpApplicationState class.

Clear()

This method deletes all items in the HttpApplicationState type. This is

 

functionally equivalent to the RemoveAll() method.

Lock()

These two methods are used when you wish to alter a set of application

Unlock()

variables in a thread-safe manner.

RemoveAll()

These methods remove a specific item (by string name) within the

Remove()

HttpApplicationState type. RemoveAt() removes the item via a numerical

RemoveAt()

indexer.

 

 

When you create data members that can be shared among all active sessions, you need to establish a set of name/value pairs. In most cases, the most natural place to do so is within the Application_Start() event handler of the HttpApplication-derived type, for example:

void Application_Start(Object sender, EventArgs e)

{

// Set up some application variables.

Application["SalesPersonOfTheMonth"] = "Chucky"; Application["CurrentCarOnSale"] = "Colt"; Application["MostPopularColorOnLot"] = "Black";

}

During the lifetime of your web application (which is to say, until the web application is manually shut down or until the final user times out), any user (on any page) may access these values as necessary. Assume you have a page that will display the current discount car within a Label via

a button click:

protected void btnShowCarDiscount_Click(object sender, EventArgs e)

{

//Must cast the returned System.Object

//to a System.String! lblCurrCarOnSale.Text =

(string)Application["CurrentCarOnSale"];

}

CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

899

Like the ViewState property, notice how you must cast the value returned from the HttpApplicationState type into the correct underlying type. Now, given that the HttpApplicationState type can hold any type, it should stand to reason that you can place custom types (or any .NET type) within your site’s application state.

To illustrate this technique, create a new ASP.NET web application named AppState. Assume you would rather maintain the three current application variables within a strongly typed object named CarLotInfo:

public class CarLotInfo

{

public CarLotInfo(string s, string c, string m)

{

salesPersonOfTheMonth = s; currentCarOnSale = c; mostPopularColorOnLot = m;

}

// Public for easy access.

public string salesPersonOfTheMonth; public string currentCarOnSale; public string mostPopularColorOnLot;

}

With this helper class in place, you could modify the Application_Start() event handler as follows:

protected void Application_Start(Object sender, EventArgs e)

{

// Place a custom object in the application data sector.

Application["CarSiteInfo"] =

new CarLotInfo("Chucky", "Colt", "Black");

}

and then access the information using the public field data within a server-side event handler:

protected void btnShowAppVariables_Click(object sender, EventArgs e)

{

CarLotInfo appVars = ((CarLotInfo)Application["CarSiteInfo"]);

string appState =

string.Format("<li>Car on sale: {0}</li>", appVars.currentCarOnSale);

appState +=

string.Format("<li>Most popular color: {0}</li>", appVars.mostPopularColorOnLot);

appState +=

string.Format("<li>Big shot SalesPerson: {0}</li>", appVars.salesPersonOfTheMonth);

lblAppVariables.Text = appState;

}

If you were now to run this page, you would find that a list of each application variable is displayed on the page’s Label type.

Modifying Application Data

You may programmatically update or delete any or all members using members of the HttpApplicationState type during the execution of your web application. For example, to delete a specific item, simply call the Remove() method. If you wish to destroy all application-level data, call RemoveAll():

900CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

private void CleanAppData()

{

//Remove a single item via string name.

Application.Remove("SomeItemIDontNeed");

//Destroy all application data!

Application.RemoveAll();

}

If you wish to simply change the value of an existing application-level variable, you only need to make a new assignment to the data item in question. Assume your page now supports a new Button type that allows your user to change the current hotshot salesperson. The Click event handler is as you would expect:

protected void btnSetNewSP_Click(object sender, EventArgs e)

{

// Set the new Salesperson.

((CarLotInfo)Application["CarSiteInfo"]).salesPersonOfTheMonth = txtNewSP.Text;

}

If you run the web application, you will find that the application-level variable has been updated. Furthermore, given that application variables are accessible from all user sessions, if you were to launch three or four instances of your web browser, you would find that if one instance changes the current hotshot salesperson, each of the other browsers displays the new value on postback.

Understand that if you have a situation where a set of application-level variables must be updated as a unit, you risk the possibility of data corruption (given that it is technically possible that an application-level data point may be changed while another user is attempting to access it!). While you could take the long road and manually lock down the logic using threading primitives of the System.Threading namespace, the HttpApplicationState type has two methods, Lock() and

Unlock(), that automatically ensure thread safety:

// Safely access related application data.

Application.Lock(); Application["SalesPersonOfTheMonth"] = "Maxine";

Application["CurrentBonusedEmployee"] = Application["SalesPersonOfTheMonth"]; Application.Unlock();

Note Much like the C# lock statement, if an exception occurs after the call to Lock() but before the call to Unlock(), the lock will automatically be released.

Handling Web Application Shutdown

The HttpApplicationState type is designed to maintain the values of the items it contains until one of two situations occurs: the last user on your site times out (or manually logs out) or someone manually shuts down the website via IIS. In each case, the Application_Exit() method of the HttpApplication-derived type will automatically be called. Within this event handler, you are able to perform whatever sort of cleanup code is necessary:

protected void Application_End(Object sender, EventArgs e)

{

//Write current application variables

//to a database or whatever else you need to do...

}

CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

901

Source Code The AppState files are included under the Chapter 24 subdirectory.

Working with the Application Cache

ASP.NET provides a second and more flexible manner to handle applicationwide data. As you recall, the values within the HttpApplicationState object remain in memory as long as your web application is alive and kicking. Sometimes, however, you may wish to maintain a piece of application data only for a specific period of time. For example, you may wish to obtain an ADO.NET DataSet that is valid for only five minutes. After that time, you may want to obtain a fresh DataSet to account for possible user modifications. While it is technically possible to build this infrastructure using HttpApplicationState and some sort of handcrafted monitor, your task is greatly simplified using the ASP.NET application cache.

As suggested by its name, the ASP.NET System.Web.Caching.Cache object (which is accessible via the Context.Cache property) allows you to define an object that is accessible by all users (from all pages) for a fixed amount of time. In its simplest form, interacting with the cache looks identical to interacting with the HttpApplicationState type:

//Add an item to the cache.

//This item will *not* expire.

Context.Cache["SomeStringItem"] = "This is the string item"; string s = (string)Context.Cache["SomeStringItem"];

Note If you wish to access the Cache from within Global.asax, you are required to use the Context property. However, if you are within the scope of a System.Web.UI.Page-derived type, you can make use of the Cache object directly.

Now, understand that if you have no interest in automatically updating (or removing) an application-level data point (as seen here), the Cache object is of little benefit, as you can directly use the HttpApplicationState type. However, when you do wish to have a data point destroyed after a fixed point of time—and optionally be informed when this occurs—the Cache type is extremely helpful.

The System.Web.Caching.Cache class defines only a small number of members beyond the type’s indexer. For example, the Add() method can be used to insert a new item into the cache that is not currently defined (if the specified item is already present, Add() does nothing). The Insert() method will also place a member into the cache. If, however, the item is currently defined, Insert() will replace the current item with the new type. Given that this is most often the behavior you will desire, I’ll focus on the Insert() method exclusively.

Fun with Data Caching

Let’s see an example. To begin, create a new ASP.NET web application named CacheState and insert a Global.asax file. Like an application-level variable maintained by the HttpApplicationState type, the Cache may hold any System.Object-derived type and is often populated within the Application_Start() event handler. For this example, the goal is to automatically update the contents of a DataSet every 15 seconds. The DataSet in question will contain the current set of records from the Inventory table of the Cars database created during our discussion of ADO.NET. Given these stats, update your Global class type as so (code analysis to follow):

902CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

<%@ Application Language="C#" %>

<%@ Import Namespace = "System.Data.SqlClient" %> <%@ Import Namespace = "System.Data" %>

<script runat="server">

//Define a static-level Cache member variable. static Cache theCache;

void Application_Start(Object sender, EventArgs e)

{

//First assign the static 'theCache' variable. theCache = Context.Cache;

//When the application starts up,

//read the current records in the

//Inventory table of the Cars DB.

SqlConnection cn = new SqlConnection

("data source=localhost;initial catalog=Cars; user id ='sa';pwd=''"); SqlDataAdapter dAdapt =

new SqlDataAdapter("Select * From Inventory", cn); DataSet theCars = new DataSet();

dAdapt.Fill(theCars, "Inventory");

//Now store DataSet in the cache.

theCache.Insert("AppDataSet", theCars, null, DateTime.Now.AddSeconds(15), Cache.NoSlidingExpiration, CacheItemPriority.Default,

new CacheItemRemovedCallback(UpdateCarInventory));

}

// The target for the CacheItemRemovedCallback delegate. static void UpdateCarInventory(string key, object item,

CacheItemRemovedReason reason)

{

//Populate the DataSet.

SqlConnection cn = new SqlConnection

("data source=localhost;initial catalog=Cars; user id ='sa';pwd=''"); SqlDataAdapter dAdapt =

new SqlDataAdapter("Select * From Inventory", cn); DataSet theCars = new DataSet();

dAdapt.Fill(theCars, "Inventory");

//Now store in the cache.

theCache.Insert("AppDataSet", theCars, null, DateTime.Now.AddSeconds(15), Cache.NoSlidingExpiration, CacheItemPriority.Default,

new CacheItemRemovedCallback(UpdateCarInventory));

}

...

</script>

CHAPTER 24 ASP.NET 2.0 WEB APPLICATIONS

903

First, notice that the Global type has defined a static-level Cache member variable. The reason is that you have also defined a static-level function (UpdateCarInventory()) that needs to access the Cache (recall that static members do not have access to inherited members, therefore you can’t use the Context property!).

Inside the Application_Start() event handler, you fill a DataSet and place the object within the application cache. As you would guess, the Context.Cache.Insert() method has been overloaded

a number of times. Here, you supply a value for each possible parameter:

// Now store in the cache.

 

theCache.Add("AppDataSet",

// Name used to identify item in the cache.

theCars,

// Object to put In the cache.

null,

// Any dependencies for this object?

DateTime.Now. AddSeconds(15),

// How long item will be in cache.

Cache.NoSlidingExpiration,

// Fixed or sliding time?

CacheItemPriority.Default,

// Priority level of cache item.

// Delegate for CacheItemRemove

event

new CacheItemRemovedCallback(UpdateCarInventory));

The first two parameters simply make up the name/value pair of the item. The third parameter allows you to define a CacheDependency type (which is null in this case, as you do not have any other entities in the cache that are dependent on the DataSet).

Note The ability to define a CacheDependency type is quite interesting. For example, you could establish

a dependency between a member and an external file. If the contents of the file were to change, the type can be automatically updated. Check out the .NET Framework 2.0 documentation for further details.

The next three parameters are used to define the amount of time the item will be allowed to remain in the application cache and its level of priority. Here, you specify the read-only Cache. NoSlidingExpiration field, which informs the cache that the specified time limit (15 seconds) is absolute. Finally, and most important for this example, you create a new CacheItemRemovedCallback delegate type, and pass in the name of the method to call when the DataSet is purged. As you can see from the signature of the UpdateCarInventory() method, the CacheItemRemovedCallback delegate can only call methods that match the following signature:

static void UpdateCarInventory(string key, object item, CacheItemRemovedReason reason)

{ ... }

So, at this point, when the application starts up, the DataSet is populated and cached. Every 15 seconds, the DataSet is purged, updated, and reinserted into the cache. To see the effects of doing this, you need to create a Page that allows for some degree of user interaction.

Modifying the *.aspx File

Update the UI of your initial *.aspx file as shown in Figure 24-4.