上下文问题

 

苏珊·沃伦
Microsoft Corporation

2002 年 1 月 14 日

编写 Web 应用程序最常见的问题之一是让代码知道要在其中执行它的上下文。 让我们看一个简单的示例,即个性化页面,以说明此问题:

     请登录。

与。

     欢迎苏珊!

看起来很简单,但即使是这个微小的 Web UI 也需要一些信息,每次请求页面时都会有所不同。 我需要知道:

  1. 用户是否已登录?
  2. 用户的显示名称是什么?

更普遍的是,请求页面 每个时间的唯一 上下文是什么? 如何编写代码,以便考虑到此信息?

事实上,由于 HTTP 的无状态性质,Web 应用程序可能需要跟踪许多不同的上下文片段。当用户与 Web 应用程序交互时,浏览器会将一系列独立的 HTTP 请求发送到 Web 服务器。 应用程序本身必须完成将这些请求编织成用户令人高兴的体验,并了解请求上下文至关重要。

ASP 引入了多个内部对象,例如 请求应用程序 来帮助跟踪 HTTP 请求的上下文。 ASP.NET 执行下一步并将这些对象捆绑在一起,并将多个其他上下文相关对象捆绑到一个名为 Context的极其方便的内部对象中。

上下文 是 system.Web.HttpContext类型的对象。 它作为 ASP.NET Page 类的属性公开。 它还可从用户控件和业务对象(稍后提供更多内容)。 下面是 HttpContext 汇总的对象的部分列表:

对象 描述
应用程序 应用程序的每个用户都可以访问的值的键/值对集合。 应用程序的类型 System.Web.HttpApplicationState
ApplicationInstance 实际运行的应用程序,它公开了一些请求处理事件。 这些事件在 Global.asax 或 HttpHandler 或 HttpModule 中处理。
缓存 ASP.NET Cache 对象,该对象提供对缓存的编程访问。 Rob Howard 的 ASP.NET 缓存列 提供了对缓存的良好介绍。
错误 处理页面时遇到第一个错误(如果有)。 有关详细信息,请参阅 Rob 的规则 例外,第 1 部分
一个键值对集合,可用于在参与处理单个请求的所有组件之间传递信息。 项的类型 System.Collections.IDictionary
请求 有关 HTTP 请求的信息,包括表单或查询字符串中传递的浏览器信息、Cookie 和值。 请求的类型 System.Web.HttpRequest
响应 用于创建 HTTP 响应的设置和内容。 请求的类型 System.Web.HttpResponse
服务器 Server 是一个实用工具类,具有多种有用的帮助程序方法,包括 Server.Execute()Server.MapPath()Server.HtmlEncode()。 Server 是 system.Web.HttpServerUtility类型的对象。
会话 应用程序的单个用户可访问的值的键/值对集合。 应用程序的类型 System.Web.HttpSessionState
跟踪 ASP.NET Trace 对象,该对象提供对跟踪功能的访问权限。 有关详细信息,请参阅 Rob 的 跟踪文章
用户 如果已经过身份验证,则为当前用户的安全上下文。 Context.User.Identity 是用户名。 User 是 system.Security.Principal.IPrincipal类型的对象。

如果你是 ASP 开发人员,上述某些对象看起来非常熟悉。 有一些增强功能,但在大多数情况下,它们的工作方式与 ASP 中的 ASP.NET 完全相同。

上下文基础知识

Context 中的某些对象也作为 Page上的顶级对象提升。 例如,Page.Context.ResponsePage.Response 引用同一对象,因此以下代码等效:

[Visual Basic® Web 窗体]

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

[C# Web 窗体]

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

还可以使用业务对象中的 Context 对象。 HttpContext.Current 是一个静态属性,可方便地返回当前请求的上下文。 这在各种方面都很有用,但下面是从业务类中的缓存中检索项的简单示例:

[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");

操作中的上下文

上下文 对象为几个常见的 ASP.NET“如何...?”问题提供了 答案。 也许传达这颗宝石的价值的最佳方法是在行动中展示它。 以下是我认识的一些最好的 上下文 技巧。

如何从我的业务类发出 ASP.NET 跟踪语句?

答案: 简单! 使用 HttpContext.Current 获取 上下文 对象,然后调用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");
        }
    }
}

