如何在派生摘要 class 中添加和减去事件处理程序?

How do I add and subtract event handlers inside a derived abstract class?

短版

在我的摘要 class MyCbo_Abstract(源自 ComboBox class)中,我想创建一个自定义 属性,当设置时将减去所有控件的事件处理程序,设置基础 属性 值,然后重新添加所有控件的事件处理程序。

我目前有什么

我有一个具体的 ComboBox class 从抽象 ComboBox class 派生自微软的 ComboBox class。

public abstract class MyCbo_Abstract : ComboBox
{
    public MyCbo_Abstract() : base()
    {
    }
}

public partial class MyCboFooList : MyCbo_Abstract
{
    public MyCboFooList() : base()
    {
    }
}

我的主要 Form class 订阅了某些基础 ComboBox 事件。

注:设计者有:this.myCboFooList = new MyCboFooList();

public partial class FormMain : Form
{
    public FormMain()
    {
        myCboFooList.SelectedIndexChanged += myCboFooList_SelectedIndexChanged;
    }

    private void myCboFooList_SelectedIndexChanged(object sender, EventArgs e)
    {
        // do stuff 
    }
}

有时我想禁止调用已定义的事件处理程序,例如,当我以编程方式设置 ComboBox 对象的 SelectedIndex 属性 时。

每次我想修改 SelectedIndex 属性 并抑制其事件时,不必记住编写代码来减去和重新添加事件处理程序,我想创建一个自定义 属性 SelectedIndex_NoEvents 设置时将减去所有控件的事件处理程序,设置基本 属性 值 SelectedIndex,然后重新添加所有控件的事件处理程序。

问题

我的问题是我不知道如何遍历 EventHandlerList,因为它没有 GetEnumerator。而且,在调试器中查看列表时,saveEventHandlerList 是一个奇怪的链接的东西,我不知道如何遍历。

public abstract class MyCbo_Abstract : ComboBox
{
    int selectedIndex_NoEvents;

    public int SelectedIndex_NoEvents
    {
        get
        {
            return base.SelectedIndex;
        }

        set
        {

            EventHandlerList saveEventHandlerList = new EventHandlerList();
            saveEventHandlerList = Events;

            //foreach won't work - no GetEnumerator available. Can't use for loop - no Count poprerty
            foreach (EventHandler eventHandler in saveEventHandlerList)
            {
                SelectedIndexChanged -= eventHandler;
            }

            base.SelectedIndex = value;

            //foreach won't work - no GetEnumerator available. Can't use for loop - no Count poprerty
            foreach (EventHandler eventHandler in saveEventHandlerList)
            {
                SelectedIndexChanged += eventHandler;
            }

            saveEventHandlerList = null;

        }
    }

    //Probably don't need this
    public override int SelectedIndex
    {
        get
        {
            return base.SelectedIndex;
        }

        set
        {
            base.SelectedIndex = value;
        }
    }

    public DRT_ComboBox_Abstract() : base()
    {
    }
}

在向您提供我创建的解决方案之前,我想说这感觉非常糟糕。我敦促您认真考虑另一种解决方案。这段代码可能会出现各种疯狂的边缘情况,除了下面显示的示例代码之外,我还没有对它进行彻底的测试。

添加以下实用程序class:

public class SuspendedEvents
{
    private Dictionary<FieldInfo, Delegate> handlers = new Dictionary<System.Reflection.FieldInfo, System.Delegate>();
    private object source;

