下载本文的代码:CuttingEdge0304.exe (187KB)
Microsoft®.NET Framework 随附了一组丰富的用于调试和跟踪应用程序的编程工具。我谈论的不是集成调试器;而是在开发周期中使用的软件组件。调试器是一种不同的工具,因为它们是通常与成熟的 IDE(例如,Visual Studio® .NET)集成的交互式工具。
Systems.Diagnostics 命名空间定义了两个类,分别名为 Trace 和 Debug,它们的方法可以用来输出包含有关代码执行信息的消息。该功能对于跟踪数据不一致、监视执行流和断言条件甚至收集分析数据都非常有用。Trace 和 Debug 类几乎完全相同,并且在名为“侦听器”的更为专用的模块之上工作。
用于跟踪 Windows® 和 ASP.NET 应用程序的工具具有颇为不同的特征。特别地,Windows 窗体应用程序所支持的跟踪机制具有基于自定义侦听器的可扩展性模型;而ASP.NET 应用程序的跟踪机制则不是这样。相反,ASP.NET 子系统提供了一种自定义跟踪模型。当页在跟踪模式下执行时,会有多种表被追加到输出中,并且这些表显示有关性能、请求和状态的信息。不管您是否将消息写入该页的跟踪日志中,这些表总是被追加到发往浏览器的输出的底部。
Visual Studio .NET 提供了一个强大的集成式调试器,该调试器支持各种前卫功能,例如,断点、快速监视和各种单步调试代码的方式。调试器是捕捉错误以及单步执行小而关键的代码部分的理想工具。因此,如果您使用 Visual Studio .NET 生成项目,则不能要求更多的功能了。但是,如果您不使用 Visual Studio .NET,该怎么办呢?如果您使用不提供调试支持的编辑器,该怎么办呢?在这样的情况下,用于跟踪应用程序行为的编程工具的重要性显著增加。
在本专栏中,我将首先分析所有 ASP.NET 应用程序都可以使用的跟踪子系统,然后编写一个 Windows 窗体应用程序,该程序执行给定的页并收集诸如页的视图状态、请求上下文以及全局对象(如 Cache、Application 和 Session)的内容等运行库信息。该应用程序(我将其称为 MyTracer)可以与 Visual Studio .NET 集成并构成默认浏览器的替代品(至少在测试阶段)。我应当指出的是,在某些位置我将使用未记录的类。这不太可能影响代码,但这些类将来可能会更改。
支持跟踪的 ASP.NET 页
尽管可以向 web.config 文件中添加跟踪配置节以便在应用程序级别配置跟踪,但跟踪是一种通常针对单个页进行控制的功能。对于大型项目,可以在应用程序的 web.config 文件中使用以下代码来启用和禁用跟踪属性:
<configuration> <system.web> <trace enabled="true" pageOutput="true" /> </system.web> </configuration>
Enabled 属性授权对应用程序进行跟踪,而 pageOutput 属性则允许输出在页中出现。如果将 pageOutput 设置为 false(默认设置),则跟踪输出被自动发送到 ASP.NET 跟踪程序工具 trace.axd。在项目收尾阶段,只须将跟踪元素的这两个属性都设置为 false。这样,就可以消除无意中将跟踪保留为启用状态的风险。
要只对特定页启用跟踪,请在 @Page 指令中使用 trace 属性。该属性的默认值为 false。当您将其设置为 true 时,系统和自定义跟踪信息都将出现在页的底部,如图 1 所示。
您应当注意,跟踪信息是实际页输出的一部分,并且因此通过访问该页的任何类型的浏览器显示。正如您可以从该图中猜测到的那样,一些信息表与该页生成的跟踪信息一起显示。附加表显示请求详细信息、控件树和某些有用的集合,例如,Cookie、标头、表单值和服务器变量。如果会话和应用程序状态不为空,则 Session 和 Application 内部对象的内容也包含在视图中。视图状态和 Cache 对象的内容不会刷新到跟踪日志中。以上就是我的工具将提供的关键增强功能。
@Page 指令还提供了 TraceMode 属性,以便让您能够选择信息的显示顺序。可行的值为字符串 SortByCategory 和 SortByTime。默认情况下,跟踪消息按照它们的发出顺序出现。如果将 TraceMode 属性设置为 SortByCategory 值,则出现在 Trace Information 节中的行按照类别名称排序。每个行所属的类别都由用来发出消息的方法确定。
发出跟踪消息
ASP.NET 页使用 TraceContext 类公开的方法来填充它的跟踪日志。当设置了 HTTP 请求以便执行时,将创建该类的实例。然后,通过 HttpContext 类的 Trace 属性公开跟踪程序对象,并且由 Page 类的 Trace 属性对其进行镜像。
TraceContext 类具有非常简单的接口,该接口具有几个属性和一样多的方法。这些属性为 IsEnabled 和 TraceMode。IsEnabled 是一个指示是否启用了跟踪的只读布尔型属性。该属性返回的值受到 @Page 指令中的 trace 属性以及 web.config 文件的 trace 节中的已启用属性的影响。TraceMode 属性获得并设置在该页中显示所跟踪行的顺序。该属性的类型是 TraceMode — 一个包含特殊值(例如,SortByCategory 和 SortByTime)的枚举。
要发出消息,可以使用 Write 或 Warn 方法。这两个方法都具有三个行为方式相同的重载。Write 和 Warn 是几乎完全相同的方法。唯一可见的差异是 Warn 总是以红色输出消息。
Write 方法具有三个重载:
Public Sub Write(msg As String) Public Sub Write(cat As String, msg As String) Public Sub Write(cat As String, msg As String, e As Exception);
最简单的重载只是在消息列中发出指定的文本。在第二个重载中,第一个字符串参数表示您要用于该消息(它是第二个参数)的类别的名称。类别名称可以用来对跟踪信息进行排序,并且是任何对该应用程序有意义且能够更好地解释该消息的名称。最后,第三个重载添加一个额外的 Exception 对象,以防该消息跟踪错误。在这种情况下,消息列中的文本是通过将您指定为参数的文本和有关异常的消息连接在一起而创建的。
尽管 Write 和 Warn 的文本输出都被呈现为 HTML 页,但未应用特殊的样式或格式。字符串作为纯文本写入,因此如果您试图使用任何特殊格式(例如,粗体标记),则您将获得的是附加了可见的 <b> 和 </b> 子字符串的跟踪消息。
无须使用完全限定名称,就可以从 .aspx 页的源代码中或者从代码隐藏类中访问 ASP.NET Trace 对象。嵌入到页中的自定义控件和它们的代码隐藏类还可以直接访问跟踪子系统。但是,其他类不能这样做。假设您的代码隐藏类委托外部类来完成某些任务。该辅助类如何在 ASP.NET 页中执行跟踪?在该辅助类的上下文中,页 Trace 对象不可用(起码在其非限定的直接形式下不可用)。如果外部类希望在当前 HTTP 请求的跟踪日志中发出文本,则必须使用以下表达式调用跟踪程序:
System.Web.HttpContext.Current.Trace.Write(cat, msg)
请注意,ASP.NET 跟踪系统不支持它自己的侦听器集,也不支持那些通过诊断跟踪节注册的侦听器。此外,不存在修改跟踪的输出以便只显示几个标准数据表的已知方式。如果要增强或修改它,则需要全新的方法。
跟踪查看器
ASP.NET 还通过跟踪查看器工具支持应用程序级别跟踪。在为应用程序启用跟踪之后,每个页请求都会将所有特定于该页的跟踪信息发送到查看器。可以通过从应用程序根目录中请求 trace.axd 来观察跟踪查看器(参见图 2)。就像我先前提到的那样,通过在应用程序的 web.config 文件(即,在根文件夹中部署的配置文件)中放置一个 trace 节,可以启用查看器:
<configuration> <system.web> <trace enabled="true" /> </system.web> </configuration>
由于 pageOutput 属性默认情况下为 false,因此只有查看器接收跟踪信息。但是,每个页都可以通过在 @Page 指令中使用 trace 属性来单独重写该设置。跟踪查看器所缓存的请求数不会超过 requestLimit 属性所指定的请求数(默认情况下为 10)。
简而言之,ASP.NET 跟踪查看器充当集中式控制台,并且收集特定应用程序中的页所生成的全部跟踪信息。每个请求(直到由 requestLimit 确定的最大编号)都由查看器界面中的行进行标识,并且可以在查看器的缓存被清除之前参考(参见图 2)。
可以通过在应用程序的根目录中请求 trace.axd URL 来激活查看器。AXD 是一种通过 aspnet_isapi.dll ISAPI 扩展名解析的特殊的资源类型,如图 3 所示。首先,显示图 2 中的页。之后,查看器自动跟踪所有请求,并且为每个请求缓存全部跟踪。在到达请求限制时,除非手动清除日志,否则不会再缓存其他请求。可以单击每个“View Details”链接以查看该特定请求的跟踪。
用于跟踪的内置机制对于 ASP.NET 来说很完美,因为它提供了许多运行库信息,并且使您可以断言和验证内部结构的动态值。另一方面,它并未显示您可能要跟踪的所有信息,并且基于文本的用户界面非常乏味。在该页的最后一个生命周期事件激发之后,跟踪信息被追加到该页中。正如您所知,ASP.NET 页通过处理它从 HttpRuntime 环境接收的事件工作。这些事件描述了页的生命周期,并且通过少数状况(包括 Page_Load 和 Page_PreRender)从 Page_Init 移动到 Page_Unload。当生成最后一个事件时,浏览器的 HTML 代码尚未准备好。当该页呈现时,HTML 被临时缓存在输出流中,并且留待已注册的模块进一步处理。因此,该页中的代码甚至无法访问作为跟踪输出附加的 HTML 信息。这种封闭的结构再加上我对于乏味的基于文本的用户界面的厌恶,促使我去寻求一种替代方案。
在讨论 MyTracer 之前,我应当概括性地对跟踪进行一点说明。尽管您应当十分小心,不要在部署页时启用跟踪属性,但可以在代码中保留跟踪调用以供将来使用。实际上,如果未启用跟踪,则 Write 和 Warn 方法都会立即返回。这可能会对性能产生影响,但是可以忽略,尤其是由于可以声明性地启用跟踪机制,而无须改动代码。通过简单地在 web.config 文件中更改跟踪语句的 enabled 属性,就可以通过查看器启用跟踪。这样,就可以在某个用户反馈之后监视应用程序,而无须停止它。尽管如此,为了使查看器正常工作,必须有条件地发出跟踪信息。
MyTracer 工具
还有其他几种在 ASP.NET 1.x 中编写替代跟踪程序工具的方式。可以将其编写为 HTTP 模块,并且同时使用 HttpApplication 事件和 ASP.NET 对象模型来收集要显示的运行库信息。此外,可以让它公开方法和事件以便与该页交互。在本专栏中,我将推荐一种不同的方法,该方法利用 Windows 窗体应用程序的丰富的用户界面并且与 Visual Studio .NET 集成。在完成一切工作之后,MyTracer 将是一个 Windows 窗体程序,它嵌入了 WebBrowser ActiveX® 控件以显示 Web 页,并且使用 Web 服务作为 ASP.NET 和 Windows 之间的桥梁。图 4 显示了正在对示例页执行操作的这一工具。
该工具导航到指定页并显示该页。如果已经将该页配置为使用 MyTracer,则会导出某些特定于该页的信息,并且将这些信息提供给与应用程序位于同一虚拟文件夹中的 Helper Web 服务。Windows 窗体应用程序处理 DocumentComplete 事件,并且在准备好以后连接到 Web 服务以下载 ASP.NET 运行库信息。之后,它只是用特定于页的数据(例如,Session、Application、Cache、视图状态、输入表单、服务器变量甚至该页中的服务器控件列表)填充各种选项卡。
MyTracer 包含三个组件:图 4 中显示的Windows 窗体程序、前面提到的 Web 服务以及被跟踪的页必须包含的 Web 用户控件。用户控件 — mydebugtool.ascx — 挂钩页生命周期中的关键事件并且将信息存储到 DataSet 对象中。当该页上载时,DataSet 以非常类似于 ASP.NET 会话状态的 SQL Server? 实现的方式复制到数据库中。Web 服务检索该数据库中的任何数据,并且将这些数据序列化到 Windows 窗体应用程序中。数据总是作为一个在用户界面的每个选项卡中都有一个子表的 DataSet 进行传播。数据库和 Web 服务各自都具有非常简单的结构。特别地,DataSet 被保留为 XML DiffGram,以便作为字符串对待和移动。图 5 描述了总体体系结构。
启用MyTracer
要进行跟踪,页必须包含 MyDebugTool 用户控件。该控件具有简单的用户界面,以告诉用户该工具正在操作。(请参见本文顶部的链接。)该控件挂钩 Page_Unload 和 Page_PreRender 事件,并且缓存特定于应用程序和请求的信息(例如,Application、Cache 和 Session)。数据容器是一个 DataSet 对象,它在控件的构造函数内部实例化,并且在通知控件该页将要卸载时被存留到 SQL Server 数据库中,以作为最后一个操作。给定现有的 ASP.NET 页,要启用 MyTracer,则必须注册 MyDebugTool 控件并将其添加到控件集合中。用户控件是通过 @Register 指令注册的:
<%@Register TagPrefix="msdn" TagName="debug" Src="MyDebugTool.ascx"%>
请注意,TagPrefix 和 TagName 可以取任何值,并且用户控件必须与应用程序驻留在同一 Web 域中。您可以将该控件的实例放在该页中的任何地方,甚至可以放在最顶端。给定我刚刚显示的注册,您可以使用以下代码:
<msdn:debug runat="server" ID="mytracer" UserKey="dino" />ID 并不绝对需要,但是正如您稍后将看到的那样,如果您要访问视图状态信息,则必须分配一个 ID。UserKey 属性 — 用户控件类上的公用属性 — 用于标识所有信息将停放的数据库行。它必须是唯一的名称,但没有其他要求。当您完成上述配置任务时,请让任何浏览器指向该页。该页会显示一个蓝条,其中带有一些文本,声称该页现在处于 MyTracer 工具的控制之下。当该页被发送到浏览器时,数据库将立即更新。使用 ConnString 属性可以任意设置该数据库的连接字符串。它默认为名为 MyTracer 的 SQL Server 数据库,且带有一个名为 InternalCache 的表。该表有两个字段 — UserKey 和 Data。UserKey 是一个字符串;Data 是一个文本字段,它包含 DataSet(它包含收集到的所有信息)的 DiffGram 表示。对于本文而言,代码假定具有指定 userkey 的记录已经存在于数据库中。换句话说,只有 UPDATE 语句被使用,并且绝对不会执行 INSERT。只要能够成功地理解和执行以下查询,您就可以通过任何登录名使用任何服务器上的任何数据库。
SELECT data FROM internalcache WHERE userkey=@TheUser
用户控件在一个临时 DataSet 中创建了很多表。所有表都具有两个字符串列,分别名为 Key 和 Value。该控件创建表以存放下列集合的内容:Request.Headers、Request.Form、Request.Cookies、Request.ServerVariables 以及 Application、Cache、Session,外加该页的视图状态和控件。图 6 中的代码显示了如何检索有关 Cache 和页控件的信息。特别地,Cache 对象同时包含用户和系统信息。正如跟踪程序所显示的那样,该集合中的几个槽由服务器端 HTTP 运行库管理,并且指向用来包含运行库信息的内部类。例如,如果会话状态被配置为 ASP.NET 进程的本地信息(默认),则会话项集合作为单个对象存储在 Cache 中。内部会话集合称为 SessionStateItem,如图 7 所示。
请观察一下图 7,您可以看到缓存中存储的所有系统项都具有以“System”或“ISAPIWorkerRequest”开头的名称。如果要查看应用程序中的任何会话在缓存中放置的项,请将 MyDebugTool 控件的 ShowAll 属性设置为 false。在图 7 中,Value 列中的某些值被括在花括号中,这意味着该项包含一个对象,并且所显示的字符串是该类的名称。下面的代码分析了该对象的类型,并且根据需要返回类名称或实际值:
string DisplayFormat(object o) { return (o is string || o.GetType().IsPrimitive ?Convert.ToString(o) :"{ " + o.ToString() + " }"); }
如果类型是数值类型、char 类型或布尔型,则 Type 类上的 IsPrimitive 属性返回 true。请注意,在该上下文中,字符串不被视为基元类型;相反,它们被视为引用类型和 System.String 类的实例。
用来提取 Cache 中存储的所有信息的代码与用来提取 Application、Session 或任何 Request 集合的内容的代码类似。
要列出页中的控件,请遍历 Page.Controls 集合。然而,请注意,该集合只包含第一个级别的控件 — 即只包含该页的总体 HTML 模型中的体元素的直接子元素。实际上,如果您要遍历 Page.Controls 集合,则将获得很少数量的控件 — 大多数为文本字符和 ASP.NET 唯一窗体控件。按照设计,使页工作的服务器控件包含在服务器端窗体中。要访问所有这些控件,必须基于该窗体的 Controls 集合执行第二次循环。要获得完整的控件树(就像在默认的 ASP.NET 跟踪模式下一样),必须递归遍历列出的所有控件的所有 Controls 集合。MyTracer 工具显示每个控件的 ID。请注意,按照设计,所有服务器控件都必须具有一个客户端 ID。该 ID 必须唯一,并且基于服务器端 ID 属性的值正常分配。如果服务器控件不具有 ID,则 ASP.NET 运行库会生成一个能够保证在该页中唯一的 ID。要访问客户端 ID,请使用 ClientID 属性。
Helper Web 服务
从体系结构来说,Web 服务相当有趣,因为它可以用来在 ASP.NET 和桌面应用程序之间创建桥梁,甚至还可以在不同的 ASP.NET 应用程序之间创建桥梁。在这种情况下,Web 服务是将 Windows 窗体程序和被监视的 ASP.NET 页粘接在一起的元素。它提供了一个公共接口,以便使应用程序可以获得特定于 ASP.NET 的信息。我在这里使用 Web 服务的原因是它易于使用。也存在其他可能性。例如,可以使用外部的通过远程处理进行操作的基于 Windows 的服务,从用户控件中获得数据并将其传递给 Windows 窗体应用程序。类似地,选择使用数据库纯属个人意愿。您可以改而使用服务内部的全局内存。大体上,可以为 MyTracer 复制您所具有的用于存储 ASP.NET 应用程序的会话状态的 StateServer 和 SqlServer 选项。
Web 服务的编程接口极其简单(参见图 8)。GetInfo 方法针对数据库执行查询,并且获得一个表示原始 DataSet 的 XML DiffGram 表示的字符串。GetInfo 将该字符串反序列化,并且使用 DataSet 的 ReadXml 方法重新生成有效的 DataSet 对象。接下来,刚刚创建和填充的 DataSet 对象返回给调用方。请注意,如果不检查 UserKey,则该 Web 服务易于受到 SQL 注入式攻击。理想情况下,您需要擦除 UserKey 字段或者使用参数化查询。
基于 Windows 的应用程序
MyTracer 应用程序利用一个 Tabstrip 控件和 WebBrowser ActiveX 控件显示页,从而触发用户控件。当页下载完成后,应用程序调用 Web 服务和 GetInfo 方法以便将数据绑定到其他选项卡页。下面的代码显示了如何为 WebBrowser 的几个事件(StatusTextChange 和 DocumentComplete)编写处理程序:
// Hook up the StatusTextChange event DWebBrowserEvents2_StatusTextChangeEventHandler e1; e1 = new DWebBrowserEvents2_StatusTextChangeEventHandler(StatusTextChange); webBrowser.StatusTextChange += e1; // Hook up the DocumentComplete event DWebBrowserEvents2_DocumentCompleteEventHandler e2; e2_= new DWebBrowserEvents2_DocumentCompleteEventHandler(DocumentComplete); webBrowser.DocumentComplete += e2;
该事件可用的信息被包装在一个自定义事件数据类中,该类的名称与事件处理程序的名称相匹配。例如,对于 DocumentComplete 事件,类的名称为DWebBrowserEvents2_DocumentCompleteEvent。
所有页选项卡都具有公共的结构,该结构包含一个或多个 DataGrid 控件,这些控件被绑定到通过 Web 服务检索的 DataSet 中的表之一。
跟踪程序应用程序还检索页的视图状态(参见图 9)。该功能胜过了 ASP.NET 默认跟踪程序的功能,后者被限制为显示该页中每个控件的视图状态的大小(字节)。MyTracer 只提供有关该页的视图状态中存储的视图状态元素的信息。它未考虑由单个控件创建的状态元素。正如我在 2003 年 2 月刊中所讨论的那样,ASP.NET 视图状态是通过将每个构成控件的视图状态与宿主页的视图状态聚合在一起而创建的。页或控件的视图状态是由 ViewState 属性公开的 StateBag 对象。然而,困难的是,ViewState 是一个保护属性,并且该级别的保护使得这些信息无法从外部组件中访问。
那么,用户控件如何检索宿主页的视图状态呢?我尚未充分探索和研究的一种可能性是反射。在理论上,公共语言运行库中的反射 API 使您可以用编程方式访问类的内部和非公共元素。但是,还需要证明可以从 ASP.NET 应用程序的安全上下文中完成该操作。(顺便说一下,如果您进行了试验并且获得了一些有趣的结果,那么请让我知道!)
MyTracer 使用一种更为直接但不那么强大的方式来读取页的视图状态。用户控件只是使页(以及该页的作者)可以故意向该工具公开它的视图状态。MyDebugTool 控件具有一个 BindViewState 方法,被监视的页可以随时调用它并向其传递它的视图状态对象,如下所示:
void Page_Load(object sender, EventArgs e) { ••• // Bind the viewstate to display mytracer.BindViewState(ViewState); }
这样,该页还可以传递它可能保持的任何视图状态对象 — 例如,自定义控件可能决定公开以便在开发时达到调试目的的视图状态。可以用来观察控件的视图状态的一种简单技巧是:为只公开 ViewState 属性的控件创建一个包装类。例如:
public class MyDataGrid : DataGrid { public StateBag ExternalViewState { get {return ViewState;} } }
接下来,使用页中的新控件并且将视图状态的公开副本绑定到 MyTracer 应用程序。如果控件类被密封并且无法进一步派生,则该技巧无效。因此,如果您要开发商业控件,并且不希望用户窥探控件的状态,则密封继承的类会获得成功。但是,该选择具有其优缺点。例如,我不会购买我无法继承的第三方控件。而且,请注意我只是在谈论窥探视图状态,而不是修改它。正如我在 2003 年 2 月刊的专栏中所讨论的那样,修改视图状态几乎是不可能的。
小结
在开发和测试特定的 ASP.NET 页时,MyTracer 应用程序很有用。该工具运行页,捕获任何运行库信息,并以友好方式向您显示这些信息。它还提供了比您在通过传统方法跟踪页时可以查看的信息更多的信息。该版本的代码不支持自定义消息的输出,也不捕获使用 Trace 对象发送的输出。但是,添加消息容器非常容易,只须向用户控件中添加一个新的类似于 Write 的方法,并且在另外一个表中收集传递的所有字符串。
MyTracer 的目的是充当帮助器工具,以便在测试 ASP.NET 应用程序的过程中替换浏览器(甚至在 Visual Studio .NET 内部)。在 Visual Studio ASP.NET 项目的“Configuration Properties”中,选中“Start external program”选项,键入 MyTracer 的路径,并且将页 URL 设置为该工具的命令行参数(参见图 10)。
应该注意,在使用 MyTracer 的过程中,ASP.NET 调试功能无法完美地工作。如果需要单步执行代码,则没有什么工具能比调试器更好地工作,它的快速监视功能使您可以跟踪应用程序中的几乎任何值。如果不需要调试,则 MyTracer 当然比浏览器更有用。要重新启用调试器,请在同一对话框中单击“Start project”选项(参见图 10)。
在本专栏的代码下载中,您可以找到 MyTracer 的源代码以及一个可用来进行试验的示例 ASP.NET 项目。我迫不及待地想知道您的意见!
请将针对 Dino 的问题和意见发送到 cutting@microsoft.com。
Dino Esposito 是一位定居在意大利罗马的培训讲师和顾问。作为 Building Web Solutions with ASP.NET and ADO.NET 和 Applied XML Programming for .NET (Microsoft Press) 的作者,他花费了大量时间从事有关 ASP.NET 的教学活动以及在各种学术会议上发表演讲。Dino 目前正在为 Microsoft Press 编写 Programming ASP.NET。请通过 dinoe@wintellect.com 与 Dino 联系。