ControlInspector - 监控 Windows Forms 事件的触发(类似于 .net 的 Spy++)
ControlInspector 可以挂接到给定控件、用户控件或窗体上的所有事件,并显示它们何时被触发,以及任何 eventArgs。它甚至可以使用动态生成的程序集来处理自定义事件和自定义 eventArgs。
引言
您有多少次编写了一个简单的 Windows Form 应用程序来区分相似事件之间的区别?在 KeyDown、KeyPress、KeyUp 的序列中,TextChanged 在何时触发?在一个复杂的控件上,Load 事件究竟是在其他事件的哪个时间点触发的?
ControlInspector 的设计宗旨就是通过挂接到任意 Windows Form 控件、用户控件或完整的 Windows Form 上的所有事件来回答这些以及其他许多问题。它会递归遍历控件集合,挂接每个子控件上的事件,并对窗体上的上下文菜单和主菜单进行特殊处理,以确保它们不会被排除在外。
总而言之;Control Inspector 就像是 .NET 事件的原生 .NET 版 Spy++。
本文旨在配合 ControlInspector 并深入探讨它使用的技术。如果您只想使用 ControlInspector 来诊断自己的应用程序,或者想更好地理解 Windows Form 事件,那么只需下载编译好的软件版本,不必担心源代码!
使用 Control Inspector
首次打开 Control Inspector 时,您将看到一个空白屏幕。您可以使用“文件/打开”选项打开任意程序集(.net exe 或 dll 文件);然后,您将看到该程序集中包含的 Windows Forms 控件和窗体列表。您选择的条目将被加载到内存中并被实例化,方法是将其托管在一个窗体中,或者如果它本身就是一个窗体,则直接构造它。您还可以使用“文件/Windows Forms”选项来显示 System.Windows.Forms 命名空间中可用控件的列表。请注意,您将无法构造某些控件(例如 ButtonBase),因为尽管它们派生自 Control,但它们不能直接使用。
如果控件被托管在窗体中,您将看到上面的 ControlHostForm。它被编写成在您的控件周围显示一个红色网格,以便您可以看到正在分析的控件的边界。
事件查看器的第一个选项卡,“所有事件”以触发顺序显示 ControlInspector 捕获的所有事件。您可以单击各个控件的选项卡来查看单个控件。当您聚焦于某个特定控件时,您可以使用属性编辑器在运行时更改控件(这有助于查看事件由此触发,以及它们的顺序)。例如,如果您启用了锚定,您就能看到由调整托管窗体大小时生成的 resize 事件。
事件在控件显示之前就被挂接了,因此您将获得所有初始化事件;如果您关闭托管窗体,您将看到直到控件销毁之前触发的事件。
您可以通过聚焦于控件并使用复选框列表,或者右键单击事件视图中的特定事件并选择“停止跟踪此事件”来取消选中要处理的特定事件。此选项仅停止跟踪 **单个控件** 的事件。提供选项来停止跟踪所有控件的事件组(例如,所有鼠标移动相关事件),以防止事件列表过于拥挤。
如果您对 ControlInspector 的内部工作原理不感兴趣,我建议您现在停止阅读!
背景
上周,我参加了 DevelopMentor 主办的 Guerilla .NET 课程。我强烈推荐这家培训公司,因为整个星期都令人鼓舞,而且讲师知识渊博:伙计们,你们知道是谁!
讲师向班级提出了一个挑战,要求大家利用本周所学到的任何技术,设计出最好的程序。挑战将在周四进行评判,所以我只有几天时间来完成。
课程涵盖的主题之一是 Reflection,我决定利用它来发现 Windows Forms 控件的信息并挂接它们的事件。
ControlInspector 还必须使用 Reflection.Emit 来生成一个与事件类型完全对应的函数和委托,以便它能够挂接任意事件。它只能挂接遵循函数原型的事件
void eventName(object sender, eventargstype x)
其中 eventargstype 派生自 EventArgs 类型。所有标准的 WindowsForms 事件都是如此;您的事件也应该使用这种结构,因此这种方法应该没有问题。
关于代码
代码的主要部分分为 MainForm.cs(包含 UI 和挂接事件的代码)和 GenerateEventAssembly.cs(生成与给定委托匹配的函数的 IL)。让我们先谈谈事件的挂接方式。
void HookEvents(object o, string name) {
Type t = o.GetType();
...
使用 Reflection,我们遍历特定类型的所有事件。EventHandlerType 将是挂接此事件所需的委托类型;例如:void EventHandler(object sender, EventArgs e)
foreach(EventInfo ei in t.GetEvents())
{
// Discover type of event handler
Type eventHandlerType = ei.EventHandlerType;
// eventHandlerType is the type of the delegate
// (eg System.EventHandler)
// what we need, is to find the type of the second parameter of the
// delegate, eg System.EventArgs
MethodInfo mi = eventHandlerType.GetMethod("Invoke");
ParameterInfo[] pi = mi.GetParameters();
现在是神奇之处。函数 GetEventConsumerType
动态生成一个类,该类具有一个方法“HandleEvent
”,其类型完全正确。此类派生自 ControlEvent,其中包含一个函数 void GenericHandleEvent(object sender, object eventargs)
,因此生成的代码被最小化(我写不出好的 IL:我用 C# 编写了一个执行所需类型转换的类,然后用 ILDASM 对其进行反编译,然后以此为基础自动生成这些任意类型)。
// Get a class derived from ControlEvent which has a HandleEvent method
// taking the appropriate parameters
ControlEvent ce
= GenerateEventAssembly.Instance.GetEventConsumerType(pi[1].ParameterType);
现在我们有了一个类型正确的函数,我们只需要将它挂接起来。我们还将“ControlEvent”上的 EventFired 事件挂接到我们的通用事件处理程序,该处理程序负责所有显示工作。 // Hook onto this control event to get the details of all events fired
ce.ControlName = name;
ce.EventName = ei.Name;
ce.EventTrackInfo = trackInfo;
ce.EventFired += new EventHandler(eventFired);
controlEventList.Add(ce);
// Wire up the event handler to our new consumer
Delegate d = Delegate.CreateDelegate(eventHandlerType, ce, "HandleEvent");
ei.AddEventHandler(o, d);
}
...
最后,如果这是一个控件类型,我们就递归遍历子控件。
if (o is Control) {
Control c = (Control) o;
if (c.Controls != null) {
foreach(Control subControl in c.Controls) {
HookEvents(subControl, name + "/" + ControlName(subControl));
}
}
...
}
执行 IL 生成的代码有很好的注释,所以这里我不再详细介绍。
该函数void AddEventsToTreeView(ControlEvent ce, TreeView treeView,
bool includeControlName)
是向树形列表添加事件的例程。它再次使用反射来显示 EventArgs 中包含的任何信息(例如,在 KeyPressedEventArgs 中按下的键)。由于事件的通用挂接方式,ControlInspector.exe 中包含一个测试用户控件 - UserControlTest。它包含一个按钮,该按钮会触发一个用户定义的事件,以证明一切正常。
关注点
在加载新控件时移除窗体的选项卡页面会在 1.0 框架中显示一个奇怪的 bug,我已经尽力解决了。感谢讲师 Ian Griffiths 帮助我解决了这个问题。
我没有赢得周四挑战赛!
当前项目已在 VS.NET 2002 和 VS.NET 2003 下进行了测试,并且在这两者上都能正常运行。可下载的文件是一个 VS.NET 2002 项目,但升级后也能正常工作。我将很快发布一个包含更多更改的新版本。
历史
1.0 初始版本