2

Short version

In my abstract class MyCbo_Abstract (derived from ComboBox class), I want to create a custom property that when set will subtract all the control's event handlers, set the base property value, then re-add all the control's event handlers.

What I have so far

I have a concrete ComboBox class derived from an abstract ComboBox class derived from Microsoft's ComboBox class.

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

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

My main Form class subscribes to certain base ComboBox events.

Note: The designer has: 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 
    }
}

There are times when I want to suppress the invocation of defined event handlers, e.g., when I programmatically set a ComboBox object's SelectedIndex property.

Instead of having to remember to write the code to subtract and re-add event handlers each time I want to modify the SelectedIndex property and suppress its events, I want to create a custom property SelectedIndex_NoEvents that when set will subtract all the control's event handlers, set the base property value SelectedIndex, then re-add all the control's event handlers.

The problem

My problem is that I don't know how to iterate over a EventHandlerList because it has no GetEnumerator. And, in looking at the list in the debugger, saveEventHandlerList is a weird chained thing that I can't figure out how to otherwise traverse.

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()
    {
    }
}
VA systems engineer
  • 2,856
  • 2
  • 14
  • 38
  • You should be able to use similar techniques as described here: https://stackoverflow.com/questions/91778/how-to-remove-all-event-handlers-from-an-event – Bradley Uffner Jun 08 '18 at 16:44
  • @BradleyUffner: [How to remove all event handlers from an event](https://stackoverflow.com/questions/91778/how-to-remove-all-event-handlers-from-an-event) is in the ball park, but it removes only one category of event (the Click event in his case). In the end, I need to remove and then re-add all of a control's event handlers regardless of category: Click, SelectedIndexChanged, TextChanged, etc., etc. Most importantly, I don't want to have to modify my custom property's Set code each time I subscribe to a new event category for a control – VA systems engineer Jun 08 '18 at 16:53
  • You can use reflection to loop over all the events. – Bradley Uffner Jun 08 '18 at 16:54
  • That would work except that the `Events` class has the same problem as the `EventHandlerList` class, i.e., no GetEnumerator. So, same question for the `Events` class – VA systems engineer Jun 08 '18 at 16:59
  • I'm fairly sure this can all be solved by reflection. Give me a few minutes to work out the details, and I'll report back. – Bradley Uffner Jun 08 '18 at 17:10
  • This is a fundamentally wrong way to do this, the basic reason why you have trouble doing this. You just don't need it, set a *bool* field of your class instead. In the protected OnFoo() method that raises the Foo event you simply check the field and do nothing if it is set. – Hans Passant Jun 08 '18 at 17:25
  • @HansPassant: My goal is to bake this into the abstract class so I don't have to manage settings outside the class (I want to subtract and re-add ALL event handlers without having continuously write more code). Using bool fields will just make me have to manage a bunch of bool fields. I might as well go back to writing individual pairs of -= and += statements each time I want to change the setting of control property backed by an event handler when I want to suppress the control's events – VA systems engineer Jun 08 '18 at 17:42
  • I've got a working test, I'm just trying to make it pretty. – Bradley Uffner Jun 08 '18 at 18:11

3 Answers3

1

Before giving you the solution that I created, let me say that this feels extremely hacky. I urge you to seriously think about another solution. There may be all kinds of crazy edge cases where this code breaks down, I haven't thoroughly tested it beyond the example code shown below.

Add the following utility 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);
        }
    }
}

You can use it like this:

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

I used the following test setup:

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

It produces the following output:

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

Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
  • A good effort and thanks for taking the time. You're correct re: the "hacky" characterization, but its not your fault. In googling and finding other "solutions", its become clear that the data that the `Events` class and the `EventHandlerList` class contain are not meant for human consumption :-). Thanks again, I'll go back to the old ways – VA systems engineer Jun 08 '18 at 22:21
  • @NovaSysEng, If all you are concerned with is disabling the _standard_ events implemented on a control that are stored in the `Events` property, you can use a simpler version of this answer to set its backing field to null to disable events and then restore its value when done. `typeof(Component).GetField("events", BindingFlags.Instance | BindingFlags.NonPublic)` will get the necessary `FieldInfo` that can be used to manipulate the `Events` backing field. – TnTinMn Jun 09 '18 at 03:49
  • @TnTinMn: Thanks for the input, but my understanding of Bradley's `SuspendedEvents` class is, at this time, shaky at best. If you have time, perhaps you could add an answer with a version of the `SuspendedEvents` class designed just for `standard events`. I'm asking because in looking over my app, I believe it is just standard events (so far, `TextChanged`, `SelectedValueChanged`, and `Click`) that are what need to be managed – VA systems engineer Jun 09 '18 at 12:39
1

There is no way to easily disable event firing of WinForm controls exposed in the .Net framework. However, the Winform controls follow a standard design pattern for events in that all event signatures are based on the EventHandler Delegate and the registered event handlers are stored in an EventHandlerList that is defined in the Control Class. This list is stored in a field (variable) named "events" and is only publicly exposed via the read-only property Events.

The class presented below uses reflection to temporarily assign null to the events field effectively removing all event handlers registered for the Control.

While it may be an abuse of the pattern, the class implements the IDisposable Interface to restore the events field on disposal of the class instance. The reason for this is to facilitate the use of the using block to wrap the class usage.

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

Example usage in the button1_Click method:

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

Many will say that the use of Reflection to access non-public class members is dirty or some other derogatory term and that it introduces a brittleness to the code as someone may change the underlying code definition such that the code that relies on member names (magic strings) is no longer valid. This is a valid concern, but I view it as no different than code that accesses external databases.

Reflection can be thought of a query of a type (datatable) from an assembly (database) for specific fields (members: fields, properties, events). It is no more brittle than a SQL statement such as Select SomeField From SomeTable Where AnotherField=5. This type of SQL code is prevent in the world and no one thinks twice about writing it, but some external force could easily redefine the database you code relies on an render all the magic string SQL statements invalid as well.

Use of hard coded names is always at risk of being made invalid by change. You have to weigh the risks of moving forward versus the option of being frozen in fear of proceeding because someone wants to sound authoritative (typically a parroting of other such individuals) and criticize you for implementing a solution that solves the current problem.

TnTinMn
  • 11,522
  • 3
  • 18
  • 39
0

I was hoping to write code that would programatically locate all event handler method names created using controlObject.Event += EventHandlerMethodName, but as you see in the other answers, code to do this is complicated, limited, and perhaps not able to work in all cases

This is what I came up with. It satisfies my desire to consolidate the code that subtracts and re-adds event handler method names into my abstract class, but at the expense of having to write code to store and manage event handler method names and having to write code for each control property where I want to suppress the event handler, modify the property value, and finally re-add the event handler.

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

Designer has

this.myCboFooList = new MyCboFooList();

Main form code

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

And now, if I want to set a property and suppress event(s), I can write something like the following and not have to remember to re-add the event handler method name

myCboFooList._DataSource_NoEvents = null;
VA systems engineer
  • 2,856
  • 2
  • 14
  • 38