Browse Source

Added BindingNotification.

This will replace BindingError and IValidationStatus for bindings that want to notify binding/validation errors. Still WIP.
pull/691/head
Steven Kirk 10 years ago
committed by Steven Kirk
parent
commit
abdbcac79d
  1. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  2. 36
      src/Avalonia.Base/AvaloniaObject.cs
  3. 59
      src/Avalonia.Base/Data/BindingError.cs
  4. 169
      src/Avalonia.Base/Data/BindingNotification.cs
  5. 2
      src/Avalonia.Base/IPriorityValueOwner.cs
  6. 30
      src/Avalonia.Base/PriorityBindingEntry.cs
  7. 4
      src/Avalonia.Base/PriorityLevel.cs
  8. 6
      src/Avalonia.Base/PriorityValue.cs
  9. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  10. 16
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  11. 19
      src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
  12. 2
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  13. 2
      src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  14. 4
      src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs
  15. 2
      src/Markup/Avalonia.Markup/DefaultValueConverter.cs
  16. 7
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject
  17. 17
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  18. 9
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  19. 10
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  20. 2
      tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
  21. 2
      tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs

2
src/Avalonia.Base/Avalonia.Base.csproj

@ -43,7 +43,7 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\BindingError.cs" />
<Compile Include="Data\BindingNotification.cs" />
<Compile Include="Data\IndexerBinding.cs" />
<Compile Include="Data\IValidationStatus.cs" />
<Compile Include="Data\ObjectValidationStatus.cs" />

36
src/Avalonia.Base/AvaloniaObject.cs

