Browse Source

Surface BindingNotifications in AvaloniaObject.

pull/691/head
Steven Kirk 10 years ago
parent
commit
06b0d15fc2
  1. 31
      src/Avalonia.Base/AvaloniaObject.cs
  2. 22
      src/Avalonia.Base/AvaloniaProperty.cs
  3. 57
      src/Avalonia.Base/Data/BindingNotification.cs
  4. 8
      src/Avalonia.Base/DirectPropertyMetadata`1.cs
  5. 8
      src/Avalonia.Base/IPriorityValueOwner.cs
  6. 22
      src/Avalonia.Base/PriorityValue.cs
  7. 19
      src/Avalonia.Base/PropertyMetadata.cs
  8. 8
      src/Avalonia.Base/StyledPropertyMetadata`1.cs
  9. 232
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

31
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(

22
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);

57
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;
}
}

8
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
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);
}
}

22
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
{

19
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>

8
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;

232
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();
}
}
}

Loading…
Cancel
Save