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/> /// <inheritdoc/>
Delegate[] IAvaloniaObjectDebug.GetPropertyChangedSubscribers() 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> /// <summary>
/// Called when a avalonia property changes on the object. /// Called when a avalonia property changes on the object.
/// </summary> /// </summary>
@ -580,15 +598,20 @@ namespace Avalonia
/// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns> /// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
private static object CastOrDefault(object value, Type type) 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); return TypeUtilities.CastOrDefault(value, type);
} }
else else
{ {
return error; if (notification.HasValue)
{
notification.Value = TypeUtilities.CastOrDefault(value, type);
}
return notification;
} }
} }
@ -637,6 +660,8 @@ namespace Avalonia
SetValue(property, notification.Value); SetValue(property, notification.Value);
} }
BindingNotificationReceived(property, notification);
if (notification.ErrorType == BindingErrorType.Error) if (notification.ErrorType == BindingErrorType.Error)
{ {
Logger.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="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param> /// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A validation function.</param> /// <param name="validate">A validation function.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
/// <param name="notifying"> /// <param name="notifying">
/// A method that gets called before and after the property starts being notified on an /// 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 /// object; the bool argument will be true before and false afterwards. This callback is
@ -263,6 +266,7 @@ namespace Avalonia
bool inherits = false, bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay, BindingMode defaultBindingMode = BindingMode.OneWay,
Func<TOwner, TValue, TValue> validate = null, Func<TOwner, TValue, TValue> validate = null,
bool enableDataValidation = false,
Action<IAvaloniaObject, bool> notifying = null) Action<IAvaloniaObject, bool> notifying = null)
where TOwner : IAvaloniaObject where TOwner : IAvaloniaObject
{ {
@ -294,13 +298,17 @@ namespace Avalonia
/// <param name="inherits">Whether the property inherits its value.</param> /// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param> /// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A validation function.</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> /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static AttachedProperty<TValue> RegisterAttached<TOwner, THost, TValue>( public static AttachedProperty<TValue> RegisterAttached<TOwner, THost, TValue>(
string name, string name,
TValue defaultValue = default(TValue), TValue defaultValue = default(TValue),
bool inherits = false, bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay, BindingMode defaultBindingMode = BindingMode.OneWay,
Func<THost, TValue, TValue> validate = null) Func<THost, TValue, TValue> validate = null,
bool enableDataValidation = false)
where THost : IAvaloniaObject where THost : IAvaloniaObject
{ {
Contract.Requires<ArgumentNullException>(name != null); Contract.Requires<ArgumentNullException>(name != null);
@ -326,6 +334,9 @@ namespace Avalonia
/// <param name="inherits">Whether the property inherits its value.</param> /// <param name="inherits">Whether the property inherits its value.</param>
/// <param name="defaultBindingMode">The default binding mode for the property.</param> /// <param name="defaultBindingMode">The default binding mode for the property.</param>
/// <param name="validate">A validation function.</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> /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static AttachedProperty<TValue> RegisterAttached<THost, TValue>( public static AttachedProperty<TValue> RegisterAttached<THost, TValue>(
string name, string name,
@ -333,7 +344,8 @@ namespace Avalonia
TValue defaultValue = default(TValue), TValue defaultValue = default(TValue),
bool inherits = false, bool inherits = false,
BindingMode defaultBindingMode = BindingMode.OneWay, BindingMode defaultBindingMode = BindingMode.OneWay,
Func<THost, TValue, TValue> validate = null) Func<THost, TValue, TValue> validate = null,
bool enableDataValidation = false)
where THost : IAvaloniaObject where THost : IAvaloniaObject
{ {
Contract.Requires<ArgumentNullException>(name != null); 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"/> /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
/// </param> /// </param>
/// <param name="defaultBindingMode">The default binding mode for the property.</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> /// <returns>A <see cref="AvaloniaProperty{TValue}"/></returns>
public static DirectProperty<TOwner, TValue> RegisterDirect<TOwner, TValue>( public static DirectProperty<TOwner, TValue> RegisterDirect<TOwner, TValue>(
string name, string name,
Func<TOwner, TValue> getter, Func<TOwner, TValue> getter,
Action<TOwner, TValue> setter = null, Action<TOwner, TValue> setter = null,
TValue unsetValue = default(TValue), TValue unsetValue = default(TValue),
BindingMode defaultBindingMode = BindingMode.OneWay) BindingMode defaultBindingMode = BindingMode.OneWay,
bool enableDataValidation = false)
where TOwner : IAvaloniaObject where TOwner : IAvaloniaObject
{ {
Contract.Requires<ArgumentNullException>(name != null); 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"/> /// Gets the value that should be passed to the target when <see cref="HasValue"/>
/// is true. /// is true.
/// </summary> /// </summary>
public object Value { get; } public object Value { get; set; }
/// <summary> /// <summary>
/// Gets a value indicating whether <see cref="Value"/> should be pushed to the target. /// Gets a value indicating whether <see cref="Value"/> should be pushed to the target.
@ -97,13 +97,19 @@ namespace Avalonia.Data
/// <summary> /// <summary>
/// Gets the error that occurred on the source, if any. /// Gets the error that occurred on the source, if any.
/// </summary> /// </summary>
public Exception Error { get; } public Exception Error { get; private set; }
/// <summary> /// <summary>
/// Gets the type of error that <see cref="Error"/> represents, if any. /// Gets the type of error that <see cref="Error"/> represents, if any.
/// </summary> /// </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) public static bool operator ==(BindingNotification a, BindingNotification b)
{ {
if (object.ReferenceEquals(a, b)) if (object.ReferenceEquals(a, b))
@ -122,45 +128,68 @@ namespace Avalonia.Data
(a.ErrorType == BindingErrorType.None || ExceptionEquals(a.Error, b.Error)); (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) public static bool operator !=(BindingNotification a, BindingNotification b)
{ {
return !(a == 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) public override bool Equals(object obj)
{ {
return Equals(obj as BindingNotification); 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) public bool Equals(BindingNotification other)
{ {
return this == 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() public override int GetHashCode()
{ {
return base.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) Contract.Requires<ArgumentNullException>(e != null);
{ Contract.Requires<ArgumentException>(type != BindingErrorType.None);
return this;
}
if (Error != null) if (Error != null)
{ {
e = new AggregateException(Error, e); Error = new AggregateException(Error, e);
} }
else
if (HasValue)
{ {
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"/> /// The value to use when the property is set to <see cref="AvaloniaProperty.UnsetValue"/>
/// </param> /// </param>
/// <param name="defaultBindingMode">The default binding mode.</param> /// <param name="defaultBindingMode">The default binding mode.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
public DirectPropertyMetadata( public DirectPropertyMetadata(
TValue unsetValue = default(TValue), TValue unsetValue = default(TValue),
BindingMode defaultBindingMode = BindingMode.Default) BindingMode defaultBindingMode = BindingMode.Default,
: base(defaultBindingMode) bool enableDataValidation = false)
: base(defaultBindingMode, enableDataValidation)
{ {
UnsetValue = unsetValue; UnsetValue = unsetValue;
} }

8
src/Avalonia.Base/IPriorityValueOwner.cs

@ -17,5 +17,13 @@ namespace Avalonia
/// <param name="oldValue">The old value.</param> /// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param> /// <param name="newValue">The new value.</param>
void Changed(PriorityValue sender, object oldValue, object newValue); 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> /// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority) private void UpdateValue(object value, int priority)
{ {
var notification = value as BindingNotification;
object castValue; object castValue;
if (notification != null)
{
value = (notification.HasValue) ? notification.Value : null;
}
if (TypeUtilities.TryCast(_valueType, value, out castValue)) if (TypeUtilities.TryCast(_valueType, value, out castValue))
{ {
var old = _value; var old = _value;
@ -250,7 +256,21 @@ namespace Avalonia
ValuePriority = priority; ValuePriority = priority;
_value = castValue; _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 else
{ {

19
src/Avalonia.Base/PropertyMetadata.cs

@ -17,9 +17,15 @@ namespace Avalonia
/// Initializes a new instance of the <see cref="PropertyMetadata"/> class. /// Initializes a new instance of the <see cref="PropertyMetadata"/> class.
/// </summary> /// </summary>
/// <param name="defaultBindingMode">The default binding mode.</param> /// <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; _defaultBindingMode = defaultBindingMode;
EnabledDataValidation = enableDataValidation;
} }
/// <summary> /// <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> /// <summary>
/// Merges the metadata with the base metadata. /// Merges the metadata with the base metadata.
/// </summary> /// </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="defaultValue">The default value of the property.</param>
/// <param name="validate">A validation function.</param> /// <param name="validate">A validation function.</param>
/// <param name="defaultBindingMode">The default binding mode.</param> /// <param name="defaultBindingMode">The default binding mode.</param>
/// <param name="enableDataValidation">
/// Whether the property is interested in data validation.
/// </param>
public StyledPropertyMetadata( public StyledPropertyMetadata(
TValue defaultValue = default(TValue), TValue defaultValue = default(TValue),
Func<IAvaloniaObject, TValue, TValue> validate = null, Func<IAvaloniaObject, TValue, TValue> validate = null,
BindingMode defaultBindingMode = BindingMode.Default) BindingMode defaultBindingMode = BindingMode.Default,
: base(defaultBindingMode) bool enableDataValidation = false)
: base(defaultBindingMode, enableDataValidation)
{ {
DefaultValue = defaultValue; DefaultValue = defaultValue;
Validate = validate; Validate = validate;

232
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

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

Loading…
Cancel
Save