2008年(884)
分类: C/C++
2008-08-06 09:58:41
原文出处:MSDN
Magazine May 2004(C Q&A)
下载源代码 CQA0405.exe(198KB)
我在 MDI 程序中打算通过 CMainFrame 中的定时器事件来更新所有的子窗口。 视图用于显示许多图表。用如下的代码只能更新当前活动窗口:
GetActiveWindow()->GetActiveView()->GetDocument()
是否有其它的方法从 CMDIFrame 类中获得所有的子窗口或者所有的文档?
for (/* each CDocTemplate in app */) { for (/* each CDocument in CDocTemplate */) { // do something } }
既然列举文档是如此的有用,我写了一个很小的类 CDocEnumerator,隐藏了MFC中所有的模板和位置的机制。 实际上,这个类是我早在1995年9月写的——呵呵,这都几乎是十年前的事了。代码如 Figure 1 所示。 使用 CDocEnumerator 很容易在程序中列举所有打开的文档。
CDocEnumerator it; CDocument* pdoc; while ((pdoc=it.Next())!=NULL) { // do something }
还有什么比这个更容易?为了在实际的例子中示范这个类的用法,我写了一个小程序 UpdView,该程序将模拟实时数据采集程序。UpdView 中每个文档对其打开的秒数 进行计数。Figure 2 显示了工作中的 UpdView。如果下载、生成并运行 UpdView,你便能看到每个视图每秒更新显示文档打开的秒数。在 Figure 2 中, 名字为 file2.dat 的文档有两个视图,它们都显示同一个底层文档。每个文档维持自己的自打开后的时间数(数据),视图只是进行显示(表现)。在你自己的程序中,UpdView通过主框架的定时器 设置工作。这个定时器处理事件使用 CDocEnumerator 告诉每一个文档收集更多的数据,如下面所示:
void CMainFrame::OnTimer(UINT_PTR nIDEvent) { CDocEnumerator it; CDocument* pdoc; while ((pdoc=it.Next())!=NULL) { ((CMyDoc*)pdoc)->CollectMoreData(); } }
void CMyDoc::CollectMoreData() { iData ; // time waits for no man... UpdateAllViews(NULL, 0, NULL); }现在,MFC 调用每个视图的 OnUpdate 方法,再调用 Invalidate/UpdateWindow 刷新视图。既然UpdView 是如此的简单,就没有必要提示了。在一个实际的程序中,你 可能需要传递提示信息来帮助视图更有效地重绘它的窗口。
LRESULT MyWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { TRACE(_T("Message: x, wp=x, lp=x\n"), msg, wp, lp); ... }我试图象这样来跟踪 C# 中的事件,但是我无法实现,因为没有中枢事件处理例程。是否有其它方法来观察这些事件,或者 有没有在 .NET 中使用的第三方的 Spy 工具?
EventInfo[] allEvents = typeof(Form).GetEvents();
这获得了一个 EventInfo 对象数组,每个元素对应于一个 Form 类中定义的事件。每个 EventInfo 对象包括描述 该事件的信息。EventInfo.Name 是事件的名字(例如,Click 或Closing)。EventInfo.EventHandlerType 是 需要用来处理该事件的处理器的类型(delegate)。你甚至可以调用 EventInfo.AddEventHandler 增加另一个处理器。这会使你猜想一个跟踪事件的方法 是写一个通用的处理器,把它挂到类种你想要跟踪的每一个事件上。这是一种正确的途径,但是细节有点复杂。如何写出这个通用处理器?署名是什么?答案显然应该是:
// generic event handler-for any event public void OnAnyEvent(Object sender, EventArgs e);
然而,并不是所有的处理器都使用 EventArgs。例如,Form.Closing 事件使用一个要求CancelEventArgs 的处理 器。
void OnClosing(Object sender, CancelEventArgs e);
当然,CancelEventArgs 从 EventArgs 派生而来,所以将 CancelEventArgs
传递给某个期望 EventArgs 的方法是完全合法的——但是.NET 1.1版本中委托不是这样工作的。当使用 = 或者 EventInfo.AddEventHandler
添加事件处理器时,你必须提供一个委托,其类型必须与该事件处理器的类型完全匹配。这意味着你不能用单一的通用处理器来处理所有事件。每一个事件委托署名至少需要一个处理
器。
但可能会更糟。即使你能使用一个通用的处理器,处理器如何知道哪个事件被触发?跟踪工具的关键是报告哪个事件被触发?如若没有其它的,跟踪工具应该显示事件的名字。但是 Framework
并不传递被触发的是哪个事件的信息,因为该信息被隐含在处理器自身信息当中。你知道是 Fooble 事件,因为你的 OnFooble 处理器被调用了。如果你对多个事件使用同样的
处理器,你将失去辨别它们的能力。因此跟踪事件的唯一方法是为每个事件编写不同的处理器。这似乎是一个无法克服的难题:
直到运行时你才知道期望跟踪的对象及其公开的事件,那你怎么可能编写代码呢?
当然,编写运行时的代码本身。反射不仅意味着可以查询系统中的对象,还意味着可以创建它们。.NET Framework 中有几种方法生成这些代码。System.CodeDom 提供了一
种高级的、语言无关的代码模型,你可以用它来创建诸如程序集、模块、类和方法等代码对象,然后使用象Microsoft.CSharp.CSharpCodeProvider 或者 Microsoft.VisualBasic.VBCodeProvider
这样的类在你最喜欢的语言中表达。另一种生成代码的方法是用你选择的语言将代码显式地写到一个文件或者一个 StringBuilder中,并使用 Process 类来调用适当的命令行编译器(如 C# 编译器 csc.exe)。实际上,友好的 Redmondtonians(译者注:指微软公司)已经保证“整个 Framework
都是这么做的”。例如,XmlSerializer 使用 csc.exe 动态编译所产生的为序列化(serializing)和 反序列化(deserializing)特殊类型
而优化的 C# 代码。
Code Document Object Model(CodeDOM)——代码文档对象模型被设计用
于代码生成器,为了在多语言中表达单一内部表示和随意编译性,该生成器需要处理抽象代码。运行象 csc.exe 这样的编译器需要性能作为代价,也许
仍然可以通过重复调用该代码来证明——你可以用同样的方法编译一个打算经常使用的正则表达式(RegEx)。但还是有另外的方发法来生成代码:你可以使用System.Reflection.Emit.ILGenerator 直接生成低级
的 MS 中间语言(MSIL)指令。很明显这个方法也不错,快速、有效,有时它是快速开发软件的杀手锏——比如写一个事件跟踪器。
为了示范具体做法,我写了一个类 EventSpy,它报告某些目标对象(被跟踪对象)触发的每一个事件。我还写了一个叫 SpyTest 的程序来示范如何使用
这个类。(EventSpy 和 SpyTest 可以从本文开始的链接处下载)。EventSpy 的使用方法很简单:只要实例化它并为 SpyEvent 事件
添加一个处理器,象这样:
// spy on myself spy = new EventSpy("MySpy", this); spy.SpyEvent = new SpyEventHandler(OnEventSpy);
当目标对象(spyee)产生任何一种事件,EventSpy就产生一个SpyEvent事件。此时,SpyEventArgs.EventName 就是事件的名字,SpyEventArgs.EventArgs 包 含原始的事件参数。如何处理这些事件是你自己的事。SpyTest 将该事件报告给诊断流。
// in main form private void OnEventSpy(Object sender, SpyEventArgs e) { Trace.WriteLine(String.Format("{0}: On{1}: {2}", sender.GetType(), e.EventName, e.EventArgs)); }
Figure 3 示范了一个典型的运行结果。先去下载代码,自己试着运行一下——它
确实能运行!EventSpy 还有一个 DumpEvents 函数,列出所有你的目标类暴露的事件。
Figure 3 SpyTrace
如果你只是想用 EventSpy 来跟踪事件,你可以不用阅读此文,直接到 MSDN Magazine 网站下载源代码。对于那些忍不住想自己实现类似 EventSpy
功能的狂热者(祝你好运!),我下面简要概述一下 EventSpy 的工作原理。
讨论事件最终结果是为了跟踪事件,你必须动态生成一个类似
Figure
4 那样的类。在创建这个类之前,你需要为它创建一个集合和模板。System.Reflection.Emit 使用 AssemblyBuilder、ModuleBuilder、TypeBuilder、FieldBuilder、ConstructorBuilder
和 MethodBuilder 来创建你需要的任何东西。这里演示如何创建一个集合。
AssemblyName an = new AssemblyName(); an.Name = "EventSpyHelper"; AssemblyBuilder asm = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
创建模块更简单。创建这个类、字段和方法很象给每个东西取一个名字和标记那样直截了当。最难的部分是何时生成实际代码——换句话说, 也就是每个事件的容器和事件处理器。关键的类叫ILGenerator。Figure 4 演示 EventSpy 如何编写这个事件处理器,
// create a new method OnEventXxx MethodBuilder mthd = helperClass.DefineMethod(...); // generate its code ILGenerator il = mthd.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld,fld); il.Emit(OpCodes.Ldstr,ev.Name); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Callvirt, miReportEvent); il.Emit(OpCodes.Ret);
对 ILGenerator.Emit 的每一个调用产生一个简单的 MSIL 指令。但是我如何知道哪个指令被生成
了呢?你是否真的认为我知道在 MSIL 中如何编程?当然不是。你不必一定要使用 MSIL 来生成代码。你只要编写拟在 C#中使用的代码(或者任何其它面向.NET的语言)
即可,编译它,并用 ILDASM 来检查生成的 MSIL。这就是我要做的;Figure 5 演示了部分 Figure 4 代码 dump 出的 ILDASM。正如你看到的,MSIL和以前的片断显示的一模一样。
Figure 5 ILDASM Dump
一旦你知道了发现正确的 MSIL 诀窍,剩下的事情就简单了,虽然少不了会有几个 bug:你所犯的任何微不足道的错误归咎于将系统陷入混乱的死亡消息,这些消息根本无法
帮你找定位错误在哪儿。正如我说过的,MSIL 不是给懦弱者的。至少没有人能四平八稳。
EventSpy 相当简单。你不能关闭或打开跟踪,你只能跟踪实例(与静态相反)事件,并且你必须为每个打算跟踪的对象创建一个新的 EventSpy 实例。好了,你对一个自由下载的东西有什么期望?
但当你进行调试时, EventSpy 会完成工作,并且我已经多次成功地使用它查看事件流中所发生的一切。添加更多的特性留给你来做。我主要任务是
抛砖引玉,针对 System.Reflection.Emit 和动态代码生成给出一些建议。Figure
6 总结了在.NET中动态产生代码的不同方法,想获得更多信息请查看相关文档和 Adam J. Steinert 的文章
:“Bring the Power of Templates to Your .NET Applications with the CodeDOM
Namespace”。
编程快乐。
有任意问题或建议情发邮件给 Paul,邮箱是
cppqa@microsoft.com。