Channels ▼
RSS

.NET

ASP.NET Page Persistence & Extended Attributes

Source Code Accompanies This Article. Download It Now.


Dec02: ASP.NET Page Persistence & Extended Attributes

David was a member of the original Windows NT group at Microsoft before moving to Microsoft Research where he focused on probability models and the analysis of human-computer interaction. He is now an independent consultant and can be contacted at davidhov@exmsft.com.


The HTTP protocol has succeeded in large measure because of its simplicity—it uses text messaging, layers on top of TCP, and is stateless. However, while streamlining web-server operations, HTTP statelessness presents difficulties to web-site builders developing active content.

The statelessness of HTTP means that there is no formal means of remembering—either in the client or server—any past interaction involving the same user. Every web-development tool ensemble provides some means of recognizing sessions and storing information associated with a particular session between HTTP requests.

In building a reusable solution for this problem for .NET-based Active Server Pages (ASP.NET), I exploited the reflection capability of .NET languages. Reflection allows detailed, run-time examination of the contents of classes, and appears to provide opportunities for metareasoning in imperative or procedural programs that have generally been available only in symbolic languages such as Prolog, LISP, and Scheme.

Mechanisms for State Storage in ASP.NET

Running under control of Microsoft's Internet Information Server (IIS), the standard classes of ASP.NET provide several distinct mechanisms for storing information about the client and the client's session. While there are clear tradeoffs between these mechanisms, they all have one thing in common—each requires special coding by application developers on each page.

ASP.NET lets ASPX pages access information from the following collections:

  • Cookies. Applications can create private cookie files on the client's machine that store key information for long periods of time.
  • ViewState. Graphical controls and other mechanisms in ASP.NET use a hidden HTML INPUT tag to store previous state information. This information is presented as a key-value collection during page rendering.

  • Session. Applications have access to a collection of key-value pairs that can be used to store variables associated with the current session.

  • Application. Programs have access to another dictionary that is global to the ASP.NET HTTP application.

In addition, important configuration information can be added to an XML file called "web.config." Data entered in this file is loaded every time the application starts.

ASP.NET can create the illusion of session orientation in several ways. Most commonly, it stores cookies that contain a unique session identifier on the client's machine. When the client makes a new request, the cookie's information is encoded into the new POST request and passed to the server. ASP.NET uses this information to index into a collection of session-state information.

ASPX Pages and The Code-Behind Module

Each of these state-storage mechanisms must be used wisely, and each must be used explicitly by invocation in the "code-behind" module for a given web page.

