【问题标题】:How do I add and subtract event handlers inside a derived abstract class?如何在派生抽象类中添加和减去事件处理程序?
【发布时间】:2018-06-08 16:31:20
【问题描述】:

短版

在我的抽象类MyCbo_Abstract(派生自ComboBox 类)中,我想创建一个自定义属性,当设置该属性时将减去所有控件的事件处理程序,设置基本属性值,然后重新添加所有控件的事件处理程序。

我目前所拥有的

我有一个具体的ComboBox 类派生自一个抽象的ComboBox 类,该类派生自微软的ComboBox 类。

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

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

我的主要Form 类订阅某些基本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()
    {
    }
}

【问题讨论】:

  • 您应该能够使用此处描述的类似技术:stackoverflow.com/questions/91778/…
  • @BradleyUffner: How to remove all event handlers from an event 在球场上,但它只删除了一类事件(在他的例子中是 Click 事件)。最后,无论类别如何,我都需要删除然后重新添加所有控件的事件处理程序:Click、SelectedIndexChanged、TextChanged 等。最重要的是,我不想修改自定义属性的 Set每次为控件订阅新事件类别时的代码
  • 您可以使用反射来循环所有事件。
  • 除了Events 类与EventHandlerList 类有相同的问题,即没有GetEnumerator 之外,这将起作用。所以,Events 类的同样问题
  • 我很确定这一切都可以通过反射来解决。给我几分钟的时间来弄清楚细节,我会回来报告的。

标签: c# winforms loops eventhandler


【解决方案1】:

在给你我创建的解决方案之前,让我说这感觉非常hacky。我敦促您认真考虑另一种解决方案。这段代码可能会出现各种疯狂的边缘情况,除了下面显示的示例代码之外,我还没有彻底测试过。

添加以下实用程序类:

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);
    }
}

它产生以下输出:

附上证明事件
活动一
活动一
活动 2
活动 2
活动 3
活动 3
证明事件是分离的
证明事件已重新附加
活动一
活动一
活动 2
活动 2
活动 3
活动三

【讨论】:

  • 辛苦了,感谢您抽出宝贵的时间。你是正确的:“hacky”特征,但这不是你的错。在谷歌搜索和寻找其他“解决方案”时,很明显Events 类和EventHandlerList 类包含的数据不适合人类消费:-)。再次感谢,我会回到旧的方式
  • @NovaSysEng,如果您只关心禁用在存储在 Events 属性中的控件上实现的 标准 事件,则可以使用更简单的版本这个答案将其支持字段设置为 null 以禁用事件,然后在完成后恢复其值。 typeof(Component).GetField("events", BindingFlags.Instance | BindingFlags.NonPublic) 将获得必要的FieldInfo,可用于操作Events 支持字段。
  • @TnTinMn:感谢您的意见,但目前我对 Bradley 的SuspendedEvents 课程的理解充其量是不稳定的。如果您有时间,也许您可​​以使用专为standard events 设计的SuspendedEvents 类版本添加答案。我之所以问,是因为在查看我的应用程序时,我认为需要管理的只是标准事件(到目前为止,TextChangedSelectedValueChangedClick
【解决方案2】:

无法轻松禁用 .Net 框架中公开的 WinForm 控件的事件触发。但是,Winform 控件遵循事件的标准设计模式,因为所有事件签名都基于EventHandler Delegate,并且注册的事件处理程序存储在EventHandlerList 中,Control Class 中定义。此列表存储在名为“events”的字段(变量)中,并且仅通过只读属性Events 公开。

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

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

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();
    }

}

SoapBox 诽谤

许多人会说使用反射来访问非公共类成员是肮脏的或其他贬义词,并且它会给代码带来脆弱性,因为有人可能会更改底层代码定义,例如依赖于成员名称(魔术字符串)的代码不再有效。这是一个有效的问题,但我认为它与访问外部数据库的代码没有什么不同。

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

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

【讨论】:

    【解决方案3】:

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

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

    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;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多