A Matter of Context

 

Susan Warren
Microsoft Corporation

January 14, 2002

One of the most common problems with writing Web applications is letting your code know the context in which it's being executed. Let's look at a simple example—personalizing a page—that illustrates this problem:

     Please sign in.

vs.

     Welcome Susan!

Seems simple enough, but even this tiny bit of Web UI requires a couple of bits of information that will vary each time the page is requested. I'll need to know:

  1. Is the user signed in?
  2. What is the user's display name?

More generally, what is the unique context each time the page is requested? And how can I write my code so that it takes this information into account?

In fact, due to the stateless nature of HTTP, there are many different pieces of context a Web application might need to track. When a user interacts with a Web application, the browser sends a series of independent HTTP requests to the Web server. The application itself has to do the work of knitting these requests into a pleasing experience for the user and knowing the context of the request is critical.

ASP introduced several intrinsic objects like Request and Application to help track the context for an HTTP request. ASP.NET takes the next step and bundles these objects, plus several additional context-related objects into an extremely handy intrinsic object called Context.

Context is an object of type System.Web.HttpContext. It is exposed as a property of the ASP.NET Page class. It's also available from user controls and your business objects (more on that later). Here's a partial list of the objects rolled up by HttpContext:

Object Description
Application A key/value pair collection of values that is accessible by every user of the application. Application is of type System.Web.HttpApplicationState.
ApplicationInstance The actual running application, which exposes some request processing events. These events are handled in Global.asax, or an HttpHandler or HttpModule.
Cache The ASP.NET Cache object, which provides programmatic access to the cache. Rob Howard's ASP.NET Caching column provides a good introduction to caching.
Error The first error (if any) encountered while processing the page. See Rob's Exception to the Rule, Part 1 for more information.
Items A key-value pair collection that you can use to pass information between all of the components that participate in the processing of a single request. Items is of type System.Collections.IDictionary.
Request Information about the HTTP request, including browser information, cookies, and values passed in a form or on the query string. Request is of type System.Web.HttpRequest.
Response Settings and content for creating the HTTP response. Request is of type System.Web.HttpResponse.
Server Server is a utility class with several useful helper methods, including Server.Execute(), Server.MapPath(), and Server.HtmlEncode(). Server is an object of type System.Web.HttpServerUtility.
Session A key/value pair collection of values that are accessible by a single user of the application. Application is of type System.Web.HttpSessionState.
Trace The ASP.NET Trace object, which provides access to tracing functionality. See Rob's Tracing article for more information.
User The security context of the current user, if authenticated. Context.User.Identity is the user's name. User is an object of type System.Security.Principal.IPrincipal.

If you're an ASP developer, some of the objects above will look quite familiar. There are a few enhancements, but for the most part, they work exactly the same in ASP.NET as in ASP.

Context Basics

Some of the objects in Context are also promoted as top-level objects on Page. For example, Page.Context.Response and Page.Response reference the same object so the following code is equivalent:

[Visual Basic® Web Form]

   Response.Write ("Hello ")
   Context.Response.Write ("There")

