diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs
index 97f538d862..9351443e03 100644
--- a/src/Avalonia.Base/Data/BindingNotification.cs
+++ b/src/Avalonia.Base/Data/BindingNotification.cs
@@ -92,7 +92,7 @@ namespace Avalonia.Data
///
/// Gets a value indicating whether should be pushed to the target.
///
- public bool HasValue { get; }
+ public bool HasValue { get; set; }
///
/// Gets the error that occurred on the source, if any.
diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
index aae2b75dec..5a7bb80e87 100644
--- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
+++ b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
@@ -183,56 +183,121 @@ namespace Avalonia.Markup.Data
{
var notification = value as BindingNotification;
- if (notification?.HasValue == true)
+ if (notification == null)
{
- value = notification.Value;
- }
+ var converted = Converter.Convert(
+ value,
+ _targetType,
+ ConverterParameter,
+ CultureInfo.CurrentUICulture);
- var converted = Converter.Convert(
- value,
- _targetType,
- ConverterParameter,
- CultureInfo.CurrentUICulture);
+ notification = converted as BindingNotification;
- if (_fallbackValue != AvaloniaProperty.UnsetValue &&
- (converted == AvaloniaProperty.UnsetValue ||
- converted is BindingNotification))
- {
- var error = converted as BindingNotification;
-
- if (TypeUtilities.TryConvert(
- _targetType,
- _fallbackValue,
- CultureInfo.InvariantCulture,
- out converted))
+ if (notification?.ErrorType == BindingErrorType.None)
{
- if (error != null)
- {
- converted = new BindingNotification(error.Error, BindingErrorType.Error, converted);
- }
+ converted = notification.Value;
}
- else
+
+ if (_fallbackValue != AvaloniaProperty.UnsetValue &&
+ (converted == AvaloniaProperty.UnsetValue || converted is BindingNotification))
{
- converted = new BindingNotification(
- new InvalidCastException(
- $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"),
- BindingErrorType.Error);
+ var fallback = ConvertFallback();
+ converted = Merge(converted, fallback);
}
+
+ return converted;
}
+ else
+ {
+ return ConvertValue(notification);
+ }
+ }
- if (notification == null)
+ private BindingNotification ConvertValue(BindingNotification notification)
+ {
+ if (notification.HasValue)
{
- return converted;
+ var converted = ConvertValue(notification.Value);
+ notification = Merge(notification, converted);
+ }
+ else if (_fallbackValue != AvaloniaProperty.UnsetValue)
+ {
+ var fallback = ConvertFallback();
+ notification = Merge(notification, fallback);
+ }
+
+ return notification;
+ }
+
+ private BindingNotification ConvertFallback()
+ {
+ object converted;
+
+ if (_fallbackValue == AvaloniaProperty.UnsetValue)
+ {
+ throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value");
+ }
+
+ if (TypeUtilities.TryConvert(
+ _targetType,
+ _fallbackValue,
+ CultureInfo.InvariantCulture,
+ out converted))
+ {
+ return new BindingNotification(converted);
}
else
+ {
+ return new BindingNotification(
+ new InvalidCastException(
+ $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"),
+ BindingErrorType.Error);
+ }
+ }
+
+ private BindingNotification Merge(object a, BindingNotification b)
+ {
+ var an = a as BindingNotification;
+
+ if (an != null)
{
- if (notification.HasValue)
- {
- notification.Value = converted;
- }
+ Merge(an, b);
+ return an;
+ }
+ else
+ {
+ return b;
+ }
+ }
- return notification;
+ private BindingNotification Merge(BindingNotification a, object b)
+ {
+ var bn = b as BindingNotification;
+
+ if (bn != null)
+ {
+ Merge(a, bn);
}
+ else
+ {
+ a.Value = b;
+ a.HasValue = true;
+ }
+
+ return a;
+ }
+
+ private BindingNotification Merge(BindingNotification a, BindingNotification b)
+ {
+ a.Value = b.Value;
+ a.HasValue = b.HasValue;
+
+ if (b.Error != null)
+ {
+ a.AddError(b.Error, b.ErrorType);
+ }
+
+ return a;
}
}
}
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
index cc3e5c4052..51d5d19834 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
@@ -284,7 +284,6 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("newvalue", target.Foo);
}
-
[Fact]
public void UnsetValue_Is_Used_On_AddOwnered_Property()
{
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
index ce8a5b6d3d..5498926fe4 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
@@ -108,6 +108,92 @@ namespace Avalonia.Markup.UnitTests.Data
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 ExpressionSubject(
+ new ExpressionObserver(data, "StringValue"),
+ typeof(int),
+ 42,
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+ 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 ExpressionSubject(
+ new ExpressionObserver(data, "StringValue", true),
+ typeof(int),
+ 42,
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+ 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 ExpressionSubject(
+ 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 ExpressionSubject(
+ 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()
{