65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WinForms 设计器中公开动态事件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (17投票s)

2011年1月20日

CPOL

9分钟阅读

viewsIcon

37186

downloadIcon

954

在设计时为控件数组声明动态事件的解决方案

Sample Image

引言

对于包含动态加载子控件的容器控件,存在一个常见问题:当我们在编译时不知道这些子控件时,如何为单个子控件公开事件?演示项目中使用的 ButtonContainer 控件就是这个问题的例证;它为 Enum 类型定义的每个值创建一个按钮,该类型由“EnumType”属性指定。一个通用的“ButtonClick”事件在按钮被点击时发出信号。通过将按钮指定为发送者,消费者知道按钮实例。

public class ButtonContainer : UserControl
{
    public event EventHandler ButtonClick;
    
    private Type enumType = typeof(AnchorStyles);
    
    public Type EnumType
    {
        get { return enumType; }
        set
        {
            enumType = value;
            createButtons();
        }
    }
    
    private void createButtons()
    {
        foreach (string name in Enum.GetNames(enumType))
        {
            Button btn = new Button();
            btn.Name = name;
            btn.Text = name;
            ...
            btn.Click += new EventHandler(btn_Click);
            
            Controls.Add(btn);
        }
    }
    
    void btn_Click(object sender, EventArgs e)
    {
        Button btn = (Button) sender;
        
        if (ButtonClick != null)
        {
            ButtonClick(btn, EventArgs.Empty);
        }
    }
}

现在,为了公开与单个按钮关联的单独“Click”事件,我们不能声明一个事件数组:它无法编译。

public event EventHandler[] SingleClick;

已知的替代方法是提供“AddEventHandler”和“RemoveEventHandler”方法,这些方法允许在运行时为单个事件(此处由一个 enum 值标识)附加/分离侦听器。

[NonSerialized]
private Dictionary<object,> eventDictionary;

public void AddEventHandler(object value, EventHandler listener)
{
    // all sanity omitted
    eventDictionary = eventDictionary ?? new Dictionary<object,>();

    if (eventDictionary.ContainsKey(value))
    {
        EventHandler newDel = (EventHandler)Delegate.Combine
		(eventDictionary[value], listener);
        eventDictionary[value] = newDel;
    }
    else
    {
        eventDictionary.Add(value, listener);
    }
}

public void RemoveEventHandler(object value, EventHandler listener)
{
    if (eventDictionary.ContainsKey(value))
    {
        EventHandler newDel = (EventHandler)Delegate.Remove
		(eventDictionary[value], listener);
        if (newDel != null)
        {
            eventDictionary[value] = newDel;
        }
        else
        {
            eventDictionary.Remove(value);
        }
    }
}

private void OnButtonClick(Button sender, object value)
{
    if (eventDictionary == null) return;

    if (eventDictionary.ContainsKey(value))
    {
        EventHandler listener = eventDictionary[value];
        listener.Invoke(sender, EventArgs.Empty);
    }
}

当您在网上搜索时,这是目前关于动态事件的主题。本文通过提供设计时支持来扩展此解决方法,该支持允许像往常一样从属性网格管理动态事件。

背景:设计时的事件

事件由 EventDescriptor 实例描述,该实例由 TypeDescriptor.CreateEvent 方法创建,并且可以通过 TypeDescriptor.GetEvents() 获取类型。IEventBindingService 哈希描述符并创建一个关联的 PropertyDescriptor(一个 private EventBindingService+EventPropertyDescriptor 实例),该实例将其附加的侦听器方法名存储为 string 值。
当组件序列化为设计器代码时,带有附加侦听器的事件在 CodeDom 中表示为 CodeAttachEventStatement。在反序列化中,CodeDomSerializerBaseIEventBindingService 获取关联的 EventPropertyDescriptor 并通过方法名持久化侦听器。在运行时保存附加侦听器的组件的 EventHandlerList 未使用。
VS 属性网格查询 TypeDescriptor 以获取组件的所有 EventDescriptor,并将其关联的 EventPropertyDescriptor 封装在其自己的内部网格条目中,这些条目显示在事件选项卡上。与属性和特性一样,事件可以通过 IDesignerFilterITypeDescriptorFilterService 实现对类型或特定组件进行过滤。