    public SuspendedEvents(object obj)
    {
        source = obj;
        var fields = obj.GetType().GetFields(BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        foreach (var fieldInfo in fields.Where(fi => fi.FieldType.IsSubclassOf(typeof(Delegate))))
        {
            var d = (Delegate)fieldInfo.GetValue(obj);
            handlers.Add(fieldInfo, (Delegate)d.Clone());
            fieldInfo.SetValue(obj, null);
        }
    }

    public void Restore()
    {
        foreach (var storedHandler in handlers)
        {
            storedHandler.Key.SetValue(source, storedHandler.Value);
        }
    }
}

你可以这样使用它:

var events = new SuspendedEvents(obj); //all event handlers on obj are now detached
events.Restore(); // event handlers on obj are now restored.

我使用了以下测试设置:

void Main()
{
    var obj = new TestObject();

    obj.Event1 += (sender, e) => Handler("Event 1");
    obj.Event1 += (sender, e) => Handler("Event 1");

    obj.Event2 += (sender, e) => Handler("Event 2");
    obj.Event2 += (sender, e) => Handler("Event 2");

    obj.Event3 += (sender, e) => Handler("Event 3");
    obj.Event3 += (sender, e) => Handler("Event 3");

    Debug.WriteLine("Prove events are attached");
    obj.RaiseEvents();

    var events = new SuspendedEvents(obj);    
    Debug.WriteLine("Prove events are detached");
    obj.RaiseEvents();

    events.Restore();
    Debug.WriteLine("Prove events are reattached");
    obj.RaiseEvents();
}

public void Handler(string message)
{
    Debug.WriteLine(message);
}

public class TestObject
{
    public event EventHandler<EventArgs> Event1;
    public event EventHandler<EventArgs> Event2;
    public event EventHandler<EventArgs> Event3;

    public void RaiseEvents()
    {
        Event1?.Invoke(this, EventArgs.Empty);
        Event2?.Invoke(this, EventArgs.Empty);
        Event3?.Invoke(this, EventArgs.Empty);
    }
}

它产生以下输出:

Prove events are attached
Event 1
Event 1
Event 2
Event 2
Event 3
Event 3
Prove events are detached
Prove events are reattached
Event 1
Event 1
Event 2
Event 2
Event 3
Event 3

无法轻松禁用 .Net 框架中公开的 WinForm 控件的事件触发。但是,Winform 控件遵循事件的标准设计模式,因为所有事件签名都基于 EventHandler Delegate and the registered event handlers are stored in an EventHandlerList that is defined in the Control Class。此列表存储在名为 "events" 的字段(变量)中,并且仅 public 通过只读 属性 Events.

公开

下面介绍的 class 使用反射临时将 null 分配给 events 字段,有效地删除了为控件注册的所有事件处理程序。

虽然这可能是对模式的滥用,但 class 实施 IDisposable Interface 以在处置 class 实例时恢复 events 字段。这样做的原因是为了方便使用 using 块来包装 class 用法。

public class ControlEventSuspender : IDisposable
{
    private const string eventsFieldName = "events";
    private const string headFieldName = "head";

    private static System.Reflection.FieldInfo eventsFieldInfo;
    private static System.Reflection.FieldInfo headFieldInfo;

    private System.Windows.Forms.Control target;
    private object eventHandlerList;
    private bool disposedValue;

    static ControlEventSuspender()
    {
        Type compType = typeof(System.ComponentModel.Component);
        eventsFieldInfo = compType.GetField(eventsFieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        headFieldInfo = typeof(System.ComponentModel.EventHandlerList).GetField(headFieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
    }

    private static bool FieldInfosAquired()
    {
        if (eventsFieldInfo == null)
        {
            throw new Exception($"{typeof(ControlEventSuspender).Name} could not find the field '{ControlEventSuspender.eventsFieldName}' on type Component.");
        }

        if (headFieldInfo == null)
        {
            throw new Exception($"{typeof(ControlEventSuspender).Name} could not find the field '{ControlEventSuspender.headFieldName}' on type System.ComponentModel.EventHandlerList.");
        }

        return true;
    }

    private ControlEventSuspender(System.Windows.Forms.Control target) // Force using the the Suspend method to create an instance
    {
        this.target = target;
        this.eventHandlerList = eventsFieldInfo.GetValue(target); // backup event hander list
        eventsFieldInfo.SetValue(target, null); // clear event handler list
    }

    public static ControlEventSuspender Suspend(System.Windows.Forms.Control target)
    {
        ControlEventSuspender ret = null;
        if (FieldInfosAquired() && target != null)
        {
            ret = new ControlEventSuspender(target);
        }
        return ret;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposedValue)
        {
            if (disposing)
            {
                if (this.target != null)
                {
                    RestoreEventList();
                }
            }
        }
        this.disposedValue = true;
    }

    public void Dispose()
    {
        Dispose(true);
    }

    private void RestoreEventList()
    {
        object o = eventsFieldInfo.GetValue(target);

        if (o != null && headFieldInfo.GetValue(o) != null)
        {
            throw new Exception($"Events on {target.GetType().Name} (local name: {target.Name}) added while event handling suspended.");
        }
        else
        {
            eventsFieldInfo.SetValue(target, eventHandlerList);
            eventHandlerList = null;
            target = null;
        }
    }
}

button1_Click 方法中的用法示例:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        using (ControlEventSuspender.Suspend(comboBox1))
        {
            comboBox1.SelectedIndex = 3; // SelectedIndexChanged does not fire
        }
    }

