Browse Source

Finished wiring up validation support. Now exceptions and INotifyDataErrorInfo implementers will set controls into their ":invalid" pseudo-class state.

pull/514/head
Jeremy Koritzinsky 10 years ago
parent
commit
ee486113e5
  1. 5
      src/Markup/Perspex.Markup/Data/ExpressionNode.cs
  2. 11
      src/Markup/Perspex.Markup/Data/ExpressionObserver.cs
  3. 1
      src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
  4. 23
      src/Markup/Perspex.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs
  5. 4
      src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  6. 22
      src/Markup/Perspex.Markup/Data/Plugins/IValidationCheckerPlugin.cs
  7. 24
      src/Markup/Perspex.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs
  8. 44
      src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  9. 4
      src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs
  10. 11
      src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
  11. 9
      src/Perspex.Base/IPriorityValueOwner.cs
  12. 13
      src/Perspex.Base/PerspexObject.cs
  13. 8
      src/Perspex.Base/PriorityBindingEntry.cs
  14. 11
      src/Perspex.Base/PriorityLevel.cs
  15. 10
      src/Perspex.Base/PriorityValue.cs
  16. 30
      src/Perspex.Controls/Control.cs
  17. 99
      tests/Perspex.Markup.UnitTests/Data/ExceptionValidatorTests.cs
  18. 5
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  19. 120
      tests/Perspex.Markup.UnitTests/Data/IndeiValidatorTests.cs
  20. 142
      tests/Perspex.Markup.UnitTests/Data/InpcPluginTests.cs
  21. 3
      tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

5
src/Markup/Perspex.Markup/Data/ExpressionNode.cs

@ -105,6 +105,11 @@ namespace Perspex.Markup.Data
CurrentValue = reference;
}
protected virtual void SendValidationStatus(ValidationStatus status)
{
_subject?.OnNext(status);
}
protected virtual void Unsubscribe(object target)
{
}

11
src/Markup/Perspex.Markup/Data/ExpressionObserver.cs

@ -28,6 +28,17 @@ namespace Perspex.Markup.Data
new InpcPropertyAccessorPlugin(),
};
/// <summary>
/// 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<IValidationCheckerPlugin> ValidationCheckers =
new List<IValidationCheckerPlugin>
{
new IndeiValidationCheckerPlugin(),
new ExceptionValidationCheckerPlugin()
};
private readonly WeakReference _root;
private readonly Func<object> _rootGetter;
private readonly IObservable<object> _rootObservable;

1
src/Markup/Perspex.Markup/Data/ExpressionSubject.cs

@ -155,6 +155,7 @@ namespace Perspex.Markup.Data
{
var converted =
value as BindingError ??
value as ValidationStatus ??
Converter.Convert(
value,
_targetType,

23
src/Markup/Perspex.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs

@ -7,8 +7,17 @@ using Perspex.Data;
namespace Perspex.Markup.Data.Plugins
{
/// <summary>
/// Validates properties that report errors by throwing exceptions.
/// </summary>
public class ExceptionValidationCheckerPlugin : IValidationCheckerPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference) => true;
/// <inheritdoc/>
public ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
{
return new ExceptionValidationChecker(reference, name, accessor, callback);
@ -37,16 +46,24 @@ namespace Perspex.Markup.Data.Plugins
}
}
private class ExceptionValidationStatus : ValidationStatus
/// <summary>
/// Describes the current validation status after setting a property value.
/// </summary>
public class ExceptionValidationStatus : ValidationStatus
{
public ExceptionValidationStatus(Exception exception)
internal ExceptionValidationStatus(Exception exception)
{
Exception = exception;
}
/// <summary>
/// The thrown exception. If there was no thrown exception, null.
/// </summary>
public Exception Exception { get; }
public override bool IsValid => Exception != null;
/// <inheritdoc/>
public override bool IsValid => Exception == null;
}
}
}

4
src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs

@ -25,7 +25,6 @@ namespace Perspex.Markup.Data.Plugins
/// <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>
/// <param name="validationChanged">A function to call when the validation status of the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
@ -33,7 +32,6 @@ namespace Perspex.Markup.Data.Plugins
IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed,
Action<IEnumerable> validationChanged);
Action<object> changed);
}
}

22
src/Markup/Perspex.Markup/Data/Plugins/IValidationCheckerPlugin.cs

@ -7,8 +7,30 @@ using System.Threading.Tasks;
namespace Perspex.Markup.Data.Plugins
{
/// <summary>
/// Defines how view model data validation is observed by an <see cref="ExpressionObserver"/>.
/// </summary>
public interface IValidationCheckerPlugin
{
/// <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 monitering 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="ValidationCheckerBase"/> subclass through which future interactions with the
/// property will be made.
/// </returns>
ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback);
}
}

24
src/Markup/Perspex.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs

@ -10,8 +10,18 @@ using Perspex.Utilities;
namespace Perspex.Markup.Data.Plugins
{
class IndeiValidationCheckerPlugin : IValidationCheckerPlugin
/// <summary>
/// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationCheckerPlugin : IValidationCheckerPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference)
{
return reference.Target is INotifyDataErrorInfo;
}
/// <inheritdoc/>
public ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
{
return new IndeiValidationChecker(reference, name, accessor, callback);
@ -59,14 +69,22 @@ namespace Perspex.Markup.Data.Plugins
}
}
private class IndeiValidationStatus : ValidationStatus
/// <summary>
/// Describes the current validation status of a property as reported by an object that implements <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationStatus : ValidationStatus
{
public IndeiValidationStatus(IEnumerable errors)
internal IndeiValidationStatus(IEnumerable errors)
{
Errors = errors;
}
/// <inheritdoc/>
public override bool IsValid => !Errors.OfType<object>().Any();
/// <summary>
/// The errors on the given property and on the object as a whole.
/// </summary>
public IEnumerable Errors { get; }
}
}

44
src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs

@ -37,7 +37,6 @@ namespace Perspex.Markup.Data.Plugins
/// <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>
/// <param name="validationChanged">A function to call when the validation state of the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
@ -45,8 +44,7 @@ namespace Perspex.Markup.Data.Plugins
public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed,
Action<IEnumerable> validationChanged)
Action<object> changed)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
@ -57,7 +55,7 @@ namespace Perspex.Markup.Data.Plugins
if (p != null)
{
return new Accessor(reference, p, changed, validationChanged);
return new Accessor(reference, p, changed);
}
else
{
@ -67,18 +65,16 @@ namespace Perspex.Markup.Data.Plugins
}
}
private class Accessor : IPropertyAccessor, IWeakSubscriber<PropertyChangedEventArgs>, IWeakSubscriber<DataErrorsChangedEventArgs>
private class Accessor : IPropertyAccessor, IWeakSubscriber<PropertyChangedEventArgs>
{
private readonly WeakReference _reference;
private readonly PropertyInfo _property;
private readonly Action<object> _changed;
private readonly Action<IEnumerable> _validationChanged;
public Accessor(
WeakReference reference,
PropertyInfo property,
Action<object> changed,
Action<IEnumerable> validationChanged)
Action<object> changed)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(property != null);
@ -86,7 +82,6 @@ namespace Perspex.Markup.Data.Plugins
_reference = reference;
_property = property;
_changed = changed;
_validationChanged = validationChanged;
var inpc = reference.Target as INotifyPropertyChanged;
@ -107,19 +102,6 @@ namespace Perspex.Markup.Data.Plugins
reference.Target,
reference.Target.GetType());
}
var indei = _reference.Target as INotifyDataErrorInfo;
if (indei != null)
{
if (indei.HasErrors)
{
_validationChanged(indei.GetErrors(property.Name));
}
WeakSubscriptionManager.Subscribe<DataErrorsChangedEventArgs>(
indei,
nameof(indei.ErrorsChanged),
this);
}
}
public Type PropertyType => _property.PropertyType;
@ -137,15 +119,6 @@ namespace Perspex.Markup.Data.Plugins
nameof(inpc.PropertyChanged),
this);
}
var indei = _reference.Target as INotifyDataErrorInfo;
if (indei != null)
{
WeakSubscriptionManager.Unsubscribe<DataErrorsChangedEventArgs>(
indei,
nameof(indei.ErrorsChanged),
this);
}
}
public bool SetValue(object value, BindingPriority priority)
@ -166,15 +139,6 @@ namespace Perspex.Markup.Data.Plugins
_changed(Value);
}
}
void IWeakSubscriber<DataErrorsChangedEventArgs>.OnEvent(object sender, DataErrorsChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{
var indei = _reference.Target as INotifyDataErrorInfo;
_validationChanged(indei.GetErrors(e.PropertyName));
}
}
}
}
}