我们不能做的事情

  • 为不存在的事件创建 EventDescriptor
    TypeDescriptor.CreateEvent() 实际上返回一个描述符,但只要反射发现没有定义的事件,它就会抛出异常。
  • 使用自定义 EventPropertyDescriptor
    IEventBindingService 仅识别其自己的 private EventPropertyDescriptor
  • 使用自定义 EventBindingService
    该服务不是特定于组件 Site 的,它负责 DesignSurface 上的所有组件。从我们的组件类型交换表面服务很棘手,因为我们必须确保它随第一个加载的组件一起添加,并随最后一个释放的组件一起删除。此外,当项目重新编译时,控件会频繁重新加载。
    虽然我们可以解决上述同步问题,但我们仍然需要获取初始 EventPropertyDescriptor 值,因为默认的 IEventBindingService 总是由设计器加载器首先加载,远早于我们的组件加载。
    只有当我们“借用”Microsoft 的内部 VSCodeDomDesignerLoader+VSCodeDomEventBindingService 代码时,服务的实现才是微不足道的。然后,我们的组件将需要引用一些“Microsoft.VisualStudio.*”程序集,这是不可取的。

解决方案

该解决方案由一个 ControlDesigner、一个 CodeDomSerializer、一个 EventDescriptor、一个 TypeDelegator 和一个 Attribute 类组成。

  • EnumMemberAttribute

    此属性特定于您的控件,并且必须通过其属性标识每个生成的事件。由于演示控件使用 Enum 名称和值来标识所包含的按钮,因此 EnumMemberAttribute 公开适当的属性。

  • [AttributeUsage(AttributeTargets.Class , AllowMultiple= false, Inherited= true)]
    internal sealed class EnumMemberAttribute : Attribute
    {
        public EnumMemberAttribute(Type type, object value, string name);
    
        public object EnumValue { get;}
        public string EnumName { get;}
        public Type EnumType { get;}
    }
  • TypeAlias

    TypeDelegator 提供了一种非常方便的方式来从 System.Type 类继承,因为它将所有方法委托给构造函数中传入的类型。我们的 TypeAlias 包装事件类型(演示:System.EventHandler)。通过覆盖 Equals()GetHashCode(),我们可以为每个 EnumMemberAttribute 创建单独的事件类型,这些事件类型的行为与事件类型相同,但在比较中是独立的。
    TypeAlias 是该解决方案的小英雄,在两个场合拯救了我们,我们稍后将看到。我必须感谢 Schalk van Wyk 给予的启发

    internal sealed class TypeAlias : TypeDelegator
    {
        private readonly EnumMemberAttribute enumMember;
    
        public TypeAlias(Type type, EnumMemberAttribute enumMember) : base(type)
        {
            this.enumMember = enumMember;
        }
    
        public EnumMemberAttribute EnumMember
        {
            get { return enumMember; }
        }
    
        public override bool Equals(object obj)
        {
            TypeAlias that = obj as TypeAlias;
            if (that != null)
            {
                return (that.typeImpl == typeImpl) && (that.enumMember == enumMember);
            }
            return base.Equals(obj);
        }
    
        public override int GetHashCode()
        {
            return base.GetHashCode() ^ enumMember.GetHashCode();
        }
    }
  • CustomEventDescriptor

    从进一步阅读中可以看出,我们使用继承的 EventDescriptor 来表示每个动态事件。CustomEventDescriptor 返回 TypeAlias 委托器而不是事件类型。

    internal sealed class CustomEventDescriptor : EventDescriptor
    {
        private readonly TypeAlias eventTypeAlias;
        private readonly Type componentType;
    
        public CustomEventDescriptor(EventDescriptor descr, 
    	EnumMemberAttribute enumMember)
            : base(descr, null)
        {
            componentType = descr.ComponentType;
            eventTypeAlias = new TypeAlias(descr.EventType, enumMember);
        }
    
        /// Gets the type of component this event is bound to.
        public override Type ComponentType
        {
            get { return componentType; }
        }
    
        /// Gets the type of delegate for the event. 
        /// Returned type is a TypeAlias delegator !
        public override Type EventType
        {
            get { return eventTypeAlias; }
        }
    
        irrelevant implemented members (EventDescriptor is an abstract class) ...
    }
  • ButtonContainerDesigner

    在设计器上,我们声明一个仅在设计时可用的虚拟事件。

    public event EventHandler ButtonClick_;

    设计器创建必要的属性,以识别控件的每个动态事件。

    private IEnumerable<EnumMemberAttribute> createAttributes()
    {
        ButtonContainer control = (ButtonContainer)Control;
        List<EnumMemberAttribute> attributes = new List<EnumMemberAttribute>();
    
        foreach (string name in Enum.GetNames(control.EnumType))
        {
            EnumMemberAttribute attr = new EnumMemberAttribute
    	(control.EnumType, Enum.Parse(control.EnumType, name), name);
            attributes.Add(attr);
        }
    
        return attributes;
    }

    对于所有动态事件,我们创建 EventDescriptor 实例,都指向虚拟设计事件并共享其名称,但它们在属性网格中以其各自的名称作为单独的事件显示。事实证明,属性网格对所有生成的描述符使用一个相同的 EventPropertyDescriptor,尽管我可以验证 IEventBindingService 已正确哈希并为每个 EventDescriptor 实例提供了不同的 EventPropertyDescriptor。添加 CustomEventDescriptor 实例而不是,规避了这种不必要的合并行为。在我们 TypeAlias 的帮助下,属性网格认为生成的事件具有不同的事件类型,并且每个条目都使用正确的关联 EventPropertyDescriptor

    protected override void PreFilterEvents(IDictionary events)
    {
        Type componentType = GetType();
    
        foreach (EnumMemberAttribute enumMemberAttribute in createAttributes())
        {
            string enumName = enumMemberAttribute.EnumName;
    
            EventDescriptor ed = TypeDescriptor.CreateEvent
            (componentType, "ButtonClick_", typeof(EventHandler),
                new DesignOnlyAttribute(true),
                new DisplayNameAttribute("ButtonClick_" + enumName),
                new MergablePropertyAttribute(false),
                enumMemberAttribute);
    
            CustomEventDescriptor ced = 
    		new CustomEventDescriptor(ed, enumMemberAttribute);
    
            events.Add("ButtonClick_" + enumName, ced);
        }
    }

    当属性网格中的空条目被双击时,其底层 EventPropertyDescriptor 在表单文件中创建一个新的侦听器方法,在设计器文件中生成一个 CodeAttachEventStatement 并跳转到侦听器方法。该操作封装在 DesignerTransaction 中,并通过 IComponentChangeService 发出信号。
    我们通过监听 IComponentChangeService.ComponentChanging 并抛出 CheckoutException 来默默地阻止它来拦截它。附加到 Application.Idle 事件,允许我们在 DesignerTransaction 被取消后处理拦截的描述符。两个单独的 bool 变量用于重新进入代码,一次是我们取消原始值,一次是在 Application.Idle 上设置我们的值。

    private CustomEventDescriptor interceptedEventDescriptor;
    private bool reEntrantCodeCancel;
    private bool reEntrantCodeSet;
    
    void IComponentChangeService_ComponentChanging
    	(object sender, ComponentChangingEventArgs e)
    {
        if (e.Component != Component || e.Member == null) return;
    
        if (reEntrantCodeSet)
        {
            // e.Member is either interceptedEventDescriptor or 
            // its associated EventPropertyDescriptor
            return;
        }
    
        if (reEntrantCodeCancel)
        {
            // e.Member is never interceptedEventDescriptor or 
            // its associated EventPropertyDescriptor
            // Cancel triggered Undo operation for a previous intercepted 
            // EventPropertyDescriptor or other descriptor
            throw CheckoutException.Canceled;
        }
    
        CustomEventDescriptor ced = e.Member as CustomEventDescriptor;
        if (ced == null)
        {
            return;
        }
    
        //-- EventPropertyDescriptor is setting value
        interceptedEventDescriptor = ced;
    
        reEntrantCodeCancel = true;
        Application.Idle += new EventHandler(Application_Idle);
        throw CheckoutException.Canceled;
    }

    现在我们可以使用单独显示的事件名称而不是共享的虚拟名称来设置值。我们不知道最初输入的值(可能由用户输入),我们总是使用我们的算法来生成侦听器名称。如果一个具有适当名称的侦听器方法已经存在,IEventBindingService 将会附加它,否则该服务将为我们创建它。如果 EventPropertyDescriptor 已经附加了一个侦听器,我们将其重置为 nullIEventBindingService 将从文件中删除侦听器方法。

    void Application_Idle(object sender, EventArgs e)
    {
        Application.Idle -= Application_Idle;
    
        reEntrantCodeCancel = false;
        reEntrantCodeSet = true;
    
        IEventBindingService svc = 
        (IEventBindingService)GetService(typeof(IEventBindingService));
        PropertyDescriptor eventProperty = 
    	svc.GetEventProperty(interceptedEventDescriptor);
        EnumMemberAttribute enumMemberAttribute = 
        (EnumMemberAttribute)interceptedEventDescriptor.Attributes
    	[typeof(EnumMemberAttribute)];
    
        object curValue = eventProperty.GetValue(Component);
        if (curValue == null)
        {
            // -- create new listener or attach existing having identical name
            string methodName = svc.CreateUniqueMethodName
    		(Component, interceptedEventDescriptor);
            methodName += enumMemberAttribute.EnumName;
    
            eventProperty.SetValue(Component, methodName);
        }
        else
        {
            // -- always reset existing listener
            eventProperty.SetValue(Component, null);
        }
    
        interceptedEventDescriptor = null;
        reEntrantCodeSet = false;
    }

    此时,已创建正确的侦听器方法,但生成的 CodeAttachEventStatement 将无法编译,因为虚拟事件仅在设计时存在。

    // listener for the button, identified by System.Windows.Forms.AnchorStyles.Left:
    
    // in Form1.cs
    private void buttonContainer1_ButtonClick_Left(object sender, EventArgs e)
    {
    }
    
    // in Form1.Designer.cs
    this.buttonContainer1.ButtonClick_ += 
    	new System.EventHandler(this.buttonContainer1_ButtonClick_Left);
  • DesignEventSerializer

    序列化器负责将错误的 CodeAttachEventStatement 替换为有效的 CodeMethodInvokeExpression,该表达式针对控件上存在的“AddEventHandler”方法。上面的语句被转换为

    this.buttonContainer1.AddEventHandler(System.Windows.Forms.AnchorStyles.Left,
    	new System.EventHandler(this.buttonContainer1_ButtonClick_Left));

    我们让默认的 ControlCodeDomSerializer 将控件序列化到 CodeStatementCollection 中,并根据需要交换语句。

    public override object Serialize
    	(IDesignerSerializationManager manager, object value)
    {
        CodeDomSerializer defaultSerializer =
        (CodeDomSerializer)manager.GetSerializer
        (typeof(System.Windows.Forms.Control), typeof(CodeDomSerializer));
        CodeStatementCollection statements =
        (CodeStatementCollection)defaultSerializer.Serialize(manager, value);
    
        foreach (CodeAttachEventStatement cas in
        	findAttachEventStatements(statements, "ButtonClick_"))
        {
            CodeMethodInvokeExpression cmie =
            	createMethodInvokeExpression(manager, "AddEventHandler", cas);
            statements.Remove(cas);
            statements.Add(cmie);
        }
    
        return statements;
    }

    我们可以从原始 CodeAttachEventStatement 获取构建 CodeMethodInvokeExpression 所需的数据。表示侦听器方法的 CodeDelegateCreateExpression 和表示我们控件的 CodeFieldReferenceExpression 可直接使用和重用。糟糕!!!我们需要什么枚举值才能正确识别事件?CodeDom 语句没有用属性装饰,无法获取我们到目前为止使用的 EnumMemberAttribute。我们可以从侦听器名称推断出枚举名称,这不是一种安全做法。设计器和序列化器在设计和意图上是真正分离的实体,我们是否应该在这里提供一些同步?糟糕!!!

    private static CodeMethodInvokeExpression createMethodInvokeExpression
    	(IDesignerSerializationManager manager, string methodName, 
    	CodeAttachEventStatement cas)
    {
        CodeDelegateCreateExpression listener = 
        (CodeDelegateCreateExpression) cas.Listener;
        CodeFieldReferenceExpression targetObject = cas.Event.TargetObject;
    
        CodeExpression enumValue = ???;
    
        CodeMethodInvokeExpression cmie = 
        	new CodeMethodInvokeExpression
        	(targetObject, methodName, enumValue == ??? , listener);
        return cmie;
    }

    嗯,每个 CodeDom CodeObject 都公开了一个“UserDataIDictionary 属性,内部设计器序列化基础设施在此存储辅助名称/值对,以帮助相关的序列化器进行各自的操作。在 CodeAttachEventStatement 的情况下,只在此存储事件类型。由于我们的 CustomEventDescriptor 返回了 TypeAlias 作为事件类型,因此它存储在字典中并为我们提供了缺失的属性。所以小小的 TypeDelegator 两次帮助了我们,并在庞大的序列化基础设施中走私了所需的数据。

    private static CodeMethodInvokeExpression createMethodInvokeExpression
    	(IDesignerSerializationManager manager, string methodName, 
    	CodeAttachEventStatement cas)
    {
        CodeDelegateCreateExpression listener = 
    	(CodeDelegateCreateExpression) cas.Listener;
        CodeFieldReferenceExpression targetObject = cas.Event.TargetObject;
    
        Debug.Assert(!(cas.UserData[typeof(Delegate)] is Delegate));
        TypeAlias typeAlias = (TypeAlias)cas.UserData[typeof(Delegate)];
    
        // {System.ComponentModel.Design.Serialization.EnumCodeDomSerializer}
        CodeDomSerializer serializer = (CodeDomSerializer)manager.GetSerializer
        (typeAlias.EnumMember.EnumType, typeof(CodeDomSerializer));
        CodeFieldReferenceExpression enumValue =
        (CodeFieldReferenceExpression)serializer.Serialize
        	(manager, typeAlias.EnumMember.EnumValue);
    
        return new CodeMethodInvokeExpression
    	(targetObject, methodName, enumValue, listener);
    }

    序列化器的第二个职责是在反序列化控件时获取 EventPropertyDescriptor 的初始值。在我们让默认的 ControlCodeDomSerializerCodeStatementCollection 反序列化控件之前,我们剥离所有“AddEventHandler”方法调用。默认序列化将任何事件侦听器 CodeDom 表达式反序列化为 null,它会用 null 值填充控件的 eventDictionary 字段。
    相反,我们从每个 CodeMethodInvokeExpression 推断出侦听器名称和标识正确事件的 enum 值,找到相应的 EventPropertyDescriptor 并将其侦听器名称设置为其值。

    private void setEventProperties
    (IDesignerSerializationManager manager, 
    object instance, IEnumerable<CodeMethodInvokeExpression> list)
    {
        // Designer has added CustomEventDescriptors
        EventDescriptorCollection eventDescriptors =
        TypeDescriptor.GetEvents(instance, new Attribute[] 
        { new DesignOnlyAttribute(true) });
    
        IEventBindingService service =
        (IEventBindingService)manager.GetService(typeof(IEventBindingService));
    
        foreach (CodeMethodInvokeExpression cmie in list)
        {
            // void AddEventHandler(object enumValue /*0*/, Delegate listener /*1*/)
            object enumValue = DeserializeExpression(manager, null, cmie.Parameters[0]);
            string listenerName = getListenerName(manager, cmie.Parameters[1]);
    
            CustomEventDescriptor ced =
            findEventDescriptor(eventDescriptors, "ButtonClick_", enumValue);
            PropertyDescriptor eventProperty = service.GetEventProperty(ced);
            eventProperty.SetValue(instance, listenerName);
        }
    }

    带有许多 ForEach 循环的序列化器代码无疑会受益于 Linq,可惜我不得不针对 NET 2.0。我为CodeDom 可视化工具而自豪,它有助于序列化器的开发。

