9 Ways State Management
9 Ways State Management
Application
Let's set the object use criteria by answering the state questions I asked earlier. Who needs this data? All users need
access to it. How long does this data need to be persisted? It has to live forever, or for the life of the application.
How large is this data? It can be almost any size—only one copy of the data will exist at any given time.
In classic ASP, the Application object provided a great place to store frequently used pieces of data that changed
infrequently, such as the contents of menus or other reference data. While the Application object is still available as
a data container in ASP.NET, other objects are generally better suited for the kinds of data that would have been
stored in the Application collection of a classic ASP application.
In classic ASP, the Application object was an ideal choice if the data that was to be stored did not vary at all (or very
rarely) for the life of the application (such as read-only or read-mostly data). Connection strings were one of the
more common pieces of data stored in Application variables, but in ASP.NET such configuration data is best stored
in the Web.config file. One thing to consider if you are using the Application object is that any writes to it should be
done either in its Application_OnStart event (in global.asax) or within an Application.Lock section. While using
Application.Lock is necessary to ensure that writes are performed properly, it also serializes requests for the
Application object, which can be a serious performance bottleneck for the application. Figure 2 demonstrates how
to use the Application object; it consists of a Web form and its code-behind file. An example of its output is shown
in Figure 3.
Figure 2 Accessing the Application Object in ASP.NET
Application.aspx
<form id="Application" method="post" runat="server">
</asp:validationsummary>
<table>
<tr>
<tr>
<td>Name</td>
</td>
<td><asp:requiredfieldvalidator id="nameRequired"
required." ControlToValidate="txtName">*
</asp:requiredfieldvalidator></td>
</tr>
<tr>
<td>Value</td>
</asp:textbox></td>
<td><asp:requiredfieldvalidator id="valueRequired"
required." ControlToValidate="txtValue">*
</asp:requiredfieldvalidator></td>
</tr>
<tr>
Text="Update Value"></asp:button></td>
</tr>
</table>
</form>
Application.aspx.cs
private void btnSubmit_Click(object sender, System.EventArgs e)
if(IsValid)
Application.Lock();
Application[txtName.Text] = txtValue.Text;
Application.UnLock();
Application[txtName.Text].ToString() + "</b>";
Cookies
Cookies are handy when a particular user needs a specific piece of data, and it needs to be persisted for a variable
period of time. It can be as brief as the life of the browser window, or as long as months or even years. As far as size
goes, cookies are very small. Cookies can be as small as only a few bytes of data, and since they are passed with
every browser request, their contents should be kept as small as possible.
[ Editor's Update - 1/11/2005: The best way to secure sensitive state that should not be viewed or modified by a
hostile user is to store that state on the server. If sensitive data must be sent to the client, it should be encrypted
beforehand, regardless of the storage mechanism employed.]
A particular named cookie can store a single value or a collection of name/value pairs. Figure 4 shows an example
of both single- and multi-value cookies, as output by the built-in trace features of ASP.NET. These values can be
manipulated within an ASP.NET page by using the Request.Cookies and Response.Cookies collections, as Figure
5 demonstrates.
Figure 5 Accessing Cookies in ASP.NET
Cookies.aspx.cs
// Setting a cookie's value and/or subvalue using the HttpCookie class
HttpCookie cookie;
if(Request.Cookies[txtName.Text] == null)
else
cookie = Request.Cookies[txtName.Text];
if(txtSubValueName.Text.Length > 0)
cookie.Values.Add(txtSubValueName.Text, txtSubValueValue.Text);
Response.AppendCookie(cookie);
if(!Request.Cookies[txtName.Text].HasKeys)
"</b>";
else
Request.Cookies[txtName.Text].Values[key].ToString() + "]<br>";
Delete a Cookie
// Set the value of the cookie to null and
Response.Cookies[txtName.Text].Value = null;
Response.Cookies[txtName.Text].Expires =
System.DateTime.Now.AddMonths(-1); // last month
Form1.aspx
<h1>Form 1</h1>
<p>Your username:
</p>
</asp:validationsummary>
<TABLE>
<TR>
<TR>
<TD>Username</TD>
<TD>
<asp:textbox id="txtName" Runat="server"></asp:textbox></TD>
<TD>
Display="Dynamic">*</asp:requiredfieldvalidator></TD></TR>
<TR>
<TD colSpan="3">
</asp:button></TD></TR></TABLE>
</asp:Panel>
</form>
</form>
Form1.aspx.cs
private void Page_Load(object sender, System.EventArgs e)
if(Request.Form["username"] == null)
pnlSetValue.Visible = true;
else
pnlSetValue.Visible = false;
username = Request.Form["username"].ToString();
lblUsername.Text = username;
this.DataBind();
}
}
if(IsValid)
pnlSetValue.Visible = false;
username = txtName.Text;
lblUsername.Text = username;
this.DataBind();
Form2.aspx
<h1>Form 2</h1>
</form>
</form>
Form2.aspx.cs
private void Page_Load(object sender, System.EventArgs e)
if(Request.Form["username"] != null)
username = Request.Form["username"].ToString();
lblUsername.Text = username;
this.DataBind();
}
}
In ASP.NET, only one server-side form can exist on a page, and that form must submit back to itself (client-side
forms can still be used, without limitations). One of the major reasons that hidden form fields are no longer used to
pass data around applications built on the Microsoft® .NET Framework is that all .NET Framework controls are
capable of maintaining their own state automatically using ViewState. ViewState simply encapsulates the work
involved in setting and retrieving values using hidden form fields into a simple-to-use collection object.
QueryString
The data stored in the QueryString object is used by the individual user. Its lifetime can be as brief as a single
request, or as long as the user continues to use the application (if architected appropriately). This data is typically
less than 1KB. Data in a QueryString is passed in the URL and is visible to the user, so as you might guess, sensitive
data or data that can be used to control the application should be encrypted when using this technique.
That said, the QueryString is a great way to send information between Web forms in ASP.NET. For example, if you
have a DataGrid with a list of products, and a hyperlink in the grid that goes to a product detail page, it would be an
ideal use of the QueryString to include the product ID in the QueryString of the link to the product details page (for
example, productdetails.aspx?id=4). Another advantage of using QueryStrings is that the state of the page is
contained in the URL. This means that a user can put a page in their Favorites folder in its generated form when it's
created with a QueryString. When they return to it as a favorite, it will be the same as when they actually made it a
favorite. Obviously, this only works if the page doesn't rely on any state outside the QueryString and nothing else
changes.
Along with sensitive data, any variable that you don't want the user to be able to manipulate should be avoided here
(unless encryption is used to remove human-readability). Also, keep in mind that characters that are not valid in a
URL must be encoded using Server.UrlEncode, as Figure 7 shows. When dealing with a single ASP.NET page,
ViewState is a better choice than QueryString for maintaining state. For long-term data storage, Cookie, Session, or
Cache are more appropriate data containers than QueryStrings.
Figure 7 Using QueryStrings to Pass Data in ASP.NET
Querystring.aspx
<form id="Querystring" method="post" runat="server">
</asp:validationsummary>
<table>
<tr>
</tr>
<tr>
<td>Name</td>
</td>
<td><asp:requiredfieldvalidator id="nameRequired"
required." ControlToValidate="txtName">*
</asp:requiredfieldvalidator></td>
</tr>
<tr>
<td>Value</td>
</asp:textbox></td>
<td><asp:requiredfieldvalidator id="valueRequired"
required." ControlToValidate="txtValue">*
</asp:requiredfieldvalidator></td>
</tr>
<tr>
Text="Update Value"></asp:button></td>
</tr>
</table>
</form>
Querystring.aspx.cs
private void Page_Load(object sender, System.EventArgs e)
if(Request.QueryString.HasKeys())
Request.QueryString[key].ToString() + "]<br>";
}
private void btnSubmit_Click(object sender, System.EventArgs e)
if(IsValid)
Server.UrlEncode(txtValue.Text));
Session
Session data is specific to a particular user. It lives for as long as the user continues to makes requests plus some
period of time afterward (typically 20 minutes). The Session object can hold large or small amounts of data, but total
storage should be kept minimal if the application is intended to scale to hundreds of users or more.
Unfortunately, the Session object earned itself a very bad name in classic ASP because it tied an application to a
particular machine, precluding the use of clustering and Web farms for scalability. In ASP.NET, this is less of an
issue, since it is a simple matter to change the location where the session is stored. By default (and for best
performance), session data is still stored in the memory of the local Web server, but ASP.NET also supports an
external state server or database for managing Session data.
Using the Session object is easy and its syntax is identical to classic ASP. However, the Session object is one of the
less efficient ways of storing user data, since it is held in memory for some time even after the user has stopped
using the application. This can have serious effects on scalability for a very busy site. Other options allow more
control over the release of memory, such as the Cache object, which may be better suited for some large data values.
Also, ASP.NET sessions rely on cookies by default so if the user disables or doesn't support cookies, sessions won't
work. Support for cookie-free sessions can be configured, however. For small amounts of data, the Session object
can be a perfectly valid place to store user-specific data that needs to persist only for the duration of the user's
current session. The following example demonstrates how to set and retrieve values from the Session object:
private void btnSubmit_Click(object sender, System.EventArgs e)
if(IsValid)
Session[txtName.Text] = txtValue.Text;
// Read and display the value we just set
Session[txtName.Text].ToString() + "</b>";
The Web form is almost identical to the one used for the Application object, and the contents of the Session
collection are also visible when page tracing is enabled.
You should be aware that even when not in use, sessions carry some overhead for an application. You can squeeze a
little bit more performance out of your pages if you turn off sessions on pages that do not use it. Also, setting session
state to read-only can also optimize pages that read but do not write data to sessions. Configure sessions in this
fashion by adding an attribute to the @Page directive in one of these two ways::
<%@ Page EnableSessionState="false" %>
ASP.NET sessions can be configured in the Web.config or Machine.config with the sessionState element. This
element supports the attributes listed in Figure 8.
Figure 8 sessionState Attributes
Cache
Cache data is specific to the single user, a subset of users, or even all users. This data persists for multiple requests.
It can persist for a long time, but not across application restarts. Also, data can expire based on time or other
dependencies. It can hold both large and small amounts of data effectively.
The Cache is one of the coolest objects in all of ASP.NET. It offers incredible flexibility, versatility, and
performance, and is therefore often a better choice than Application or Session for persisting data within an
ASP.NET application. A complete description of the ways in which the Cache object can be used (both declaratively
and programmatically) is beyond the scope of this article, but suffice to say, it's a versatile object. Like the other
collection objects, it is simply a name-value collection, but by using a key value that is specific to a user, you can
make the cached values user-specific. Similarly, you can cache multiple sets of data for different related data, like
several sets of car data with keys like "fordcars", "chevycars", and "gmcars". Data in the Cache can be given an
expiration period that is absolute, sliding, or based on changes to a file. They also implement a callback function that
is invoked whenever the cached value is ejected from the cache, which is useful because you can then check to see if
there is a more recent version of the data available, and if not (or if the data source is unavailable), re-cache the
value that was just expired.
Adding and accessing data in the cache is done using a syntax similar to what I have already covered. However, in
addition to the standard indexer method of accessing this collection's contents, Cache also supports a number of
methods to allow more control over the data that is cached. The method you will most often use is Insert, which
supports several overloads that allow you to specify dependencies, timeouts, priority, and callbacks. Some simple
examples are shown in the following code:
// Add item to cache
Cache["myKey"] = myValue;
Response.Write(Cache["myKey"]);
System.Web.Caching.Cache.NoSlidingExpiration);
One of the more powerful features of the Cache object is its ability to execute a callback when an item in the cache
expires. This uses delegates or function pointers, a fairly advanced topic that I won't be covering in this article.
Fortunately, once you have a bit of sample code showing how this technique works, you can take advantage of it in
your applications by simply cutting and pasting, without knowing all the intricacies of how delegates work. There
are many reasons why you might use this functionality, the most common being to refill the cache with current data
whenever it expires, or restoring the old cache data if the data source to repopulate the cache is unavailable.
In my example, I am simply going to cache the current time, and whenever the cache expires, I am going to add an
asterisk character (*) to the end of the string in the cache. Over time, you will be able to determine how many times
the cache has expired by counting the asterisks. Figure 9 demonstrates the important concept of callbacks and
provides a good template for building more functional callback routines into your use of the cache.
Figure 9 Caching Callback Example
private void Page_Load(object sender, System.EventArgs e)
{
if(Cache[cacheKey]==null)
data = System.DateTime.Now.ToString();
CacheItemRemovedCallback callBack =
new CacheItemRemovedCallback(onRemove);
Cache.Insert(cacheKey,data,null,
System.DateTime.Now.AddSeconds(5),
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Default,
callBack);
else
CacheItemRemovedReason reason)
CacheItemRemovedCallback callBack =
new CacheItemRemovedCallback(onRemove);
Cache.Insert(key,val.ToString() +
"*",null,System.DateTime.Now.AddSeconds(5),Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Default, callBack);
One important feature to note in Figure 9 is the pattern used in the Page_Load to determine whether or not to use
the data in the cache. You will always want to use this pattern when you deal with items in the cache. Use an if
statement to check if the current contents of the cache are null (use a variable for your cache key since you'll be
referencing it several times). If it is null, generate the data from its source and place it in the cache. If it is not null,
return the data from the cache. If you have some very complex data access logic, you should place the whole if
statement in a separate function that's tasked with retrieving the data.
The Cache object has a lot more functionality than most of the other objects I have discussed. It is one of the more
powerful features of ASP.NET, and I would definitely recommend reading more on it. The summary at the
beginning of this article lists some places to start your search for more information.
Context
The Context object holds data for a single user, for a single request, and it is only persisted for the duration of the
request. The Context container can hold large amounts of data, but typically it is used to hold small pieces of data
because it is often implemented for every request through a handler in the global.asax.
The Context container (accessible from the Page object or using System.Web.HttpContext.Current) is provided to
hold values that need to be passed between different HttpModules and HttpHandlers. It can also be used to hold
information that is relevant for an entire request. For example, the IBuySpy portal stuffs some configuration
information into this container during the Application_BeginRequest event handler in the global.asax. Note that this
only applies during the current request; if you need something that will still be around for the next request, consider
using ViewState.
Setting and getting data from the Context collection uses syntax identical to what you have already seen with other
collection objects, like the Application, Session, and Cache. Two simple examples are shown here:
// Add item to Context
Context.Items["myKey"] = myValue;
Response.Write(Context["myKey"]);
ViewState
ViewState holds the state information for a single user, for as long as he is working with this ASPX page. The
ViewState container can hold large amounts of data, but care must be taken to manage the size of ViewState since it
adds to the download size of every request and response.
ViewState is one of the new containers in ASP.NET that you're probably already using, even if you don't know it.
That's because all of the built-in Web controls use ViewState to persist their values between page postbacks. This is
all done behind the scenes, so you don't need to worry about it most of the time. You should be aware of it, though,
since it does impose a performance penalty on your application. How big this penalty is depends on how much
ViewState you are carrying between postbacks—for most Web forms the amount of data is quite small.
The easiest way to determine the amount of ViewState being used by each control on a page is to turn on page
tracing and examine how much ViewState each control is carrying. If a particular control doesn't need to have its
data persisted between postbacks, turn off ViewState for that control by setting EnableViewState to false. You can
also see the total size of the ViewState on a given ASP.NET page by viewing the HTML source of the page in a
browser and examining the hidden form field, __VIEWSTATE. Note that the contents are Base64-encoded to
prevent casual viewing and manipulation. ViewState can also be disabled for an entire page by adding
EnableViewState="false" to the @Page directive.
A typical Web form won't need to manipulate ViewState directly. If you build custom Web controls, however, you
will want to understand how ViewState works and implement it for your controls so that they work similarly to the
Web controls that ship with ASP.NET. Reading and writing values to and from ViewState is done using the same
syntax I've used for the other collection objects:
// Add item to ViewState
ViewState["myKey"] = myValue;
Response.Write(ViewState["myKey"]);
When building your own custom Web controls, you may also want them to take advantage of ViewState. This is
simply done at the property level in your controls. Figure 10 shows how you might store the PersonName property
of a simple custom control in ViewState, and use it in the control's Render method.
Figure 10 Storing a Property in ViewState
namespace MSDN.StateManagement
get
string s = (string)ViewState["PersonName"];
set
ViewState["PersonName"] = value;
writer)
}
}
<appSettings>
uid=myUID;pwd=myPassword;database=myDB" />
</appSettings>
<system.web>
</system.web>
</configuration>
To access these values within your ASP.NET pages, you use the ConfigurationSettings collection, which is in the
System.Configuration namespace. The following simple example demonstrates how to extract the previous
connection string into a local variable:
using System.Configuration;
•••
String strConnString =
ConfigurationSettings.AppSettings["connectionString"];
Adding a reference to the System.Configuration namespace reduces the amount of code required to reference these
values. Since changes to the Web.config or Machine.config result in an immediate application restart, these values
are typically only modified by the server administrator, usually by hand. Thus, you should think of these files as
being a good place to store read-only data, not data that you will need to modify within your application.
Conclusion
Effective state management can mean the difference between a frustrating user experience with the potential for data
corruption and a smooth, fast page or transaction process. While state management in ASP 3.0 was somewhat
unwieldy, ASP.NET brings it under control with the state objects discussed here. With their careful use, you'll be on
your way to presenting the best Web experience possible to your customers