如何从业务类访问会话状态值?

答案: 简单! 使用 HttpContext.Current 获取 上下文 对象,然后访问 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"];
        }
    }
}

答案: 处理应用程序的 BeginRequestEndRequest 事件,并使用 Context.Response.Write 为页眉和页脚发出 HTML。

从技术上而言,可以在 HttpModule 中或使用 Global.asax 处理应用程序事件,例如 BeginRequest。 HttpModules 更难写入,通常不用于单个应用程序使用的功能,如此示例所示。 因此,我们将改用应用程序范围的 Global.asax 文件。

与 ASP 页一样,将多个 ASP.NET 上下文内部函数提升为 HttpApplication 类的属性,从中继承表示 Global.asax 的类。 我们不需要使用 HttpContext.Current 来获取对 Context 对象的引用;它已在 Global.asax 中提供。

在此示例中,我将 <html><body> 标记以及水平规则放入页眉部分,将另一个水平规则加上这些标记的结束标记放入页脚部分。 页脚还包含版权消息。 结果如下图所示:

图 1. 浏览器中呈现的标准页眉和页脚示例

这是一个微不足道的示例,但可以轻松扩展它,以包含标准标头和导航,或只是输出这些语句的 <!-- #include ---> 语句。 一个警告 - 如果希望页眉或页脚包含交互式内容,应考虑改用 ASP.NET 用户控件。

[SomePage.aspx源 — 示例内容]

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

如何在对用户进行身份验证时显示欢迎消息?

答案: 测试 用户 上下文对象,以查看用户是否已进行身份验证。 如果是这样,则从 用户 对象中获取用户名。 当然,这是本文开头的示例。

[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>

现在对于一些非常美妙的东西: Context.Items

我希望上面的示例演示了使用一点上下文信息编写 Web 应用程序是多么容易。 能否以相同的方式访问 应用程序特有的一些上下文,这难道不是很好?

这是 Context.Items 集合的用途。 它以参与处理请求的每个代码部分可用的方式保存应用程序的特定于请求的值。 例如,可以在 Global.asax、ASPX 页、页面内的用户控件以及页面调用的业务逻辑中使用相同的信息片段。

请考虑 IBuySpy 门户 示例应用程序。 它使用单个主page—DesktopDefault.aspx来显示门户内容。 显示哪些内容取决于选择哪个选项卡,以及用户的角色(如果已经过身份验证)。

图 2. IbuySpy 主页

查询字符串包括所请求选项卡的 TabIndex 和 TabId 参数。 此信息用于处理请求以筛选向用户显示的数据。 http://www.ibuyspy.com/portal/DesktopDefault.aspx?tabindex=1&tabid=2

若要使用 querystring 值,首先需要确保它是一个有效的值,如果不是,则执行一些错误处理。 它不是很多代码,但是否确实要在使用该值的每个页面和组件中复制它? 当然不是! 在门户示例中,它更加涉及,因为我们知道 TabId 后,可以预加载其他信息。

门户使用 querystring 值作为参数来构造新的“PortalSettings”对象,并将其 添加到 Global.asax 中 BeginRequest 事件中的 context.Items。 由于开始请求在每个请求的开头执行,这使得选项卡相关值可供应用程序中的所有页面和组件使用。 请求完成后,对象会自动丢弃-非常整洁!

[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));
}

DesktopPortalBanner.ascx 用户控件从 Context 拉取 PortalSetting 的对象,以访问门户的名称和安全设置。 事实上,这一个模块是 上下文 操作的一个很好的全能示例。 为了说明这一点,我稍微简化了代码,并用粗体标记了 HTTP 或特定于应用程序的 上下文 的所有位置。

[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>

可以在 Visual Basic 和 C# http://www.ibuyspy.com中在线浏览和运行 IBuySpy 门户的完整源,也可以下载并自行运行。

总结

上下文 是其中另一个“好事在 ASP.NET”功能中变得更好。 它扩展了 ASP 的已伟大的上下文支持,以将这两个挂钩添加到 ASP.NET 的新运行时功能中。 此外,它还 Context.Items 作为非常短生存期值的新状态机制。 但是,作为开发人员而言,最终的好处是更紧凑、更易于维护代码,这就是我们都可以隐藏的上下文。