局限性

当我们在 WinForms 设计器中附加一个普通事件时,属性网格会向我们显示一个现有的兼容方法列表,我们可以从中选择或通过双击创建一个新方法。对于我们的动态事件,我们总是为每个事件生成一个相同的侦听器名称,无论用户输入了什么。因此,兼容方法的概念被替换为相同方法的概念。如果一个具有我们预期名称的方法已经存在,我们就附加它,否则我们就创建它。不允许不同的方法名称,句号。但是用户可以自由地从代码编辑器中重命名侦听器。

这种行为正是我想要的,它避免了因相似的侦听器名称而产生的虚假信息,并且事件不应该跨不同控件合并。因此,我使用了一个自定义的 TypeConverter,它只返回一个现有侦听器而不是兼容方法作为标准值。该转换器通过反射设置相应 EventPropertyDescriptor 上的 private 字段进行交换。

如果您想要兼容方法,则必须找到一种直接从属性网格读取输入值的方法。
使用相同方法概念时不太可能出现名称冲突,我完全省略了名称验证。您可能希望研究 Microsoft 在

Microsoft.VisualStudio.Shell.Design.Serialization.CodeDom.<br />CodeDomEventBindingService
中的实现。所需的 CodeTypeDeclaration 可作为设计器中的服务使用

