Browse Source

Reimplemented data validation using BindingNotifications.

pull/691/head
Steven Kirk 10 years ago
parent
commit
4906a472b0
  1. 2
      src/Avalonia.Base/Avalonia.Base.csproj
  2. 28
      src/Avalonia.Base/AvaloniaObject.cs
  3. 14
      src/Avalonia.Base/Data/BindingNotification.cs
  4. 17
      src/Avalonia.Base/Data/IValidationStatus.cs
  5. 44
      src/Avalonia.Base/Data/ObjectValidationStatus.cs
  6. 6
      src/Avalonia.Controls/Control.cs
  7. 2
      src/Avalonia.Controls/TextBox.cs
  8. 5
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  9. 13
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  10. 31
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  11. 2
      src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
  12. 55
      src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
  13. 80
      src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs
  14. 43
      src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
  15. 36
      src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs
  16. 5
      src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs
  17. 5
      src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  18. 35
      src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs
  19. 107
      src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
  20. 114
      src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  21. 68
      src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs
  22. 7
      src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs
  23. 46
      src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs
  24. 55
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  25. 4
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  26. 33
      tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs
  27. 5
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  28. 93
      tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
  29. 71
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
  30. 2
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  31. 109
      tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
  32. 71
      tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs
  33. 138
      tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs
  34. 8
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

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

@ -45,8 +45,6 @@
</Compile>
<Compile Include="Data\BindingNotification.cs" />
<Compile Include="Data\IndexerBinding.cs" />
<Compile Include="Data\IValidationStatus.cs" />
<Compile Include="Data\ObjectValidationStatus.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" />

28
src/Avalonia.Base/AvaloniaObject.cs

@ -50,29 +50,6 @@ namespace Avalonia
/// </summary>
private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
/// <summary>
/// Defines the <see cref="ValidationStatus"/> property.
/// </summary>
public static readonly DirectProperty<AvaloniaObject, ObjectValidationStatus> ValidationStatusProperty =
AvaloniaProperty.RegisterDirect<AvaloniaObject, ObjectValidationStatus>(nameof(ValidationStatus), c => c.ValidationStatus);
private ObjectValidationStatus validationStatus;
/// <summary>
/// The current validation status of the control.
/// </summary>
public ObjectValidationStatus ValidationStatus
{
get
{
return validationStatus;
}
private set
{
SetAndRaise(ValidationStatusProperty, ref validationStatus, value);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
/// </summary>
@ -497,7 +474,7 @@ namespace Avalonia
/// </summary>
/// <param name="property">The property whose validation state changed.</param>
/// <param name="status">The new validation state.</param>
protected virtual void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
protected virtual void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
{
}
@ -505,9 +482,8 @@ namespace Avalonia
/// Updates the validation status of the current object.
/// </summary>
/// <param name="status">The new validation status.</param>
protected void UpdateValidationState(IValidationStatus status)
protected void UpdateValidationState(BindingNotification status)
{
ValidationStatus = ValidationStatus.UpdateValidationStatus(status);
}
/// <inheritdoc/>

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

@ -30,7 +30,7 @@ namespace Avalonia.Data
/// Represents a binding notification that can be a valid binding value, or a binding or
/// data validation error.
/// </summary>
public class BindingNotification : IValidationStatus
public class BindingNotification
{
/// <summary>
/// A binding notification representing the null value.
@ -77,7 +77,7 @@ namespace Avalonia.Data
/// <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)
: this(error, errorType)
{
Value = fallbackValue;
HasValue = true;
@ -104,8 +104,6 @@ namespace Avalonia.Data
/// </summary>
public BindingErrorType ErrorType { get; }
bool IValidationStatus.IsValid => ErrorType == BindingErrorType.None;
public static bool operator ==(BindingNotification a, BindingNotification b)
{
if (object.ReferenceEquals(a, b))
@ -121,7 +119,7 @@ namespace Avalonia.Data
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));
(a.ErrorType == BindingErrorType.None || ExceptionEquals(a.Error, b.Error));
}
public static bool operator !=(BindingNotification a, BindingNotification b)
@ -165,5 +163,11 @@ namespace Avalonia.Data
return new BindingNotification(e, BindingErrorType.Error, Value);
}
}
private static bool ExceptionEquals(Exception a, Exception b)
{
return a?.GetType() == b?.GetType() &&
a.Message == b.Message;
}
}
}

17
src/Avalonia.Base/Data/IValidationStatus.cs

@ -1,17 +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.
namespace Avalonia.Data
{
/// <summary>
/// Contains information on if the current object passed validation.
/// Subclasses of this class contain additional information depending on the method of validation checking.
/// </summary>
public interface IValidationStatus
{
/// <summary>
/// True when the data passes validation; otherwise, false.
/// </summary>
bool IsValid { get; }
}
}

44
src/Avalonia.Base/Data/ObjectValidationStatus.cs

