更新MFC中的视图,跟踪.NET Framework中的事件
C++ Q&A...
更新MFC中的视图,跟踪.NET Framework中的事件
翻译:肖进
原文出处:MSDN Magazine May 2004(C++ Q&A)
下载源代码 CQA0405.exe(198KB)

视图用于显示许多图表。用如下的代码只能更新当前活动窗口:GetActiveWindow()->GetActiveView()->GetDocument()
是否有其它的方法从 CMDIFrame 类中获得所有的子窗口或者所有的文档?
Makarand

实际事件改变了底层对象、数据或者文档,就会通过某些视图立即更新事件传递给显示机制。
对于同一个文档如果有数个视图,MFC已经有了一种机制一步到位地更新所有视图。这个函数就是CDocument::UpdateAllViews,它对打开文档的每
一个视图调用 CView::OnUpdate。你可以传递应用程序专用的,描述要执行哪一种更新操作的“提示”。例如,如果你知道仅仅是文档的标题改变了,你就可以定义一个枚举值 CHANGED_TITLE,将它作为
提示代码进行传递。如果你的文档包含了图片和文字,你可以定义枚举值 CHANGED_TEXT 和 CHANGED_GRAPHICS。这些
提示代码的目的是提高性能。通过“提示”来告诉视图什么东西改变了,这样就可以更智能地只重绘那些真正需要刷新的屏幕区域,从而避免潜在的耗时的绘制操作或屏幕闪烁。
UpdateAllViews 更新所有与某个文档关联的视图,但是如何更新所有的文档呢?MFC中没有UpdateAllDocuments 这样的函数,因此你需要自己列举所有的文档。这
就要求实现一个对文档模板和相关文档的循环操作,如下所示:
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();
}
}

Figure 2 运行中的UpdView
CMyDoc::CollectMoreData 增加了一个简单计数器的值。在一个实时程序中,CMyDoc::CollectMoreData 将获得最新的数据,如下载最近的火星图片,或者读取 Bill Gates 浴缸的温度。重要的采集数据后,CMyDoc 通知它的视图自动更新:
void CMyDoc::CollectMoreData()
{
iData++; // time waits for no man...
UpdateAllViews(NULL, 0, NULL);
}
现在,MFC 调用每个视图的 OnUpdate 方法,再调用 Invalidate/UpdateWindow 刷新视图。既然UpdView 是如此的简单,就没有必要提示了。在一个实际的程序中,你
可能需要传递提示信息来帮助视图更有效地重绘它的窗口。

中用 C# 写了一个程序。我知道事件是如何工作的,但有时很难知道什么时候发生的是哪个事件。当我使用 C++ 编程时,我总是可以运行 Spy++
来查看窗口消息是何时发送的。即使没有Spy++,我也可以写一个窗口过程将消息显示到 TRACE 输出。例如:
LRESULT MyWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
TRACE(_T("Message: %04x, wp=%04x, lp=%04x\n"), msg, wp, lp);
...
}
我试图象这样来跟踪 C# 中的事件,但是我无法实现,因为没有中枢事件处理例程。是否有其它方法来观察这些事件,或者
有没有在 .NET 中使用的第三方的 Spy 工具?
Francine Wiggins

比处理消息的 WndProc 例子难那么一点点。
为了使大家都能明白,我先强调一下 Windows 消息和 Framework 事件的区别。在 Windows® 中,
当某件事情发生的时候,操作系统与程序的通讯是通过将消息发送到窗口过程(WndProc)来实现
的,因为所有的消息都要通过单一的回调过程,即窗口过程。所以很容易跟踪消息。无论使用 C/C++ 编程还是.NET Framework 都是如此。在.NET中,你可以通过
改写 Control.WndProc 来实现对消息的跟踪,与C的例子一样。但是,虽然窗口消息从概念上将是一种事件(许多程序员都把它们称为事件),并且
虽然许多 Framework 事件都对应于窗口消息(例如,OnClick 和 WM_COMMAND/BN_CLICKED 一样),但 Framework 使用完全不同的机制,
一种基于委托(delegates)机制而不是全局回调过程。然而每个消息通过相同的 WndProc,每个 FrameWork 有其自己的委托。
那么在没有中枢处理器的情况下,如何跟踪事件?甚至是如何知道处理的是哪个事件呢?
找到这些事件相对容易。.NET Framework 一个最强大的特性之一就是反射,这是一种奇特的方式,我们把它叫做.NET的自我意识。例如,为找出 Form 类中的所有的 public 事件,可以这样写: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。

Paul DiLascia 是一个自由撰稿人、顾问和 Web/UI 高级设计师。他是《Windows++: Writing Reusable Windows Code in C++》(Addison-Wesley, 1992)一书的作者。你可以通过http://www.dilascia.com.与他联系。

肖进:南京中萃食品有限公司资讯部(210061)
本文来自MSDN Magazine,May 2004。可以从附近的报亭获得,或者是
订阅。