[C# Web Form]

   Response.Write ("Hello ");
   Context.Response.Write ("There");

You can also use the Context object from your business objects. HttpContext.Current is a static property that conveniently returns the context for the current request. This is useful in all kinds of ways, but here's a simple example of retrieving an item from the cache in your business class:

[Visual Basic]

      ' get the request context
      Dim _context As HttpContext = HttpContext.Current

   ' get dataset from the cache
   Dim _data As DataSet = _context.Cache("MyDataSet")

[C#]

      // get the request context
      HttpContext _context = HttpContext.Current;

   // get dataset from cache
   DataSet _data = _context.Cache("MyDataSet");

Context in Action

The Context object provides The Answer to several common ASP.NET "How Do I ...?" questions. Perhaps the best way to communicate just how valuable this gem can be is to show it in action. Here are a few of the best Context tricks I know.

How Do I Emit an ASP.NET Trace Statement From My Business Class?

Answer: Easy! Use HttpContext.Current to get the Context object, then call Context.Trace.Write().

[Visual Basic]

Imports System
Imports System.Web

Namespace Context

   ' Demonstrates emitting an ASP.NET trace statement from a
   ' business class.

   Public Class TraceEmit
      
      Public Sub SomeMethod()
         
         ' get the request context
         Dim _context As HttpContext = HttpContext.Current
         
         ' use context to write the trace statement
         _context.Trace.Write("in TraceEmit.SomeMethod")

      End Sub

   End Class

End Namespace   

[C#]

using System;
using System.Web;

namespace Context
{
   // Demonstrates emitting an ASP.NET trace statement from a
   // business class.

   public class TraceEmit
   {

        public void SomeMethod() {
        
            // get the request context
            HttpContext _context = HttpContext.Current;

            // use context to write the trace statement
            _context.Trace.Write("in TraceEmit.SomeMethod");
        }
    }
}

How Can I Access a Session State Value From My Business Class?

Answer: Easy! Use HttpContext.Current to get the Context object, then access Context.Session.

[Visual Basic]

Imports System
Imports System.Web

Namespace Context

   ' Demonstrates accessing the ASP.NET Session intrinsic 
   ' from a business class.

   Public Class UseSession
   
      Public Sub SomeMethod()
         
         ' get the request context
         Dim _context As HttpContext = HttpContext.Current
         
         ' access the Session intrinsic
         Dim _value As Object = _context.Session("TheValue")

      End Sub

   End Class

End Namespace

[C#]

using System;
using System.Web;

namespace Context
{
   // Demonstrates accessing the ASP.NET Session intrinsic 
   // from a business class.

   public class UseSession
   {

        public void SomeMethod() {
        
            // get the request context
            HttpContext _context = HttpContext.Current;

            // access the Session intrinsic
            object _value = _context.Session["TheValue"];
        }
    }
}

Answer: Handle the application's BeginRequest and EndRequest events, and use Context.Response.Write to emit the HTML for the header and footer.

Technically, you can handle the application events like BeginRequest in either an HttpModule or by using Global.asax. HttpModules are a bit harder to write, and aren't typically used for functionality that is used by a single application, as in this example. So, we'll use the application-scoped Global.asax file instead.

As with an ASP page, several of the ASP.NET context intrinsics are promoted to be properties of the HttpApplication class, from which the class representing Global.asax inherits. We won't need to use HttpContext.Current to get a reference to the Context object; it's already available in Global.asax.

In this example, I'm putting the <html> and <body> tags, plus a horizontal rule into the header section, and another horizontal rule plus the end tags for these into the footer section. The footer also contains a copyright message. The result looks like the figure below:

Figure 1. Example of standard header and footer as rendered in the browser

This is a trivial example, but you can easily extend this to include your standard header and navigation, or simply output the <!-- #include ---> statements for these. One caveat—if you want the header or footer to include interactive content, you should consider using ASP.NET user controls instead.

[SomePage.aspx source—sample content]

<FONT face="Arial" color="#cc66cc" size="5">
Normal Page Content
</FONT>

[Visual Basic Global.asax]

<%@ Application Language="VB" %>

<script runat="server">

      Sub Application_BeginRequest(sender As Object, e As EventArgs)

         ' emit page header
         Context.Response.Write("<html>" + ControlChars.Lf + _
"<body bgcolor=#efefef>" + ControlChars.Lf + "<hr>" + _ ControlChars.Lf)

      End Sub 
      
      
      Sub Application_EndRequest(sender As Object, e As EventArgs)

         ' emit page footer
         Context.Response.Write("<hr>" + ControlChars.Lf + _
      "Copyright 2002 Microsoft Corporation" + _
      ControlChars.Lf + "</body>" + ControlChars.Lf + "</html>")

      End Sub 

</script>

[C# Global.asax]

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

<script runat="server">

        void Application_BeginRequest(Object sender, EventArgs e) {

            // emit page header
            Context.Response.Write("<html>\n<body bgcolor=#efefef>\n<hr>\n");
        }

        void Application_EndRequest(Object sender, EventArgs e) {

            // emit page footer
            Context.Response.Write("<hr>\nCopyright 2002 Microsoft Corporation\n");
            Context.Response.Write("</body>\n</html>");
        }

</script>

How Can I Show A Welcome Message When The User Is Authenticated?

The Answer: Test the User context object to see if the user is authenticated. If so, get the user's name from the User object too. This is, of course, the example from the beginning of the article.

[Visual Basic]

<script language="VB" runat="server">

    Sub Page_Load(sender As Object, e As EventArgs) {

        If User.Identity.IsAuthenticated Then
            welcome.Text = "Welcome " + User.Identity.Name
        Else
            ' not signed in yet, add a link to signin page
            welcome.Text = "please sign in!"
            welcome.NavigateUrl = "signin.aspx"
        End If

    End Sub

</script>

<asp:HyperLink id="welcome" runat="server" maintainstate="false">
</asp:HyperLink>

[C#]

<script language="C#" runat="server">

    void Page_Load(object sender, EventArgs e) {

        if (User.Identity.IsAuthenticated) {
            welcome.Text = "Welcome " + User.Identity.Name;
        }
        else {
            // not signed in yet, add a link to signin page
            welcome.Text = "please sign in!";
            welcome.NavigateUrl = "signin.aspx";
        }
    }

</script>

<asp:HyperLink id="welcome" runat="server" maintainstate="false">
</asp:HyperLink>

And Now for Something Really Wonderful: Context.Items

I hope the examples above show how much easier it is to write your Web application with a little context information at hand. Wouldn't it be great to be able to access some context that is unique to your application in the same way?

That's the purpose of the Context.Items collection. It holds your application's request-specific values in a way that is available to every part of your code that participates in the processing of a request. For example, the same piece of information can be used in Global.asax, in your ASPX page, in the user controls within the page, and by the business logic the page calls.

Consider the IBuySpy Portal sample application. It uses a single main page—DesktopDefault.aspx—to display portal content. Which content is displayed depends on which tab is selected, as well as the roles of the user, if authenticated.

Figure 2. IbuySpy home page

The querystring includes the TabIndex and TabId parameters for the tab being requested. This information is used throughout the processing of the request to filter which data is displayed to the user. http://www.ibuyspy.com/portal/DesktopDefault.aspx?tabindex=1&tabid=2

To use a querystring value, you need to first make sure it's a valid value and, if not, do a little error handling. It's not a lot of code, but do you really want to duplicate it in every page and component that uses the value? Of course not! In the Portal sample it is even more involved since there is other information that can be preloaded once we know the TabId.

The Portal uses the querystring values as parameters to construct a new "PortalSettings" object and add it to Context.Items in the BeginRequest event in Global.asax. Since the begin request is executed at the beginning of each request, this makes the tab-related values available to all of the pages and components in the application. When the request is complete, the object is automatically discarded—very tidy!

[Visual Basic Global.asax]

      Sub Application_BeginRequest(sender As [Object], e As EventArgs)
         
         Dim tabIndex As Integer = 0
         Dim tabId As Integer = 0
         
         ' Get TabIndex from querystring
         If Not (Request.Params("tabindex") Is Nothing) Then
            tabIndex = Int32.Parse(Request.Params("tabindex"))
         End If
         
         ' Get TabID from querystring
         If Not (Request.Params("tabid") Is Nothing) Then
            tabId = Int32.Parse(Request.Params("tabid"))
         End If
         
         Context.Items.Add("PortalSettings", _
New PortalSettings(tabIndex, tabId))

      End Sub

[C# Global.asax]

void Application_BeginRequest(Object sender, EventArgs e) {
        
    int tabIndex = 0;
    int tabId = 0;

    // Get TabIndex from querystring

    if (Request.Params["tabindex"] != null) {               
        tabIndex = Int32.Parse(Request.Params["tabindex"]);
    }
                
    // Get TabID from querystring

    if (Request.Params["tabid"] != null) {              
        tabId = Int32.Parse(Request.Params["tabid"]);
    }

    Context.Items.Add("PortalSettings", 
new PortalSettings(tabIndex, tabId));
}

The DesktopPortalBanner.ascx user control pulls the PortalSetting's object from Context to access the Portal's name and security settings. In fact, this one module is a great all-around example of Context in action. To illustrate the point, I've simplified the code a little, and marked all of the places either HTTP or application-specific Context is accessed in bold.

[C# DesktopPortalBanner.ascx]

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

<script language="C#" runat="server">

    public int          tabIndex;
    public bool         ShowTabs = true;
    protected String    LogoffLink = "";

    void Page_Load(Object sender, EventArgs e) {

        // Obtain PortalSettings from Current Context
  PortalSettings portalSettings = 
(PortalSettings) Context.Items["PortalSettings"];

        // Dynamically Populate the Portal Site Name
        siteName.Text = portalSettings.PortalName;

        // If user logged in, customize welcome message
        if (Request.IsAuthenticated == true) {
        
            WelcomeMessage.Text = "Welcome " + 
Context.User.Identity.Name + "! <" + 
"span class=Accent" + ">|<" + "/span" + ">";

            // if authentication mode is Cookie, provide a logoff link
            if (Context.User.Identity.AuthenticationType == "Forms") {
                LogoffLink = "<" + "span class=\"Accent\">|</span>\n" + 
"<a href=" + Request.ApplicationPath + 
"/Admin/Logoff.aspx class=SiteLink> Logoff" + 
"</a>";
            }
        }

        // Dynamically render portal tab strip
        if (ShowTabs == true) {

            tabIndex = portalSettings.ActiveTab.TabIndex;

            // Build list of tabs to be shown to user                                   
            ArrayList authorizedTabs = new ArrayList();
            int addedTabs = 0;

            for (int i=0; i < portalSettings.DesktopTabs.Count; i++) {
            
                TabStripDetails tab = 
(TabStripDetails)portalSettings.DesktopTabs[i];

                if (PortalSecurity.IsInRoles(tab.AuthorizedRoles)) { 
                    authorizedTabs.Add(tab);
                }

                if (addedTabs == tabIndex) {
                    tabs.SelectedIndex = addedTabs;
                }

                addedTabs++;
            }          

            // Populate Tab List at Top of the Page with authorized 
// tabs
            tabs.DataSource = authorizedTabs;
            tabs.DataBind();
        }
    }

</script>
<table width="100%" cellspacing="0" class="HeadBg" border="0">
    <tr valign="top">
        <td colspan="3" align="right">
            <asp:label id="WelcomeMessage" runat="server" />
            <a href="<%= Request.ApplicationPath %>">Portal Home</a>
<span class="Accent"> |</span> 
<a href="<%= Request.ApplicationPath %>/Docs/Docs.htm">
                Portal Documentation</a>
            <%= LogoffLink %>
            &nbsp;&nbsp;
        </td>
    </tr>
    <tr>
        <td width="10" rowspan="2">
            &nbsp;
        </td>
        <td height="40">
            <asp:label id="siteName" runat="server" />
        </td>
        <td align="center" rowspan="2">
      &nbsp;
        </td>
    </tr>
    <tr>
        <td>
            <asp:datalist id="tabs" runat="server">
               <ItemTemplate>
                  &nbsp;
<a href='<%= Request.ApplicationPath %>
/DesktopDefault.aspx?tabindex=<%# Container.ItemIndex %>&tabid=
<%# ((TabStripDetails) Container.DataItem).TabId %>'>
<%# ((TabStripDetails) Container.DataItem).TabName %>
</a>&nbsp;
                </ItemTemplate>
                <SelectedItemTemplate>
                  &nbsp;
                  <span class="SelectedTab">
<%# ((TabStripDetails) Container.DataItem).TabName %>
</span>&nbsp;
                </SelectedItemTemplate>
            </asp:datalist>
        </td>
    </tr>
</table>

You can browse and run the complete source for the IBuySpy portal online in both Visual Basic and C# at http://www.ibuyspy.com, or download it and run it yourself.

Summary

Context is another one of those "good things get even better in ASP.NET" features. It extends the already great context support of ASP to add both hooks into the new runtime features of ASP.NET. Plus it adds Context.Items as a new state mechanism for very short-lived values. But the ultimate benefit to you as a developer is more compact, easier to maintain code, and that's a context we can all get behind.