@ -1,44 +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.Generic;
using System.Linq;
namespace Avalonia.Data
{
/// <summary>
/// An immutable struct that contains validation information for a <see cref="AvaloniaObject"/> that validates a single property.
/// </summary>
public struct ObjectValidationStatus : IValidationStatus
{
private Dictionary<Type, IValidationStatus> currentValidationStatus;
public bool IsValid => currentValidationStatus?.Values.All(status => status.IsValid) ?? true;
/// <summary>
/// Constructs the structure with the given validation information.
/// </summary>
/// <param name="validations">The validation information</param>
public ObjectValidationStatus(Dictionary<Type, IValidationStatus> validations)
:this()
{
currentValidationStatus = validations;
}
/// <summary>
/// Creates a new status with the updated information.
/// </summary>
/// <param name="status">The updated status information.</param>
/// <returns>The new validation status.</returns>
public ObjectValidationStatus UpdateValidationStatus(IValidationStatus status)
{
var newStatus = new Dictionary<Type, IValidationStatus>(currentValidationStatus ??
new Dictionary<Type, IValidationStatus>());
newStatus[status.GetType()] = status;
return new ObjectValidationStatus(newStatus);
}
public IEnumerable<IValidationStatus> StatusInformation => currentValidationStatus.Values;
}
}

6
src/Avalonia.Controls/Control.cs

@ -108,7 +108,7 @@ namespace Avalonia.Controls
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass(IsFocusedProperty, ":focus");
PseudoClass(IsPointerOverProperty, ":pointerover");
PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid");
////PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid");
}
/// <summary>
@ -401,10 +401,10 @@ namespace Avalonia.Controls
protected IPseudoClasses PseudoClasses => Classes;
/// <inheritdoc/>
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
protected override void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
{
base.DataValidationChanged(property, status);
ValidationStatus.UpdateValidationStatus(status);
////ValidationStatus.UpdateValidationStatus(status);
}
/// <summary>

2
src/Avalonia.Controls/TextBox.cs

@ -235,7 +235,7 @@ namespace Avalonia.Controls
HandleTextInput(e.Text);
}
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
protected override void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
{
if (property == TextProperty)
{

5
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -48,7 +48,7 @@
<Compile Include="ControlLocator.cs" />
<Compile Include="Data\Plugins\ExceptionValidationPlugin.cs" />
<Compile Include="Data\Plugins\IndeiValidationPlugin.cs" />
<Compile Include="Data\Plugins\IValidationPlugin.cs" />
<Compile Include="Data\Plugins\IDataValidationPlugin.cs" />
<Compile Include="Data\Plugins\AvaloniaPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\IPropertyAccessor.cs" />
@ -59,8 +59,9 @@
<Compile Include="Data\Parsers\IdentifierParser.cs" />
<Compile Include="Data\Parsers\ExpressionParser.cs" />
<Compile Include="Data\Parsers\Reader.cs" />
<Compile Include="Data\Plugins\PropertyAccessorBase.cs" />
<Compile Include="Data\Plugins\PropertyError.cs" />
<Compile Include="Data\Plugins\ValidatingPropertyAccessorBase.cs" />
<Compile Include="Data\Plugins\DataValidatiorBase.cs" />
<Compile Include="Data\PropertyAccessorNode.cs" />
<Compile Include="Data\ExpressionNode.cs" />
<Compile Include="Data\ExpressionObserver.cs" />

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

@ -105,19 +105,6 @@ namespace Avalonia.Markup.Data
CurrentValue = reference;
}
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);
//}
}
protected virtual void Unsubscribe(object target)
{
}

31
src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs

@ -31,10 +31,11 @@ namespace Avalonia.Markup.Data
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
/// </summary>
public static readonly IList<IValidationPlugin> ValidationCheckers =
new List<IValidationPlugin>
public static readonly IList<IDataValidationPlugin> DataValidators =
new List<IDataValidationPlugin>
{
new IndeiValidationPlugin(),
ExceptionValidationPlugin.Instance,
};
private readonly WeakReference _root;
@ -45,24 +46,24 @@ namespace Avalonia.Markup.Data
private IDisposable _updateSubscription;
private int _count;
private readonly ExpressionNode _node;
private bool _enableValidation;
private bool _enableDataValidation;
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableValidation">Whether property validation should be enabled.</param>
public ExpressionObserver(object root, string expression, bool enableValidation = false)
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
public ExpressionObserver(object root, string expression, bool enableDataValidation = false)
{
Contract.Requires<ArgumentNullException>(expression != null);
_root = new WeakReference(root);
_enableValidation = enableValidation;
_enableDataValidation = enableDataValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
_node = ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
Expression = expression;
@ -73,21 +74,21 @@ namespace Avalonia.Markup.Data
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="enableValidation">Whether property validation should be enabled.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
public ExpressionObserver(
IObservable<object> rootObservable,
string expression,
bool enableValidation = false)
bool enableDataValidation = false)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_rootObservable = rootObservable;
_enableValidation = enableValidation;
_enableDataValidation = enableDataValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
_node = ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
Expression = expression;
@ -99,12 +100,12 @@ namespace Avalonia.Markup.Data
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
/// <param name="update">An observable which triggers a re-read of the getter.</param>
/// <param name="enableValidation">Whether property validation should be enabled.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled.</param>
public ExpressionObserver(
Func<object> rootGetter,
string expression,
IObservable<Unit> update,
bool enableValidation = false)
bool enableDataValidation = false)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
@ -112,11 +113,11 @@ namespace Avalonia.Markup.Data
_rootGetter = rootGetter;
_update = update;
_enableValidation = enableValidation;
_enableDataValidation = enableDataValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
_node = ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
Expression = expression;

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

