如何在派生的抽象类中添加和减去事件处理程序?

时间:2018-06-08 16:31:20

标签: c# winforms loops eventhandler

简短版

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

到目前为止我有什么

我有一个具体的ComboBox类派生自派生自Microsoft 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()
    {
    }
}

3 个答案:

答案 0 :(得分:1)

在向您提供我创建的解决方案之前,请允许我说这感觉非常黑客。我恳请你认真思考另一种解决方案。可能存在这种代码崩溃的各种疯狂边缘情况,除了下面显示的示例代码之外,我还没有彻底测试它。

添加以下实用程序类:

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

它产生以下输出:

  

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

答案 1 :(得分:1)

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

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

虽然它可能是滥用模式,但是类实现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 Diatribe

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

可以考虑对特定字段(成员:字段,属性,事件)的程序集(数据库)中的类型(数据表)进行查询。它不比Select SomeField From SomeTable Where AnotherField=5之类的SQL语句脆弱。这种类型的SQL代码在世界范围内是可以防止的,并且没有人会考虑编写它,但是一些外部力量可以轻松地重新定义您编写的数据库依赖于渲染所有魔术字符串SQL语句也是无效的。

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

答案 2 :(得分:0)

我希望编写代码,以编程方式找到使用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;