@ -379,12 +379,11 @@ namespace Avalonia
}
subscription = source
.Where(x => !(x is IValidationStatus))
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => DirectBindingSet(property, x));
validationSubcription = source
.OfType<IValidationStatus>()
.OfType<BindingNotification>()
.Subscribe(x => DataValidationChanged(property, x));
_directBindings.Add(subscription);
@ -487,7 +486,7 @@ namespace Avalonia
}
/// <inheritdoc/>
void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, IValidationStatus status)
void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, BindingNotification status)
{
var property = sender.Property;
DataValidationChanged(property, status);
@ -623,14 +622,14 @@ namespace Avalonia
/// <summary>
/// Tries to cast a value to a type, taking into account that the value may be a
/// <see cref="BindingError"/>.
/// <see cref="BindingNotification"/>.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="type">The type.</param>
/// <returns>The cast value, or a <see cref="BindingError"/>.</returns>
/// <returns>The cast value, or a <see cref="BindingNotification"/>.</returns>
private static object CastOrDefault(object value, Type type)
{
var error = value as BindingError;
var error = value as BindingNotification;
if (error == null)
{
@ -674,26 +673,29 @@ namespace Avalonia
/// <returns></returns>
private void DirectBindingSet(AvaloniaProperty property, object value)
{
var error = value as BindingError;
var notification = value as BindingNotification;
if (error == null)
if (notification == null)
{
SetValue(property, value);
}
else
{
if (error.UseFallbackValue)
if (notification.HasValue)
{
SetValue(property, error.FallbackValue);
SetValue(property, notification.Value);
}
Logger.Error(
LogArea.Binding,
this,
"Error binding to {Target}.{Property}: {Message}",
this,
property,
error.Exception.Message);
if (notification.ErrorType == BindingErrorType.Error)
{
Logger.Error(
LogArea.Binding,
this,
"Error binding to {Target}.{Property}: {Message}",
this,
property,
notification.Error.Message);
}
}
}

59
src/Avalonia.Base/Data/BindingError.cs

@ -1,59 +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;
namespace Avalonia.Data
{
/// <summary>
/// Represents a recoverable binding error.
/// </summary>
/// <remarks>
/// When produced by a binding source observable, informs the binding system that an error
/// occurred. It can also provide an optional fallback value to be pushed to the binding
/// target.
///
/// Instead of using <see cref="BindingError"/>, one could simply not push a value (in the
/// case of a no fallback value) or push a fallback value, but BindingError also causes an
/// error to be logged with the correct binding target.
/// </remarks>
public class BindingError
{
/// <summary>
/// Initializes a new instance of the <see cref="BindingError"/> class.
/// </summary>
/// <param name="exception">An exception describing the binding error.</param>
public BindingError(Exception exception)
{
Exception = exception;
}
/// <summary>
/// Initializes a new instance of the <see cref="BindingError"/> class.
/// </summary>
/// <param name="exception">An exception describing the binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public BindingError(Exception exception, object fallbackValue)
{
Exception = exception;
FallbackValue = fallbackValue;
UseFallbackValue = true;
}
/// <summary>
/// Gets the exception describing the binding error.
/// </summary>
public Exception Exception { get; }
/// <summary>
/// Get the fallback value.
/// </summary>
public object FallbackValue { get; }
/// <summary>
/// Get a value indicating whether the fallback value should be pushed to the binding
/// target.
/// </summary>
public bool UseFallbackValue { get; }
}
}

169
src/Avalonia.Base/Data/BindingNotification.cs

@ -0,0 +1,169 @@
// 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;
namespace Avalonia.Data
{
/// <summary>
/// Defines the types of binding errors for a <see cref="BindingNotification"/>.
/// </summary>
public enum BindingErrorType
{
/// <summary>
/// There was no error.
/// </summary>
None,
/// <summary>
/// There was a binding error.
/// </summary>
Error,
/// <summary>
/// There was a data validation error.
/// </summary>
DataValidationError,
}
/// <summary>
/// Represents a binding notification that can be a valid binding value, or a binding or
/// data validation error.
/// </summary>
public class BindingNotification : IValidationStatus
{
/// <summary>
/// A binding notification representing the null value.
/// </summary>
public static readonly BindingNotification Null =
new BindingNotification(null);
/// <summary>
/// A binding notification representing <see cref="AvaloniaProperty.UnsetValue"/>.
/// </summary>
public static readonly BindingNotification UnsetValue =
new BindingNotification(AvaloniaProperty.UnsetValue);
/// <summary>
/// Initializes a new instance of the <see cref="BindingNotification"/> class.
/// </summary>
/// <param name="value">The binding value.</param>
public BindingNotification(object value)
{
Value = value;
HasValue = true;
}
/// <summary>
/// Initializes a new instance of the <see cref="BindingNotification"/> class.
/// </summary>
/// <param name="error">The binding error.</param>
/// <param name="errorType">The type of the binding error.</param>
public BindingNotification(Exception error, BindingErrorType errorType)
{
if (errorType == BindingErrorType.None)
{
throw new ArgumentException($"'errorType' may not be None");
}
Error = error;
ErrorType = errorType;
}
/// <summary>
/// Initializes a new instance of the <see cref="BindingNotification"/> class.
/// </summary>
/// <param name="error">The binding error.</param>
/// <param name="errorType">The type of the binding error.</param>
/// <param name="fallbackValue">The fallback value.</param>
public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue)
: this(error)
{
Value = fallbackValue;
HasValue = true;
}
/// <summary>
/// Gets the value that should be passed to the target when <see cref="HasValue"/>
/// is true.
/// </summary>
public object Value { get; }
/// <summary>
/// Gets a value indicating whether <see cref="Value"/> should be pushed to the target.
/// </summary>
public bool HasValue { get; }
/// <summary>
/// Gets the error that occurred on the source, if any.
/// </summary>
public Exception Error { get; }
/// <summary>
/// Gets the type of error that <see cref="Error"/> represents, if any.
/// </summary>
public BindingErrorType ErrorType { get; }
bool IValidationStatus.IsValid => ErrorType == BindingErrorType.None;
public static bool operator ==(BindingNotification a, BindingNotification b)
{
if (object.ReferenceEquals(a, b))
{
return true;
}
if ((object)a == null || (object)b == null)
{
return false;
}
return a.HasValue == b.HasValue &&
a.ErrorType == b.ErrorType &&
(!a.HasValue || object.Equals(a.Value, b.Value)) &&
(a.ErrorType == BindingErrorType.None || object.Equals(a.Error, b.Error));
}
public static bool operator !=(BindingNotification a, BindingNotification b)
{
return !(a == b);
}
public override bool Equals(object obj)
{
return Equals(obj as BindingNotification);
}
public bool Equals(BindingNotification other)
{
return this == other;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public BindingNotification WithError(Exception e)
{
if (e == null)
{
return this;
}
if (Error != null)
{
e = new AggregateException(Error, e);
}
if (HasValue)
{
return new BindingNotification(e, BindingErrorType.Error, Value);
}
else
{
return new BindingNotification(e, BindingErrorType.Error, Value);
}
}
}
}

2
src/Avalonia.Base/IPriorityValueOwner.cs

@ -23,6 +23,6 @@ namespace Avalonia
/// </summary>
/// <param name="sender">The source of the change.</param>
/// <param name="status">The validation status.</param>
void DataValidationChanged(PriorityValue sender, IValidationStatus status);
void DataValidationChanged(PriorityValue sender, BindingNotification status);
}
}

30
src/Avalonia.Base/PriorityBindingEntry.cs

@ -93,22 +93,28 @@ namespace Avalonia
private void ValueChanged(object value)
{
var bindingError = value as BindingError;
var notification = value as BindingNotification;
if (bindingError != null)
if (notification != null)
{
_owner.Error(this, bindingError);
if (notification.ErrorType == BindingErrorType.Error)
{
_owner.Error(this, notification);
}
else if (notification.ErrorType == BindingErrorType.DataValidationError)
{
_owner.Validation(this, notification);
}
if (notification.HasValue)
{
Value = notification.Value;
_owner.Changed(this);
}
}
var validationStatus = value as IValidationStatus;
if (validationStatus != null)
{
_owner.Validation(this, validationStatus);
}
else if (bindingError == null || bindingError.UseFallbackValue)
else
{
Value = bindingError == null ? value : bindingError.FallbackValue;
Value = value;
_owner.Changed(this);
}
}

4
src/Avalonia.Base/PriorityLevel.cs

@ -159,7 +159,7 @@ namespace Avalonia
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="error">The error.</param>
public void Error(PriorityBindingEntry entry, BindingError error)
public void Error(PriorityBindingEntry entry, BindingNotification error)
{
_owner.LevelError(this, error);
}
@ -169,7 +169,7 @@ namespace Avalonia
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="validationStatus">The validation status.</param>
public void Validation(PriorityBindingEntry entry, IValidationStatus validationStatus)
public void Validation(PriorityBindingEntry entry, BindingNotification validationStatus)
{
_owner.LevelValidation(this, validationStatus);
}

6
src/Avalonia.Base/PriorityValue.cs

@ -184,7 +184,7 @@ namespace Avalonia
/// </summary>
/// <param name="priorityLevel">The priority level of the changed entry.</param>
/// <param name="validationStatus">The validation status.</param>
public void LevelValidation(PriorityLevel priorityLevel, IValidationStatus validationStatus)
public void LevelValidation(PriorityLevel priorityLevel, BindingNotification validationStatus)
{
_owner.DataValidationChanged(this, validationStatus);
}
@ -194,7 +194,7 @@ namespace Avalonia
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
/// <param name="error">The binding error.</param>
public void LevelError(PriorityLevel level, BindingError error)
public void LevelError(PriorityLevel level, BindingNotification error)
{
Logger.Log(
LogEventLevel.Error,
@ -203,7 +203,7 @@ namespace Avalonia
"Error binding to {Target}.{Property}: {Message}",
_owner,
Property,
error.Exception.Message);
error.Error.Message);
}
/// <summary>

