diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs index b1c98c72d0..97f538d862 100644 --- a/src/Avalonia.Base/Data/BindingNotification.cs +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -97,12 +97,12 @@ namespace Avalonia.Data /// /// Gets the error that occurred on the source, if any. /// - public Exception Error { get; private set; } + public Exception Error { get; set; } /// /// Gets the type of error that represents, if any. /// - public BindingErrorType ErrorType { get; private set; } + public BindingErrorType ErrorType { get; set; } /// /// Compares two instances of for equality. diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs index b26ec3402e..aae2b75dec 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs @@ -21,6 +21,7 @@ namespace Avalonia.Markup.Data private readonly Type _targetType; private readonly object _fallbackValue; private readonly BindingPriority _priority; + private readonly Subject _errors = new Subject(); /// /// Initializes a new instance of the class. @@ -130,14 +131,18 @@ namespace Avalonia.Markup.Data } else if (converted is BindingNotification) { - var error = converted as BindingNotification; + var notification = converted as BindingNotification; - Logger.Error( - LogArea.Binding, - this, - "Error binding to {Expression}: {Message}", - _inner.Expression, - error.Error.Message); + if (notification.ErrorType == BindingErrorType.None) + { + throw new AvaloniaInternalException( + "IValueConverter should not return non-errored BindingNotification."); + } + + notification.Error = new InvalidCastException( + $"Error setting '{_inner.Expression}': {notification.Error.Message}"); + notification.ErrorType = BindingErrorType.Error; + _errors.OnNext(notification); if (_fallbackValue != AvaloniaProperty.UnsetValue) { @@ -171,19 +176,23 @@ namespace Avalonia.Markup.Data /// public IDisposable Subscribe(IObserver observer) { - return _inner.Select(ConvertValue).Subscribe(observer); + return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); } private object ConvertValue(object value) { - var converted = - value as BindingNotification ?? - ////value as IValidationStatus ?? - Converter.Convert( - value, - _targetType, - ConverterParameter, - CultureInfo.CurrentUICulture); + var notification = value as BindingNotification; + + if (notification?.HasValue == true) + { + value = notification.Value; + } + + var converted = Converter.Convert( + value, + _targetType, + ConverterParameter, + CultureInfo.CurrentUICulture); if (_fallbackValue != AvaloniaProperty.UnsetValue && (converted == AvaloniaProperty.UnsetValue || @@ -211,7 +220,19 @@ namespace Avalonia.Markup.Data } } - return converted; + if (notification == null) + { + return converted; + } + else + { + if (notification.HasValue) + { + notification.Value = converted; + } + + return notification; + } } } } diff --git a/src/Markup/Avalonia.Markup/IValueConverter.cs b/src/Markup/Avalonia.Markup/IValueConverter.cs index 23117a3fac..10d5c626c2 100644 --- a/src/Markup/Avalonia.Markup/IValueConverter.cs +++ b/src/Markup/Avalonia.Markup/IValueConverter.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using Avalonia.Data; namespace Avalonia.Markup { @@ -21,8 +22,8 @@ namespace Avalonia.Markup /// The converted value. /// /// This method should not throw exceptions. If the value is not convertible, return - /// . Any exception thrown will be treated as - /// an application exception. + /// a in an error state. Any exceptions thrown will be + /// treated as an application exception. /// object Convert(object value, Type targetType, object parameter, CultureInfo culture); @@ -36,8 +37,8 @@ namespace Avalonia.Markup /// The converted value. /// /// This method should not throw exceptions. If the value is not convertible, return - /// . Any exception thrown will be treated as - /// an application exception. + /// a in an error state. Any exceptions thrown will be + /// treated as an application exception. /// object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs index 449f402850..ce8a5b6d3d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs @@ -10,6 +10,8 @@ using Avalonia.Data; using Avalonia.Markup.Data; using Xunit; using System.Threading; +using System.Collections.Generic; +using Avalonia.UnitTests; namespace Avalonia.Markup.UnitTests.Data { @@ -186,13 +188,48 @@ namespace Avalonia.Markup.UnitTests.Data converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture)); } - private class Class1 : INotifyPropertyChanged + [Fact] + public void Should_Handle_DataValidation() { - public event PropertyChangedEventHandler PropertyChanged; + var data = new Class1 { DoubleValue = 5.6 }; + var converter = new Mock(); + var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue", true), typeof(string)); + var result = new List(); - public string StringValue { get; set; } + target.Subscribe(x => result.Add(x)); + target.OnNext(1.2); + target.OnNext("3.4"); + target.OnNext("bar"); - public double DoubleValue { get; set; } + Assert.Equal( + new[] + { + new BindingNotification("5.6"), + new BindingNotification("1.2"), + new BindingNotification("3.4"), + new BindingNotification( + new InvalidCastException("Error setting 'DoubleValue': Could not convert 'bar' to 'System.Double'"), + 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(); } + } } } }