@ -175,7 +175,7 @@ namespace Avalonia.Markup.Data
{
var converted =
value as BindingNotification ??
value as IValidationStatus ??
////value as IValidationStatus ??
Converter.Convert(
value,
_targetType,

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

@ -4,7 +4,6 @@
using System;
using System.Reactive.Linq;
using Avalonia.Data;
using Avalonia.Logging;
namespace Avalonia.Markup.Data.Plugins
{
@ -13,36 +12,22 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
{
/// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
public bool Match(WeakReference reference)
{
Contract.Requires<ArgumentNullException>(reference != null);
return reference.Target is AvaloniaObject;
}
/// <inheritdoc/>
public bool Match(WeakReference reference) => reference.Target is AvaloniaObject;
/// <summary>
/// Starts monitoring the value of a property on an object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed)
public IPropertyAccessor Start(WeakReference reference, string propertyName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
Contract.Requires<ArgumentNullException>(changed != null);
var instance = reference.Target;
var o = (AvaloniaObject)instance;
@ -50,7 +35,7 @@ namespace Avalonia.Markup.Data.Plugins
if (p != null)
{
return new Accessor(new WeakReference<AvaloniaObject>(o), p, changed);
return new Accessor(new WeakReference<AvaloniaObject>(o), p);
}
else if (instance != AvaloniaProperty.UnsetValue)
{
@ -64,23 +49,19 @@ namespace Avalonia.Markup.Data.Plugins
}
}
private class Accessor : IPropertyAccessor
private class Accessor : PropertyAccessorBase
{
private readonly WeakReference<AvaloniaObject> _reference;
private readonly AvaloniaProperty _property;
private IDisposable _subscription;
public Accessor(
WeakReference<AvaloniaObject> reference,
AvaloniaProperty property,
Action<object> changed)
public Accessor(WeakReference<AvaloniaObject> reference, AvaloniaProperty property)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null);
_reference = reference;
_property = property;
_subscription = Instance.GetWeakObservable(property).Skip(1).Subscribe(changed);
}
public AvaloniaObject Instance
@ -93,17 +74,10 @@ namespace Avalonia.Markup.Data.Plugins
}
}
public Type PropertyType => _property.PropertyType;
public object Value => Instance.GetValue(_property);
public override Type PropertyType => _property.PropertyType;
public override object Value => Instance?.GetValue(_property);
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
}
public bool SetValue(object value, BindingPriority priority)
public override bool SetValue(object value, BindingPriority priority)
{
if (!_property.IsReadOnly)
{
@ -113,6 +87,17 @@ namespace Avalonia.Markup.Data.Plugins
return false;
}
protected override void Dispose(bool disposing)
{
_subscription?.Dispose();
_subscription = null;
}
protected override void SubscribeCore(IObserver<object> observer)
{
_subscription = Instance.GetWeakObservable(_property).Subscribe(observer);
}
}
}
}

80
src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs

@ -0,0 +1,80 @@
// 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 Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Base class for data validators.
/// </summary>
/// <remarks>
/// Data validators are <see cref="IPropertyAccessor"/>s that are returned from an
/// <see cref="IDataValidationPlugin"/>. They wrap an inner <see cref="IPropertyAccessor"/>
/// and convert any values received from the inner property accessor into
/// <see cref="BindingNotification"/>s.
/// </remarks>
public abstract class DataValidatiorBase : PropertyAccessorBase, IObserver<object>
{
private readonly IPropertyAccessor _inner;
/// <summary>
/// Initializes a new instance of the <see cref="DataValidatiorBase"/> class.
/// </summary>
/// <param name="inner">The inner property accessor.</param>
protected DataValidatiorBase(IPropertyAccessor inner)
{
_inner = inner;
}
/// <inheritdoc/>
public override Type PropertyType => _inner.PropertyType;
/// <inheritdoc/>
public override object Value => _inner.Value;
/// <inheritdoc/>
public override bool SetValue(object value, BindingPriority priority) => _inner.SetValue(value, priority);
/// <summary>
/// Should never be called: the inner <see cref="IPropertyAccessor"/> should never notify
/// completion.
/// </summary>
void IObserver<object>.OnCompleted() { }
/// <summary>
/// Should never be called: the inner <see cref="IPropertyAccessor"/> should never notify
/// an error.
/// </summary>
void IObserver<object>.OnError(Exception error) { }
/// <summary>
/// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
/// </summary>
/// <param name="value">The value.</param>
void IObserver<object>.OnNext(object value) => InnerValueChanged(value);
/// <inheritdoc/>
protected override void Dispose(bool disposing) => _inner.Dispose();
/// <summary>
/// Begins listening to the inner <see cref="IPropertyAccessor"/>.
/// </summary>
protected override void SubscribeCore(IObserver<object> observer) => _inner.Subscribe(this);
/// <summary>
/// Called when the inner <see cref="IPropertyAccessor"/> notifies with a new value.
/// </summary>
/// <param name="value">The value.</param>
/// <remarks>
/// Notifies the observer that the value has changed. The value will be wrapped in a
/// <see cref="BindingNotification"/> if it is not already a binding notification.
/// </remarks>
protected virtual void InnerValueChanged(object value)
{
var notification = value as BindingNotification ?? new BindingNotification(value);
Observer.OnNext(notification);
}
}
}