2
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

@ -57,7 +57,7 @@ namespace Avalonia.Markup.Xaml.Templates
{
return null;
}
else if (result is BindingError)
else if (result is BindingNotification)
{
return null;
}

16
src/Markup/Avalonia.Markup/Data/ExpressionNode.cs

@ -108,14 +108,14 @@ namespace Avalonia.Markup.Data
protected virtual void SendValidationStatus(IValidationStatus status)
{
//Even if elements only bound to sub-values, send validation changes along so they will be surfaced to the UI level.
if (_subject != null)
{
_subject.OnNext(status);
}
else
{
Next?.SendValidationStatus(status);
}
//if (_subject != null)
//{
// _subject.OnNext(status);
//}
//else
//{
// Next?.SendValidationStatus(status);
//}
}
protected virtual void Unsubscribe(object target)

19
src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs

@ -126,16 +126,16 @@ namespace Avalonia.Markup.Data
converted = TypeUtilities.Default(type);
_inner.SetValue(converted, _priority);
}
else if (converted is BindingError)
else if (converted is BindingNotification)
{
var error = converted as BindingError;
var error = converted as BindingNotification;
Logger.Error(
LogArea.Binding,
this,
"Error binding to {Expression}: {Message}",
_inner.Expression,
error.Exception.Message);
error.Error.Message);
if (_fallbackValue != AvaloniaProperty.UnsetValue)
{
@ -174,7 +174,7 @@ namespace Avalonia.Markup.Data
private object ConvertValue(object value)
{
var converted =
value as BindingError ??
value as BindingNotification ??
value as IValidationStatus ??
Converter.Convert(
value,
@ -184,9 +184,9 @@ namespace Avalonia.Markup.Data
if (_fallbackValue != AvaloniaProperty.UnsetValue &&
(converted == AvaloniaProperty.UnsetValue ||
converted is BindingError))
converted is BindingNotification))
{
var error = converted as BindingError;
var error = converted as BindingNotification;
if (TypeUtilities.TryConvert(
_targetType,
@ -196,14 +196,15 @@ namespace Avalonia.Markup.Data
{
if (error != null)
{
converted = new BindingError(error.Exception, converted);
converted = new BindingNotification(error.Error, BindingErrorType.Error, converted);
}
}
else
{
converted = new BindingError(
converted = new BindingNotification(
new InvalidCastException(
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"));
$"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"),
BindingErrorType.Error);
}
}

2
src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -56,7 +56,7 @@ namespace Avalonia.Markup.Data.Plugins
{
var message = $"Could not find AvaloniaProperty '{propertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
return new PropertyError(new BindingError(exception));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
else
{

2
src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs

@ -61,7 +61,7 @@ namespace Avalonia.Markup.Data.Plugins
{
var message = $"Could not find CLR property '{propertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
return new PropertyError(new BindingError(exception));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
}

4
src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs

@ -8,13 +8,13 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
public class PropertyError : IPropertyAccessor
{
private BindingError _error;
private BindingNotification _error;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyError"/> class.
/// </summary>
/// <param name="error">The error to report.</param>
public PropertyError(BindingError error)
public PropertyError(BindingNotification error)
{
_error = error;
}

2
src/Markup/Avalonia.Markup/DefaultValueConverter.cs

@ -44,7 +44,7 @@ namespace Avalonia.Markup
if (value != null)
{
var message = $"Could not convert '{value}' to '{targetType}'";
return new BindingError(new InvalidCastException(message));
return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
}
return AvaloniaProperty.UnsetValue;

7
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject

@ -17,10 +17,11 @@
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseBuildConfiguration></UseBuildConfiguration>
<UseBuildPlatform></UseBuildPlatform>
<ProxyProcessPath></ProxyProcessPath>
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
<HiddenWarnings>LongTestTimesWithoutParallelExecution</HiddenWarnings>
</ProjectConfiguration>

17
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@ -273,26 +273,31 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error));
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void BindingError_With_FallbackValue_Causes_Target_Update()
public void BindingNotification_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject<object>();
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingError(new InvalidOperationException("Foo"), 8.9));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error,
8.9));
Assert.Equal(8.9, target.GetValue(Class1.QuxProperty));
}
[Fact]
public void Bind_Logs_BindingError()
public void Bind_Logs_Binding_Error()
{
var target = new Class1();
var source = new Subject<object>();
@ -313,7 +318,9 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error));
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
Assert.True(called);