In traditional ASP, a single text file with the extension ASP contained raw HTML interspersed with server-side and client-side scripts in VBScript or JScript (Microsoft's version of ECMAScript). While convenient, such intermixture quickly led to serious coding anarchy.

Insofar as possible, Microsoft decided to segregate the HTML presentation code and client-side scripts from the server-side scripting code. In ASP.NET, a typical ASPX page is represented by two files:

  • The ASPX file itself, containing the invariant HTML to be rendered.
  • A second code-behind file, typically written in C# or Visual Basic .NET, containing the procedural code to generate the dynamic HTML tags.

An extensive data and eventing model allows interaction between the ASPX file and the code-behind file.

A code-behind file defines a .NET web-page handler class that encapsulates the server-side rendering logic for the page. When an HTTP request is received for a particular ASPX page, the ASP.NET mechanism automatically creates a freshly minted instance of the page class declared in the code-behind module. Very convenient—except that this page instance has no history.

A Declarative Solution

Since ASP.NET supports object-oriented representations of web pages, I found that the first thing I wanted to do with ASP.NET was formalize the handling of session state. My older ASP code was sprinkled with references to the Application and Session collections. Incorrectly handling any one of these would have brought the application down quickly.

I realized that the ideal solution would be object oriented—the code-behind page instance should automatically contain the exact same values in its data field members that it did the last time such an instance existed for the same session. In other words, I wanted to be able to "pretend" that the same instance of the page class had lain dormant just waiting for the next request to come down the fiber from the same client session. This would let me employ the same coding patterns that I'd used for many years.

Rules of Persistence

Assuming I could implement such a declarative solution, how should it work? Again, there are five repositories of useful information: Cookies, the ViewState, Session and Application collections, and web.config file.

I decided I didn't want to use cookies or the ViewState collection for automatic persistence. Both have similar constraints. Not all types of objects can be stored in them, since cookies store only textual information and the ViewState is limited to objects that can be serialized (converted back and forth to text). Also, these mechanisms consume bandwidth, since such information is transmitted repeatedly between the server and the client.

In the end, the following rules seemed reasonable:

  • Only protected fields can be persisted. Since the .NET-design pattern recommends exposing fields only through properties (called "accessors" in C++), I decided that persistence would apply only to noninherited, protected fields.
  • Instance, that is, nonstatic fields are saved in the Session collection only.

  • Static or class-common fields should be saved in the Application collection, but may be saved in the Session collection.

  • Information in the web.config file may be restored into either instance or static fields, but there is no saving mechanism—such data is read-only and is limited to strings.

  • Classes, other than ASPX pages, could use persistence for their static fields only.

Extended Attributes

How could this work? Ideally, I wanted to just mark a field as "please persist" and have it work magically. Considering this, I remembered a similar problem in object serialization under the .NET Framework. When class objects are serialized for storage or transmission, there are always fields that shouldn't or needn't be handled in this way. The .NET team decided to decorate these fields with additional information that could be examined by serialization code at run time. If marked as "don't serialize," the field would be ignored, saving time and bandwidth.

They did this feat by exploiting extended attributes. In .NET languages, classes and their elements can be prefaced with nonprocedural attributes in square braces ([ and ]) that are parsed and validated by the compiler and stored as part of the type information in the "assembly"—the executable file that results from the compilation. This means that such information is always available for inspection at run time. An extended attribute is declared like a standard class. The C# code in Listing One declares an enumeration and an attribute class called PagePersist that contains a value from the enumeration.

The AttributeUsage declaration specifies that the PagePersist attribute is applicable only to fields—the data members of a class. To use the PagePersist attribute, a field in a normal class is prefaced by an attribute declaration; see Listing Two. The C# compiler kindly verifies that the attribute is correctly constructed, then stores the attribute information along with the complete class type information in the resulting assembly.

Using Extended Attributes

It is straightforward to examine the components of a class (fields, methods, enumerations, and so on) at run time and take action accordingly. The .NET Framework calls this sort of behavior "reflection," and it is similar to Java's "introspection" mechanism. This capability applies to extended attributes also.

The next step in my persistence solution was to develop a class that could examine another class's type information, locate the fields to be persisted, and take the correct action.

The file PersistAttr.cs (available electronically; see "Resource Center," page 5) defines a PagePersister class that uses reflection to find the fields within a class marked with persistence attributes and either restore or save their values using the appropriate persistence mechanism.

PersistAttr.cs contains the PagePersister class. Its primary method, Action, saves values from or loads values into static or instance fields of a particular class. Example 1 is the pseudocode description of this algorithm.

Field data is stored into the Session and Application collections using string keys with a special prefix and the page's class name.This helps identify persisted variables and avoids collisions between similarly named values in different pages. As objects are recovered from the persistent collections, their references are removed so that the ASPX code page may recreate or dispose of them as required.

Integrating the Implementation

Once the fields of a code-behind page class have been decorated with the persistence attribute, all that remains is for its values to be restored when a new instance is created and stored when the instance is discarded.Remember that a new page instance will be created for each incoming request. The active lifetime of the page is bounded by two events—Page_Load and Page_Unload.

In Visual Studio.NET, the creation of a new web project in C# generates a module called "Global.asax.cs" that represents the HTTP application. ASP.NET provides a pair of events for this generated Global module that are useful for centralizing persistence handling: PreRequestHandlerExecute and PostRequestHandlerExecute. During the prerequest event, the new page object has been created, but its Page_Load function has not yet been called.When the post-request event occurs, Page_Unload has just been called.In other words, these events scope the active lifetime of the page and are ideal for the logic required. In the sample code, prerequest and postrequest event functions in Global call the Listing Three helper function in PersistAttr.cs. The PersistPage function accesses the current handler defined for this request in the HttpApplication Context object.It checks to see if the handler is a subclass of System.Web.UI.Page, the standard superclass for ASPX pages; if not, it returns.Then it creates a PagePersister object.If the page is being loaded and it's not an initial request, its fields are restored; otherwise, its fields are saved. Each ASPX page class has a property called IsPostBack, but it does not appear to be set before PreRequestHandlerExecute is called, so this routine checks other variables to see if the request is in fact a POST reply. In Listing Four, an ASPX page class is declared with a single instance variable--an array of strings. The field arrayString is prefaced by a PagePersist attribute.Any number of fields in a page may be decorated in that manner.

The result is that between the execution of Page_Load and Page_Unload, you may treat the C# class as though it were part of any other type of application—that is, "stateful." Previous changes to its fields are present, new changes will be remembered. If it later appears that previously unsaved variables must now be persisted, only the persistence attribute needs to be added.

Global Initialization

Since the static fields of a class may be initialized in C#, I decided to mimic this behavior by providing the option of a secondary static initialization for classes in the ASP.NET application's Global module. The Global module receives an Application_Start event when it's loaded for the first time; see Listing Five. In this function, the call to RestoreStatic restores the static fields of the Global object itself from the web.config file. Then a call to RestorePageStatics restores the static variables of the code-behind classes of all ASPX pages used in the application.

How does RestorePageStatics know the identities of all the page classes in the application? The .NET reflection capability lets me iterate over all classes in the application and discover those classes that are immediate descendants of System.Web.UI.Page.

Listing Six demonstrates how straightforward type analysis can be in .NET. First, the routine gets a collection of all the types exported by the executing assembly. Then it iterates over the types, looking for those that are direct descendants of the base class for ASP.NET pages. The static fields of each such class are restored.

Session Termination

Creating a standard procedure for handling persistence information also simplifies end-of-session logic. When this happens, usually due to a time-out, the Global module receives the Session_End event; see Listing Seven.

The PagePersister's SessionEnd function searches the Session collection. All persisted objects that support the IDisposable interface are correctly disposed. (Typically, objects supporting IDisposable have precious system resources that should be released immediately without waiting for garbage collection.)

Metareasoning in Imperative Programming

The page persistence class in effect creates, through the use of standard facilities, what could almost be considered a new language feature and its corresponding mechanics. All of the features it uses are fully supported because they are integral to the .NET Framework.

One reason that I tackled such a project while still a novice to the .NET paradigm was that I wanted to kick the tires of the reflection capability. As an old Prolog fan, I always wanted the ability in, say, C++, to examine classes at run time to pretty-print objects or create smart "deep-copy" object cloning routines.

As the sample code shows, reflection is easy to use. In particular, there are two areas that were simpler than I anticipated. The first concerns assigning values, since assigning arbitrary objects to typed fields would normally require casting. However, the SetValue method of the FieldInfo class handles all such assignments easily. Exceptions are thrown if the values are incompatible, but the type coercion is transparent. In addition, the boxing capability of the .NET languages means that I need not be concerned if the variable is a struct (handled by value) or a class (handled by reference).

The second area that is intriguing is the ability to examine the assembly as a whole. As stated before, I used this feature to discover all the code-behind ASPX page classes declared in the assembly that contained my sample web application.

Some other obvious uses for type analysis come to mind, such as intelligent dump analyzers, automated style checkers, parser generators, and functional programming. .NET lets classes be created and loaded dynamically. Coupled with reflection, many types of data-driven and declarative programming common in the LISP and Prolog communities are well within the reach of the .NET Framework. Unlike the older symbolic languages, however, the .NET languages provide additional benefits such as speed, safety, and portability.

DDJ

Listing One

  public enum PagePersistEnum {
        ePersistSession,
        ePersistApplication,
        ePersistWebConfig };
  [AttributeUsage(AttributeTargets.Field)]
  public class PagePersistAttribute : System.Attribute
  {
        public PagePersistAttribute ( PagePersistEnum ePersist )
        {
              this.ePersist = ePersist;
        }
        public PagePersistEnum PersistLevel
        {
              get { return ePersist; }
        }
        protected PagePersistEnum ePersist;
  }

Back to Article

Listing Two

class Demo
{
  public Demo() {}
  [PagePersist(PagePersistEnum.ePersistSession)]
  protected string sTest = "this is the test string";
}

Back to Article

Listing Three

public static void PersistPage(HttpApplication app, bool bRestore)
{
 // Access the page object that will handle this request.
 System.Web.UI.Page page = app.Context.Handler as System.Web.UI.Page;
 if ( page == null )
 return; // We only handle page classes 
 // Create a persister helper for operating on this page
 PagePersister persist = new PagePersister(page);
 
 string sMethod = app.Context.Request.HttpMethod.Trim().ToUpper();
 bool bInitialRequest = app.Session.IsNewSession || sMethod != "POST";
 if ( bRestore )
 {
  if ( ! bInitialRequest )
  persist.Restore();
 }
 else
  persist.Save();
}

Back to Article

Listing Four

public class PersistPage : System.Web.UI.Page
{
 private void Page_Load(object sender, System.EventArgs e)
 {
 if ( ! IsPostBack )
 {  // Initialize all variables to desired original state  string[] sa = {"First","Second","Third","Fourth"};  arrayString = sa;
 }
 }
 [PagePersist(PagePersistEnum.ePersistSession)]
 protected string[] arrayString;


Back to Article

Listing Five

protected void Application_Start(Object sender, EventArgs e)
{
   //  Restore static variables of the Global class and PersistPage class.
  PagePersister.RestoreStatic(Application,typeof(Global));
  //  Restore static variables of all subclasses of System.Web.UI.Page class.
  PagePersister.RestorePageStatics(Application);
}

Back to Article

Listing Six

public static void RestorePageStatics(HttpApplicationState app)
{
  Assembly asmb = Assembly.GetExecutingAssembly();
  Type[] types = asmb.GetExportedTypes();
  Type tPage = typeof(System.Web.UI.Page);
  foreach ( Type t in types )
  {
     if ( tPage == t.BaseType )
        RestoreStatic(app, t);
  }
}

Back to Article

Listing Seven

protected void Session_End(Object sender, EventArgs e)
{
  PagePersister.SessionEnd(Session);
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video