浏览代码

Surface BindingNotifications in AvaloniaObject.

Steven Kirk 9 年之前
父节点
当前提交
06b0d15fc2

+ 28 - 3
src/Avalonia.Base/AvaloniaObject.cs

@@ -461,6 +461,12 @@ namespace Avalonia
             }
         }
 
+        /// <inheritdoc/>
+        void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification)
+        {
+            BindingNotificationReceived(sender.Property, notification);
+        }
+
         /// <inheritdoc/>
         Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers()
         {
@@ -492,6 +498,18 @@ namespace Avalonia
             });
         }
 
+        /// <summary>
+        /// Occurs when a <see cref="BindingNotification"/> is received for a property which has
+        /// data validation enabled.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <param name="notification">The binding notification.</param>
+        protected virtual void BindingNotificationReceived(
+            AvaloniaProperty property,
+            BindingNotification notification)
+        {
+        }
+
         /// <summary>
         /// Called when a avalonia property changes on the object.
         /// </summary>
@@ -580,15 +598,20 @@ namespace Avalonia
         /// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
         private static object CastOrDefault(object value, Type type)
         {
-            var error = value as BindingNotification;
+            var notification = value as BindingNotification;
 
-            if (error == null)
+            if (notification == null)
             {
                 return TypeUtilities.CastOrDefault(value, type);
             }
             else
             {
-                return error;
+                if (notification.HasValue)
+                {
+                    notification.Value = TypeUtilities.CastOrDefault(value, type);
+                }
+
+                return notification;
             }
         }
 
@@ -637,6 +660,8 @@ namespace Avalonia
                     SetValue(property, notification.Value);
                 }
 
+                BindingNotificationReceived(property, notification);
+
                 if (notification.ErrorType == BindingErrorType.Error)
                 {
                     Logger.Error(

+ 19 - 3
src/Avalonia.Base/AvaloniaProperty.cs

@@ -251,6 +251,9 @@ namespace Avalonia
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
         /// <param name="validate">A validation function.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         /// <param name="notifying">
         /// A method that gets called before and after the property starts being notified on an
         /// object; the bool argument will be true before and false afterwards. This callback is
@@ -263,6 +266,7 @@ namespace Avalonia
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
             Func<TOwner, TValue, TValue> validate = null,
+            bool enableDataValidation = false,
             Action<IAvaloniaObject, bool> notifying = null)
                 where TOwner : IAvaloniaObject
         {
@@ -294,13 +298,17 @@ namespace Avalonia
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
         /// <param name="validate">A validation function.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
         public static AttachedProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
             string name,
             TValue defaultValue = default(TValue),
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
-            Func<THost, TValue, TValue> validate = null)
+            Func<THost, TValue, TValue> validate = null,
+            bool enableDataValidation = false)
                 where THost : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);
@@ -326,6 +334,9 @@ namespace Avalonia
         /// <param name="inherits">Whether the property inherits its value.</param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
         /// <param name="validate">A validation function.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
         public static AttachedProperty<TValue> RegisterAttached<THost, TValue>(
             string name,
@@ -333,7 +344,8 @@ namespace Avalonia
             TValue defaultValue = default(TValue),
             bool inherits = false,
             BindingMode defaultBindingMode = BindingMode.OneWay,
-            Func<THost, TValue, TValue> validate = null)
+            Func<THost, TValue, TValue> validate = null,
+            bool enableDataValidation = false)
                 where THost : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);
