代码之家  ›  专栏  ›  技术社区  ›  VA systems engineer

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

  •  2
  • VA systems engineer  · 技术社区  · 8 年前

    短版

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

    到目前为止我所拥有的

    我有混凝土 组合框 从抽象派生的类 组合框 类派生自Microsoft的 组合框 班级。

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

    我的主要 Form 类订阅某些基 组合框 事件。

    注:设计单位有: 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 
        }
    }
    

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

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

    问题

    我的问题是我不知道如何遍历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 回复  |  直到 8 年前
        1
  •  1
  •   Bradley Uffner    8 年前

    在给出我创建的解决方案之前,让我说这感觉非常糟糕。我敦促你认真考虑另一个解决办法。可能会有各种疯狂的边缘情况,这些代码会崩溃,除了下面的示例代码之外,我还没有对它进行彻底的测试。

    添加以下实用程序类:

    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

        2
  •  1
  •   TnTinMn    8 年前

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

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

    虽然这可能是对模式的滥用,但类实现了 IDisposable Interface 恢复 事件 处理类实例的字段。这样做的原因是为了方便 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();
        }
    
    }
    

    肥皂盒谩骂

    许多人会说,使用反射来访问非公共类成员是肮脏的,或者是其他贬义词,它引入了 脆性 对代码进行更改,因为有人可能更改基础代码定义,从而使依赖成员名称(魔术字符串)的代码不再有效。这是一个有效的问题,但我认为它与访问外部数据库的代码没有什么不同。

    反射可以被认为是针对特定字段(成员:字段、属性、事件)从组件(数据库)查询类型(DATABATE)。它并不比sql语句更脆弱,比如 Select SomeField From SomeTable Where AnotherField=5 . 这种类型的sql代码在世界上是不允许的,没有人会三思而后行,但是一些外力可以很容易地重新定义您所编写的数据库,从而使所有的magic string sql语句都无效。

    使用硬编码名称总是有被更改为无效的风险。你必须权衡前进的风险与害怕继续前进的选择,因为有人想听起来权威(通常是 parroting 并批评你实施了解决当前问题的解决方案。

        3
  •  0
  •   VA systems engineer    8 年前

    我希望编写代码,可以编程定位使用的所有事件处理程序方法名称。 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;
    
    推荐文章