diff --git a/src/Eto/Forms/Binding/Binding.helpers.cs b/src/Eto/Forms/Binding/Binding.helpers.cs index 8e97b2538..4cc77ab4c 100644 --- a/src/Eto/Forms/Binding/Binding.helpers.cs +++ b/src/Eto/Forms/Binding/Binding.helpers.cs @@ -189,12 +189,15 @@ public static IndirectBinding Property(string propertyName, bool /// INotifyPropertyChanged object to attach the event handler to /// Name of the property to trigger the changed event. /// Event handler delegate to trigger when the specified property changes - /// + /// + /// public static void AddPropertyEvent(object obj, string propertyName, EventHandler eh) { - var notifyObject = obj as INotifyPropertyChanged; - if (notifyObject != null) - new PropertyNotifyHelper(notifyObject, propertyName).Changed += eh; + if (obj is INotifyPropertyChanged notifyObject) + { + var helper = new PropertyNotifyHelper(notifyObject, propertyName); + helper.Changed += eh; + } } /// @@ -208,32 +211,109 @@ public static void AddPropertyEvent(object obj, string propertyName, EventHandle /// INotifyPropertyChanged object to attach the event handler to /// Expression to the property to trigger the changed event. /// Event handler delegate to trigger when the specified property changes - /// + /// + /// public static void AddPropertyEvent(T obj, Expression> propertyExpression, EventHandler eh) { - var notifyObject = obj as INotifyPropertyChanged; - if (notifyObject != null) + var propertyInfo = propertyExpression.GetMemberInfo(); + if (propertyInfo != null) { - var propertyInfo = propertyExpression.GetMemberInfo(); - if (propertyInfo != null) - new PropertyNotifyHelper(notifyObject, propertyInfo.Member.Name).Changed += eh; + AddPropertyEvent(obj, propertyInfo.Member.Name, eh); } } /// /// Removes an event handler previously attached with the AddPropertyEvent method. /// - /// INotifyPropertyChanged object to remove the event handler from + /// + /// Note that this will unsubscribe from all property handlers that point to the same delegate specified by . + /// Use to only unsubscribe for a single property. + /// + /// Object the event is subscribed to /// Event handler delegate to remove /// public static void RemovePropertyEvent(object obj, EventHandler eh) { - var helper = eh.Target as PropertyNotifyHelper; - if (helper != null) + if (obj is PropertyNotifyHelper helper) { helper.Changed -= eh; helper.Unregister(obj); } + else if (obj is INotifyPropertyChanged notifyObject) + { + var propertyChangedField = GetPropertyChangedField(notifyObject); + if (propertyChangedField == null) + return; + + var propertyChangedDelegates = ((Delegate)propertyChangedField.GetValue(notifyObject))?.GetInvocationList().OfType() ?? Enumerable.Empty(); + foreach (var del in propertyChangedDelegates) + { + // find ones hooked up to the PropertyNotifyHelper, regardless of property + if (del.Target is PropertyNotifyHelper h && h.IsHookedTo(eh)) + { + h.Changed -= eh; + h.Unregister(obj); + } + } + } + } + + /// + /// Removes an event handler previously attached with the AddPropertyEvent method. + /// + /// INotifyPropertyChanged object to unsubscribe from + /// Expression for the property to remove the event handler for + /// Event handler delegate to remove + /// + public static void RemovePropertyEvent(T obj, Expression> propertyExpression, EventHandler eh) + { + var propertyInfo = propertyExpression.GetMemberInfo(); + if (propertyInfo != null) + { + RemovePropertyEvent(obj, propertyInfo.Member.Name, eh); + } + } + + /// + /// Removes an event handler previously attached with the AddPropertyEvent method. + /// + /// INotifyPropertyChanged object to unsubscribe from + /// Name of the property to remove the event handler for + /// Event handler delegate to remove + /// + public static void RemovePropertyEvent(object obj, string propertyName, EventHandler eh) + { + if (obj is INotifyPropertyChanged notifyObject) + { + // this only works when PropertyChanged is a simple public event + // if it is implemented explicitly via the interface this won't work + var propertyChangedField = GetPropertyChangedField(notifyObject); + if (propertyChangedField == null) + return; + + var propertyChangedDelegates = ((Delegate)propertyChangedField.GetValue(notifyObject))?.GetInvocationList().OfType() ?? Enumerable.Empty(); + foreach (var del in propertyChangedDelegates) + { + // find ones hooked up to the PropertyNotifyHelper + if (del.Target is PropertyNotifyHelper h && h.PropertyName == propertyName && h.IsHookedTo(eh)) + { + h.Changed -= eh; + h.Unregister(obj); + } + } + } + } + + static FieldInfo GetPropertyChangedField(object obj) + { + FieldInfo field = null; + var type = obj?.GetType(); + while (field == null && type != null) + { + field = type.GetField(nameof(INotifyPropertyChanged.PropertyChanged), BindingFlags.Instance | BindingFlags.NonPublic); + type = type.BaseType; + } + return field; } /// @@ -289,4 +369,4 @@ public static void ExecuteCommand(object dataContext, Expression : IndirectBinding DefaultSetValue = defaultSetValue; if (!string.IsNullOrEmpty(notifyProperty)) { - AddChangeEvent = (obj, eh) => { AddPropertyEvent(obj, notifyProperty, eh); return obj; }; - RemoveChangeEvent = (obj, eh) => RemovePropertyEvent(obj, eh); + var binding = new PropertyBinding(notifyProperty); + AddChangeEvent = (obj, eh) => binding.AddValueChangedHandler(obj, eh); + RemoveChangeEvent = (obj, eh) => binding.RemoveValueChangedHandler(obj, eh); } } diff --git a/src/Eto/Forms/Binding/PropertyNotifyHelper.cs b/src/Eto/Forms/Binding/PropertyNotifyHelper.cs index aa49c2b71..b384d1d68 100644 --- a/src/Eto/Forms/Binding/PropertyNotifyHelper.cs +++ b/src/Eto/Forms/Binding/PropertyNotifyHelper.cs @@ -4,15 +4,15 @@ namespace Eto.Forms; /// Helper to turn a property changed event to an EventHandler for binding /// /// -/// Use and to access -/// this functionality. +/// Use and to access +/// this functionality, or better yet use the class. /// class PropertyNotifyHelper { public string PropertyName { get; private set; } public event EventHandler Changed; - + public PropertyNotifyHelper(INotifyPropertyChanged obj, string propertyName) { PropertyName = propertyName; @@ -35,4 +35,15 @@ void obj_PropertyChanged(object sender, PropertyChangedEventArgs e) } } + public bool IsHookedTo(EventHandler eh) + { + foreach (var invocation in Changed.GetInvocationList()) + { + if (invocation == (Delegate)eh) + { + return true; + } + } + return false; + } } \ No newline at end of file diff --git a/test/Eto.Test/UnitTests/Forms/Bindings/BindingHelpersTests.cs b/test/Eto.Test/UnitTests/Forms/Bindings/BindingHelpersTests.cs new file mode 100644 index 000000000..caf7f0a4b --- /dev/null +++ b/test/Eto.Test/UnitTests/Forms/Bindings/BindingHelpersTests.cs @@ -0,0 +1,197 @@ +using NUnit.Framework; + +namespace Eto.Test.UnitTests.Forms.Bindings; + +[TestFixture] +public class BindingHelpersTests +{ + [Test] + public void AddingAPropertyEventByNameShouldWork() + { + var propertyValueChanged = false; + void Handler(object sender, EventArgs e) => propertyValueChanged = true; + + var bindObject = new BindObject { BoolProperty = true }; + Binding.AddPropertyEvent(bindObject, "BoolProperty", Handler); + + // Act + bindObject.BoolProperty = false; + + // Assert + Assert.That(propertyValueChanged, Is.True); + } + + [Test] + public void AddingAPropertyEventByExpressionShouldWork() + { + var propertyValueChanged = false; + void Handler(object sender, EventArgs e) => propertyValueChanged = true; + + var bindObject = new BindObject { BoolProperty = true }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, Handler); + + // Act + bindObject.BoolProperty = false; + + // Assert + Assert.That(propertyValueChanged, Is.True); + } + + [Test] + public void AddingMultiplePropertyEventsShouldWork() + { + var boolPropertyValueChanged = false; + var intPropertyValueChanged = false; + var stringPropertyValueChanged = false; + void BoolHandler(object sender, EventArgs e) => boolPropertyValueChanged = true; + void IntHandler(object sender, EventArgs e) => intPropertyValueChanged = true; + void StringHandler(object sender, EventArgs e) => stringPropertyValueChanged = true; + + var bindObject = new BindObject { BoolProperty = true, IntProperty = 3, StringProperty = "Test1" }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, BoolHandler); + Binding.AddPropertyEvent(bindObject, obj => obj.IntProperty, IntHandler); + Binding.AddPropertyEvent(bindObject, obj => obj.StringProperty, StringHandler); + + // Act + bindObject.BoolProperty = false; + bindObject.IntProperty = 4; + bindObject.StringProperty = "Test2"; + + // Assert + Assert.That(boolPropertyValueChanged, Is.True); + Assert.That(intPropertyValueChanged, Is.True); + Assert.That(stringPropertyValueChanged, Is.True); + } + + [Test] + public void APropertyEventShouldNotRespondToADifferentProperty() + { + var propertyValueChanged = false; + void Handler(object sender, EventArgs e) => propertyValueChanged = true; + + var bindObject = new BindObject { BoolProperty = true, IntProperty = 1 }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, Handler); + + // Act + bindObject.IntProperty = 2; + + // Assert + Assert.That(propertyValueChanged, Is.False); + } + + [Test] + public void RemovingAPropertyEventShouldWork() + { + var propertyValueChanged = false; + void Handler(object sender, EventArgs e) => propertyValueChanged = true; + + var bindObject = new BindObject { BoolProperty = true }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, Handler); + + // Act + Binding.RemovePropertyEvent(bindObject, Handler); + bindObject.BoolProperty = false; + + // Assert + Assert.That(propertyValueChanged, Is.False); + } + + [Test] + public void RemovingAPropertyEventShouldKeepOtherEvents() + { + var boolPropertyValueChanged = false; + var intPropertyValueChanged = false; + var stringPropertyValueChanged = false; + void BoolHandler(object sender, EventArgs e) => boolPropertyValueChanged = true; + void IntHandler(object sender, EventArgs e) => intPropertyValueChanged = true; + void StringHandler(object sender, EventArgs e) => stringPropertyValueChanged = true; + + var bindObject = new BindObject { BoolProperty = true, IntProperty = 3, StringProperty = "Test1" }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, BoolHandler); + Binding.AddPropertyEvent(bindObject, obj => obj.IntProperty, IntHandler); + Binding.AddPropertyEvent(bindObject, obj => obj.StringProperty, StringHandler); + + // Act + Binding.RemovePropertyEvent(bindObject, IntHandler); + bindObject.BoolProperty = false; + bindObject.IntProperty = 4; + bindObject.StringProperty = "Test2"; + + // Assert + Assert.That(boolPropertyValueChanged, Is.True); + Assert.That(intPropertyValueChanged, Is.False); + Assert.That(stringPropertyValueChanged, Is.True); + } + + [Test] + public void RemovingAllPropertyEventsShouldWork() + { + var boolPropertyValueChanged = false; + var intPropertyValueChanged = false; + var stringPropertyValueChanged = false; + void BoolHandler(object sender, EventArgs e) => boolPropertyValueChanged = true; + void IntHandler(object sender, EventArgs e) => intPropertyValueChanged = true; + void StringHandler(object sender, EventArgs e) => stringPropertyValueChanged = true; + + var bindObject = new BindObject { BoolProperty = true, IntProperty = 3, StringProperty = "Test1" }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, BoolHandler); + Binding.AddPropertyEvent(bindObject, obj => obj.IntProperty, IntHandler); + Binding.AddPropertyEvent(bindObject, obj => obj.StringProperty, StringHandler); + + // Act + Binding.RemovePropertyEvent(bindObject, StringHandler); + Binding.RemovePropertyEvent(bindObject, BoolHandler); + Binding.RemovePropertyEvent(bindObject, IntHandler); + bindObject.BoolProperty = false; + bindObject.IntProperty = 4; + bindObject.StringProperty = "Test2"; + + // Assert + Assert.That(boolPropertyValueChanged, Is.False); + Assert.That(intPropertyValueChanged, Is.False); + Assert.That(stringPropertyValueChanged, Is.False); + } + + [Test] + public void UnsubscribingToSameEventShouldntUnsubscribeAllWhenPassingProperty() + { + var numchanged = 0; + void Handler(object sender, EventArgs e) => numchanged++; + + var bindObject = new BindObject { BoolProperty = true, IntProperty = 3, StringProperty = "Test1" }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, Handler); + Binding.AddPropertyEvent(bindObject, obj => obj.IntProperty, Handler); + Binding.AddPropertyEvent(bindObject, obj => obj.StringProperty, Handler); + + // Act + Binding.RemovePropertyEvent(bindObject, obj => obj.BoolProperty, Handler); + bindObject.BoolProperty = false; + bindObject.IntProperty = 4; + bindObject.StringProperty = "Test2"; + + // Assert + Assert.That(numchanged, Is.EqualTo(2)); + } + + [Test] + public void UnsubscribingToSameEventWillUnsubscribeAll() + { + var numchanged = 0; + void Handler(object sender, EventArgs e) => numchanged++; + + var bindObject = new BindObject { BoolProperty = true, IntProperty = 3, StringProperty = "Test1" }; + Binding.AddPropertyEvent(bindObject, obj => obj.BoolProperty, Handler); + Binding.AddPropertyEvent(bindObject, obj => obj.IntProperty, Handler); + Binding.AddPropertyEvent(bindObject, obj => obj.StringProperty, Handler); + + // Act + Binding.RemovePropertyEvent(bindObject, Handler); + bindObject.BoolProperty = false; + bindObject.IntProperty = 4; + bindObject.StringProperty = "Test2"; + + // Assert + // ambiguous which one to remove, so it removed all -- need to specify property or return value from AddPropertyEvent + Assert.That(numchanged, Is.EqualTo(0)); + } +} diff --git a/test/Eto.Test/UnitTests/Forms/Bindings/DelegateBindingTests.cs b/test/Eto.Test/UnitTests/Forms/Bindings/DelegateBindingTests.cs new file mode 100644 index 000000000..047b8c689 --- /dev/null +++ b/test/Eto.Test/UnitTests/Forms/Bindings/DelegateBindingTests.cs @@ -0,0 +1,33 @@ +using NUnit.Framework; + +namespace Eto.Test.UnitTests.Forms.Bindings; + +[TestFixture] +public class DelegateBindingTests +{ + [Test] + public void SubscribingToPropertyChangesShouldWork() + { + int propertyValueChanged = 0; + void Handler(object sender, EventArgs e) => propertyValueChanged++; + + var bindObject = new BindObject { BoolProperty = true }; + + var binding = new DelegateBinding(o => o.BoolProperty, (o,v) => o.BoolProperty = v, nameof(BindObject.BoolProperty)); + + // wire up handler + var bindingReference = binding.AddValueChangedHandler(bindObject, Handler); + + bindObject.BoolProperty = true; + + Assert.That(propertyValueChanged, Is.EqualTo(1), "Handler should have been fired"); + + // Remove the handler binding + binding.RemoveValueChangedHandler(bindingReference, Handler); + + bindObject.BoolProperty = false; + + // Handler should no longer be triggered + Assert.That(propertyValueChanged, Is.EqualTo(1), "Handler should not fire after being removed"); + } +}