@@ -360,13 +372,17 @@ namespace Avalonia
         /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
         /// </param>
         /// <param name="defaultBindingMode">The default binding mode for the property.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
         public static DirectProperty<TOwner, TValue> RegisterDirect<TOwner, TValue>(
             string name,
             Func<TOwner, TValue> getter,
             Action<TOwner, TValue> setter = null,
             TValue unsetValue = default(TValue),
-            BindingMode defaultBindingMode = BindingMode.OneWay)
+            BindingMode defaultBindingMode = BindingMode.OneWay,
+            bool enableDataValidation = false)
                 where TOwner : IAvaloniaObject
         {
             Contract.Requires<ArgumentNullException>(name != null);

+ 43 - 14
src/Avalonia.Base/Data/BindingNotification.cs

@@ -87,7 +87,7 @@ namespace Avalonia.Data
         /// Gets the value that should be passed to the target when <see cref="HasValue"/>
         /// is true.
         /// </summary>
-        public object Value { get; }
+        public object Value { get; set; }
 
         /// <summary>
         /// Gets a value indicating whether <see cref="Value"/> should be pushed to the target.
@@ -97,13 +97,19 @@ namespace Avalonia.Data
         /// <summary>
         /// Gets the error that occurred on the source, if any.
         /// </summary>
-        public Exception Error { get; }
+        public Exception Error { get; private set; }
 
         /// <summary>
         /// Gets the type of error that <see cref="Error"/> represents, if any.
         /// </summary>
-        public BindingErrorType ErrorType { get; }
+        public BindingErrorType ErrorType { get; private set; }
 
+        /// <summary>
+        /// Compares two instances of <see cref="BindingNotification"/> for equality.
+        /// </summary>
+        /// <param name="a">The first instance.</param>
+        /// <param name="b">The second instance.</param>
+        /// <returns>true if the two instances are equal; otherwise false.</returns>
         public static bool operator ==(BindingNotification a, BindingNotification b)
         {
             if (object.ReferenceEquals(a, b))
@@ -122,45 +128,68 @@ namespace Avalonia.Data
                    (a.ErrorType == BindingErrorType.None || ExceptionEquals(a.Error, b.Error));
         }
 
+        /// <summary>
+        /// Compares two instances of <see cref="BindingNotification"/> for inequality.
+        /// </summary>
+        /// <param name="a">The first instance.</param>
+        /// <param name="b">The second instance.</param>
+        /// <returns>true if the two instances are unequal; otherwise false.</returns>
         public static bool operator !=(BindingNotification a, BindingNotification b)
         {
             return !(a == b);
         }
 
+        /// <summary>
+        /// Compares an object to an instance of <see cref="BindingNotification"/> for equality.
+        /// </summary>
+        /// <param name="obj">The object to compare.</param>
+        /// <returns>true if the two instances are equal; otherwise false.</returns>
         public override bool Equals(object obj)
         {
             return Equals(obj as BindingNotification);
         }
 
+        /// <summary>
+        /// Compares a value to an instance of <see cref="BindingNotification"/> for equality.
+        /// </summary>
+        /// <param name="other">The value to compare.</param>
+        /// <returns>true if the two instances are equal; otherwise false.</returns>
         public bool Equals(BindingNotification other)
         {
             return this == other;
         }
 
+        /// <summary>
+        /// Gets the hash code for this instance of <see cref="BindingNotification"/>. 
+        /// </summary>
+        /// <returns>A hash code.</returns>
         public override int GetHashCode()
         {
             return base.GetHashCode();
         }
 
-        public BindingNotification WithError(Exception e)
+        /// <summary>
+        /// Adds an error to the <see cref="BindingNotification"/>.
+        /// </summary>
+        /// <param name="e">The error to add.</param>
+        /// <param name="type">The error type.</param>
+        public void AddError(Exception e, BindingErrorType type)
         {
-            if (e == null)
-            {
-                return this;
-            }
+            Contract.Requires<ArgumentNullException>(e != null);
+            Contract.Requires<ArgumentException>(type != BindingErrorType.None);
 
             if (Error != null)
             {
-                e = new AggregateException(Error, e);
+                Error = new AggregateException(Error, e);
             }
-
-            if (HasValue)
+            else
             {
-                return new BindingNotification(e, BindingErrorType.Error, Value);
+                Error = e;
             }
-            else
+
+            if (type == BindingErrorType.Error || ErrorType == BindingErrorType.Error)
             {
-                return new BindingNotification(e, BindingErrorType.Error, Value);
+                ErrorType = BindingErrorType.Error;
             }
         }
 

+ 6 - 2
src/Avalonia.Base/DirectPropertyMetadata`1.cs

@@ -17,10 +17,14 @@ namespace Avalonia
         /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
         /// </param>
         /// <param name="defaultBindingMode">The default binding mode.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         public DirectPropertyMetadata(
             TValue unsetValue = default(TValue),
-            BindingMode defaultBindingMode = BindingMode.Default)
-                : base(defaultBindingMode)
+            BindingMode defaultBindingMode = BindingMode.Default,
+            bool enableDataValidation = false)
+                : base(defaultBindingMode, enableDataValidation)
         {
             UnsetValue = unsetValue;
         }

+ 8 - 0
src/Avalonia.Base/IPriorityValueOwner.cs

@@ -17,5 +17,13 @@ namespace Avalonia
         /// <param name="oldValue">The old value.</param>
         /// <param name="newValue">The new value.</param>
         void Changed(PriorityValue sender, object oldValue, object newValue);
+
+        /// <summary>
+        /// Called when a <see cref="BindingNotification"/> is received by a 
+        /// <see cref="PriorityValue"/>.
+        /// </summary>
+        /// <param name="sender">The source of the change.</param>
+        /// <param name="notification">The notification.</param>
+        void BindingNotificationReceived(PriorityValue sender, BindingNotification notification);
     }
 }

+ 21 - 1
src/Avalonia.Base/PriorityValue.cs

@@ -237,8 +237,14 @@ namespace Avalonia
         /// <param name="priority">The priority level that the value came from.</param>
         private void UpdateValue(object value, int priority)
         {
+            var notification = value as BindingNotification;
             object castValue;
 
+            if (notification != null)
+            {
+                value = (notification.HasValue) ? notification.Value : null;
+            }
+
             if (TypeUtilities.TryCast(_valueType, value, out castValue))
             {
                 var old = _value;
@@ -250,7 +256,21 @@ namespace Avalonia
 
                 ValuePriority = priority;
                 _value = castValue;
-                _owner?.Changed(this, old, _value);
+
+                if (notification?.HasValue == true)
+                {
+                    notification.Value = castValue;
+                }
+
+                if (notification == null || notification.HasValue)
+                {
+                    _owner?.Changed(this, old, _value);
+                }
+
+                if (notification != null)
+                {
+                    _owner?.BindingNotificationReceived(this, notification);
+                }
             }
             else
             {

+ 18 - 1
src/Avalonia.Base/PropertyMetadata.cs

@@ -17,9 +17,15 @@ namespace Avalonia
         /// Initializes a new instance of the <see cref="PropertyMetadata"/> class.
         /// </summary>
         /// <param name="defaultBindingMode">The default binding mode.</param>
-        public PropertyMetadata(BindingMode defaultBindingMode = BindingMode.Default)
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
+        public PropertyMetadata(
+            BindingMode defaultBindingMode = BindingMode.Default,
+            bool enableDataValidation = false)
         {
             _defaultBindingMode = defaultBindingMode;
+            EnabledDataValidation = enableDataValidation;
         }
 
         /// <summary>
@@ -34,6 +40,17 @@ namespace Avalonia
             }
         }
 
+        /// <summary>
+        /// Gets a value indicating whether the property is interested in data validation.
+        /// </summary>
+        /// <remarks>
+        /// Data validation is validation performed at the target of a binding, for example in a
+        /// view model using the INotifyDataErrorInfo interface. Only certain properties on a
+        /// control (such as a TextBox's Text property) will be interested in recieving data
+        /// validation messages so this feature must be explicitly enabled by setting this flag.
+        /// </remarks>
+        public bool EnabledDataValidation { get; }
+
         /// <summary>
         /// Merges the metadata with the base metadata.
         /// </summary>

+ 6 - 2
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@@ -18,11 +18,15 @@ namespace Avalonia
         /// <param name="defaultValue">The default value of the property.</param>
         /// <param name="validate">A validation function.</param>
         /// <param name="defaultBindingMode">The default binding mode.</param>
+        /// <param name="enableDataValidation">
+        /// Whether the property is interested in data validation.
+        /// </param>
         public StyledPropertyMetadata(
             TValue defaultValue = default(TValue),
             Func<IAvaloniaObject, TValue, TValue> validate = null,
-            BindingMode defaultBindingMode = BindingMode.Default)
-                : base(defaultBindingMode)
+            BindingMode defaultBindingMode = BindingMode.Default,
+            bool enableDataValidation = false)
+                : base(defaultBindingMode, enableDataValidation)
         {
             DefaultValue = defaultValue;
             Validate = validate;

+ 143 - 89
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

@@ -1,7 +1,10 @@
 using System;
+using System.Collections;
+using System.Collections.Generic;
 using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Markup.Xaml.Data;
+using Avalonia.UnitTests;
 using Xunit;
 
 namespace Avalonia.Markup.Xaml.UnitTests.Data
@@ -9,124 +12,173 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
     public class BindingTests_Validation
     {
         [Fact]
-        public void Disabled_Validation_Should_Trigger_Validation_Change_On_Exception()
+        public void Non_Validated_Property_Does_Not_Receive_BindingNotifications()
         {
             var source = new ValidationTestModel { MustBePositive = 5 };
-            var target = new TestControl { DataContext = source };
-            var binding = new Binding
+            var target = new TestControl
             {
-                Path = nameof(source.MustBePositive),
-                Mode = BindingMode.TwoWay,
-
-                // Even though EnableValidation = false, exception validation is enabled.
-                EnableValidation = false,
+                DataContext = source,
+                [!TestControl.NonValidatedProperty] = new Binding(nameof(source.MustBePositive)),
             };
 
-            target.Bind(TestControl.ValidationTestProperty, binding);
-
-            target.ValidationTest = -5;
-
-            Assert.True(false);
-            //Assert.False(target.ValidationStatus.IsValid);
+            Assert.Empty(target.Notifications);
         }
 
         [Fact]
-        public void Enabled_Validation_Should_Trigger_Validation_Change_On_Exception()
+        public void Validated_Property_Does_Not_Receive_BindingNotifications()
         {
             var source = new ValidationTestModel { MustBePositive = 5 };
-            var target = new TestControl { DataContext = source };
-            var binding = new Binding
+            var target = new TestControl
             {
-                Path = nameof(source.MustBePositive),
-                Mode = BindingMode.TwoWay,
-                EnableValidation = true,
+                DataContext = source,
+                [!TestControl.ValidatedProperty] = new Binding(nameof(source.MustBePositive)),
             };
 
-            target.Bind(TestControl.ValidationTestProperty, binding);
+            source.MustBePositive = 6;
 
-            target.ValidationTest = -5;
-            Assert.True(false);
-            //Assert.False(target.ValidationStatus.IsValid);
+            Assert.Equal(
+                new[]
+                {
+                    new BindingNotification(5),
+                    new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError),
+                    new BindingNotification(6),
+                },
+                target.Notifications);
         }
 
+        //[Fact]
+        //public void Disabled_Validation_Should_Trigger_Validation_Change_On_Exception()
+        //{
+        //    var source = new ValidationTestModel { MustBePositive = 5 };
+        //    var target = new TestControl { DataContext = source };
+        //    var binding = new Binding
+        //    {
+        //        Path = nameof(source.MustBePositive),
+        //        Mode = BindingMode.TwoWay,
+
+        //        // Even though EnableValidation = false, exception validation is enabled.
+        //        EnableValidation = false,
+        //    };
+
+        //    target.Bind(TestControl.ValidationTestProperty, binding);
+
+        //    target.ValidationTest = -5;
+
+        //    Assert.True(false);
+        //    //Assert.False(target.ValidationStatus.IsValid);
+        //}
+
+        //[Fact]
+        //public void Enabled_Validation_Should_Trigger_Validation_Change_On_Exception()
+        //{
+        //    var source = new ValidationTestModel { MustBePositive = 5 };
+        //    var target = new TestControl { DataContext = source };
+        //    var binding = new Binding
+        //    {
+        //        Path = nameof(source.MustBePositive),
+        //        Mode = BindingMode.TwoWay,
+        //        EnableValidation = true,
+        //    };
+
+        //    target.Bind(TestControl.ValidationTestProperty, binding);
+
+        //    target.ValidationTest = -5;
+        //    Assert.True(false);
+        //    //Assert.False(target.ValidationStatus.IsValid);
+        //}
+
+
+        //[Fact]
+        //public void Passed_Validation_Should_Not_Add_Invalid_Pseudo_Class()
+        //{
+        //    var control = new TestControl();
+        //    var model = new ValidationTestModel { MustBePositive = 1 };
+        //    var binding = new Binding
+        //    {
+        //        Path = nameof(model.MustBePositive),
+        //        Mode = BindingMode.TwoWay,
+        //        EnableValidation = true,
+        //    };
+
+        //    control.Bind(TestControl.ValidationTestProperty, binding);
+        //    control.DataContext = model;
+        //    Assert.DoesNotContain(control.Classes, x => x == ":invalid");
+        //}
+
+        //[Fact]
+        //public void Failed_Validation_Should_Add_Invalid_Pseudo_Class()
+        //{
+        //    var control = new TestControl();
+        //    var model = new ValidationTestModel { MustBePositive = 1 };
+        //    var binding = new Binding
+        //    {
+        //        Path = nameof(model.MustBePositive),
+        //        Mode = BindingMode.TwoWay,
+        //        EnableValidation = true,
+        //    };
+
+        //    control.Bind(TestControl.ValidationTestProperty, binding);
+        //    control.DataContext = model;
+        //    control.ValidationTest = -5;
+        //    Assert.Contains(control.Classes, x => x == ":invalid");
+        //}
+
+        //[Fact]
+        //public void Failed_Then_Passed_Validation_Should_Remove_Invalid_Pseudo_Class()
+        //{
+        //    var control = new TestControl();
+        //    var model = new ValidationTestModel { MustBePositive = 1 };
+
+        //    var binding = new Binding
+        //    {
+        //        Path = nameof(model.MustBePositive),
+        //        Mode = BindingMode.TwoWay,
+        //        EnableValidation = true,
+        //    };
+
+        //    control.Bind(TestControl.ValidationTestProperty, binding);
+        //    control.DataContext = model;
+
+
+        //    control.ValidationTest = -5;
+        //    Assert.Contains(control.Classes, x => x == ":invalid");
+        //    control.ValidationTest = 5;
+        //    Assert.DoesNotContain(control.Classes, x => x == ":invalid");
+        //}
 
-        [Fact]
-        public void Passed_Validation_Should_Not_Add_Invalid_Pseudo_Class()
+        private class TestControl : Control
         {
-            var control = new TestControl();
-            var model = new ValidationTestModel { MustBePositive = 1 };
-            var binding = new Binding
-            {
-                Path = nameof(model.MustBePositive),
-                Mode = BindingMode.TwoWay,
-                EnableValidation = true,
-            };
+            public static readonly StyledProperty<int> NonValidatedProperty =
+                AvaloniaProperty.Register<TestControl, int>(
+                    nameof(Validated),
+                    enableDataValidation: false);
 
-            control.Bind(TestControl.ValidationTestProperty, binding);
-            control.DataContext = model;
-            Assert.DoesNotContain(control.Classes, x => x == ":invalid");
-        }
+            public static readonly StyledProperty<int> ValidatedProperty =
+                AvaloniaProperty.Register<TestControl, int>(
+                    nameof(Validated),
+                    enableDataValidation: true);
 
-        [Fact]
-        public void Failed_Validation_Should_Add_Invalid_Pseudo_Class()
-        {
-            var control = new TestControl();
-            var model = new ValidationTestModel { MustBePositive = 1 };
-            var binding = new Binding
+            public int NonValidated
             {
-                Path = nameof(model.MustBePositive),
-                Mode = BindingMode.TwoWay,
-                EnableValidation = true,
-            };
-
-            control.Bind(TestControl.ValidationTestProperty, binding);
-            control.DataContext = model;
-            control.ValidationTest = -5;
-            Assert.Contains(control.Classes, x => x == ":invalid");
-        }
-
-        [Fact]
-        public void Failed_Then_Passed_Validation_Should_Remove_Invalid_Pseudo_Class()
-        {
-            var control = new TestControl();
-            var model = new ValidationTestModel { MustBePositive = 1 };
+                get { return GetValue(NonValidatedProperty); }
+                set { SetValue(NonValidatedProperty, value); }
+            }
 
-            var binding = new Binding
+            public int Validated
             {
-                Path = nameof(model.MustBePositive),
-                Mode = BindingMode.TwoWay,
-                EnableValidation = true,
-            };
-
-            control.Bind(TestControl.ValidationTestProperty, binding);
-            control.DataContext = model;
-
-
-            control.ValidationTest = -5;
-            Assert.Contains(control.Classes, x => x == ":invalid");
-            control.ValidationTest = 5;
-            Assert.DoesNotContain(control.Classes, x => x == ":invalid");
-        }
+                get { return GetValue(ValidatedProperty); }
+                set { SetValue(ValidatedProperty, value); }
+            }
 
-        private class TestControl : Control
-        {
-            public static readonly StyledProperty<int> ValidationTestProperty
-                = AvaloniaProperty.Register<TestControl, int>(nameof(ValidationTest), 1, defaultBindingMode: BindingMode.TwoWay);
+            public IList<BindingNotification> Notifications { get; } = new List<BindingNotification>();
 
-            public int ValidationTest
+            protected override void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
             {
-                get
-                {
-                    return GetValue(ValidationTestProperty);
-                }
-                set
-                {
-                    SetValue(ValidationTestProperty, value);
-                }
+                Notifications.Add(notification);
             }
         }
         
-        private class ValidationTestModel
+        private class ValidationTestModel : NotifyingBase
         {
             private int mustBePositive;
 
@@ -139,7 +191,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
                     {
                         throw new ArgumentOutOfRangeException(nameof(value));
                     }
+
                     mustBePositive = value;
+                    RaisePropertyChanged();
                 }
             }
         }