committed by
GitHub
29 changed files with 953 additions and 441 deletions
@ -1,121 +1,25 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class LocalValueBindingObserver<T> : IObserver<T>, |
|||
IObserver<BindingValue<T>>, |
|||
IDisposable |
|||
internal class LocalValueBindingObserver<T> : LocalValueBindingObserverBase<T>, |
|||
IObserver<object?> |
|||
{ |
|||
private readonly ValueStore _owner; |
|||
private IDisposable? _subscription; |
|||
private T? _defaultValue; |
|||
private bool _isDefaultValueInitialized; |
|||
|
|||
public LocalValueBindingObserver(ValueStore owner, StyledProperty<T> property) |
|||
: base(owner, property) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
} |
|||
|
|||
public StyledProperty<T> Property { get;} |
|||
|
|||
public void Start(IObservable<T> source) |
|||
{ |
|||
_subscription = source.Subscribe(this); |
|||
} |
|||
|
|||
public void Start(IObservable<BindingValue<T>> source) |
|||
{ |
|||
_subscription = source.Subscribe(this); |
|||
} |
|||
public void Start(IObservable<object?> source) => _subscription = source.Subscribe(this); |
|||
|
|||
public void Dispose() |
|||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.ImplicitTypeConvertionSupressWarningMessage)] |
|||
public void OnNext(object? value) |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
_owner.OnLocalValueBindingCompleted(Property, this); |
|||
} |
|||
|
|||
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); |
|||
public void OnError(Exception error) => OnCompleted(); |
|||
|
|||
public void OnNext(T value) |
|||
{ |
|||
static void Execute(LocalValueBindingObserver<T> instance, T value) |
|||
{ |
|||
var owner = instance._owner; |
|||
var property = instance.Property; |
|||
|
|||
if (property.ValidateValue?.Invoke(value) == false) |
|||
value = instance.GetCachedDefaultValue(); |
|||
|
|||
owner.SetValue(property, value, BindingPriority.LocalValue); |
|||
} |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Execute(this, value); |
|||
} |
|||
else |
|||
{ |
|||
// To avoid allocating closure in the outer scope we need to capture variables
|
|||
// locally. This allows us to skip most of the allocations when on UI thread.
|
|||
var instance = this; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => Execute(instance, newValue)); |
|||
} |
|||
} |
|||
|
|||
public void OnNext(BindingValue<T> value) |
|||
{ |
|||
static void Execute(LocalValueBindingObserver<T> instance, BindingValue<T> value) |
|||
{ |
|||
var owner = instance._owner; |
|||
var property = instance.Property; |
|||
|
|||
LoggingUtils.LogIfNecessary(owner.Owner, property, value); |
|||
|
|||
if (value.HasValue) |
|||
{ |
|||
var effectiveValue = value.Value; |
|||
if (property.ValidateValue?.Invoke(effectiveValue) == false) |
|||
effectiveValue = instance.GetCachedDefaultValue(); |
|||
owner.SetValue(property, effectiveValue, BindingPriority.LocalValue); |
|||
} |
|||
else |
|||
{ |
|||
owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); |
|||
} |
|||
} |
|||
|
|||
if (value.Type is BindingValueType.DoNothing or BindingValueType.DataValidationError) |
|||
if (value == BindingOperations.DoNothing) |
|||
return; |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Execute(this, value); |
|||
} |
|||
else |
|||
{ |
|||
// To avoid allocating closure in the outer scope we need to capture variables
|
|||
// locally. This allows us to skip most of the allocations when on UI thread.
|
|||
var instance = this; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => Execute(instance, newValue)); |
|||
} |
|||
} |
|||
|
|||
private T GetCachedDefaultValue() |
|||
{ |
|||
if (!_isDefaultValueInitialized) |
|||
{ |
|||
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); |
|||
_isDefaultValueInitialized = true; |
|||
} |
|||
|
|||
return _defaultValue!; |
|||
base.OnNext(BindingValue<T>.FromUntyped(value, Property.PropertyType)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,133 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class LocalValueBindingObserverBase<T> : IObserver<T>, |
|||
IObserver<BindingValue<T>>, |
|||
IDisposable |
|||
{ |
|||
private readonly ValueStore _owner; |
|||
private readonly bool _hasDataValidation; |
|||
protected IDisposable? _subscription; |
|||
private T? _defaultValue; |
|||
private bool _isDefaultValueInitialized; |
|||
|
|||
protected LocalValueBindingObserverBase(ValueStore owner, StyledProperty<T> property) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
_hasDataValidation = property.GetMetadata(owner.Owner.GetType()).EnableDataValidation ?? false; |
|||
} |
|||
|
|||
public StyledProperty<T> Property { get;} |
|||
|
|||
public void Start(IObservable<T> source) |
|||
{ |
|||
_subscription = source.Subscribe(this); |
|||
} |
|||
|
|||
public void Start(IObservable<BindingValue<T>> source) |
|||
{ |
|||
_subscription = source.Subscribe(this); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
OnCompleted(); |
|||
} |
|||
|
|||
public void OnCompleted() |
|||
{ |
|||
if (_hasDataValidation) |
|||
_owner.Owner.OnUpdateDataValidation(Property, BindingValueType.UnsetValue, null); |
|||
|
|||
_owner.OnLocalValueBindingCompleted(Property, this); |
|||
} |
|||
|
|||
public void OnError(Exception error) => OnCompleted(); |
|||
|
|||
public void OnNext(T value) |
|||
{ |
|||
static void Execute(LocalValueBindingObserverBase<T> instance, T value) |
|||
{ |
|||
var owner = instance._owner; |
|||
var property = instance.Property; |
|||
|
|||
if (property.ValidateValue?.Invoke(value) == false) |
|||
value = instance.GetCachedDefaultValue(); |
|||
|
|||
owner.SetLocalValue(property, value); |
|||
|
|||
if (instance._hasDataValidation) |
|||
owner.Owner.OnUpdateDataValidation(property, BindingValueType.Value, null); |
|||
} |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Execute(this, value); |
|||
} |
|||
else |
|||
{ |
|||
// To avoid allocating closure in the outer scope we need to capture variables
|
|||
// locally. This allows us to skip most of the allocations when on UI thread.
|
|||
var instance = this; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => Execute(instance, newValue)); |
|||
} |
|||
} |
|||
|
|||
public void OnNext(BindingValue<T> value) |
|||
{ |
|||
static void Execute(LocalValueBindingObserverBase<T> instance, BindingValue<T> value) |
|||
{ |
|||
var owner = instance._owner; |
|||
var property = instance.Property; |
|||
var originalType = value.Type; |
|||
|
|||
LoggingUtils.LogIfNecessary(owner.Owner, property, value); |
|||
|
|||
// Revert to the default value if the binding value fails validation, or if
|
|||
// there was no value (though not if there was a data validation error).
|
|||
if ((value.HasValue && property.ValidateValue?.Invoke(value.Value) == false) || |
|||
(!value.HasValue && value.Type != BindingValueType.DataValidationError)) |
|||
value = value.WithValue(instance.GetCachedDefaultValue()); |
|||
|
|||
if (value.HasValue) |
|||
owner.SetLocalValue(property, value.Value); |
|||
if (instance._hasDataValidation) |
|||
owner.Owner.OnUpdateDataValidation(property, originalType, value.Error); |
|||
} |
|||
|
|||
if (value.Type is BindingValueType.DoNothing) |
|||
return; |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Execute(this, value); |
|||
} |
|||
else |
|||
{ |
|||
// To avoid allocating closure in the outer scope we need to capture variables
|
|||
// locally. This allows us to skip most of the allocations when on UI thread.
|
|||
var instance = this; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => Execute(instance, newValue)); |
|||
} |
|||
} |
|||
|
|||
private T GetCachedDefaultValue() |
|||
{ |
|||
if (!_isDefaultValueInitialized) |
|||
{ |
|||
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); |
|||
_isDefaultValueInitialized = true; |
|||
} |
|||
|
|||
return _defaultValue!; |
|||
} |
|||
} |
|||
} |
|||
@ -1,94 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
using Avalonia.Threading; |
|||
|
|||
namespace Avalonia.PropertyStore |
|||
{ |
|||
internal class LocalValueUntypedBindingObserver<T> : IObserver<object?>, |
|||
IDisposable |
|||
{ |
|||
private readonly ValueStore _owner; |
|||
private IDisposable? _subscription; |
|||
private T? _defaultValue; |
|||
private bool _isDefaultValueInitialized; |
|||
|
|||
public LocalValueUntypedBindingObserver(ValueStore owner, StyledProperty<T> property) |
|||
{ |
|||
_owner = owner; |
|||
Property = property; |
|||
} |
|||
|
|||
public StyledProperty<T> Property { get; } |
|||
|
|||
public void Start(IObservable<object?> source) |
|||
{ |
|||
_subscription = source.Subscribe(this); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_subscription?.Dispose(); |
|||
_subscription = null; |
|||
_owner.OnLocalValueBindingCompleted(Property, this); |
|||
} |
|||
|
|||
public void OnCompleted() => _owner.OnLocalValueBindingCompleted(Property, this); |
|||
public void OnError(Exception error) => OnCompleted(); |
|||
|
|||
public void OnNext(object? value) |
|||
{ |
|||
static void Execute(LocalValueUntypedBindingObserver<T> instance, object? value) |
|||
{ |
|||
var owner = instance._owner; |
|||
var property = instance.Property; |
|||
|
|||
if (value is BindingNotification n) |
|||
{ |
|||
value = n.Value; |
|||
LoggingUtils.LogIfNecessary(owner.Owner, property, n); |
|||
} |
|||
|
|||
if (value == AvaloniaProperty.UnsetValue) |
|||
{ |
|||
owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); |
|||
} |
|||
else if (UntypedValueUtils.TryConvertAndValidate(property, value, out var typedValue)) |
|||
{ |
|||
owner.SetValue(property, typedValue, BindingPriority.LocalValue); |
|||
} |
|||
else |
|||
{ |
|||
owner.SetValue(property, instance.GetCachedDefaultValue(), BindingPriority.LocalValue); |
|||
LoggingUtils.LogInvalidValue(owner.Owner, property, typeof(T), value); |
|||
} |
|||
} |
|||
|
|||
if (value == BindingOperations.DoNothing) |
|||
return; |
|||
|
|||
if (Dispatcher.UIThread.CheckAccess()) |
|||
{ |
|||
Execute(this, value); |
|||
} |
|||
else if (value != BindingOperations.DoNothing) |
|||
{ |
|||
// To avoid allocating closure in the outer scope we need to capture variables
|
|||
// locally. This allows us to skip most of the allocations when on UI thread.
|
|||
var instance = this; |
|||
var newValue = value; |
|||
Dispatcher.UIThread.Post(() => Execute(instance, newValue)); |
|||
} |
|||
} |
|||
|
|||
private T GetCachedDefaultValue() |
|||
{ |
|||
if (!_isDefaultValueInitialized) |
|||
{ |
|||
_defaultValue = Property.GetDefaultValue(_owner.Owner.GetType()); |
|||
_isDefaultValueInitialized = true; |
|||
} |
|||
|
|||
return _defaultValue!; |
|||
} |
|||
} |
|||
} |
|||
@ -1,72 +1,404 @@ |
|||
using System; |
|||
using System.Reactive.Linq; |
|||
using System.Collections; |
|||
using System.ComponentModel; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.Data.Core; |
|||
using Avalonia.Markup.Data; |
|||
using Avalonia.Styling; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Markup.UnitTests.Data |
|||
{ |
|||
public class BindingTests_DataValidation |
|||
{ |
|||
[Fact] |
|||
public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_LocalValue() |
|||
public abstract class TestBase<T> |
|||
where T : AvaloniaProperty<int> |
|||
{ |
|||
var textBlock = new TextBlock |
|||
[Fact] |
|||
public void Setter_Exception_Causes_DataValidation_Error() |
|||
{ |
|||
var (target, property) = CreateTarget(); |
|||
var binding = new Binding(nameof(ExceptionValidatingModel.Value)) |
|||
{ |
|||
Mode = BindingMode.TwoWay |
|||
}; |
|||
|
|||
target.DataContext = new ExceptionValidatingModel(); |
|||
target.Bind(property, binding); |
|||
|
|||
Assert.Equal(20, target.GetValue(property)); |
|||
|
|||
target.SetValue(property, 200); |
|||
|
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<ArgumentOutOfRangeException>(target.DataValidationError); |
|||
|
|||
target.SetValue(property, 10); |
|||
|
|||
Assert.Equal(10, target.GetValue(property)); |
|||
Assert.Null(target.DataValidationError); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Indei_Error_Causes_DataValidation_Error() |
|||
{ |
|||
var (target, property) = CreateTarget(); |
|||
var binding = new Binding(nameof(IndeiValidatingModel.Value)) |
|||
{ |
|||
Mode = BindingMode.TwoWay |
|||
}; |
|||
|
|||
target.DataContext = new IndeiValidatingModel(); |
|||
target.Bind(property, binding); |
|||
|
|||
Assert.Equal(20, target.GetValue(property)); |
|||
|
|||
target.SetValue(property, 200); |
|||
|
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
|
|||
target.SetValue(property, 10); |
|||
|
|||
Assert.Equal(10, target.GetValue(property)); |
|||
Assert.Null(target.DataValidationError); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Disposing_Binding_Subscription_Clears_DataValidation() |
|||
{ |
|||
DataContext = new Class1(), |
|||
}; |
|||
var (target, property) = CreateTarget(); |
|||
var binding = new Binding(nameof(ExceptionValidatingModel.Value)) |
|||
{ |
|||
Mode = BindingMode.TwoWay |
|||
}; |
|||
|
|||
target.DataContext = new IndeiValidatingModel |
|||
{ |
|||
Value = 200, |
|||
}; |
|||
|
|||
var sub = target.Bind(property, binding); |
|||
|
|||
var target = new Binding(nameof(Class1.Foo)); |
|||
var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); |
|||
var subject = (BindingExpression)instanced.Source; |
|||
object result = null; |
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
|
|||
subject.Subscribe(x => result = x); |
|||
sub.Dispose(); |
|||
|
|||
Assert.IsType<string>(result); |
|||
Assert.Null(target.DataValidationError); |
|||
} |
|||
|
|||
private protected abstract (DataValidationTestControl, T) CreateTarget(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue() |
|||
public class DirectPropertyTests : TestBase<DirectPropertyBase<int>> |
|||
{ |
|||
var textBlock = new TextBlock |
|||
private protected override (DataValidationTestControl, DirectPropertyBase<int>) CreateTarget() |
|||
{ |
|||
DataContext = new Class1(), |
|||
}; |
|||
return (new ValidatedDirectPropertyClass(), ValidatedDirectPropertyClass.ValueProperty); |
|||
} |
|||
} |
|||
|
|||
var target = new Binding(nameof(Class1.Foo)); |
|||
var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); |
|||
var subject = (BindingExpression)instanced.Source; |
|||
object result = null; |
|||
public class StyledPropertyTests : TestBase<StyledProperty<int>> |
|||
{ |
|||
[Fact] |
|||
public void Style_Binding_Supports_Data_Validation() |
|||
{ |
|||
var (target, property) = CreateTarget(); |
|||
var binding = new Binding(nameof(IndeiValidatingModel.Value)) |
|||
{ |
|||
Mode = BindingMode.TwoWay |
|||
}; |
|||
|
|||
var model = new IndeiValidatingModel(); |
|||
var root = new TestRoot |
|||
{ |
|||
DataContext = model, |
|||
Styles = |
|||
{ |
|||
new Style(x => x.Is<DataValidationTestControl>()) |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(property, binding) |
|||
} |
|||
} |
|||
}, |
|||
Child = target, |
|||
}; |
|||
|
|||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|||
|
|||
Assert.Equal(20, target.GetValue(property)); |
|||
|
|||
subject.Subscribe(x => result = x); |
|||
model.Value = 200; |
|||
|
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
|
|||
model.Value = 10; |
|||
|
|||
Assert.Equal(10, target.GetValue(property)); |
|||
Assert.Null(target.DataValidationError); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Style_With_Activator_Binding_Supports_Data_Validation() |
|||
{ |
|||
var (target, property) = CreateTarget(); |
|||
var binding = new Binding(nameof(IndeiValidatingModel.Value)) |
|||
{ |
|||
Mode = BindingMode.TwoWay |
|||
}; |
|||
|
|||
Assert.Equal(new BindingNotification("foo"), result); |
|||
var model = new IndeiValidatingModel |
|||
{ |
|||
Value = 200, |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
DataContext = model, |
|||
Styles = |
|||
{ |
|||
new Style(x => x.Is<DataValidationTestControl>().Class("foo")) |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(property, binding) |
|||
} |
|||
} |
|||
}, |
|||
Child = target, |
|||
}; |
|||
|
|||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|||
target.Classes.Add("foo"); |
|||
|
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
|
|||
target.Classes.Remove("foo"); |
|||
Assert.Equal(0, target.GetValue(property)); |
|||
Assert.Null(target.DataValidationError); |
|||
|
|||
target.Classes.Add("foo"); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
|
|||
model.Value = 10; |
|||
|
|||
Assert.Equal(10, target.GetValue(property)); |
|||
Assert.Null(target.DataValidationError); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Data_Validation_Can_Switch_Between_Style_And_LocalValue_Binding() |
|||
{ |
|||
var (target, property) = CreateTarget(); |
|||
var model1 = new IndeiValidatingModel { Value = 200 }; |
|||
var model2 = new IndeiValidatingModel { Value = 300 }; |
|||
var binding1 = new Binding(nameof(IndeiValidatingModel.Value)); |
|||
var binding2 = new Binding(nameof(IndeiValidatingModel.Value)) { Source = model2 }; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
DataContext = model1, |
|||
Styles = |
|||
{ |
|||
new Style(x => x.Is<DataValidationTestControl>()) |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(property, binding1) |
|||
} |
|||
} |
|||
}, |
|||
Child = target, |
|||
}; |
|||
|
|||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|||
|
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
|
|||
var sub = target.Bind(property, binding2); |
|||
Assert.Equal(300, target.GetValue(property)); |
|||
Assert.Equal("Invalid value: 300.", target.DataValidationError?.Message); |
|||
|
|||
sub.Dispose(); |
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public void Data_Validation_Can_Switch_Between_Style_And_StyleTrigger_Binding() |
|||
{ |
|||
var (target, property) = CreateTarget(); |
|||
var model1 = new IndeiValidatingModel { Value = 200 }; |
|||
var model2 = new IndeiValidatingModel { Value = 300 }; |
|||
var binding1 = new Binding(nameof(IndeiValidatingModel.Value)); |
|||
var binding2 = new Binding(nameof(IndeiValidatingModel.Value)) { Source = model2 }; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
DataContext = model1, |
|||
Styles = |
|||
{ |
|||
new Style(x => x.Is<DataValidationTestControl>()) |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(property, binding1) |
|||
} |
|||
}, |
|||
new Style(x => x.Is<DataValidationTestControl>().Class("foo")) |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter(property, binding2) |
|||
} |
|||
}, |
|||
}, |
|||
Child = target, |
|||
}; |
|||
|
|||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|||
|
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
|
|||
target.Classes.Add("foo"); |
|||
Assert.Equal(300, target.GetValue(property)); |
|||
Assert.Equal("Invalid value: 300.", target.DataValidationError?.Message); |
|||
|
|||
target.Classes.Remove("foo"); |
|||
Assert.Equal(200, target.GetValue(property)); |
|||
Assert.IsType<DataValidationException>(target.DataValidationError); |
|||
Assert.Equal("Invalid value: 200.", target.DataValidationError?.Message); |
|||
} |
|||
|
|||
private protected override (DataValidationTestControl, StyledProperty<int>) CreateTarget() |
|||
{ |
|||
return (new ValidatedStyledPropertyClass(), ValidatedStyledPropertyClass.ValueProperty); |
|||
} |
|||
} |
|||
|
|||
internal class DataValidationTestControl : Control |
|||
{ |
|||
public Exception? DataValidationError { get; protected set; } |
|||
} |
|||
|
|||
private class ValidatedStyledPropertyClass : DataValidationTestControl |
|||
{ |
|||
public static readonly StyledProperty<int> ValueProperty = |
|||
AvaloniaProperty.Register<ValidatedStyledPropertyClass, int>( |
|||
"Value", |
|||
enableDataValidation: true); |
|||
|
|||
public int Value |
|||
{ |
|||
get => GetValue(ValueProperty); |
|||
set => SetValue(ValueProperty, value); |
|||
} |
|||
|
|||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) |
|||
{ |
|||
if (property == ValueProperty) |
|||
{ |
|||
DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_TemplatedParent() |
|||
private class ValidatedDirectPropertyClass : DataValidationTestControl |
|||
{ |
|||
var textBlock = new TextBlock |
|||
public static readonly DirectProperty<ValidatedDirectPropertyClass, int> ValueProperty = |
|||
AvaloniaProperty.RegisterDirect<ValidatedDirectPropertyClass, int>( |
|||
"Value", |
|||
o => o.Value, |
|||
(o, v) => o.Value = v, |
|||
enableDataValidation: true); |
|||
|
|||
private int _value; |
|||
|
|||
public int Value |
|||
{ |
|||
DataContext = new Class1(), |
|||
}; |
|||
get => _value; |
|||
set => SetAndRaise(ValueProperty, ref _value, value); |
|||
} |
|||
|
|||
var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template }; |
|||
var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); |
|||
var subject = (BindingExpression)instanced.Source; |
|||
object result = null; |
|||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) |
|||
{ |
|||
if (property == ValueProperty) |
|||
{ |
|||
DataValidationError = state.HasAnyFlag(BindingValueType.DataValidationError) ? error : null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
subject.Subscribe(x => result = x); |
|||
private class ExceptionValidatingModel |
|||
{ |
|||
public const int MaxValue = 100; |
|||
private int _value = 20; |
|||
|
|||
Assert.IsType<string>(result); |
|||
public int Value |
|||
{ |
|||
get => _value; |
|||
set |
|||
{ |
|||
if (value > MaxValue) |
|||
throw new ArgumentOutOfRangeException(nameof(value)); |
|||
_value = value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class Class1 |
|||
private class IndeiValidatingModel : INotifyDataErrorInfo |
|||
{ |
|||
public string Foo { get; set; } = "foo"; |
|||
public const int MaxValue = 100; |
|||
private bool _hasErrors; |
|||
private int _value = 20; |
|||
|
|||
public int Value |
|||
{ |
|||
get => _value; |
|||
set |
|||
{ |
|||
_value = value; |
|||
HasErrors = value > MaxValue; |
|||
} |
|||
} |
|||
|
|||
public bool HasErrors |
|||
{ |
|||
get => _hasErrors; |
|||
private set |
|||
{ |
|||
if (_hasErrors != value) |
|||
{ |
|||
_hasErrors = value; |
|||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value))); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged; |
|||
|
|||
public IEnumerable GetErrors(string? propertyName) |
|||
{ |
|||
if (propertyName == nameof(Value) && _value > MaxValue) |
|||
yield return $"Invalid value: {_value}."; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Loading…
Reference in new issue