4
src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs

@ -31,7 +31,6 @@ namespace Perspex.Markup.Data.Plugins
/// <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>
/// <param name="validationChanged">A function to call when the validation state of the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made.
@ -39,8 +38,7 @@ namespace Perspex.Markup.Data.Plugins
public IPropertyAccessor Start(
WeakReference reference,
string propertyName,
Action<object> changed,
Action<System.Collections.IEnumerable> validationChanged)
Action<object> changed)
{
Contract.Requires<ArgumentNullException>(reference != null);
Contract.Requires<ArgumentNullException>(propertyName != null);

11
src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs

@ -50,11 +50,16 @@ namespace Perspex.Markup.Data
if (instance != null && instance != PerspexProperty.UnsetValue)
{
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
var accessorPlugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
if (plugin != null)
if (accessorPlugin != null)
{
_accessor = plugin.Start(reference, PropertyName, SetCurrentValue, _ => { });
_accessor = accessorPlugin.Start(reference, PropertyName, SetCurrentValue);
var validationPlugin = ExpressionObserver.ValidationCheckers.FirstOrDefault(x => x.Match(reference));
if (validationPlugin != null)
{
_accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
}
if (_accessor != null)
{

9
src/Perspex.Base/IPriorityValueOwner.cs

@ -1,6 +1,8 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Data;
namespace Perspex
{
/// <summary>
@ -15,5 +17,12 @@ namespace Perspex
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
void Changed(PriorityValue sender, object oldValue, object newValue);
/// <summary>
/// Called when the validation state of a <see cref="PriorityValue"/> changes.
/// </summary>
/// <param name="sender">The source of the change.</param>
/// <param name="status">The validation status.</param>
void ValidationChanged(PriorityValue sender, ValidationStatus status);
}
}

13
src/Perspex.Base/PerspexObject.cs

@ -403,6 +403,7 @@ namespace Perspex
IDisposable subscription = null;
subscription = source
.Where(x => !(x is ValidationStatus))
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => s_directBindings.Remove(subscription))
.Subscribe(x => DirectBindingSet(property, x));
@ -505,6 +506,18 @@ namespace Perspex
}
}
/// <inheritdoc/>
void IPriorityValueOwner.ValidationChanged(PriorityValue sender, ValidationStatus status)
{
var property = sender.Property;
ValidationChanged(property, status);
}
protected virtual void ValidationChanged(PerspexProperty property, ValidationStatus status)
{
}
/// <inheritdoc/>
Delegate[] IPerspexObjectDebug.GetPropertyChangedSubscribers()
{

8
src/Perspex.Base/PriorityBindingEntry.cs

@ -99,7 +99,13 @@ namespace Perspex
_owner.Error(this, bindingError);
}
if (bindingError == null || bindingError.UseFallbackValue)
var validationStatus = value as ValidationStatus;
if (validationStatus != null)
{
_owner.Validation(this, validationStatus);
}
else if (bindingError == null || bindingError.UseFallbackValue)
{
Value = bindingError == null ? value : bindingError.FallbackValue;
_owner.Changed(this);

11
src/Perspex.Base/PriorityLevel.cs

@ -164,6 +164,17 @@ namespace Perspex
_owner.LevelError(this, error);
}
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> reports validation status.
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="validationStatus">The validation status.</param>
public void Validation(PriorityBindingEntry entry, ValidationStatus validationStatus)
{
_owner.LevelValidation(this, validationStatus);
}
/// <summary>
/// Activates the first binding that has a value.
/// </summary>