9
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@ -406,7 +406,7 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.Error));
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
@ -419,7 +419,10 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
source.OnNext(new BindingError(new InvalidOperationException("Foo"), "fallback"));
source.OnNext(new BindingNotification(
new InvalidOperationException("Foo"),
BindingErrorType.Error,
"fallback"));
Assert.Equal("fallback", target.GetValue(Class1.FooProperty));
}
@ -449,7 +452,7 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.FooProperty, source);
source.OnNext("baz");
source.OnNext(new BindingError(new InvalidOperationException("Binding Error Message")));
source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error));
}
Assert.True(called);

10
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

@ -81,11 +81,11 @@ namespace Avalonia.Markup.UnitTests.Data
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.IsType<BindingError>(result);
Assert.IsType<BindingNotification>(result);
var error = result as BindingError;
Assert.IsType<MissingMemberException>(error.Exception);
Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Exception.Message);
var error = result as BindingNotification;
Assert.IsType<MissingMemberException>(error.Error);
Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Error.Message);
}
[Fact]
@ -216,7 +216,7 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(3, result.Count);
Assert.Equal("bar", result[0]);
Assert.IsType<BindingError>(result[1]);
Assert.IsType<BindingNotification>(result[1]);
Assert.Equal("baz", result[2]);
sub.Dispose();

2
tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs

@ -55,7 +55,7 @@ namespace Avalonia.Markup.UnitTests.Data
var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
var result = await target.Take(1);
Assert.IsType<BindingError>(result);
Assert.IsType<BindingNotification>(result);
}
[Fact]

2
tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs

@ -115,7 +115,7 @@ namespace Avalonia.Markup.UnitTests
null,
CultureInfo.InvariantCulture);
Assert.IsType<BindingError>(result);
Assert.IsType<BindingNotification>(result);
}
private enum TestEnum

Loading…
Cancel
Save