1

It's often necessary to prevent some control's event handling when setting is properties in code like check-boxes or radio-buttons etc. To achieve this, I use the following helper:

public static class PropertySetter
{
    private static readonly object locker = new object();

    public static object CurrentObject { get; private set; }

    public static string CurrentPropertyName { get; private set; }

    public static void SetValueFor(this object obj, string propertyName, object value)
    {
        if (obj == null) { throw new ArgumentNullException("obj"); }
        if (string.IsNullOrEmpty(propertyName) == true) { throw new ArgumentNullException("'propertyName"); }

        PropertyInfo propertyInfo = obj.GetType().GetProperty(propertyName);

        if (propertyInfo == null)
        {
            throw new ArgumentOutOfRangeException("propertyName", "Property not found.");
        }

        lock (locker)
        {
            CurrentObject = obj;
            CurrentPropertyName = propertyName;
            try
            {
                propertyInfo.SetValue(obj, value, null);
            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                CurrentObject = null;
                CurrentPropertyName = null;
            }
        }
    }

    public static bool CanHandleEventFor(this object obj, string propertyName)
    {
        return object.ReferenceEquals(obj, CurrentObject) == false && CurrentPropertyName.Equals(propertyName) == false;
    }
}

Usage:

checkBox.SetValueFor("Checked", false);

void checkBox_Checked(object sender, EventArgs e)
{
    if (sender.CanHandleEventFor("Checked")) return;

    // ...
}

However I thougt perhaps there is a better way without writing the if inside the event handler but to detach all event handlers for some event by either passing it's name as an additinal parameter or the event field itself and after the property is set reattach them. Is that even possible?

t3chb0t
  • 16,340
  • 13
  • 78
  • 118
  • It is feasible to detach event handlers - they are just a collection of delegates (roughly speaking). However its not a pattern I've come across. Why would you not want a subscriber to know that a change event has occurred - its the foundation of MVVM. The listener needs to make a judgement as to what to do with the value its been passed based on the state of the system as it seems to it. I will admit to have put a dirty form level variable "SettingUp_IgnoreValueChanges = true" and check it in the event handlers. Horrible, but simple. – PhillipH Dec 01 '14 at 16:40
  • Could you not pass an argument through when you raise your event and have the event listeners decide whether they should respond or not? – sQuir3l Dec 01 '14 at 16:47
  • The code you've shown here is broken. First, using reflection this way is going to really slow your program down. Second, the expression in `CanHandleEventFor()` should be this instead: `!object.ReferenceEquals(obj, CurrentObject) || !CurrentPropertyName.Equals(propertyName);`. I.e. it's only the same property if both the object and the name are the same, so it's okay to set if either are not. Finally, you should not have to do this in general. Show a _specific_ scenario where you need to do this, and you'll receive good advice as to how to deal with it, without this helper. – Peter Duniho Dec 01 '14 at 18:28
  • I did show a specific scenario using the example of a `checkBox` that I set to `Checked = false` in code, not by the user in the UI. When set it code I don't want the event handler to do its job. Let's say it's a check box for the visibility of something. When I load a model in to a document I want to show the model by default so I need to update my checkBox. It's just a simple example. There are a lot of situations when you need to updated the state of something in code. Another example setting the SelectedIndex of a combobox etc. – t3chb0t Dec 01 '14 at 18:44
  • @PeterDuniho: oh, you're write about the code. I didn't copy paste it but wrote it from memory and didn't think of it. – t3chb0t Dec 01 '14 at 19:21
  • @t3chb0t: the example shows only the _usage_ of your class, which is easily inferred anyway. The point is, _why_ don't you want to react to changes in all cases? It is very rare for this to be necessary, and even less so in WPF where binding-mediated behaviors are so prevalent. If you felt it necessary to suppress reaction to events so often that it required a helper class to support, there's probably a broader design mistake at work. – Peter Duniho Dec 01 '14 at 19:50
  • @PeterDuniho: it is not always possible or easy to use databinding for setting control states or values like `Checked`, `Pressed` or `EditValue` in code and the `Control.CanRaiseEvents` Property is unfortunately protected. – t3chb0t Dec 02 '14 at 06:13

1 Answers1

1

I finally managed to solve it:

class Program
{
    static void Main(string[] args)
    {
        var fc = new TestCheckbox();
        fc.CheckedChanged += (sender, e) =>
        {
            int i = 0;
        };

        // 'CheckedChanged' event is not raised.
        fc.SetValueFor(x => x.Checked = true, "CheckedChanged");

        // 'CheckedChanged' event it raised.
        fc.Checked = false;
    }
}

class TestCheckbox
{
    private bool _checked;

    public event EventHandler CheckedChanged;

    public bool Checked
    {
        get { return _checked; }
        set
        {
            _checked = value;
            if (CheckedChanged != null)
            {
                CheckedChanged(this, EventArgs.Empty);
            }
        }
    }
}

public static class PropertySetter
{
    private static readonly object locker = new object();

    public static void SetValueFor<T>(this T obj, Action<T> action, string eventName)
    {
        lock (locker)
        {
            try
            {
                // Get event handlers.
                Type type = obj.GetType();
                var eventInfo = type.GetEvent(eventName);
                var fieldInfo = type.GetField(eventName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
                var multicastDelegate = fieldInfo.GetValue(obj) as MulticastDelegate;
                Delegate[] delegates = multicastDelegate.GetInvocationList();

                // Remove event handlers.
                foreach (var item in delegates)
                {
                    eventInfo.RemoveEventHandler(obj, item);
                }

                try
                {
                    action(obj);
                }
                catch (Exception)
                {
                    throw;
                }
                finally
                {
                    // Restore event handlers.
                    foreach (var item in delegates)
                    {
                        eventInfo.AddEventHandler(obj, item);
                    }
                }
            }
            catch (Exception)
            {
                throw;
            }
        }
    }
}

Unfortunately this works only in theory and doesn't work that easy with .NET because there is no exclusive field for each event and it requires some hacking so I'll stick to the simplest solution which is this one I think:

class Program
{
    static void Main(string[] args)
    {
        CheckBox checkBox = new CheckBox();
        checkBox.CheckedChanged += (sender, e) =>
        {
            if (!sender.CanHandleEvent("CheckedChanged")) return;
            int i = 0;
        };

        checkBox.SetValueFor(x => x.Checked = true, "CheckedChanged");
        checkBox.Checked = false;
    }
}       

public static class PropertySetter
{
    private static readonly object locker = new object();

    public static object CurrentObject { get; private set; }

    public static string CurrentEventName { get; private set; }

    public static void SetValueFor<T>(this T obj, Action<T> action, string eventName)
    {
        lock (locker)
        {
            CurrentObject = obj;
            CurrentEventName = eventName;
            try
            {
                action(obj);
            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                CurrentObject = null;
                CurrentEventName = null;
            }
        }
    }

    public static bool CanHandleEvent(this object obj, string eventName)
    {
        return !(object.ReferenceEquals(obj, CurrentObject) == true && CurrentEventName.Equals(eventName) == true);
    }
}
Community
  • 1
  • 1
t3chb0t
  • 16,340
  • 13
  • 78
  • 118