diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index a99582cea7..f2dbd263a0 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -77,6 +77,8 @@ namespace Perspex.Markup.Xaml.Data /// public object Source { get; set; } + public ValidationMethods ValidationMethods { get; set; } + /// public InstancedBinding Initiate( IPerspexObject target, @@ -126,7 +128,7 @@ namespace Perspex.Markup.Xaml.Data FallbackValue, Priority); - return new InstancedBinding(subject, Mode, Priority); + return new InstancedBinding(subject, Mode, Priority, ValidationMethods); } private static PathInfo ParsePath(string path) diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 1e996b2b2a..d0dcd069a3 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -29,6 +29,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions Mode = Mode, Path = Path, Priority = Priority, + ValidationMethods = ValidationMethods }; } @@ -40,5 +41,6 @@ namespace Perspex.Markup.Xaml.MarkupExtensions public string Path { get; set; } public BindingPriority Priority { get; set; } = BindingPriority.LocalValue; public object Source { get; set; } + public ValidationMethods ValidationMethods { get; set; } = ValidationMethods.None; } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs index 4c918b8b46..5110f4713e 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs @@ -64,6 +64,11 @@ namespace Perspex.Markup.Data.Plugins /// public override bool IsValid => Exception == null; + + public override bool Match(ValidationMethods enabledMethods) + { + return (enabledMethods & ValidationMethods.Exceptions) != 0; + } } } } diff --git a/src/Markup/Perspex.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs index 0959f9b6fc..ee4e87076f 100644 --- a/src/Markup/Perspex.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs +++ b/src/Markup/Perspex.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs @@ -86,6 +86,11 @@ namespace Perspex.Markup.Data.Plugins /// The errors on the given property and on the object as a whole. /// public IEnumerable Errors { get; } + + public override bool Match(ValidationMethods enabledMethods) + { + return (enabledMethods & ValidationMethods.INotifyDataErrorInfo) != 0; + } } } } diff --git a/src/Perspex.Base/Data/BindingOperations.cs b/src/Perspex.Base/Data/BindingOperations.cs index efa0c8500d..dbeacfbe90 100644 --- a/src/Perspex.Base/Data/BindingOperations.cs +++ b/src/Perspex.Base/Data/BindingOperations.cs @@ -44,10 +44,10 @@ namespace Perspex.Data { case BindingMode.Default: case BindingMode.OneWay: - return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority); + return target.Bind(property, binding.Observable ?? binding.Subject, binding.Priority, binding.ValidationMethods); case BindingMode.TwoWay: return new CompositeDisposable( - target.Bind(property, binding.Subject, binding.Priority), + target.Bind(property, binding.Subject, binding.Priority, binding.ValidationMethods), target.GetObservable(property).Subscribe(binding.Subject)); case BindingMode.OneTime: var source = binding.Subject ?? binding.Observable; diff --git a/src/Perspex.Base/Data/InstancedBinding.cs b/src/Perspex.Base/Data/InstancedBinding.cs index 545d690aa4..3b911fef35 100644 --- a/src/Perspex.Base/Data/InstancedBinding.cs +++ b/src/Perspex.Base/Data/InstancedBinding.cs @@ -29,12 +29,16 @@ namespace Perspex.Data /// /// The value used for the binding. /// + /// The validation methods for this binding. /// The binding priority. - public InstancedBinding(object value, BindingPriority priority = BindingPriority.LocalValue) + public InstancedBinding(object value, + BindingPriority priority = BindingPriority.LocalValue, + ValidationMethods methods = ValidationMethods.None) { Mode = BindingMode.OneTime; Priority = priority; Value = value; + ValidationMethods = methods; } /// @@ -43,10 +47,12 @@ namespace Perspex.Data /// The observable for a one-way binding. /// The binding mode. /// The binding priority. + /// The validation methods for this binding. public InstancedBinding( IObservable observable, BindingMode mode = BindingMode.OneWay, - BindingPriority priority = BindingPriority.LocalValue) + BindingPriority priority = BindingPriority.LocalValue, + ValidationMethods methods = ValidationMethods.None) { Contract.Requires(observable != null); @@ -60,6 +66,7 @@ namespace Perspex.Data Mode = mode; Priority = priority; Observable = observable; + ValidationMethods = methods; } /// @@ -68,16 +75,19 @@ namespace Perspex.Data /// The subject for a two-way binding. /// The binding mode. /// The binding priority. + /// The validation methods for this binding. public InstancedBinding( ISubject subject, BindingMode mode = BindingMode.OneWay, - BindingPriority priority = BindingPriority.LocalValue) + BindingPriority priority = BindingPriority.LocalValue, + ValidationMethods methods = ValidationMethods.None) { Contract.Requires(subject != null); Mode = mode; Priority = priority; Subject = subject; + ValidationMethods = methods; } /// @@ -104,5 +114,10 @@ namespace Perspex.Data /// Gets the subject for a two-way binding. /// public ISubject Subject { get; } + + /// + /// Gets the validation methods for this binding. + /// + public ValidationMethods ValidationMethods { get; } } } diff --git a/src/Perspex.Base/Data/ValidationMethods.cs b/src/Perspex.Base/Data/ValidationMethods.cs new file mode 100644 index 0000000000..491a7bbe50 --- /dev/null +++ b/src/Perspex.Base/Data/ValidationMethods.cs @@ -0,0 +1,13 @@ +using System; + +namespace Perspex.Data +{ + [Flags] + public enum ValidationMethods + { + None = 0, + Exceptions = 1, + INotifyDataErrorInfo = 2, + All = -1 + } +} \ No newline at end of file diff --git a/src/Perspex.Base/Data/ValidationStatus.cs b/src/Perspex.Base/Data/ValidationStatus.cs index df0dede70d..ac2600c13c 100644 --- a/src/Perspex.Base/Data/ValidationStatus.cs +++ b/src/Perspex.Base/Data/ValidationStatus.cs @@ -19,5 +19,12 @@ namespace Perspex.Data /// True when the data passes validation; otherwise, false. /// public abstract bool IsValid { get; } + + /// + /// Checks if this validation status came from a currently enabled method of validation checking. + /// + /// The enabled methods of validation checking. + /// True if enabled; otherwise, false. + public abstract bool Match(ValidationMethods enabledMethods); } } diff --git a/src/Perspex.Base/IPerspexObject.cs b/src/Perspex.Base/IPerspexObject.cs index 4290bfe792..c92898bffb 100644 --- a/src/Perspex.Base/IPerspexObject.cs +++ b/src/Perspex.Base/IPerspexObject.cs @@ -67,13 +67,15 @@ namespace Perspex /// The property. /// The observable. /// The priority of the binding. + /// The validation methods of the binding. /// /// A disposable which can be used to terminate the binding. /// IDisposable Bind( PerspexProperty property, IObservable source, - BindingPriority priority = BindingPriority.LocalValue); + BindingPriority priority = BindingPriority.LocalValue, + ValidationMethods validation = ValidationMethods.None); /// /// Binds a to an observable. @@ -82,12 +84,14 @@ namespace Perspex /// The property. /// The observable. /// The priority of the binding. + /// The validation methods of the binding. /// /// A disposable which can be used to terminate the binding. /// IDisposable Bind( PerspexProperty property, IObservable source, - BindingPriority priority = BindingPriority.LocalValue); + BindingPriority priority = BindingPriority.LocalValue, + ValidationMethods validation = ValidationMethods.None); } } \ No newline at end of file diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index cc8cb6ada6..234af801ca 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -44,6 +44,7 @@ Properties\SharedAssemblyInfo.cs + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index c0a361783a..08b91a9afb 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -375,13 +375,15 @@ namespace Perspex /// The property. /// The observable. /// The priority of the binding. + /// The validation methods of the binding. /// /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( PerspexProperty property, IObservable source, - BindingPriority priority = BindingPriority.LocalValue) + BindingPriority priority = BindingPriority.LocalValue, + ValidationMethods validation = ValidationMethods.None) { Contract.Requires(property != null); VerifyAccess(); @@ -408,15 +410,16 @@ namespace Perspex .Select(x => CastOrDefault(x, property.PropertyType)) .Do(_ => { }, () => s_directBindings.Remove(subscription)) .Subscribe(x => DirectBindingSet(property, x)); - validationSubcription = source.OfType().Subscribe(x => ValidationChanged(property, x)); + validationSubcription = source + .OfType() + .Where(v => v.Match(validation)) + .Subscribe(x => ValidationChanged(property, x)); s_directBindings.Add(subscription); - s_directBindings.Add(validationSubcription); return Disposable.Create(() => { validationSubcription.Dispose(); - s_directBindings.Remove(validationSubcription); subscription.Dispose(); s_directBindings.Remove(subscription); }); @@ -444,7 +447,7 @@ namespace Perspex GetDescription(source), priority); - return v.Add(source, (int)priority); + return v.Add(source, (int)priority, validation); } } @@ -455,13 +458,15 @@ namespace Perspex /// The property. /// The observable. /// The priority of the binding. + /// The validation methods of the binding. /// /// A disposable which can be used to terminate the binding. /// public IDisposable Bind( PerspexProperty property, IObservable source, - BindingPriority priority = BindingPriority.LocalValue) + BindingPriority priority = BindingPriority.LocalValue, + ValidationMethods validation = ValidationMethods.None) { Contract.Requires(property != null); diff --git a/src/Perspex.Base/PriorityBindingEntry.cs b/src/Perspex.Base/PriorityBindingEntry.cs index 61aeb89323..4f923e3a9f 100644 --- a/src/Perspex.Base/PriorityBindingEntry.cs +++ b/src/Perspex.Base/PriorityBindingEntry.cs @@ -13,6 +13,7 @@ namespace Perspex { private PriorityLevel _owner; private IDisposable _subscription; + private ValidationMethods _validation; /// /// Initializes a new instance of the class. @@ -21,10 +22,12 @@ namespace Perspex /// /// The binding index. Later bindings should have higher indexes. /// - public PriorityBindingEntry(PriorityLevel owner, int index) + /// The validation settings for the binding. + public PriorityBindingEntry(PriorityLevel owner, int index, ValidationMethods validation) { _owner = owner; Index = index; + _validation = validation; } /// @@ -103,7 +106,10 @@ namespace Perspex if (validationStatus != null) { - _owner.Validation(this, validationStatus); + if (validationStatus.Match(_validation)) + { + _owner.Validation(this, validationStatus); + } } else if (bindingError == null || bindingError.UseFallbackValue) { diff --git a/src/Perspex.Base/PriorityLevel.cs b/src/Perspex.Base/PriorityLevel.cs index a5f078f73c..3049c793d9 100644 --- a/src/Perspex.Base/PriorityLevel.cs +++ b/src/Perspex.Base/PriorityLevel.cs @@ -97,12 +97,13 @@ namespace Perspex /// Adds a binding. /// /// The binding to add. + /// Validation settings for the binding. /// A disposable used to remove the binding. - public IDisposable Add(IObservable binding) + public IDisposable Add(IObservable binding, ValidationMethods validation) { Contract.Requires(binding != null); - var entry = new PriorityBindingEntry(this, _nextIndex++); + var entry = new PriorityBindingEntry(this, _nextIndex++, validation); var node = Bindings.AddFirst(entry); entry.Start(binding); diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index ffd37e62f3..155fa47c06 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -77,12 +77,13 @@ namespace Perspex /// /// The binding. /// The binding priority. + /// Validation settings for the binding. /// /// A disposable that will remove the binding. /// - public IDisposable Add(IObservable binding, int priority) + public IDisposable Add(IObservable binding, int priority, ValidationMethods validation = ValidationMethods.None) { - return GetLevel(priority).Add(binding); + return GetLevel(priority).Add(binding, validation); } /// diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs index 1979d5d9bf..9cfcac0825 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs @@ -32,7 +32,8 @@ namespace Perspex.Markup.Xaml.UnitTests.Data target.Verify(x => x.Bind( TextBox.TextProperty, It.IsAny>(), - BindingPriority.TemplatedParent)); + BindingPriority.TemplatedParent, + ValidationMethods.None)); } [Fact] @@ -52,7 +53,8 @@ namespace Perspex.Markup.Xaml.UnitTests.Data target.Verify(x => x.Bind( TextBox.TextProperty, It.IsAny>(), - BindingPriority.TemplatedParent)); + BindingPriority.TemplatedParent, + ValidationMethods.None)); } private Mock CreateTarget( diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs new file mode 100644 index 0000000000..799486bd5f --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs @@ -0,0 +1,112 @@ +using Perspex.Controls; +using Perspex.Markup.Xaml.Data; +using System; +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.Xaml.UnitTests.Data +{ + public class BindingTests_Validation + { + + public class Data : INotifyPropertyChanged + { + private string mustbeNonEmpty; + + public string MustBeNonEmpty + { + get { return mustbeNonEmpty; } + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(nameof(value)); + } + mustbeNonEmpty = value; + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + [Fact] + public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Direct() + { + var source = new Data { MustBeNonEmpty = "Test" }; + var target = new TextBlock { DataContext = source }; + var binding = new Binding + { + Path = nameof(source.MustBeNonEmpty), + Mode = Perspex.Data.BindingMode.TwoWay, + ValidationMethods = Perspex.Data.ValidationMethods.None + }; + target.Bind(TextBlock.TextProperty, binding); + + target.Text = ""; + + Assert.Null(target.ValidationStatus); + } + + [Fact] + public void Enabled_Validation_Should_Trigger_Validation_Change_Direct() + { + var source = new Data { MustBeNonEmpty = "Test" }; + var target = new TextBlock { DataContext = source }; + var binding = new Binding + { + Path = nameof(source.MustBeNonEmpty), + Mode = Perspex.Data.BindingMode.TwoWay, + ValidationMethods = Perspex.Data.ValidationMethods.All + }; + target.Bind(TextBlock.TextProperty, binding); + + target.Text = ""; + Assert.NotNull(target.ValidationStatus); + } + + [Fact] + public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Styled() + { + var source = new Data { MustBeNonEmpty = "Test" }; + var target = new TextBlock { DataContext = source }; + var binding = new Binding + { + Path = nameof(source.MustBeNonEmpty), + Mode = Perspex.Data.BindingMode.TwoWay, + ValidationMethods = Perspex.Data.ValidationMethods.None + }; + target.Bind(Control.TagProperty, binding); + + target.Tag = ""; + + Assert.Null(target.ValidationStatus); + } + + [Fact] + public void Enabled_Validation_Should_Trigger_Validation_Change_Styled() + { + var source = new Data { MustBeNonEmpty = "Test" }; + var target = new TextBlock { DataContext = source }; + var binding = new Binding + { + Path = nameof(source.MustBeNonEmpty), + Mode = Perspex.Data.BindingMode.TwoWay, + ValidationMethods = Perspex.Data.ValidationMethods.All + }; + target.Bind(Control.TagProperty, binding); + + target.Tag = ""; + Assert.NotNull(target.ValidationStatus); + } + } +} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index 243c0c53f4..cf3e856d75 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -94,6 +94,7 @@ + diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs index 465610b045..b6cb7a0d91 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs @@ -129,7 +129,7 @@ namespace Perspex.Styling.UnitTests throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority, ValidationMethods validation) { throw new NotImplementedException(); } @@ -139,7 +139,7 @@ namespace Perspex.Styling.UnitTests throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue, ValidationMethods validation = ValidationMethods.None) { throw new NotImplementedException(); } diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 286a5ec269..83f0672a6f 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -160,7 +160,7 @@ namespace Perspex.Styling.UnitTests throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue, ValidationMethods validation = ValidationMethods.None) { throw new NotImplementedException(); } @@ -170,7 +170,7 @@ namespace Perspex.Styling.UnitTests throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue, ValidationMethods validation = ValidationMethods.None) { throw new NotImplementedException(); } diff --git a/tests/Perspex.Styling.UnitTests/TestControlBase.cs b/tests/Perspex.Styling.UnitTests/TestControlBase.cs index d5bd85f1dd..ac613ac75e 100644 --- a/tests/Perspex.Styling.UnitTests/TestControlBase.cs +++ b/tests/Perspex.Styling.UnitTests/TestControlBase.cs @@ -62,12 +62,12 @@ namespace Perspex.Styling.UnitTests throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue, ValidationMethods validation = ValidationMethods.None) { throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue, ValidationMethods validation = ValidationMethods.None) { throw new NotImplementedException(); } diff --git a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs index 3e16e8a18c..156fdfa035 100644 --- a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs @@ -57,12 +57,12 @@ namespace Perspex.Styling.UnitTests throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue, ValidationMethods validation = ValidationMethods.None) { throw new NotImplementedException(); } - public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue) + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority = BindingPriority.LocalValue, ValidationMethods validation = ValidationMethods.None) { throw new NotImplementedException(); }