43
src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs

@ -10,23 +10,26 @@ namespace Avalonia.Markup.Data.Plugins
/// <summary>
/// Validates properties that report errors by throwing exceptions.
/// </summary>
public class ExceptionValidationPlugin : IValidationPlugin
public class ExceptionValidationPlugin : IDataValidationPlugin
{
/// <summary>
/// Gets the default instance of the <see cref="ExceptionValidationPlugin"/>/
/// </summary>
public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin();
/// <inheritdoc/>
public bool Match(WeakReference reference) => true;
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
{
return new ExceptionValidationChecker(reference, name, accessor, callback);
return new Validator(reference, name, inner);
}
private class ExceptionValidationChecker : ValidatingPropertyAccessorBase
private class Validator : DataValidatiorBase
{
public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
: base(reference, name, accessor, callback)
public Validator(WeakReference reference, string name, IPropertyAccessor inner)
: base(inner)
{
}
@ -34,39 +37,19 @@ namespace Avalonia.Markup.Data.Plugins
{
try
{
var success = base.SetValue(value, priority);
SendValidationCallback(new ExceptionValidationStatus(null));
return success;
return base.SetValue(value, priority);
}
catch (TargetInvocationException ex)
{
SendValidationCallback(new ExceptionValidationStatus(ex.InnerException));
Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
}
catch (Exception ex)
{
SendValidationCallback(new ExceptionValidationStatus(ex));
Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError));
}
return false;
}
}
/// <summary>
/// Describes the current validation status after setting a property value.
/// </summary>
public class ExceptionValidationStatus : IValidationStatus
{
internal ExceptionValidationStatus(Exception exception)
{
Exception = exception;
return false;
}
/// <summary>
/// The thrown exception. If there was no thrown exception, null.
/// </summary>
public Exception Exception { get; }
/// <inheritdoc/>
public bool IsValid => Exception == null;
}
}
}

36
src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs

@ -0,0 +1,36 @@
// 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 Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Defines how data validation is observed by an <see cref="ExpressionObserver"/>.
/// </summary>
public interface IDataValidationPlugin
{
/// <summary>
/// Checks whether this plugin can handle data validation on the specified object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
bool Match(WeakReference reference);
/// <summary>
/// Starts monitoring the data validation state of a property on an object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="inner">The inner property accessor used to aceess the property.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(
WeakReference reference,
string propertyName,
IPropertyAccessor inner);
}
}

5
src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs

@ -10,11 +10,14 @@ namespace Avalonia.Markup.Data.Plugins
/// Defines an accessor to a property on an object returned by a
/// <see cref="IPropertyAccessorPlugin"/>
/// </summary>
public interface IPropertyAccessor : IDisposable
public interface IPropertyAccessor : IObservable<object>, IDisposable
{
/// <summary>
/// Gets the type of the property.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The accessor has not been subscribed to yet.
/// </exception>
Type PropertyType { get; }
/// <summary>

5
src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs

@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
namespace Avalonia.Markup.Data.Plugins
{
@ -24,14 +23,12 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed);
string propertyName);
}
}

35
src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs

@ -1,35 +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 Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Defines how view model data validation is observed by an <see cref="ExpressionObserver"/>.
/// </summary>
public interface IValidationPlugin
{
/// <summary>
/// Checks whether the data uses a validation scheme supported by this plugin.
/// </summary>
/// <param name="reference">A weak reference to the data.</param>
/// <returns><c>true</c> if this plugin can observe the validation; otherwise, <c>false</c>.</returns>
bool Match(WeakReference reference);
/// <summary>
/// Starts monitoring the validation state of an object for the given property.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="name">The property name.</param>
/// <param name="accessor">An underlying <see cref="IPropertyAccessor"/> to access the property.</param>
/// <param name="callback">A function to call when the validation state changes.</param>
/// <returns>
/// A <see cref="ValidatingPropertyAccessorBase"/> subclass through which future interactions with the
/// property will be made.
/// </returns>
IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback);
}
}

107
src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs

@ -2,7 +2,7 @@
// 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 Avalonia.Data;
@ -13,79 +13,106 @@ namespace Avalonia.Markup.Data.Plugins
/// <summary>
/// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationPlugin : IValidationPlugin
public class IndeiValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference)
{
return reference.Target is INotifyDataErrorInfo;
}
public bool Match(WeakReference reference) => reference.Target is INotifyDataErrorInfo;
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor)
{
return new IndeiValidationChecker(reference, name, accessor, callback);
return new Validator(reference, name, accessor);
}
private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
private class Validator : DataValidatiorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
{
public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
: base(reference, name, accessor, callback)
WeakReference _reference;
string _name;
public Validator(WeakReference reference, string name, IPropertyAccessor inner)
: base(inner)
{
_reference = reference;
_name = name;
}
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object sender, DataErrorsChangedEventArgs e)
{
var target = reference.Target as INotifyDataErrorInfo;
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{
Observer.OnNext(CreateBindingNotification(Value));
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
var target = _reference.Target as INotifyDataErrorInfo;
if (target != null)
{
if (target.HasErrors)
{
SendValidationCallback(new IndeiValidationStatus(target.GetErrors(name)));
}
WeakSubscriptionManager.Subscribe(
WeakSubscriptionManager.Unsubscribe(
target,
nameof(target.ErrorsChanged),
this);
}
}
public override void Dispose()
protected override void SubscribeCore(IObserver<object> observer)
{
base.Dispose();
var target = _reference.Target as INotifyDataErrorInfo;
if (target != null)
{
WeakSubscriptionManager.Unsubscribe(
WeakSubscriptionManager.Subscribe(
target,
nameof(target.ErrorsChanged),
this);
}
base.SubscribeCore(observer);
}
public void OnEvent(object sender, DataErrorsChangedEventArgs e)
protected override void InnerValueChanged(object value)
{
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
base.InnerValueChanged(CreateBindingNotification(value));
}
private BindingNotification CreateBindingNotification(object value)
{
var target = (INotifyDataErrorInfo)_reference.Target;
if (target != null)
{
var indei = _reference.Target as INotifyDataErrorInfo;
SendValidationCallback(new IndeiValidationStatus(indei.GetErrors(e.PropertyName)));
var errors = target.GetErrors(_name)?
.Cast<String>()
.Where(x => x != null).ToList();
if (errors?.Count > 0)
{
return new BindingNotification(
GenerateException(errors),
BindingErrorType.DataValidationError,
value);
}
}
return new BindingNotification(value);
}
}
/// <summary>
/// Describes the current validation status of a property as reported by an object that implements <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationStatus : IValidationStatus
{
internal IndeiValidationStatus(IEnumerable errors)
private Exception GenerateException(IList<string> errors)
{
Errors = errors;
if (errors.Count == 1)
{
return new Exception(errors[0]);
}
else
{
return new AggregateException(
errors.Select(x => new Exception(x)));
}
}
/// <inheritdoc/>
public bool IsValid => !Errors?.OfType<object>().Any() ?? true;
/// <summary>
/// The errors on the given property and on the object as a whole.
/// </summary>
public IEnumerable Errors { get; }
}
}
}

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

@ -9,7 +9,6 @@ using System.Reflection;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
using System.Collections;
namespace Avalonia.Markup.Data.Plugins
{
@ -19,43 +18,29 @@ namespace Avalonia.Markup.Data.Plugins
/// </summary>
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
/// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary>
/// <param name="reference">The object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
public bool Match(WeakReference reference)
{
Contract.Requires<ArgumentNullException>(reference != null);
return true;
}
/// <inheritdoc/>
public bool Match(WeakReference reference) => true;
/// <summary>
/// Starts monitoring the value of a property on an object.
/// </summary>
/// <param name="reference">The object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
/// </returns>
public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed)
public IPropertyAccessor Start(WeakReference reference, string propertyName)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
Contract.Requires<ArgumentNullException>(changed != null);
var instance = reference.Target;
var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName);
if (p != null)
{
return new Accessor(reference, p, changed);
return new Accessor(reference, p);
}
else
{
@ -65,78 +50,99 @@ namespace Avalonia.Markup.Data.Plugins
}
}
private class Accessor : IPropertyAccessor, IWeakSubscriber<PropertyChangedEventArgs>
private class Accessor : PropertyAccessorBase, IWeakSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference _reference;
private readonly PropertyInfo _property;
private readonly Action<object> _changed;
public Accessor(
WeakReference reference,
PropertyInfo property,
Action<object> changed)
public Accessor(WeakReference reference, PropertyInfo property)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null);
_reference = reference;
_property = property;
_changed = changed;
}
var inpc = reference.Target as INotifyPropertyChanged;
public override Type PropertyType => _property.PropertyType;
if (inpc != null)
public override object Value
{
get
{
WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
var o = _reference.Target;
return (o != null) ? _property.GetValue(o) : null;
}
else
}
public override bool SetValue(object value, BindingPriority priority)
{
if (_property.CanWrite)
{
Logger.Warning(
LogArea.Binding,
this,
"Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged",
property.Name,
reference.Target,
reference.Target.GetType());
_property.SetValue(_reference.Target, value);
return true;
}
}
public Type PropertyType => _property.PropertyType;
return false;
}
public object Value => _property.GetValue(_reference.Target);
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{
SendCurrentValue();
}
}
public void Dispose()
protected override void Dispose(bool disposing)
{
var inpc = _reference.Target as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Unsubscribe<PropertyChangedEventArgs>(
WeakSubscriptionManager.Unsubscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
}
public bool SetValue(object value, BindingPriority priority)
protected override void SubscribeCore(IObserver<object> observer)
{
if (_property.CanWrite)
SendCurrentValue();
SubscribeToChanges();
}
private void SendCurrentValue()
{
try
{
_property.SetValue(_reference.Target, value);
return true;
var value = Value;
Observer.OnNext(value);
}
return false;
catch { }
}
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
private void SubscribeToChanges()
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
var inpc = _reference.Target as INotifyPropertyChanged;
if (inpc != null)
{
WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
}
else
{
_changed(Value);
Logger.Information(
LogArea.Binding,
this,
"Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged",
_property.Name,
_reference.Target,
_reference.Target.GetType());
}
}
}