CodeTypeDeclaration declaration = 
	(CodeTypeDeclaration) GetService(typeof (CodeTypeDeclaration));

关于 Visual Studio 程序集地狱的一课

以下方法是 DesignEventSerializer 的一个摘录,它为给定的 enum 值查找相应的描述符。只要描述符包含在传递的集合中,我们总是应该找到它,对吗?

private static CustomEventDescriptor findEventDescriptor
	(EventDescriptorCollection eventDescriptors, string eventName, object enumValue)
{
    foreach (EventDescriptor ed in eventDescriptors)
    {
        if (ed.Name != eventName)
        {
            continue;
        }

        EnumMemberAttribute attr = 
        (EnumMemberAttribute)ed.Attributes[typeof(EnumMemberAttribute)];
        if (attr == null)
        {
            continue;
        }

        if (enumValue.Equals(attr.EnumValue))
        {
            return ed as CustomEventDescriptor;
        }
    }

    Debug.Fail("We should always find the EventDescriptor!");
    return null;
}

如果 enum 类型属于 .NET Framework 和/或已 GAC 化,则为真。如果 enum 类型定义在项目本身中并且项目被重新编译,则完全错误。原因是 VS 在重新编译时可能会创建并加载项目程序集的新影子副本。当我们比较 enum 类型时,我们很可能最终会比较从不同 Assembly.Location 加载的类型。对我们来说,它们可能看起来是具有相同程序集限定名称的相同类型,但 NET Framework 是固执的:不相等,永远不相等!这就是为什么我在这里额外通过哈希码进行比较,并且在采用我的解决方案时,您通常应该注意程序集地狱。