    private void button2_Click(object sender, EventArgs e)
    {
        comboBox1.SelectedIndex = -1; // clear selection, SelectedIndexChanged fires
    }

    private void button3_Click(object sender, EventArgs e)
    {
        comboBox1.SelectedIndex = 3; // SelectedIndexChanged fires
    }

    private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
    {
        Console.WriteLine("index changed fired");
        System.Media.SystemSounds.Beep.Play();
    }

}

肥皂盒辱骂

许多人会说使用反射访问非public class 成员是肮脏的或其他一些贬义词,它引入了脆弱性 到代码,因为有人可能会更改底层代码定义,使得依赖于成员名称(魔术字符串)的代码不再有效。这是一个合理的担忧,但我认为它与访问外部数据库的代码没有什么不同。

反射可以被认为是从程序集(数据库)中针对特定字段(成员:字段、属性、事件)进行某种类型(数据表)的查询。它并不比 SQL 语句(例如 Select SomeField From SomeTable Where AnotherField=5)更脆弱。这种类型的 SQL 代码在世界上是被禁止的,没有人会三思而后行地编写它,但是一些外力可以很容易地重新定义您编写的数据库依赖于呈现所有魔术字符串 SQL 语句无效作为出色地。

硬编码名称的使用始终存在因更改而失效的风险。您必须权衡前进的风险与因害怕继续而被冻结的选择,因为有人想要听起来权威(通常 parroting 其他此类人)并批评您实施解决当前问题的解决方案.

我希望编写代码以编程方式定位使用 controlObject.Event += EventHandlerMethodName 创建的所有事件处理程序方法名称,但正如您在其他答案中看到的那样,执行此操作的代码很复杂、有限,而且可能无法实现在所有情况下工作

这是我想到的。它满足了我将减去和重新添加事件处理程序方法名称的代码整合到我的抽象 class 中的愿望,但代价是必须编写代码来存储和管理事件处理程序方法名称并且必须为每个控件属性我想抑制事件处理程序,修改属性值,最后重新添加事件处理程序。

public abstract class MyCbo_Abstract : ComboBox
{

    // create an event handler property for each event the app has custom code for

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    private EventHandler evSelectedValueChanged;

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public EventHandler EvSelectedValueChanged { get => evSelectedValueChanged; set => evSelectedValueChanged = value; }

    public MyCbo_Abstract() : base()
    {
    }

    // Create a property that parallels the one that would normally be set in the main body of the program
    public object _DataSource_NoEvents
    {
        get
        {
            return base.DataSource;
        }

        set
        {
            SelectedValueChanged -= EvSelectedValueChanged;

            if (value == null)
            {
                base.DataSource = null;
                SelectedValueChanged += EvSelectedValueChanged;
                return;
            }

            string valueTypeName = value.GetType().Name;

            if (valueTypeName == "Int32")
            {
                base.DataSource = null;
                SelectedValueChanged += EvSelectedValueChanged;
                return;
            }

            //assume StringCollection
            base.DataSource = value;
            SelectedValueChanged += EvSelectedValueChanged;
            return;
        }
    }
}

public partial class MyCboFooList : MyCbo_Abstract
{
    public MyCboFooList() : base()
    {
    }
}

设计师有

this.myCboFooList = new MyCboFooList();

主窗体代码

public partial class FormMain : Form
{
    public FormMain()
    {
        myCboFooList.SelectedValueChanged += OnMyCboFooList_SelectedValueChanged;
        myCboFooList.EvSelectedValueChanged = OnMyCboFooList_SelectedValueChanged;
    }

    private void OnMyCboFooList_SelectedValueChanged(object sender, EventArgs e)
    {
        // do stuff 
    }
}

现在,如果我想设置 属性 并抑制事件,我可以编写如下内容,而不必记住重新添加事件处理程序方法名称

myCboFooList._DataSource_NoEvents = null;