68
src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs

@ -0,0 +1,68 @@
// 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 Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Defines a default base implementation for a <see cref="IPropertyAccessor"/>.
/// </summary>
/// <remarks>
/// <see cref="IPropertyAccessor"/> is an observable that will only be subscribed to one time.
/// In addition, the subscription can be disposed by calling <see cref="Dispose()"/> on the
/// property accessor itself - this prevents needing to hold two references for a subscription.
/// </remarks>
public abstract class PropertyAccessorBase : IPropertyAccessor
{
/// <inheritdoc/>
public abstract Type PropertyType { get; }
/// <inheritdoc/>
public abstract object Value { get; }
/// <summary>
/// Stops the subscription.
/// </summary>
public void Dispose() => Dispose(true);
/// <inheritdoc/>
public abstract bool SetValue(object value, BindingPriority priority);
/// <summary>
/// The currently subscribed observer.
/// </summary>
protected IObserver<object> Observer { get; private set; }
/// <inheritdoc/>
public IDisposable Subscribe(IObserver<object> observer)
{
Contract.Requires<ArgumentNullException>(observer != null);
if (Observer != null)
{
throw new InvalidOperationException(
"A property accessor can be subscribed to only once.");
}
Observer = observer;
SubscribeCore(observer);
return this;
}
/// <summary>
/// Stops listening to the property.
/// </summary>
/// <param name="disposing">
/// True if the <see cref="Dispose()"/> method was called, false if the object is being
/// finalized.
/// </param>
protected virtual void Dispose(bool disposing) => Observer = null;
/// <summary>
/// When overridden in a derived class, begins listening to the property.
/// </summary>
protected abstract void SubscribeCore(IObserver<object> observer);
}
}

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

@ -1,4 +1,5 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
@ -35,5 +36,11 @@ namespace Avalonia.Markup.Data.Plugins
{
return false;
}
public IDisposable Subscribe(IObserver<object> observer)
{
observer.OnNext(_error);
return Disposable.Empty;
}
}
}

46
src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs

@ -1,46 +0,0 @@
using System;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// A base class for validating <see cref="IPropertyAccessor"/>s that wraps an <see cref="IPropertyAccessor"/> and forwards method calls to it.
/// </summary>
public abstract class ValidatingPropertyAccessorBase : IPropertyAccessor
{
protected readonly WeakReference _reference;
protected readonly string _name;
private readonly IPropertyAccessor _accessor;
private readonly Action<IValidationStatus> _callback;
protected ValidatingPropertyAccessorBase(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
{
_reference = reference;
_name = name;
_accessor = accessor;
_callback = callback;
}
/// <inheritdoc/>
public Type PropertyType => _accessor.PropertyType;
/// <inheritdoc/>
public object Value => _accessor.Value;
/// <inheritdoc/>
public virtual void Dispose() => _accessor.Dispose();
/// <inheritdoc/>
public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority);
/// <summary>
/// Sends the validation status to the callback specified in construction.
/// </summary>
/// <param name="status">The validation status.</param>
protected void SendValidationCallback(IValidationStatus status)
{
_callback?.Invoke(status);
}
}
}

55
src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs

@ -9,11 +9,12 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Markup.Data.Plugins;
namespace Avalonia.Markup.Data
{
internal class PropertyAccessorNode : ExpressionNode
internal class PropertyAccessorNode : ExpressionNode, IObserver<object>
{
private IPropertyAccessor _accessor;
private IDisposable _subscription;
@ -39,49 +40,55 @@ namespace Avalonia.Markup.Data
{
if (_accessor != null)
{
return _accessor.SetValue(value, priority);
try { return _accessor.SetValue(value, priority); } catch { }
}
return false;
}
}
void IObserver<object>.OnCompleted()
{
// Should not be called by IPropertyAccessor.
}
void IObserver<object>.OnError(Exception error)
{
// Should not be called by IPropertyAccessor.
}
void IObserver<object>.OnNext(object value)
{
SetCurrentValue(value);
}
protected override void SubscribeAndUpdate(WeakReference reference)
{
var instance = reference.Target;
if (instance != null && instance != AvaloniaProperty.UnsetValue)
{
var accessorPlugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
var accessor = plugin?.Start(reference, PropertyName);
if (accessorPlugin != null)
if (_enableValidation)
{
_accessor = ExceptionValidationPlugin.Instance.Start(
reference,
PropertyName,
accessorPlugin.Start(reference, PropertyName, SetCurrentValue),
SendValidationStatus);
if (_enableValidation)
foreach (var validator in ExpressionObserver.DataValidators)
{
foreach (var validationPlugin in ExpressionObserver.ValidationCheckers)
if (validator.Match(reference))
{
if (validationPlugin.Match(reference))
{
_accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
}
accessor = validator.Start(reference, PropertyName, accessor);
}
}
if (_accessor != null)
{
SetCurrentValue(_accessor.Value);
return;
}
}
}
CurrentValue = UnsetReference;
_accessor = accessor;
_accessor.Subscribe(this);
}
else
{
CurrentValue = UnsetReference;
}
}
protected override void Unsubscribe(object target)

4
tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs

@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names);
}
[Fact]
@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names);
}
[Fact]

33
tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs

@ -24,11 +24,12 @@ namespace Avalonia.Controls.UnitTests
binding.EnableValidation = true;
target.Bind(TextBox.TextProperty, binding);
Assert.True(target.ValidationStatus.IsValid);
target.Text = "20";
Assert.False(target.ValidationStatus.IsValid);
target.Text = "1";
Assert.True(target.ValidationStatus.IsValid);
Assert.True(false);
//Assert.True(target.ValidationStatus.IsValid);
//target.Text = "20";
//Assert.False(target.ValidationStatus.IsValid);
//target.Text = "1";
//Assert.True(target.ValidationStatus.IsValid);
}
}
@ -43,11 +44,12 @@ namespace Avalonia.Controls.UnitTests
binding.EnableValidation = true;
target.Bind(TextBox.TextProperty, binding);
Assert.True(target.ValidationStatus.IsValid);
target.Text = "foo";
Assert.False(target.ValidationStatus.IsValid);
target.Text = "1";
Assert.True(target.ValidationStatus.IsValid);
Assert.True(false);
//Assert.True(target.ValidationStatus.IsValid);
//target.Text = "foo";
//Assert.False(target.ValidationStatus.IsValid);
//target.Text = "1";
//Assert.True(target.ValidationStatus.IsValid);
}
}
@ -62,11 +64,12 @@ namespace Avalonia.Controls.UnitTests
binding.EnableValidation = true;
target.Bind(TextBox.TextProperty, binding);
Assert.True(target.ValidationStatus.IsValid);
target.Text = "20";
Assert.False(target.ValidationStatus.IsValid);
target.Text = "1";
Assert.True(target.ValidationStatus.IsValid);
Assert.True(false);
//Assert.True(target.ValidationStatus.IsValid);
//target.Text = "20";
//Assert.False(target.ValidationStatus.IsValid);
//target.Text = "1";
//Assert.True(target.ValidationStatus.IsValid);
}
}

5
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@ -85,7 +85,8 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ControlLocatorTests.cs" />
<Compile Include="Data\ExceptionValidatorTests.cs" />
<Compile Include="Data\Plugins\IndeiValidationPluginTests.cs" />
<Compile Include="Data\Plugins\ExceptionValidationPluginTests.cs" />
<Compile Include="Data\ExpressionNodeBuilderTests.cs" />
<Compile Include="Data\ExpressionNodeBuilderTests_Errors.cs" />
<Compile Include="Data\ExpressionObserverTests_Lifetime.cs" />
@ -97,7 +98,7 @@
<Compile Include="Data\ExpressionObserverTests_Property.cs" />
<Compile Include="Data\ExpressionObserverTests_SetValue.cs" />
<Compile Include="Data\ExpressionObserverTests_Task.cs" />
<Compile Include="Data\ExpressionObserverTests_Validation.cs" />
<Compile Include="Data\ExpressionObserverTests_DataValidation.cs" />
<Compile Include="Data\ExpressionSubjectTests.cs" />
<Compile Include="Data\IndeiValidatorTests.cs" />
<Compile Include="DefaultValueConverterTests.cs" />

93
tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs

@ -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);
}
}
}

71
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs → tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs

@ -14,21 +14,40 @@ using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionObserverTests_Validation
public class ExpressionObserverTests_DataValidation
{
[Fact]
public void Exception_Validation_Sends_ValidationUpdate()
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.Where(o => o is IValidationStatus).Subscribe(_ => validationMessageFound = true);
observer.OfType<BindingNotification>()
.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<BindingNotification>()
.Where(x => x.ErrorType == BindingErrorType.DataValidationError)
.Subscribe(_ => validationMessageFound = true);
observer.SetValue(-5);
Assert.True(validationMessageFound);
}
[Fact]
public void Disabled_Indei_Validation_Does_Not_Subscribe()
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);
@ -50,6 +69,42 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(0, data.SubscriptionCount);
}
[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<object>();
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);
}
public class ExceptionTest : INotifyPropertyChanged
{
private int _mustBePositive;
@ -75,7 +130,7 @@ namespace Avalonia.Markup.UnitTests.Data
}
}
private class IndeiTest : INotifyDataErrorInfo
private class IndeiTest : INotifyDataErrorInfo, INotifyPropertyChanged
{
private int _mustBePositive;
private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
@ -86,9 +141,11 @@ namespace Avalonia.Markup.UnitTests.Data
get { return _mustBePositive; }
set
{
_mustBePositive = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MustBePositive)));
if (value >= 0)
{
_mustBePositive = value;
_errors.Remove(nameof(MustBePositive));
_errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
}
@ -118,6 +175,8 @@ namespace Avalonia.Markup.UnitTests.Data
}
}
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable GetErrors(string propertyName)
{
IList<string> result;

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

@ -75,7 +75,7 @@ namespace Avalonia.Markup.UnitTests.Data
}
[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");

109
tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs

@ -3,6 +3,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Data;
@ -13,6 +14,69 @@ namespace Avalonia.Markup.UnitTests.Data
{
public class IndeiValidatorTests
{
[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));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor);
var results = new List<object>();
validator.Subscribe(x => results.Add(x));
validator.SetValue(5, BindingPriority.LocalValue);
Assert.Equal(
new[]
{
new BindingNotification(0),
new BindingNotification(5),
}, results);
}
[Fact]
public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_BindingNotification()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new IndeiValidationPlugin();
var data = new Data { MustBePositive = 1 };
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
var results = new List<object>();
validator.Subscribe(x => results.Add(x));
validator.SetValue(5, BindingPriority.LocalValue);
Assert.Equal(
new[]
{
new BindingNotification(1),
new BindingNotification(5),
}, results);
}
[Fact]
public void Setting_Validating_Property_To_Invalid_Value_Returns_DataValidationError()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new IndeiValidationPlugin();
var data = new Data { MustBePositive = 1 };
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
var results = new List<object>();
validator.Subscribe(x => results.Add(x));
validator.SetValue(-5, BindingPriority.LocalValue);
Assert.Equal(
new[]
{
new BindingNotification(1),
new BindingNotification(new Exception("MustBePositive must be positive"), BindingErrorType.DataValidationError, -5),
}, results);
}
public class Data : INotifyPropertyChanged, INotifyDataErrorInfo
{
private int nonValidated;
@ -64,50 +128,5 @@ namespace Avalonia.Markup.UnitTests.Data
}
}
}
[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);
}
}
}

71
tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs

@ -0,0 +1,71 @@
// 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;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
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<object>();
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 : INotifyPropertyChanged
{
private int _mustBePositive;
public int MustBePositive
{
get { return _mustBePositive; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
if (value != _mustBePositive)
{
_mustBePositive = value;
NotifyPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}

138
tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs

@ -0,0 +1,138 @@
// 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.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<object>();
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.SubscriptionCount);
var sub = validator.Subscribe(_ => { });
Assert.Equal(1, data.SubscriptionCount);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
}
public class Data : INotifyDataErrorInfo, INotifyPropertyChanged
{
private int _value;
private int _maximum;
private string _error;
private EventHandler<DataErrorsChangedEventArgs> _errorsChanged;
public bool HasErrors => _error != null;
public int SubscriptionCount { get; private set; }
public int Value
{
get { return _value; }
set
{
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
UpdateError();
}
}
public int Maximum
{
get { return _maximum; }
set
{
_maximum = value;
UpdateError();
}
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
{
add { _errorsChanged += value; ++SubscriptionCount; }
remove { _errorsChanged -= value; --SubscriptionCount; }
}
public event PropertyChangedEventHandler PropertyChanged;
public 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;
_errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
}
}
else
{
if (_error == null)
{
_error = "Must be less than Maximum";
_errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
}
}
}
}
}
}

8
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

@ -26,7 +26,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
target.ValidationTest = -5;
Assert.False(target.ValidationStatus.IsValid);
Assert.True(false);
//Assert.False(target.ValidationStatus.IsValid);
}
[Fact]
@ -44,7 +45,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
target.Bind(TestControl.ValidationTestProperty, binding);
target.ValidationTest = -5;
Assert.False(target.ValidationStatus.IsValid);
Assert.True(false);
//Assert.False(target.ValidationStatus.IsValid);
}
@ -123,7 +125,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
}
}
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
protected override void DataValidationChanged(AvaloniaProperty property, BindingNotification status)
{
if (property == ValidationTestProperty)
{

Loading…
Cancel
Save