if (enumValue.Equals(attr.EnumValue))
{
    return ed as CustomEventDescriptor;
}

if (enumValue.GetHashCode() == attr.EnumValue.GetHashCode())
{
    if (enumValue.GetType().AssemblyQualifiedName ==
    	attr.EnumValue.GetType().AssemblyQualifiedName)
    {
        // Equality failed due to VS Assembly hell !!!
        Debug.Assert(enumValue.GetType().Assembly.Location
        	!= attr.EnumValue.GetType().Assembly.Location);
        return ed as CustomEventDescriptor;
    }
}

Using the Code

要采用我的解决方案,请将属性属性更改为您的控件用于标识动态事件的任何内容。查找我的原始属性的使用情况并根据需要进行更改。
重命名设计器中的虚拟事件,我建议在控件上定义的通用事件名称后附加一个下划线。我使用了一个共享虚拟事件和“AddEventHandler”方法名称的 static 类,简化了更改。
TypeAliasCustomEventDescriptor 和 90% 的 DesignEventSerializer 都是可重用的。
我提出的解决方案只考虑了每个控件一个动态“事件系列”,如果您需要更多,改进我的代码应该不难。

要在设计时调试演示解决方案,请使用控件库“DynamicEvents”作为启动项目。在其项目属性页上,指向您的 devenv.exe 安装并指定“Test”DynamicEvents”项目的路径作为命令行参数。

历史

  • 2010 年 1 月 19 日:首次发布
© . All rights reserved.