diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingTest/BindingTest.csproj
index 0cdb826e11..a1d79472d8 100644
--- a/samples/BindingTest/BindingTest.csproj
+++ b/samples/BindingTest/BindingTest.csproj
@@ -50,6 +50,7 @@
True
+
..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll
@@ -80,7 +81,9 @@
TestItemView.xaml
-
+
+
+
diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingTest/MainWindow.xaml
index 149625925a..02c364346d 100644
--- a/samples/BindingTest/MainWindow.xaml
+++ b/samples/BindingTest/MainWindow.xaml
@@ -70,9 +70,19 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
new file mode 100644
index 0000000000..634498c165
--- /dev/null
+++ b/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
@@ -0,0 +1,17 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.ComponentModel.DataAnnotations;
+
+namespace BindingTest.ViewModels
+{
+ public class DataAnnotationsErrorViewModel
+ {
+ [Phone]
+ [MaxLength(10)]
+ public string PhoneNumber { get; set; }
+
+ [Range(0, 9)]
+ public int LessThan10 { get; set; }
+ }
+}
diff --git a/samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs b/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
similarity index 79%
rename from samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs
rename to samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
index 01155f1d9f..e6071e0678 100644
--- a/samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs
+++ b/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
@@ -6,7 +6,7 @@ using System;
namespace BindingTest.ViewModels
{
- public class ExceptionPropertyErrorViewModel : ReactiveObject
+ public class ExceptionErrorViewModel : ReactiveObject
{
private int _lessThan10;
@@ -21,7 +21,7 @@ namespace BindingTest.ViewModels
}
else
{
- throw new InvalidOperationException("Value must be less than 10.");
+ throw new ArgumentOutOfRangeException("Value must be less than 10.");
}
}
}
diff --git a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs b/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs
new file mode 100644
index 0000000000..b4bb528abb
--- /dev/null
+++ b/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs
@@ -0,0 +1,73 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using ReactiveUI;
+using System;
+using System.ComponentModel;
+using System.Collections;
+
+namespace BindingTest.ViewModels
+{
+ public class IndeiErrorViewModel : ReactiveObject, INotifyDataErrorInfo
+ {
+ private int _maximum = 10;
+ private int _value;
+ private string _valueError;
+
+ public IndeiErrorViewModel()
+ {
+ this.WhenAnyValue(x => x.Maximum, x => x.Value)
+ .Subscribe(_ => UpdateErrors());
+ }
+
+ public bool HasErrors
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public int Maximum
+ {
+ get { return _maximum; }
+ set { this.RaiseAndSetIfChanged(ref _maximum, value); }
+ }
+
+ public int Value
+ {
+ get { return _value; }
+ set { this.RaiseAndSetIfChanged(ref _value, value); }
+ }
+
+ public event EventHandler ErrorsChanged;
+
+ public IEnumerable GetErrors(string propertyName)
+ {
+ switch (propertyName)
+ {
+ case nameof(Value):
+ return new[] { _valueError };
+ default:
+ return null;
+ }
+ }
+
+ private void UpdateErrors()
+ {
+ if (Value <= Maximum)
+ {
+ if (_valueError != null)
+ {
+ _valueError = null;
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+ }
+ }
+ else
+ {
+ if (_valueError == null)
+ {
+ _valueError = "Value must be less than Maximum";
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+ }
+ }
+ }
+ }
+}
diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs
index 6fbfb8a23f..94f7ff595a 100644
--- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs
+++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs
@@ -69,7 +69,8 @@ namespace BindingTest.ViewModels
public ReactiveCommand
+
+ C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile7\System.ComponentModel.Annotations.dll
+
+
- ..\..\packages\System.Reactive.Core.3.0.0\lib\net46\System.Reactive.Core.dll
+ ..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll
True
@@ -53,11 +58,15 @@
True
- ..\..\packages\System.Reactive.Linq.3.0.0\lib\net46\System.Reactive.Linq.dll
+ ..\..\packages\System.Reactive.Linq.3.0.0\lib\net45\System.Reactive.Linq.dll
True
- ..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net46\System.Reactive.PlatformServices.dll
+ ..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net45\System.Reactive.PlatformServices.dll
+ True
+
+
+ ..\..\packages\System.Reactive.Windows.Threading.3.0.0\lib\net45\System.Reactive.Windows.Threading.dll
True
@@ -66,6 +75,7 @@
+
..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll
True
@@ -85,7 +95,10 @@
-
+
+
+
+
@@ -97,9 +110,8 @@
-
-
-
+
+
diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
new file mode 100644
index 0000000000..c53dc417b0
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
@@ -0,0 +1,320 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reactive.Linq;
+using System.Threading;
+using Avalonia.Data;
+using Avalonia.Markup.Data;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+ public class BindingExpressionTests
+ {
+ [Fact]
+ public async void Should_Get_Simple_Property_Value()
+ {
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
+ var result = await target.Take(1);
+
+ Assert.Equal("foo", result);
+ }
+
+ [Fact]
+ public void Should_Set_Simple_Property_Value()
+ {
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
+
+ target.OnNext("bar");
+
+ Assert.Equal("bar", data.StringValue);
+ }
+
+ [Fact]
+ public async void Should_Convert_Get_String_To_Double()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "5.6" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+ var result = await target.Take(1);
+
+ Assert.Equal(5.6, result);
+ }
+
+ [Fact]
+ public async void Getting_Invalid_Double_String_Should_Return_BindingError()
+ {
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+ var result = await target.Take(1);
+
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
+ {
+ var data = new Class1 { StringValue = null };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public void Should_Convert_Set_String_To_Double()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = (5.6).ToString() };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+
+ target.OnNext(6.7);
+
+ Assert.Equal((6.7).ToString(), data.StringValue);
+ }
+
+ [Fact]
+ public async void Should_Convert_Get_Double_To_String()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+ var result = await target.Take(1);
+
+ Assert.Equal((5.6).ToString(), result);
+ }
+
+ [Fact]
+ public void Should_Convert_Set_Double_To_String()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext("6.7");
+
+ Assert.Equal(6.7, data.DoubleValue);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue"),
+ typeof(int),
+ 42,
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException("'foo' is not a valid number."),
+ BindingErrorType.Error,
+ 42),
+ result);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue", true),
+ typeof(int),
+ 42,
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException("'foo' is not a valid number."),
+ BindingErrorType.Error,
+ 42),
+ result);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_For_Invalid_FallbackValue()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue"),
+ typeof(int),
+ "bar",
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new AggregateException(
+ new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+ new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
+ BindingErrorType.Error),
+ result);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue", true),
+ typeof(int),
+ "bar",
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new AggregateException(
+ new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+ new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
+ BindingErrorType.Error),
+ result);
+ }
+
+ [Fact]
+ public void Setting_Invalid_Double_String_Should_Not_Change_Target()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext("foo");
+
+ Assert.Equal(5.6, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Setting_Invalid_Double_String_Should_Use_FallbackValue()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "DoubleValue"),
+ typeof(string),
+ "9.8",
+ DefaultValueConverter.Instance);
+
+ target.OnNext("foo");
+
+ Assert.Equal(9.8, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Should_Coerce_Setting_Null_Double_To_Default_Value()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext(null);
+
+ Assert.Equal(0, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext(AvaloniaProperty.UnsetValue);
+
+ Assert.Equal(0, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Should_Pass_ConverterParameter_To_Convert()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var converter = new Mock();
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "DoubleValue"),
+ typeof(string),
+ converter.Object,
+ converterParameter: "foo");
+
+ target.Subscribe(_ => { });
+
+ converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
+ }
+
+ [Fact]
+ public void Should_Pass_ConverterParameter_To_ConvertBack()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var converter = new Mock();
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "DoubleValue"),
+ typeof(string),
+ converter.Object,
+ converterParameter: "foo");
+
+ target.OnNext("bar");
+
+ converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
+ }
+
+ [Fact]
+ public void Should_Handle_DataValidation()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var converter = new Mock();
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));
+ var result = new List();
+
+ target.Subscribe(x => result.Add(x));
+ target.OnNext(1.2);
+ target.OnNext("3.4");
+ target.OnNext("bar");
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification("5.6"),
+ new BindingNotification("1.2"),
+ new BindingNotification("3.4"),
+ new BindingNotification(
+ new InvalidCastException("'bar' is not a valid number."),
+ BindingErrorType.Error)
+ },
+ result);
+ }
+
+ private class Class1 : NotifyingBase
+ {
+ private string _stringValue;
+ private double _doubleValue;
+
+ public string StringValue
+ {
+ get { return _stringValue; }
+ set { _stringValue = value; RaisePropertyChanged(); }
+ }
+
+ public double DoubleValue
+ {
+ get { return _doubleValue; }
+ set { _doubleValue = value; RaisePropertyChanged(); }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
deleted file mode 100644
index 6ff336c5a6..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using Avalonia.Data;
-using Avalonia.Markup.Data.Plugins;
-using Xunit;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class ExceptionValidatorTests
- {
- public class Data : INotifyPropertyChanged
- {
- private int nonValidated;
-
- public int NonValidated
- {
- get { return nonValidated; }
- set { nonValidated = value; NotifyPropertyChanged(); }
- }
-
- private int mustBePositive;
-
- public int MustBePositive
- {
- get { return mustBePositive; }
- set
- {
- if (value <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
- mustBePositive = value;
- }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
-
- [Fact]
- public void Setting_Non_Validating_Triggers_Validation()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new ExceptionValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.NotNull(status);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new ExceptionValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.True(status.IsValid);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new ExceptionValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(-5, BindingPriority.LocalValue);
-
- Assert.False(status.IsValid);
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
new file mode 100644
index 0000000000..fb98144647
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
@@ -0,0 +1,222 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Data;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+ public class ExpressionObserverTests_DataValidation
+ {
+ [Fact]
+ public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
+ {
+ var data = new ExceptionTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
+ var validationMessageFound = false;
+
+ observer.OfType()
+ .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+ .Subscribe(_ => validationMessageFound = true);
+ observer.SetValue(-5);
+
+ Assert.False(validationMessageFound);
+ }
+
+ [Fact]
+ public void Exception_Validation_Sends_DataValidationError()
+ {
+ var data = new ExceptionTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+ var validationMessageFound = false;
+
+ observer.OfType()
+ .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+ .Subscribe(_ => validationMessageFound = true);
+ observer.SetValue(-5);
+
+ Assert.True(validationMessageFound);
+ }
+
+ [Fact]
+ public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()
+ {
+ var data = new IndeiTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
+
+ observer.Subscribe(_ => { });
+
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ }
+
+ [Fact]
+ public void Enabled_Indei_Validation_Subscribes()
+ {
+ var data = new IndeiTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+ var sub = observer.Subscribe(_ => { });
+
+ Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
+ sub.Dispose();
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ }
+
+ [Fact]
+ public void Validation_Plugins_Send_Correct_Notifications()
+ {
+ var data = new IndeiTest();
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+ var result = new List();
+
+ observer.Subscribe(x => result.Add(x));
+ observer.SetValue(5);
+ observer.SetValue(-5);
+ observer.SetValue("foo");
+ observer.SetValue(5);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(0),
+
+ // Value is notified twice as ErrorsChanged is always called by IndeiTest.
+ new BindingNotification(5),
+ new BindingNotification(5),
+
+ // Value is first signalled without an error as validation hasn't been updated.
+ new BindingNotification(-5),
+ new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5),
+
+ // Exception is thrown by trying to set value to "foo".
+ new BindingNotification(
+ new ArgumentException("Object of type 'System.String' cannot be converted to type 'System.Int32'."),
+ BindingErrorType.DataValidationError),
+
+ // Value is set then validation is updated.
+ new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5),
+ new BindingNotification(5),
+ }, result);
+ }
+
+ [Fact]
+ public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain()
+ {
+ var data = new Container
+ {
+ Inner = new IndeiTest()
+ };
+
+ var observer = new ExpressionObserver(
+ data,
+ $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
+ true);
+
+ observer.Subscribe(_ => { });
+
+ // We may want to change this but I've never seen an example of data validation on an
+ // intermediate object in a chain so for the moment I'm not sure what the result of
+ // validating such a thing should look like.
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount);
+ }
+
+ [Fact]
+ public void Sends_Correct_Notifications_With_Property_Chain()
+ {
+ var container = new Container();
+ var inner = new IndeiTest();
+
+ var observer = new ExpressionObserver(
+ container,
+ $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
+ true);
+ var result = new List();
+
+ observer.Subscribe(x => result.Add(x));
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(
+ new MarkupBindingChainNullException("Inner.MustBePositive", "Inner"),
+ BindingErrorType.Error,
+ AvaloniaProperty.UnsetValue),
+ }, result);
+ }
+
+ public class ExceptionTest : NotifyingBase
+ {
+ private int _mustBePositive;
+
+ public int MustBePositive
+ {
+ get { return _mustBePositive; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _mustBePositive = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private class IndeiTest : IndeiBase
+ {
+ private int _mustBePositive;
+ private Dictionary> _errors = new Dictionary>();
+
+ public int MustBePositive
+ {
+ get { return _mustBePositive; }
+ set
+ {
+ _mustBePositive = value;
+ RaisePropertyChanged();
+
+ if (value >= 0)
+ {
+ _errors.Remove(nameof(MustBePositive));
+ RaiseErrorsChanged(nameof(MustBePositive));
+ }
+ else
+ {
+ _errors[nameof(MustBePositive)] = new[] { "Must be positive" };
+ RaiseErrorsChanged(nameof(MustBePositive));
+ }
+ }
+ }
+
+ public override bool HasErrors => _mustBePositive >= 0;
+
+ public override IEnumerable GetErrors(string propertyName)
+ {
+ IList result;
+ _errors.TryGetValue(propertyName, out result);
+ return result;
+ }
+ }
+
+ private class Container : IndeiBase
+ {
+ private object _inner;
+
+ public object Inner
+ {
+ get { return _inner; }
+ set { _inner = value; RaisePropertyChanged(); }
+ }
+
+ public override bool HasErrors => false;
+ public override IEnumerable GetErrors(string propertyName) => null;
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
index b79498baae..f6c4540611 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
@@ -189,58 +189,22 @@ namespace Avalonia.Markup.UnitTests.Data
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
- Assert.Equal(0, data.Foo.SubscriptionCount);
- }
-
- [Fact]
- public void Should_Not_Keep_Source_Alive_ObservableCollection()
- {
- Func> run = () =>
- {
- var source = new { Foo = new AvaloniaList { "foo", "bar" } };
- var target = new ExpressionObserver(source, "Foo");
- return Tuple.Create(target, new WeakReference(source.Foo));
- };
-
- var result = run();
- result.Item1.Subscribe(x => { });
-
- GC.Collect();
-
- Assert.Null(result.Item2.Target);
- }
-
- [Fact]
- public void Should_Not_Keep_Source_Alive_NonIntegerIndexer()
- {
- Func> run = () =>
- {
- var source = new NonIntegerIndexer();
- var target = new ExpressionObserver(source, "Foo");
- return Tuple.Create(target, new WeakReference(source));
- };
-
- var result = run();
- result.Item1.Subscribe(x => { });
-
- GC.Collect();
-
- Assert.Null(result.Item2.Target);
+ Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);
}
private class NonIntegerIndexer : NotifyingBase
{
- private Dictionary storage = new Dictionary();
+ private readonly Dictionary _storage = new Dictionary();
public string this[string key]
{
get
{
- return storage[key];
+ return _storage[key];
}
set
{
- storage[key] = value;
+ _storage[key] = value;
RaisePropertyChanged(CommonPropertyNames.IndexerName);
}
}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
index 9fa753917c..2a2bf06bf1 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
@@ -27,6 +27,19 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.True(completed);
}
+ [Fact]
+ public void Should_Complete_When_Source_Observable_Errors()
+ {
+ var source = new BehaviorSubject(1);
+ var target = new ExpressionObserver(source, "Foo");
+ var completed = false;
+
+ target.Subscribe(_ => { }, () => completed = true);
+ source.OnError(new Exception());
+
+ Assert.True(completed);
+ }
+
[Fact]
public void Should_Complete_When_Update_Observable_Completes()
{
@@ -40,6 +53,19 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.True(completed);
}
+ [Fact]
+ public void Should_Complete_When_Update_Observable_Errors()
+ {
+ var update = new Subject();
+ var target = new ExpressionObserver(() => 1, "Foo", update);
+ var completed = false;
+
+ target.Subscribe(_ => { }, () => completed = true);
+ update.OnError(new Exception());
+
+ Assert.True(completed);
+ }
+
[Fact]
public void Should_Unsubscribe_From_Source_Observable()
{
@@ -55,7 +81,7 @@ namespace Avalonia.Markup.UnitTests.Data
scheduler.Start();
}
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result);
+ Assert.Equal(new[] { "foo" }, result);
Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
@@ -77,22 +103,6 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.All(update.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
- [Fact]
- public void Should_Set_Node_Target_To_Null_On_Unsubscribe()
- {
- var target = new ExpressionObserver(new { Foo = "foo" }, "Foo");
- var result = new List();
-
- using (target.Subscribe(x => result.Add(x)))
- using (target.Subscribe(_ => { }))
- {
- Assert.NotNull(target.Node.Target);
- }
-
- Assert.Equal(new[] { "foo" }, result);
- Assert.Null(target.Node.Target);
- }
-
private Recorded> OnNext(long time, object value)
{
return new Recorded>(time, Notification.CreateOnNext(value));
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
index b3046118be..6bee0d10f4 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
@@ -3,6 +3,7 @@
using System;
using System.Reactive.Linq;
+using Avalonia.Data;
using Avalonia.Markup.Data;
using Xunit;
@@ -61,23 +62,31 @@ namespace Avalonia.Markup.UnitTests.Data
}
[Fact]
- public async void Should_Return_UnsetValue_For_String_Not_Convertible_To_Boolean()
+ public async void Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException($"Unable to convert 'foo' to bool."),
+ BindingErrorType.Error),
+ result);
}
[Fact]
- public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean()
+ public async void Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException($"Unable to convert 'System.Object' to bool."),
+ BindingErrorType.Error),
+ result);
}
[Fact]
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
index c5bb2886b5..3263aaace2 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
+using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
@@ -27,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data
source.OnNext("bar");
sync.ExecutePostedCallbacks();
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result);
+ Assert.Equal(new[] { "foo", "bar" }, result);
}
}
@@ -44,10 +45,50 @@ namespace Avalonia.Markup.UnitTests.Data
data.Next.OnNext(new Class2("foo"));
sync.ExecutePostedCallbacks();
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result);
+ Assert.Equal(new[] { "foo" }, result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Simple_Observable_Value_With_DataValidation_Enabled()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var source = new BehaviorSubject("foo");
+ var data = new { Foo = source };
+ var target = new ExpressionObserver(data, "Foo", true);
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ source.OnNext("bar");
+ sync.ExecutePostedCallbacks();
+
+ // What does it mean to have data validation on an observable? Without a use-case
+ // it's hard to know what to do here so for the moment the value is returned.
+ Assert.Equal(new[] { "foo", "bar" }, result);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Property_Value_From_Observable_With_DataValidation_Enabled()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var data = new Class1();
+ var target = new ExpressionObserver(data, "Next.Foo", true);
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ data.Next.OnNext(new Class2("foo"));
+ sync.ExecutePostedCallbacks();
+
+ Assert.Equal(new[] { new BindingNotification("foo") }, result);
+
+ sub.Dispose();
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
index 643b8fccab..aa9ee7d58b 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
@@ -32,6 +32,8 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
+ target.Subscribe(_ => { });
+
Assert.Equal(typeof(string), target.ResultType);
}
@@ -55,6 +57,46 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("foo", result);
}
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Root_Null()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(default(object), "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Root_UnsetValue()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Observable_Root_Null()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(Observable.Return(default(object)), "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Observable_Root_UnsetValue()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
[Fact]
public async void Should_Get_Simple_Property_Chain()
{
@@ -71,21 +113,44 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
+ target.Subscribe(_ => { });
+
Assert.Equal(typeof(string), target.ResultType);
}
[Fact]
- public async void Should_Return_BindingError_For_Broken_Chain()
+ public async void Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
- Assert.IsType(result);
+ Assert.IsType(result);
- var error = result as BindingError;
- Assert.IsType(error.Exception);
- Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Exception.Message);
+ Assert.Equal(
+ new BindingNotification(
+ new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
+ result);
+ }
+
+ [Fact]
+ public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
+ {
+ var data = new { Foo = default(object) };
+ var target = new ExpressionObserver(data, "Foo.Bar.Baz");
+ var result = new List();
+
+ target.Subscribe(x => result.Add(x));
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(
+ new MarkupBindingChainNullException("Foo.Bar.Baz", "Foo"),
+ BindingErrorType.Error,
+ AvaloniaProperty.UnsetValue),
+ },
+ result);
}
[Fact]
@@ -111,7 +176,7 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -139,7 +204,7 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -151,13 +216,14 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
((Class2)data.Next).Bar = "baz";
+ ((Class2)data.Next).Bar = null;
- Assert.Equal(new[] { "bar", "baz" }, result);
+ Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -170,39 +236,60 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
var old = data.Next;
data.Next = new Class2 { Bar = "baz" };
+ data.Next = new Class2 { Bar = null };
- Assert.Equal(new[] { "bar", "baz" }, result);
+ Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
- Assert.Equal(0, old.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending()
{
- var data = new Class1 { Next = new Class2 { Bar = "bar" } };
- var target = new ExpressionObserver(data, "Next.Bar");
+ var data = new Class1
+ {
+ Next = new Class2
+ {
+ Next = new Class2
+ {
+ Bar = "bar"
+ }
+ }
+ };
+
+ var target = new ExpressionObserver(data, "Next.Next.Bar");
var result = new List();
var sub = target.Subscribe(x => result.Add(x));
var old = data.Next;
- data.Next = null;
data.Next = new Class2 { Bar = "baz" };
+ data.Next = old;
- Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue, "baz" }, result);
+ Assert.Equal(
+ new object[]
+ {
+ "bar",
+ new BindingNotification(
+ new MarkupBindingChainNullException("Next.Next.Bar", "Next.Next"),
+ BindingErrorType.Error,
+ AvaloniaProperty.UnsetValue),
+ "bar"
+ },
+ result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
- Assert.Equal(0, old.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
- public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending()
+ public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
@@ -214,17 +301,23 @@ namespace Avalonia.Markup.UnitTests.Data
data.Next = breaking;
data.Next = new Class2 { Bar = "baz" };
- Assert.Equal(3, result.Count);
- Assert.Equal("bar", result[0]);
- Assert.IsType(result[1]);
- Assert.Equal("baz", result[2]);
+ Assert.Equal(
+ new object[]
+ {
+ "bar",
+ new BindingNotification(
+ new MissingMemberException("Could not find CLR property 'Bar' on 'Avalonia.Markup.UnitTests.Data.ExpressionObserverTests_Property+WithoutBar'"),
+ BindingErrorType.Error),
+ "baz",
+ },
+ result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
- Assert.Equal(0, breaking.SubscriptionCount);
- Assert.Equal(0, old.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, breaking.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -258,17 +351,59 @@ namespace Avalonia.Markup.UnitTests.Data
scheduler.Start();
}
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result);
+ Assert.Equal(new[] { "foo", "bar" }, result);
Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
+ [Fact]
+ public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
+ {
+ var data = new Class1 { Foo = "foo" };
+ var target = new ExpressionObserver(data, "Foo");
+ var result1 = new List();
+ var result2 = new List();
+ var result3 = new List();
+
+ target.Subscribe(x => result1.Add(x));
+ target.Subscribe(x => result2.Add(x));
+
+ data.Foo = "bar";
+
+ target.Subscribe(x => result3.Add(x));
+
+ Assert.Equal(new[] { "foo", "bar" }, result1);
+ Assert.Equal(new[] { "foo", "bar" }, result2);
+ Assert.Equal(new[] { "bar" }, result3);
+ }
+
+ [Fact]
+ public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
+ {
+ var data = new Class1 { Foo = "foo" };
+ var target = new ExpressionObserver(data, "Foo");
+
+ var sub1 = target.Subscribe(x => { });
+ var sub2 = target.Subscribe(x => { });
+
+ Assert.Equal(1, data.PropertyChangedSubscriptionCount);
+
+ sub1.Dispose();
+ sub2.Dispose();
+
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ }
+
[Fact]
public void SetValue_Should_Set_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
- Assert.True(target.SetValue("bar"));
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.True(target.SetValue("bar"));
+ }
+
Assert.Equal("bar", data.Foo);
}
@@ -278,7 +413,11 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
- Assert.True(target.SetValue("baz"));
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.True(target.SetValue("baz"));
+ }
+
Assert.Equal("baz", ((Class2)data.Next).Bar);
}
@@ -288,25 +427,48 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Next = new WithoutBar()};
var target = new ExpressionObserver(data, "Next.Bar");
- Assert.False(target.SetValue("baz"));
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.False(target.SetValue("baz"));
+ }
}
[Fact]
- public void SetValue_Should_Return_False_For_Missing_Object()
+ public void SetValue_Should_Notify_New_Value_With_Inpc()
{
var data = new Class1();
- var target = new ExpressionObserver(data, "Next.Bar");
+ var target = new ExpressionObserver(data, "Foo");
+ var result = new List();
- Assert.False(target.SetValue("baz"));
+ target.Subscribe(x => result.Add(x));
+ target.SetValue("bar");
+
+ Assert.Equal(new[] { null, "bar" }, result);
}
[Fact]
- public async void Should_Handle_Null_Root()
+ public void SetValue_Should_Notify_New_Value_Without_Inpc()
{
- var target = new ExpressionObserver((object)null, "Foo");
- var result = await target.Take(1);
+ var data = new Class1();
+ var target = new ExpressionObserver(data, "Bar");
+ var result = new List();
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ target.Subscribe(x => result.Add(x));
+ target.SetValue("bar");
+
+ Assert.Equal(new[] { null, "bar" }, result);
+ }
+
+ [Fact]
+ public void SetValue_Should_Return_False_For_Missing_Object()
+ {
+ var data = new Class1();
+ var target = new ExpressionObserver(data, "Next.Bar");
+
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.False(target.SetValue("baz"));
+ }
}
[Fact]
@@ -325,10 +487,17 @@ namespace Avalonia.Markup.UnitTests.Data
root = null;
update.OnNext(Unit.Default);
- Assert.Equal(new[] { "foo", "bar", AvaloniaProperty.UnsetValue }, result);
-
- Assert.Equal(0, first.SubscriptionCount);
- Assert.Equal(0, second.SubscriptionCount);
+ Assert.Equal(
+ new object[]
+ {
+ "foo",
+ "bar",
+ AvaloniaProperty.UnsetValue,
+ },
+ result);
+
+ Assert.Equal(0, first.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, second.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -351,7 +520,7 @@ namespace Avalonia.Markup.UnitTests.Data
private interface INext
{
- int SubscriptionCount { get; }
+ int PropertyChangedSubscriptionCount { get; }
}
private class Class1 : NotifyingBase
@@ -390,6 +559,7 @@ namespace Avalonia.Markup.UnitTests.Data
private class Class2 : NotifyingBase, INext
{
private string _bar;
+ private INext _next;
public string Bar
{
@@ -400,6 +570,16 @@ namespace Avalonia.Markup.UnitTests.Data
RaisePropertyChanged(nameof(Bar));
}
}
+
+ public INext Next
+ {
+ get { return _next; }
+ set
+ {
+ _next = value;
+ RaisePropertyChanged(nameof(Next));
+ }
+ }
}
private class Class3 : Class1
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
index 4dabd34460..3238435841 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
@@ -18,7 +18,10 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
- target.SetValue("bar");
+ using (target.Subscribe(_ => { }))
+ {
+ target.SetValue("bar");
+ }
Assert.Equal("foo", data.Foo);
}
@@ -29,7 +32,10 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Foo.Bar");
- target.SetValue("foo");
+ using (target.Subscribe(_ => { }))
+ {
+ target.SetValue("foo");
+ }
Assert.Equal("foo", data.Foo.Bar);
}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
index 3d4c0b1b43..3dcd8a4fbc 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
-using System.Threading;
using System.Threading.Tasks;
+using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
@@ -28,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data
tcs.SetResult("foo");
sync.ExecutePostedCallbacks();
- Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray());
+ Assert.Equal(new[] { "foo" }, result);
}
}
@@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
- Assert.Equal(new object[] { "foo" }, result.ToArray());
+ Assert.Equal(new[] { "foo" }, result);
}
}
@@ -61,10 +61,84 @@ namespace Avalonia.Markup.UnitTests.Data
tcs.SetResult(new Class2("foo"));
sync.ExecutePostedCallbacks();
- Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray());
+ Assert.Equal(new[] { "foo" }, result);
}
}
+ [Fact]
+ public void Should_Return_BindingNotification_Error_On_Task_Exception()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var tcs = new TaskCompletionSource();
+ var data = new { Foo = tcs.Task };
+ var target = new ExpressionObserver(data, "Foo");
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ tcs.SetException(new NotSupportedException());
+ sync.ExecutePostedCallbacks();
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(
+ new AggregateException(new NotSupportedException()),
+ BindingErrorType.Error)
+ },
+ result);
+ }
+ }
+
+ [Fact]
+ public void Should_Return_BindingNotification_Error_For_Faulted_Task()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var data = new { Foo = TaskFromException(new NotSupportedException()) };
+ var target = new ExpressionObserver(data, "Foo");
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(
+ new AggregateException(new NotSupportedException()),
+ BindingErrorType.Error)
+ },
+ result);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Simple_Task_Value_With_Data_DataValidation_Enabled()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var tcs = new TaskCompletionSource();
+ var data = new { Foo = tcs.Task };
+ var target = new ExpressionObserver(data, "Foo", true);
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ tcs.SetResult("foo");
+ sync.ExecutePostedCallbacks();
+
+ // What does it mean to have data validation on a Task? Without a use-case it's
+ // hard to know what to do here so for the moment the value is returned.
+ Assert.Equal(new [] { "foo" }, result);
+ }
+ }
+
+ private Task TaskFromException(Exception e)
+ {
+ var tcs = new TaskCompletionSource();
+ tcs.SetException(e);
+ return tcs.Task;
+ }
+
private class Class1 : NotifyingBase
{
public Class1(Task next)
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs
deleted file mode 100644
index 59c8965cfb..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Reactive.Linq;
-using System.Runtime.CompilerServices;
-using Avalonia.Data;
-using Avalonia.Markup.Data;
-using Xunit;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class ExpressionObserverTests_Validation
- {
- [Fact]
- public void Exception_Validation_Sends_ValidationUpdate()
- {
- var data = new ExceptionTest { MustBePositive = 5 };
- var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
- var validationMessageFound = false;
- observer.Where(o => o is IValidationStatus).Subscribe(_ => validationMessageFound = true);
- observer.SetValue(-5);
- Assert.True(validationMessageFound);
- }
-
- [Fact]
- public void Disabled_Indei_Validation_Does_Not_Subscribe()
- {
- var data = new IndeiTest { MustBePositive = 5 };
- var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
-
- observer.Subscribe(_ => { });
-
- Assert.Equal(0, data.SubscriptionCount);
- }
-
- [Fact]
- public void Enabled_Indei_Validation_Subscribes()
- {
- var data = new IndeiTest { MustBePositive = 5 };
- var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
- var sub = observer.Subscribe(_ => { });
-
- Assert.Equal(1, data.SubscriptionCount);
- sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- }
-
- public class ExceptionTest : INotifyPropertyChanged
- {
- private int _mustBePositive;
-
- public int MustBePositive
- {
- get { return _mustBePositive; }
- set
- {
- if (value <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
- _mustBePositive = value;
- }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
-
- private class IndeiTest : INotifyDataErrorInfo
- {
- private int _mustBePositive;
- private Dictionary> _errors = new Dictionary>();
- private EventHandler _errorsChanged;
-
- public int MustBePositive
- {
- get { return _mustBePositive; }
- set
- {
- if (value >= 0)
- {
- _mustBePositive = value;
- _errors.Remove(nameof(MustBePositive));
- _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
- }
- else
- {
- _errors[nameof(MustBePositive)] = new[] { "Must be positive" };
- _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
- }
- }
- }
-
- public bool HasErrors => _mustBePositive >= 0;
-
- public int SubscriptionCount { get; private set; }
-
- public event EventHandler ErrorsChanged
- {
- add
- {
- _errorsChanged += value;
- ++SubscriptionCount;
- }
- remove
- {
- _errorsChanged -= value;
- --SubscriptionCount;
- }
- }
-
- public IEnumerable GetErrors(string propertyName)
- {
- IList result;
- _errors.TryGetValue(propertyName, out result);
- return result;
- }
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
deleted file mode 100644
index 0b6a507274..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.ComponentModel;
-using System.Globalization;
-using System.Reactive.Linq;
-using Moq;
-using Avalonia.Data;
-using Avalonia.Markup.Data;
-using Xunit;
-using System.Threading;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class ExpressionSubjectTests
- {
- [Fact]
- public async void Should_Get_Simple_Property_Value()
- {
- var data = new Class1 { StringValue = "foo" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string));
- var result = await target.Take(1);
-
- Assert.Equal("foo", result);
- }
-
- [Fact]
- public void Should_Set_Simple_Property_Value()
- {
- var data = new Class1 { StringValue = "foo" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string));
-
- target.OnNext("bar");
-
- Assert.Equal("bar", data.StringValue);
- }
-
- [Fact]
- public async void Should_Convert_Get_String_To_Double()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { StringValue = "5.6" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
- var result = await target.Take(1);
-
- Assert.Equal(5.6, result);
- }
-
- [Fact]
- public async void Getting_Invalid_Double_String_Should_Return_BindingError()
- {
- var data = new Class1 { StringValue = "foo" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
- var result = await target.Take(1);
-
- Assert.IsType(result);
- }
-
- [Fact]
- public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
- {
- var data = new Class1 { StringValue = null };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
- var result = await target.Take(1);
-
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
- }
-
- [Fact]
- public void Should_Convert_Set_String_To_Double()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { StringValue = (5.6).ToString() };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
-
- target.OnNext(6.7);
-
- Assert.Equal((6.7).ToString(), data.StringValue);
- }
-
- [Fact]
- public async void Should_Convert_Get_Double_To_String()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
- var result = await target.Take(1);
-
- Assert.Equal((5.6).ToString(), result);
- }
-
- [Fact]
- public void Should_Convert_Set_Double_To_String()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext("6.7");
-
- Assert.Equal(6.7, data.DoubleValue);
- }
-
- [Fact]
- public void Setting_Invalid_Double_String_Should_Not_Change_Target()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext("foo");
-
- Assert.Equal(5.6, data.DoubleValue);
- }
-
- [Fact]
- public void Setting_Invalid_Double_String_Should_Use_FallbackValue()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(
- new ExpressionObserver(data, "DoubleValue"),
- typeof(string),
- "9.8",
- DefaultValueConverter.Instance);
-
- target.OnNext("foo");
-
- Assert.Equal(9.8, data.DoubleValue);
- }
-
- [Fact]
- public void Should_Coerce_Setting_Null_Double_To_Default_Value()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext(null);
-
- Assert.Equal(0, data.DoubleValue);
- }
-
- [Fact]
- public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext(AvaloniaProperty.UnsetValue);
-
- Assert.Equal(0, data.DoubleValue);
- }
-
- [Fact]
- public void Should_Pass_ConverterParameter_To_Convert()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var converter = new Mock();
- var target = new ExpressionSubject(
- new ExpressionObserver(data, "DoubleValue"),
- typeof(string),
- converter.Object,
- converterParameter: "foo");
-
- target.Subscribe(_ => { });
-
- converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
- }
-
- [Fact]
- public void Should_Pass_ConverterParameter_To_ConvertBack()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var converter = new Mock();
- var target = new ExpressionSubject(
- new ExpressionObserver(data, "DoubleValue"),
- typeof(string),
- converter.Object,
- converterParameter: "foo");
-
- target.OnNext("bar");
-
- converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
- }
-
- private class Class1 : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged;
-
- public string StringValue { get; set; }
-
- public double DoubleValue { get; set; }
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs b/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs
new file mode 100644
index 0000000000..bd0ab71626
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs
@@ -0,0 +1,32 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Avalonia.UnitTests;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+ internal abstract class IndeiBase : NotifyingBase, INotifyDataErrorInfo
+ {
+ private EventHandler _errorsChanged;
+
+ public abstract bool HasErrors { get; }
+ public int ErrorsChangedSubscriptionCount { get; private set; }
+
+ public event EventHandler ErrorsChanged
+ {
+ add { _errorsChanged += value; ++ErrorsChangedSubscriptionCount; }
+ remove { _errorsChanged -= value; --ErrorsChangedSubscriptionCount; }
+ }
+
+ public abstract IEnumerable GetErrors(string propertyName);
+
+ protected void RaiseErrorsChanged([CallerMemberName] string propertyName = "")
+ {
+ _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs b/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
deleted file mode 100644
index 20bf164360..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using Avalonia.Data;
-using Avalonia.Markup.Data.Plugins;
-using Xunit;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class IndeiValidatorTests
- {
- public class Data : INotifyPropertyChanged, INotifyDataErrorInfo
- {
- private int nonValidated;
-
- public int NonValidated
- {
- get { return nonValidated; }
- set { nonValidated = value; NotifyPropertyChanged(); }
- }
-
- private int mustBePositive;
-
- public int MustBePositive
- {
- get { return mustBePositive; }
- set
- {
- mustBePositive = value;
- NotifyErrorsChanged();
- }
- }
-
- public bool HasErrors
- {
- get
- {
- return MustBePositive > 0;
- }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
- public event EventHandler ErrorsChanged;
-
- private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-
- private void NotifyErrorsChanged([CallerMemberName] string propertyName = "")
- {
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
- }
-
- public IEnumerable GetErrors(string propertyName)
- {
- if (propertyName == nameof(MustBePositive) && MustBePositive <= 0)
- {
- yield return $"{nameof(MustBePositive)} must be positive";
- }
- }
- }
-
- [Fact]
- public void Setting_Non_Validating_Does_Not_Trigger_Validation()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new IndeiValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.Null(status);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new IndeiValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.True(status.IsValid);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new IndeiValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(-5, BindingPriority.LocalValue);
-
- Assert.False(status.IsValid);
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs
new file mode 100644
index 0000000000..b873971e7f
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs
@@ -0,0 +1,111 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+ public class DataAnnotationsValidationPluginTests
+ {
+ [Fact]
+ public void Should_Match_Property_With_ValidatorAttribute()
+ {
+ var target = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+
+ Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10)));
+ }
+
+ [Fact]
+ public void Should_Match_Property_With_Multiple_ValidatorAttributes()
+ {
+ var target = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+
+ Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber)));
+ }
+
+ [Fact]
+ public void Should_Not_Match_Property_Without_ValidatorAttribute()
+ {
+ var target = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+
+ Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated)));
+ }
+
+ [Fact]
+ public void Produces_Range_BindingNotificationsx()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue(3, BindingPriority.LocalValue);
+ validator.SetValue(7, BindingPriority.LocalValue);
+ validator.SetValue(11, BindingPriority.LocalValue);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(5),
+ new BindingNotification(
+ new ValidationException("The field Between5And10 must be between 5 and 10."),
+ BindingErrorType.DataValidationError,
+ 3),
+ new BindingNotification(7),
+ new BindingNotification(
+ new ValidationException("The field Between5And10 must be between 5 and 10."),
+ BindingErrorType.DataValidationError,
+ 11),
+ }, result);
+ }
+
+ [Fact]
+ public void Produces_Aggregate_BindingNotificationsx()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue("123456", BindingPriority.LocalValue);
+ validator.SetValue("abcdefghijklm", BindingPriority.LocalValue);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(null),
+ new BindingNotification("123456"),
+ new BindingNotification(
+ new AggregateException(
+ new ValidationException("The PhoneNumber field is not a valid phone number."),
+ new ValidationException("The field PhoneNumber must be a string or array type with a maximum length of '10'.")),
+ BindingErrorType.DataValidationError,
+ "abcdefghijklm"),
+ }, result);
+ }
+
+ private class Data
+ {
+ [Range(5, 10)]
+ public int Between5And10 { get; set; } = 5;
+
+ public int Unvalidated { get; set; }
+
+ [Phone]
+ [MaxLength(10)]
+ public string PhoneNumber { get; set; }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs
new file mode 100644
index 0000000000..4a34791008
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs
@@ -0,0 +1,63 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+ public class ExceptionValidationPluginTests
+ {
+ [Fact]
+ public void Produces_BindingNotifications()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new ExceptionValidationPlugin();
+ var data = new Data();
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue(5, BindingPriority.LocalValue);
+ validator.SetValue(-2, BindingPriority.LocalValue);
+ validator.SetValue(6, BindingPriority.LocalValue);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(0),
+ new BindingNotification(5),
+ new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError),
+ new BindingNotification(6),
+ }, result);
+ }
+
+ public class Data : NotifyingBase
+ {
+ private int _mustBePositive;
+
+ public int MustBePositive
+ {
+ get { return _mustBePositive; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ if (value != _mustBePositive)
+ {
+ _mustBePositive = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs
new file mode 100644
index 0000000000..788bc25a34
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs
@@ -0,0 +1,127 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+ public class IndeiValidationPluginTests
+ {
+ [Fact]
+ public void Produces_BindingNotifications()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new IndeiValidationPlugin();
+ var data = new Data { Maximum = 5 };
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue(5, BindingPriority.LocalValue);
+ validator.SetValue(6, BindingPriority.LocalValue);
+ data.Maximum = 10;
+ data.Maximum = 5;
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(0),
+ new BindingNotification(5),
+
+ // Value is first signalled without an error as validation hasn't been updated.
+ new BindingNotification(6),
+
+ // Then the ErrorsChanged event is fired.
+ new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
+
+ // Maximum is changed to 10 so value is now valid.
+ new BindingNotification(6),
+
+ // And Maximum is changed back to 5.
+ new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
+ }, result);
+ }
+
+ [Fact]
+ public void Subscribes_And_Unsubscribes()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new IndeiValidationPlugin();
+ var data = new Data { Maximum = 5 };
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ var sub = validator.Subscribe(_ => { });
+ Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
+ sub.Dispose();
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ }
+
+ internal class Data : IndeiBase
+ {
+ private int _value;
+ private int _maximum;
+ private string _error;
+
+ public override bool HasErrors => _error != null;
+
+ public int Value
+ {
+ get { return _value; }
+ set
+ {
+ _value = value;
+ RaisePropertyChanged();
+ UpdateError();
+ }
+ }
+
+ public int Maximum
+ {
+ get { return _maximum; }
+ set
+ {
+ _maximum = value;
+ UpdateError();
+ }
+ }
+
+ public override IEnumerable GetErrors(string propertyName)
+ {
+ if (propertyName == nameof(Value) && _error != null)
+ {
+ return new[] { _error };
+ }
+
+ return null;
+ }
+
+ private void UpdateError()
+ {
+ if (_value <= _maximum)
+ {
+ if (_error != null)
+ {
+ _error = null;
+ RaiseErrorsChanged(nameof(Value));
+ }
+ }
+ else
+ {
+ if (_error == null)
+ {
+ _error = "Must be less than Maximum";
+ RaiseErrorsChanged(nameof(Value));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs
index 2e52dff087..fd28f2e900 100644
--- a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs
@@ -115,7 +115,7 @@ namespace Avalonia.Markup.UnitTests
null,
CultureInfo.InvariantCulture);
- Assert.IsType(result);
+ Assert.IsType(result);
}
private enum TestEnum
diff --git a/tests/Avalonia.Markup.UnitTests/app.config b/tests/Avalonia.Markup.UnitTests/app.config
index fa66e8c206..654f911514 100644
--- a/tests/Avalonia.Markup.UnitTests/app.config
+++ b/tests/Avalonia.Markup.UnitTests/app.config
@@ -1,11 +1,11 @@
-
+
-
-
+
+
-
\ No newline at end of file
+
diff --git a/tests/Avalonia.Markup.UnitTests/packages.config b/tests/Avalonia.Markup.UnitTests/packages.config
index 34563ef392..d264c076fd 100644
--- a/tests/Avalonia.Markup.UnitTests/packages.config
+++ b/tests/Avalonia.Markup.UnitTests/packages.config
@@ -1,11 +1,14 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
index 0e2e11c300..54e61fc7d2 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
@@ -93,9 +93,9 @@
+
-
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
index 7d8528c5d7..210ad2ab0b 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Data;
@@ -164,17 +166,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
[Fact]
public void DataContext_Binding_Should_Produce_Correct_Results()
{
+ var viewModel = new { Foo = "bar" };
var root = new Decorator
{
- DataContext = new { Foo = "bar" },
+ DataContext = viewModel,
};
var child = new Control();
- var dataContextBinding = new Binding("Foo");
var values = new List();
- child.GetObservable(Border.DataContextProperty).Subscribe(x => values.Add(x));
- child.Bind(ContentControl.DataContextProperty, dataContextBinding);
+ child.GetObservable(Control.DataContextProperty).Subscribe(x => values.Add(x));
+ child.Bind(Control.DataContextProperty, new Binding("Foo"));
+
+ // When binding to DataContext and the target isn't found, the binding should produce
+ // null rather than UnsetValue in order to not propagate incorrect DataContexts from
+ // parent controls while things are being set up. This logic is implemented in
+ // `Avalonia.Markup.Xaml.Binding.Initiate`.
+ Assert.True(child.IsSet(Control.DataContextProperty));
root.Child = child;
@@ -192,7 +200,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
- Assert.IsType(((ExpressionSubject)result).Converter);
+ Assert.IsType(((BindingExpression)result).Converter);
}
[Fact]
@@ -208,7 +216,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
- Assert.Same(converter.Object, ((ExpressionSubject)result).Converter);
+ Assert.Same(converter.Object, ((BindingExpression)result).Converter);
}
[Fact]
@@ -225,7 +233,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
- Assert.Same("foo", ((ExpressionSubject)result).ConverterParameter);
+ Assert.Same("foo", ((BindingExpression)result).ConverterParameter);
}
[Fact]
@@ -267,7 +275,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
///
///
/// - Items is bound to DataContext first, followed by say SelectedIndex
- /// - When the ListBox is removed from the visual tree, DataContext becomes null (as it's
+ /// - When the ListBox is removed from the logical tree, DataContext becomes null (as it's
/// inherited)
/// - This changes Items to null, which changes SelectedIndex to null as there are no
/// longer any items
@@ -294,12 +302,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
// Bind Foo and Bar to the VM.
target.Bind(OldDataContextTest.FooProperty, fooBinding);
- target.Bind(OldDataContextTest.BarProperty, barBinding);
+ //target.Bind(OldDataContextTest.BarProperty, barBinding);
target.DataContext = vm;
// Make sure the control's Foo and Bar properties are read from the VM
Assert.Equal(1, target.GetValue(OldDataContextTest.FooProperty));
- Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty));
+ //Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty));
// Set DataContext to null.
target.DataContext = null;
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs
new file mode 100644
index 0000000000..5dd8d0cdf9
--- /dev/null
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs
@@ -0,0 +1,75 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Markup.Data;
+using Avalonia.Markup.Xaml.Data;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Data
+{
+ public class BindingTests_DataValidation
+ {
+ [Fact]
+ public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_LocalValue()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo));
+ var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false);
+ var subject = (BindingExpression)instanced.Subject;
+ object result = null;
+
+ subject.Subscribe(x => result = x);
+
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo));
+ var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
+ var subject = (BindingExpression)instanced.Subject;
+ object result = null;
+
+ subject.Subscribe(x => result = x);
+
+ Assert.Equal(new BindingNotification("foo"), result);
+ }
+
+ [Fact]
+ public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_TemplatedParent()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.TemplatedParent };
+ var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
+ var subject = (BindingExpression)instanced.Subject;
+ object result = null;
+
+ subject.Subscribe(x => result = x);
+
+ Assert.IsType(result);
+ }
+
+ private class Class1
+ {
+ public string Foo { get; set; } = "foo";
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs
index 0fed786f07..8759cb42c5 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs
@@ -1,7 +1,11 @@
using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Xaml.Data;
+using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
@@ -9,143 +13,112 @@ 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.False(target.ValidationStatus.IsValid);
+ Assert.Empty(target.Notifications);
}
[Fact]
- public void Enabled_Validation_Should_Trigger_Validation_Change_On_Exception()
+ public void Validated_Direct_Property_Receives_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,
};
- target.Bind(TestControl.ValidationTestProperty, binding);
-
- target.ValidationTest = -5;
- Assert.False(target.ValidationStatus.IsValid);
- }
-
+ target.Bind(
+ TestControl.ValidatedDirectProperty,
+ new Binding(nameof(source.MustBePositive), BindingMode.TwoWay));
- [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,
- };
+ target.ValidatedDirect = 6;
+ target.ValidatedDirect = -1;
+ target.ValidatedDirect = 7;
- control.Bind(TestControl.ValidationTestProperty, binding);
- control.DataContext = model;
- Assert.DoesNotContain(control.Classes, x => x == ":invalid");
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(5),
+ new BindingNotification(6),
+ new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError),
+ new BindingNotification(7),
+ },
+ target.Notifications.AsEnumerable());
}
- [Fact]
- public void Failed_Validation_Should_Add_Invalid_Pseudo_Class()
+ private class TestControl : Control
{
- var control = new TestControl();
- var model = new ValidationTestModel { MustBePositive = 1 };
- var binding = new Binding
+ public static readonly StyledProperty NonValidatedProperty =
+ AvaloniaProperty.Register(
+ nameof(Validated),
+ enableDataValidation: false);
+
+ public static readonly StyledProperty ValidatedProperty =
+ AvaloniaProperty.Register(
+ nameof(Validated),
+ enableDataValidation: true);
+
+ public static readonly DirectProperty ValidatedDirectProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Validated),
+ o => o.ValidatedDirect,
+ (o, v) => o.ValidatedDirect = v,
+ enableDataValidation: true);
+
+ private int _direct;
+
+ 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");
- }
-
- private class TestControl : Control
- {
- public static readonly StyledProperty ValidationTestProperty
- = AvaloniaProperty.Register(nameof(ValidationTest), 1, defaultBindingMode: BindingMode.TwoWay);
+ get { return GetValue(ValidatedProperty); }
+ set { SetValue(ValidatedProperty, value); }
+ }
- public int ValidationTest
+ public int ValidatedDirect
{
- get
- {
- return GetValue(ValidationTestProperty);
- }
- set
- {
- SetValue(ValidationTestProperty, value);
- }
+ get { return _direct; }
+ set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); }
}
- protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+ public IList Notifications { get; } = new List();
+
+ protected override void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
- if (property == ValidationTestProperty)
- {
- UpdateValidationState(status);
- }
+ Notifications.Add(notification);
}
}
- private class ValidationTestModel
+ private class ValidationTestModel : NotifyingBase
{
- private int mustBePositive;
+ private int _mustBePositive;
public int MustBePositive
{
- get { return mustBePositive; }
+ get { return _mustBePositive; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
- mustBePositive = value;
+
+ if (_mustBePositive != value)
+ {
+ _mustBePositive = value;
+ RaisePropertyChanged();
+ }
}
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
index df62a1ed41..0c2151850f 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
@@ -39,7 +39,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
if (level == LogEventLevel.Error &&
area == LogArea.Binding &&
- mt == "Error binding to {Target}.{Property}: {Message}" &&
+ mt == "Error in binding to {Target}.{Property}: {Message}" &&
pv.Length == 3 &&
pv[0] is ProgressBar &&
object.ReferenceEquals(pv[1], ProgressBar.ValueProperty) &&
diff --git a/tests/Avalonia.Styling.UnitTests/SetterTests.cs b/tests/Avalonia.Styling.UnitTests/SetterTests.cs
index 4de90b7790..84536fa47b 100644
--- a/tests/Avalonia.Styling.UnitTests/SetterTests.cs
+++ b/tests/Avalonia.Styling.UnitTests/SetterTests.cs
@@ -27,7 +27,7 @@ namespace Avalonia.Styling.UnitTests
var control = new TextBlock();
var subject = new BehaviorSubject("foo");
var descriptor = new InstancedBinding(subject);
- var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null) == descriptor);
+ var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor);
var style = Mock.Of();
var setter = new Setter(TextBlock.TextProperty, binding);
diff --git a/tests/Avalonia.UnitTests/NotifyingBase.cs b/tests/Avalonia.UnitTests/NotifyingBase.cs
index c1b7a24303..c91e55d34f 100644
--- a/tests/Avalonia.UnitTests/NotifyingBase.cs
+++ b/tests/Avalonia.UnitTests/NotifyingBase.cs
@@ -16,7 +16,7 @@ namespace Avalonia.UnitTests
add
{
_propertyChanged += value;
- ++SubscriptionCount;
+ ++PropertyChangedSubscriptionCount;
}
remove
@@ -24,12 +24,12 @@ namespace Avalonia.UnitTests
if (_propertyChanged?.GetInvocationList().Contains(value) == true)
{
_propertyChanged -= value;
- --SubscriptionCount;
+ --PropertyChangedSubscriptionCount;
}
}
}
- public int SubscriptionCount
+ public int PropertyChangedSubscriptionCount
{
get;
private set;