通过控件层级广播事件






4.69/5 (12投票s)
2006年2月26日
5分钟阅读

93640

819
为 Windows Forms 应用程序实现对控件层级中所有祖先控件广播事件的支持。
引言
当 Windows 应用程序中发生事件时,会调用一组已注册的处理程序。只有直接与控件注册的处理程序才会被调用。然而,有时可能更希望控件层级中的所有控件都能处理单个事件。例如,假设一个 Panel
包含一组按钮,所有这些按钮都执行类似的行为。此时,可能更希望由 Panel
来处理 Click
事件,而不是为每个按钮设置单独的处理程序。使用原生框架功能无法实现这一点。
本文讨论了 EventBroadcastProvider
,该类旨在解决通过控件层级传递事件的问题。使用此类,可以监视特定事件并将其传递到层级中的所有处理程序。
事件广播
本文中的事件广播与 MulticastDelegate
支持的事件广播无关,而是指在一个控件中引发的事件可以传递到所有祖先控件,从而使它们也能处理该事件,而无需将子控件与其父控件耦合。
使用方法
主要设计约束之一是实现一个简单的系统,该系统可以使用很少的代码和并发症来使用。事件广播由单个类 EventBroadcastProvider
处理,该类会挂钩层级中所有控件的必要事件(如下文所述)。要添加广播支持,请在代码中添加以下类初始化。初始化后,事件和传递的所有管理都将自动处理。眼不见,心不烦。
EventBroadcastProvider broadcastEvent =
EventBroadcastProvider.CreateProvider(this.panel1, "Click");
如上所示,这会挂钩控件 panel1
及其所有子控件,并在引发 Click
事件时进行传递。
挂钩控件事件
没有直接支持事件广播。事件处理程序只能由引发事件的对象调用。下图显示,button1
和 panel1
都有一个 Click
事件处理程序,但当用户单击 button1
时,只有该处理程序被调用,而 panel1
的处理程序未被调用,尽管 button1
是 panel1
的子控件。
同样,如果只有 panel1
有处理程序,那么在单击 button1
时不会调用任何处理程序。
为了支持广播,子控件中的所有事件都会调用 Relay
,然后由 Relay
负责将事件传递到控件层级。需要挂钩两种类型的事件:一种是在控件添加或移除时发生的事件,另一种是要传递的事件。监视控件何时添加或移除的原因是为了使动态添加的控件能够响应传递的事件,就像它们的静态对应项一样。另一种类型的事件是初始化 EventBroadcastProvider
时提供的事件。由于 CreateProvider
方法接受事件名称作为字符串,因此必须使用反射动态定位该事件,并在当时关联事件处理程序。以下代码演示了事件如何被挂钩,以及如何使用递归来挂钩控件层级中的所有控件。类似的移除方法会在控件从层级移除时移除处理程序。
private void HookPrimaryEvents(Control control)
{
//
// hook static events.
//
control.ControlAdded += m_controlAddedHandler;
control.ControlRemoved += m_controlRemovedHandler;
//
// Use reflection to look for event that will be hooked.
//
Type typ = control.GetType();
EventInfo theEvent = typ.GetEvent(m_eventName);
theEvent.AddEventHandler(control, m_genericHandler);
//
// recursively hook events for nested controls
//
foreach (Control subControl in control.Controls)
{
HookPrimaryEvents(subControl);
}
}
事件传递
事件传递可确保当事件在某个控件上引发时,所有祖先控件也能接收该事件进行处理。但并非所有祖先控件都需要处理该事件。
以下代码演示了事件沿控件层级传递的过程。
protected void Relay(object sender, EventArgs ea)
{
//
// Make sure to end when the target control is the
// only one that send the event. This is because
// it is assumed that there is already
// that event being handled directly.
//
if (object.ReferenceEquals(sender, m_control))
return;
//
// Must flag object to prevent reentry.
//
if (m_noReentry) return;
//
// Locate the event method used to raise the event.
// Currently must be in the format On<EventName>(...).
//
string eventMethName =
String.Format(EventMethodTemplate, m_eventName);
//
// Invoke the event passing the event arguments for
// each ancestor
//
m_noReentry = true;
Control curControl = ((Control)sender).Parent;
while (curControl != null)
{
MethodInfo mi =
curControl.GetType().GetMethod(eventMethName,
BindingFlags.NonPublic |
BindingFlags.Public |
BindingFlags.FlattenHierarchy |
BindingFlags.Instance);
//
// Ignore any classes that do not support the event.
//
if (mi != null)
{
mi.Invoke(curControl, new object[] { ea });
}
//
// End loop it target control has been reached.
//
if (object.ReferenceEquals(curControl, m_control))
break;
//
// Get the parent, or null if no parent exists.
//
curControl = curControl.Parent;
}
m_noReentry = false;
}
对于一个公共方法,重入可能是一个问题。这是因为当调用 Click
事件(例如)时,它也会为所有祖先控件调用。因此,当事件传递给祖先控件时,它们也会响应事件调用 Relay
方法;但是,Relay
的目的是遍历控件层级,而这在第一个控件的事件被引发时已经发生过了。为了防止这种情况,一个标志会指示该方法当前正在处理中,因此不应允许重入。一旦处理程序完成了对其他控件的处理,该标志就会被清除。
主要工作在一个循环中完成,该循环会评估每个控件的父控件。调用事件的标准约定是通过 On…
方法,例如 OnClick(…)
。约定是方法应包含事件的名称;因此,可以从目标事件名称轻松推导出此方法。找到方法后,就可以调用它,从而为父控件引发事件。此过程将继续进行,直到 Relay
到达创建 EventBroadcastProvider
实例时指定的控件。
创建动态事件处理程序
在实现 EventBroadcastProvider
期间,发现 EventHandler
无法向下转型,因此必须创建一个强类型委托。此过程会自动处理,并且对使用者是透明的。
下图显示了此处理方式。使用者唯一需要关心的类是 EventBroadcastProvider
;但是,在调用 CreateProvider
时,该类会使用反射动态创建。因此,EventBroadcastProvider
可以通过 RelayDelegate
属性向派生类请求强类型委托,然后将其挂钩到事件。
以下代码负责实际创建派生类。不要被出现的众多操作吓到。派生类的唯一目的是提供一个强类型的事件处理程序,当被调用时,它会将责任转回 EventBroadcastProvider
中的 Relay
方法(这是访问祖先控件的地方)。
[编辑注释:使用换行符以避免滚动。]
private static EventBroadcastProvider CreateHandlerForEvent(
EventInfo ei)
{
EventBroadcastProvider result = null;
string namespaceName =
typeof(EventBroadcastProvider).Namespace;
string eventName = ei.Name;
string className = eventName + "BroadcastProvider";
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = className + "Assembly";
AppDomain appDomain = AppDomain.CurrentDomain;
AssemblyBuilder assBuilder =
appDomain.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.Run);
ModuleBuilder modBuilder =
assBuilder.DefineDynamicModule(className + "Module");
TypeBuilder typBuilder =
modBuilder.DefineType(className, TypeAttributes.Public,
typeof(EventBroadcastProvider));
FieldBuilder fldBuilder =
typBuilder.DefineField("m_handler", typeof(Delegate),
FieldAttributes.Private);
ILGenerator ilGen = null;
//
// Build the RelayDelegate property.
//
PropertyBuilder relayDelegateBuilder =
typBuilder.DefineProperty("RelayDelegate",
PropertyAttributes.None,
typeof(Delegate), null);
MethodBuilder get_relayDelegateBuilder =
typBuilder.DefineMethod("get_RelayDelegate",
MethodAttributes.HideBySig|
MethodAttributes.Virtual|
MethodAttributes.Family|
MethodAttributes.SpecialName,
typeof(Delegate), null);
ilGen = get_relayDelegateBuilder.GetILGenerator();
ilGen.DeclareLocal(typeof(Delegate));
ilGen.Emit(OpCodes.Nop);
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldfld, fldBuilder);
ilGen.Emit(OpCodes.Stloc_0);
ilGen.Emit(OpCodes.Ldloc_0);
ilGen.Emit(OpCodes.Ret);
relayDelegateBuilder.SetGetMethod(get_relayDelegateBuilder);
//
// Build the HandleEvent method.
//
MethodBuilder handleEventBuilder =
typBuilder.DefineMethod("HandleEvent",
MethodAttributes.Private | MethodAttributes.HideBySig,
null,
new Type[] { typeof(object),
ei.EventHandlerType.GetMethod("Invoke").
GetParameters()[1].ParameterType });
ilGen = handleEventBuilder.GetILGenerator();
ilGen.Emit(OpCodes.Nop);
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldarg_1);
ilGen.Emit(OpCodes.Ldarg_2);
ilGen.Emit(OpCodes.Call,
typeof(EventBroadcastProvider).GetMethod("Relay",
BindingFlags.Instance|BindingFlags.NonPublic));
ilGen.Emit(OpCodes.Nop);
ilGen.Emit(OpCodes.Ret);
//
// Build the constructor.
//
ConstructorBuilder ctorBuilder =
typBuilder.DefineConstructor(
MethodAttributes.Public|
MethodAttributes.HideBySig,
CallingConventions.HasThis, null);
ilGen = ctorBuilder.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldnull);
ilGen.Emit(OpCodes.Stfld, fldBuilder);
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Call,
typeof(EventBroadcastProvider).
GetConstructor(BindingFlags.Instance|
BindingFlags.NonPublic, null,
Type.EmptyTypes, null));
ilGen.Emit(OpCodes.Nop);
ilGen.Emit(OpCodes.Nop);
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldftn, handleEventBuilder);
ilGen.Emit(OpCodes.Newobj,
ei.EventHandlerType.GetConstructors()[0]);
ilGen.Emit(OpCodes.Stfld, fldBuilder);
ilGen.Emit(OpCodes.Nop);
ilGen.Emit(OpCodes.Ret);
result = (EventBroadcastProvider)Activator.
CreateInstance(typBuilder.CreateType());
return result;
}
事件冒泡
事件冒泡是事件广播的伴随概念。Web 应用程序开发人员对事件冒泡更熟悉,因为 Web 应用程序支持此概念,但 Windows 应用程序不支持。
通过进行少量修改,EventBroadcastProvider
可以转换为 EventBubbleProvider
。目前正在开发此功能,并将编写一篇配套文章来演示如何在 Windows 应用程序中冒泡事件。