10
src/Perspex.Base/PriorityValue.cs

@ -178,6 +178,16 @@ namespace Perspex
}
}
/// <summary>
/// Called whenever a priority level validation state changes.
/// </summary>
/// <param name="priorityLevel">The priority level of the changed entry.</param>
/// <param name="validationStatus">The validation status.</param>
public void LevelValidation(PriorityLevel priorityLevel, ValidationStatus validationStatus)
{
_owner.ValidationChanged(this, validationStatus);
}
/// <summary>
/// Called when a priority level encounters an error.
/// </summary>

30
src/Perspex.Controls/Control.cs

@ -86,6 +86,12 @@ namespace Perspex.Controls
public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="ValidationStatus"/> property.
/// </summary>
public static readonly DirectProperty<Control, ValidationStatus> ValidationStatusProperty =
PerspexProperty.RegisterDirect<Control, ValidationStatus>(nameof(ValidationStatus), c=> c.ValidationStatus);
private int _initCount;
private string _name;
private IControl _parent;
@ -108,6 +114,7 @@ namespace Perspex.Controls
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass(IsFocusedProperty, ":focus");
PseudoClass(IsPointerOverProperty, ":pointerover");
PseudoClass(ValidationStatusProperty, status => status != null && !status.IsValid, ":invalid");
}
/// <summary>
@ -399,6 +406,29 @@ namespace Perspex.Controls
/// </summary>
protected IPseudoClasses PseudoClasses => Classes;
private ValidationStatus validationStatus;
/// <summary>
/// The current validation status of the control.
/// </summary>
public ValidationStatus ValidationStatus
{
get
{
return validationStatus;
}
private set
{
SetAndRaise(ValidationStatusProperty, ref validationStatus, value);
}
}
protected override void ValidationChanged(PerspexProperty property, ValidationStatus status)
{
base.ValidationChanged(property, status);
ValidationStatus = status;
}
/// <summary>
/// Sets the control's logical parent.
/// </summary>

99
tests/Perspex.Markup.UnitTests/Data/ExceptionValidatorTests.cs

@ -0,0 +1,99 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Data;
using Perspex.Markup.Data.Plugins;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace Perspex.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 ExceptionValidationCheckerPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
ValidationStatus 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 ExceptionValidationCheckerPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus 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 ExceptionValidationCheckerPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus 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);
}
}
}

5
tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

@ -300,13 +300,14 @@ namespace Perspex.Markup.UnitTests.Data
Assert.False(target.SetValue("baz"));
}
[Fact]
[Fact(Skip = "Validation captures the exception")]
public void SetValue_Should_Throw_For_Wrong_Type()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
Assert.Throws<ArgumentException>(() => target.SetValue(1.2));
Assert.Throws<ArgumentException>(() =>
target.SetValue(1.2));
}
[Fact]

120
tests/Perspex.Markup.UnitTests/Data/IndeiValidatorTests.cs

@ -0,0 +1,120 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Data;
using Perspex.Markup.Data.Plugins;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using System.Collections;
namespace Perspex.Markup.UnitTests.Data
{
public class IndeiValidatorTests
{
public class Data : INotifyPropertyChanged, INotifyDataErrorInfo
{
private int nonValidated;
public int NonValidated
{
get { return nonValidated; }
set { nonValidated = value; NotifyPropertyChanged(); }
}
private int mustBePositive;
public int MustBePositive
{
get { return mustBePositive; }
set
{
mustBePositive = value;
NotifyErrorsChanged();
}
}
public bool HasErrors
{
get
{
return MustBePositive > 0;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void NotifyErrorsChanged([CallerMemberName] string propertyName = "")
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
{
if (propertyName == nameof(MustBePositive) && MustBePositive <= 0)
{
yield return $"{nameof(MustBePositive)} must be positive";
}
}
}
[Fact]
public void Setting_Non_Validating_Does_Not_Trigger_Validation()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new IndeiValidationCheckerPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
ValidationStatus 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 IndeiValidationCheckerPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus 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 IndeiValidationCheckerPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus 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);
}
}
}

