diff --git a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs index 8a431041d0..9dd4f10dbe 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; namespace Perspex.Markup.Data.Plugins { @@ -24,6 +25,7 @@ namespace Perspex.Markup.Data.Plugins /// A weak reference to the object. /// The property name. /// A function to call when the property changes. + /// A function to call when the validation status of the property changes. /// /// An interface through which future interactions with the /// property will be made. @@ -31,6 +33,7 @@ namespace Perspex.Markup.Data.Plugins IPropertyAccessor Start( WeakReference reference, string propertyName, - Action changed); + Action changed, + Action validationChanged); } } diff --git a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs index 64421ddc57..9eee066c61 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -9,6 +9,7 @@ using System.Reflection; using Perspex.Data; using Perspex.Logging; using Perspex.Utilities; +using System.Collections; namespace Perspex.Markup.Data.Plugins { @@ -36,6 +37,7 @@ namespace Perspex.Markup.Data.Plugins /// The object. /// The property name. /// A function to call when the property changes. + /// A function to call when the validation state of the property changes. /// /// An interface through which future interactions with the /// property will be made. @@ -43,7 +45,8 @@ namespace Perspex.Markup.Data.Plugins public IPropertyAccessor Start( WeakReference reference, string propertyName, - Action changed) + Action changed, + Action validationChanged) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); @@ -54,7 +57,7 @@ namespace Perspex.Markup.Data.Plugins if (p != null) { - return new Accessor(reference, p, changed); + return new Accessor(reference, p, changed, validationChanged); } else { @@ -64,16 +67,18 @@ namespace Perspex.Markup.Data.Plugins } } - private class Accessor : IPropertyAccessor, IWeakSubscriber + private class Accessor : IPropertyAccessor, IWeakSubscriber, IWeakSubscriber { private readonly WeakReference _reference; private readonly PropertyInfo _property; private readonly Action _changed; + private readonly Action _validationChanged; public Accessor( WeakReference reference, PropertyInfo property, - Action changed) + Action changed, + Action validationChanged) { Contract.Requires(reference != null); Contract.Requires(property != null); @@ -81,12 +86,13 @@ namespace Perspex.Markup.Data.Plugins _reference = reference; _property = property; _changed = changed; + _validationChanged = validationChanged; var inpc = reference.Target as INotifyPropertyChanged; if (inpc != null) { - WeakSubscriptionManager.Subscribe( + WeakSubscriptionManager.Subscribe( inpc, nameof(inpc.PropertyChanged), this); @@ -101,6 +107,19 @@ namespace Perspex.Markup.Data.Plugins reference.Target, reference.Target.GetType()); } + + var indei = _reference.Target as INotifyDataErrorInfo; + if (indei != null) + { + if (indei.HasErrors) + { + _validationChanged(indei.GetErrors(property.Name)); + } + WeakSubscriptionManager.Subscribe( + indei, + nameof(indei.ErrorsChanged), + this); + } } public Type PropertyType => _property.PropertyType; @@ -113,11 +132,20 @@ namespace Perspex.Markup.Data.Plugins if (inpc != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Unsubscribe( inpc, nameof(inpc.PropertyChanged), this); } + + var indei = _reference.Target as INotifyDataErrorInfo; + if (indei != null) + { + WeakSubscriptionManager.Unsubscribe( + indei, + nameof(indei.ErrorsChanged), + this); + } } public bool SetValue(object value, BindingPriority priority) @@ -131,13 +159,22 @@ namespace Perspex.Markup.Data.Plugins return false; } - public void OnEvent(object sender, PropertyChangedEventArgs e) + void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) { _changed(Value); } } + + void IWeakSubscriber.OnEvent(object sender, DataErrorsChangedEventArgs e) + { + if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) + { + var indei = _reference.Target as INotifyDataErrorInfo; + _validationChanged(indei.GetErrors(e.PropertyName)); + } + } } } } diff --git a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs index bc16989a7a..d27d1e8be4 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs @@ -31,6 +31,7 @@ namespace Perspex.Markup.Data.Plugins /// A weak reference to the object. /// The property name. /// A function to call when the property changes. + /// A function to call when the validation state of the property changes. /// /// An interface through which future interactions with the /// property will be made. @@ -38,7 +39,8 @@ namespace Perspex.Markup.Data.Plugins public IPropertyAccessor Start( WeakReference reference, string propertyName, - Action changed) + Action changed, + Action validationChanged) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); diff --git a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs index 14883e449a..0f76503ff3 100644 --- a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs @@ -54,7 +54,7 @@ namespace Perspex.Markup.Data if (plugin != null) { - _accessor = plugin.Start(reference, PropertyName, SetCurrentValue); + _accessor = plugin.Start(reference, PropertyName, SetCurrentValue, _ => { }); if (_accessor != null) { diff --git a/tests/Perspex.Markup.UnitTests/Data/InpcPluginTests.cs b/tests/Perspex.Markup.UnitTests/Data/InpcPluginTests.cs new file mode 100644 index 0000000000..3a32e3eabc --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Data/InpcPluginTests.cs @@ -0,0 +1,139 @@ +using Perspex.Markup.Data.Plugins; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Perspex.Markup.UnitTests.Data +{ + public class InpcPluginTests + { + private class InpcTest : INotifyPropertyChanged, INotifyDataErrorInfo + { + private int noValidationTest; + + public int NoValidationTest + { + get { return noValidationTest; } + set { noValidationTest = value; NotifyPropertyChanged(); } + } + + public bool HasErrors + { + get + { + return NonNegative < 0; + } + } + + private int nonNegative; + + public int NonNegative + { + get { return nonNegative; } + set + { + var old = nonNegative; + nonNegative = value; + NotifyPropertyChanged(); + if (old * value < 0) // If signs are different + { + NotifyErrorsChanged(); + } + } + } + + + public event EventHandler ErrorsChanged; + public event PropertyChangedEventHandler PropertyChanged; + + public IEnumerable GetErrors(string propertyName) + { + if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(NonNegative)) + { + if (NonNegative < 0) + { + yield return "Invalid Value"; + } + } + } + + private void NotifyPropertyChanged([CallerMemberName] string property = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property)); + } + + private void NotifyErrorsChanged([CallerMemberName] string property = "") + { + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(property)); + } + } + + [Fact] + public void Calls_Change_Callback_When_Value_Changes() + { + var plugin = new InpcPropertyAccessorPlugin(); + var source = new InpcTest { NoValidationTest = 0 }; + var changeFired = false; + plugin.Start(new WeakReference(source), nameof(InpcTest.NoValidationTest), _ => changeFired = true, _ => { }); + source.NoValidationTest = 1; + + Assert.True(changeFired); + } + + [Fact] + public void ValidationChanged_Does_Not_Fire_When_NonValidated_Value_Changes() + { + var plugin = new InpcPropertyAccessorPlugin(); + var source = new InpcTest { NoValidationTest = 0 }; + var validationFired = false; + plugin.Start(new WeakReference(source), nameof(InpcTest.NoValidationTest), _ => { }, _ => validationFired = true); + source.NoValidationTest = 1; + + Assert.False(validationFired); + } + + [Fact] + public void ValidationChanged_Does_Not_Fire_When_Validation_Does_Not_Change() + { + var plugin = new InpcPropertyAccessorPlugin(); + var source = new InpcTest { NonNegative = 3 }; + var validationFired = false; + plugin.Start(new WeakReference(source), nameof(InpcTest.NonNegative), _ => { }, _ => validationFired = true); + source.NonNegative = 5; + + Assert.False(validationFired); + } + + [Fact] + public void ValidationChanged_Fires_On_Start_If_Has_Errors() + { + var plugin = new InpcPropertyAccessorPlugin(); + var source = new InpcTest { NonNegative = -5 }; + + Assert.True(source.HasErrors); + + var validationFired = false; + plugin.Start(new WeakReference(source), nameof(InpcTest.NonNegative), _ => { }, _ => validationFired = true); + Assert.True(validationFired); + } + + + + [Fact] + public void ValidationChanged_Fires_When_Validation_Changes() + { + var plugin = new InpcPropertyAccessorPlugin(); + var source = new InpcTest { NonNegative = 5 }; + var validationFired = false; + plugin.Start(new WeakReference(source), nameof(InpcTest.NonNegative), _ => { }, _ => validationFired = true); + source.NonNegative = -1; + Assert.True(validationFired); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index f32a584d1d..fa2b551600 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -97,6 +97,7 @@ +