142
tests/Perspex.Markup.UnitTests/Data/InpcPluginTests.cs

@ -1,142 +0,0 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Markup.Data.Plugins;
using System;
using System.Collections;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xunit;
namespace Perspex.Markup.UnitTests.Data
{
public class InpcPluginTests
{
private class InpcTest : INotifyPropertyChanged, INotifyDataErrorInfo
{
private int noValidationTest;
public int NoValidationTest
{
get { return noValidationTest; }
set
{
noValidationTest = value;
NotifyPropertyChanged();
}
}
public bool HasErrors
{
get
{
return NonNegative < 0;
}
}
private int nonNegative;
public int NonNegative
{
get { return nonNegative; }
set
{
var old = nonNegative;
nonNegative = value;
NotifyPropertyChanged();
if (old * value < 0) // If signs are different
{
NotifyErrorsChanged();
}
}
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public event PropertyChangedEventHandler PropertyChanged;
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName) || propertyName == nameof(NonNegative))
{
if (NonNegative < 0)
{
yield return "Invalid Value";
}
}
}
private void NotifyPropertyChanged([CallerMemberName] string property = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
private void NotifyErrorsChanged([CallerMemberName] string property = "")
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(property));
}
}
[Fact]
public void Calls_Change_Callback_When_Value_Changes()
{
var plugin = new InpcPropertyAccessorPlugin();
var source = new InpcTest { NoValidationTest = 0 };
var changeFired = false;
var accessor = plugin.Start(new WeakReference(source), nameof(InpcTest.NoValidationTest), _ => changeFired = true, _ => { });
source.NoValidationTest = 1;
Assert.True(changeFired);
}
[Fact]
public void ValidationChanged_Does_Not_Fire_When_NonValidated_Value_Changes()
{
var plugin = new InpcPropertyAccessorPlugin();
var source = new InpcTest { NoValidationTest = 0 };
var validationFired = false;
plugin.Start(new WeakReference(source), nameof(InpcTest.NoValidationTest), _ => { }, _ => validationFired = true);
source.NoValidationTest = 1;
Assert.False(validationFired);
}
[Fact]
public void ValidationChanged_Does_Not_Fire_When_Validation_Does_Not_Change()
{
var plugin = new InpcPropertyAccessorPlugin();
var source = new InpcTest { NonNegative = 3 };
var validationFired = false;
plugin.Start(new WeakReference(source), nameof(InpcTest.NonNegative), _ => { }, _ => validationFired = true);
source.NonNegative = 5;
Assert.False(validationFired);
}
[Fact]
public void ValidationChanged_Fires_On_Start_If_Has_Errors()
{
var plugin = new InpcPropertyAccessorPlugin();
var source = new InpcTest { NonNegative = -5 };
Assert.True(source.HasErrors);
var validationFired = false;
plugin.Start(new WeakReference(source), nameof(InpcTest.NonNegative), _ => { }, _ => validationFired = true);
Assert.True(validationFired);
}
[Fact]
public void ValidationChanged_Fires_When_Validation_Changes()
{
var plugin = new InpcPropertyAccessorPlugin();
var source = new InpcTest { NonNegative = 5 };
var validationFired = false;
plugin.Start(new WeakReference(source), nameof(InpcTest.NonNegative), _ => { }, _ => validationFired = true);
source.NonNegative = -1;
Assert.True(validationFired);
}
}
}

3
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@ -85,6 +85,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ControlLocatorTests.cs" />
<Compile Include="Data\ExceptionValidatorTests.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_SetValue.cs" />
<Compile Include="Data\ExpressionObserverTests_Task.cs" />
<Compile Include="Data\ExpressionSubjectTests.cs" />
<Compile Include="Data\InpcPluginTests.cs" />
<Compile Include="Data\IndeiValidatorTests.cs" />
<Compile Include="DefaultValueConverterTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnitTestSynchronizationContext.cs" />

Loading…
Cancel
Save