Browse Source

Merge branch 'Validation' of https://github.com/jkoritzinsky/Perspex

pull/534/head
Steven Kirk 10 years ago
parent
commit
4270cc8291
  1. 13
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  2. 2
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  3. 7
      src/Markup/Perspex.Markup/Data/CommonPropertyNames.cs
  4. 13
      src/Markup/Perspex.Markup/Data/ExpressionNode.cs
  5. 32
      src/Markup/Perspex.Markup/Data/ExpressionObserver.cs
  6. 1
      src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
  7. 74
      src/Markup/Perspex.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs
  8. 1
      src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  9. 36
      src/Markup/Perspex.Markup/Data/Plugins/IValidationCheckerPlugin.cs
  10. 96
      src/Markup/Perspex.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs
  11. 7
      src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  12. 34
      src/Markup/Perspex.Markup/Data/Plugins/ValidationCheckerBase.cs
  13. 13
      src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
  14. 4
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  15. 3
      src/Perspex.Base/Data/InstancedBinding.cs
  16. 13
      src/Perspex.Base/Data/ValidationMethods.cs
  17. 30
      src/Perspex.Base/Data/ValidationStatus.cs
  18. 9
      src/Perspex.Base/IPriorityValueOwner.cs
  19. 2
      src/Perspex.Base/Perspex.Base.csproj
  20. 25
      src/Perspex.Base/PerspexObject.cs
  21. 9
      src/Perspex.Base/PriorityBindingEntry.cs
  22. 12
      src/Perspex.Base/PriorityLevel.cs
  23. 11
      src/Perspex.Base/PriorityValue.cs
  24. 31
      src/Perspex.Controls/Control.cs
  25. 27
      src/Perspex.Controls/ControlValidationStatus.cs
  26. 1
      src/Perspex.Controls/Perspex.Controls.csproj
  27. 3
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  28. 99
      tests/Perspex.Markup.UnitTests/Data/ExceptionValidatorTests.cs
  29. 9
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  30. 120
      tests/Perspex.Markup.UnitTests/Data/IndeiValidatorTests.cs
  31. 2
      tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj
  32. 111
      tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs
  33. 1
      tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj
  34. 2
      tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs
  35. 2
      tests/Perspex.Styling.UnitTests/TestControlBase.cs
  36. 2
      tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs

13
src/Markup/Perspex.Markup.Xaml/Data/Binding.cs

@ -77,6 +77,11 @@ namespace Perspex.Markup.Xaml.Data
/// </summary>
public object Source { get; set; }
/// <summary>
/// Gets or sets the validation methods for the binding to use.
/// </summary>
public ValidationMethods ValidationMethods { get; set; }
/// <inheritdoc/>
public InstancedBinding Initiate(
IPerspexObject target,
@ -202,7 +207,7 @@ namespace Perspex.Markup.Xaml.Data
var result = new ExpressionObserver(
() => target.GetValue(Control.DataContextProperty),
path,
update);
update, ValidationMethods);
return result;
}
@ -213,7 +218,7 @@ namespace Perspex.Markup.Xaml.Data
.OfType<IPerspexObject>()
.Select(x => x.GetObservable(Control.DataContextProperty))
.Switch(),
path);
path, ValidationMethods);
}
}
@ -223,7 +228,7 @@ namespace Perspex.Markup.Xaml.Data
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path);
path, ValidationMethods);
return result;
}
@ -231,7 +236,7 @@ namespace Perspex.Markup.Xaml.Data
{
Contract.Requires<ArgumentNullException>(source != null);
return new ExpressionObserver(source, path);
return new ExpressionObserver(source, path, ValidationMethods);
}
private ExpressionObserver CreateTemplatedParentObserver(

2
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -29,6 +29,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
Mode = Mode,
Path = Path,
Priority = Priority,
ValidationMethods = ValidationMethods
};
}
@ -40,5 +41,6 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.LocalValue;
public object Source { get; set; }
public ValidationMethods ValidationMethods { get; set; } = ValidationMethods.None;
}
}

7
src/Markup/Perspex.Markup/Data/CommonPropertyNames.cs

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 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.
namespace Perspex.Markup.Data
{

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

@ -105,6 +105,19 @@ namespace Perspex.Markup.Data
CurrentValue = reference;
}
protected virtual void SendValidationStatus(ValidationStatus 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)
{
}

32
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;
@ -36,18 +47,20 @@ namespace Perspex.Markup.Data
private IDisposable _updateSubscription;
private int _count;
private readonly ExpressionNode _node;
private ValidationMethods _methods;
/// <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>
public ExpressionObserver(object root, string expression)
/// <param name="methods">The validation methods to enable on this observer.</param>
public ExpressionObserver(object root, string expression, ValidationMethods methods = ValidationMethods.None)
{
Contract.Requires<ArgumentNullException>(expression != null);
_root = new WeakReference(root);
_methods = methods;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
@ -61,13 +74,14 @@ namespace Perspex.Markup.Data
/// </summary>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
public ExpressionObserver(IObservable<object> rootObservable, string expression)
/// <param name="methods">The validation methods to enable on this observer.</param>
public ExpressionObserver(IObservable<object> rootObservable, string expression, ValidationMethods methods = ValidationMethods.None)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_rootObservable = rootObservable;
_methods = methods;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
@ -82,10 +96,12 @@ namespace Perspex.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="methods">The validation methods to enable on this observer.</param>
public ExpressionObserver(
Func<object> rootGetter,
string expression,
IObservable<Unit> update)
IObservable<Unit> update,
ValidationMethods methods = ValidationMethods.None)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
@ -93,7 +109,7 @@ namespace Perspex.Markup.Data
_rootGetter = rootGetter;
_update = update;
_methods = methods;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
@ -205,8 +221,8 @@ namespace Perspex.Markup.Data
{
source = source.TakeUntil(_update.LastOrDefaultAsync());
}
var subscription = source.Subscribe(observer);
var validationFiltered = source.Where(o => (o as ValidationStatus)?.Match(_methods) ?? true);
var subscription = validationFiltered.Subscribe(observer);
return Disposable.Create(() =>
{

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,

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

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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);
}
private class ExceptionValidationChecker : ValidationCheckerBase
{
public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
: base(reference, name, accessor, callback)
{
}
public override bool SetValue(object value, BindingPriority priority)
{
try
{
var success = base.SetValue(value, priority);
SendValidationCallback(new ExceptionValidationStatus(null));
return success;
}
catch (Exception ex)
{
SendValidationCallback(new ExceptionValidationStatus(ex));
}
return false;
}
}
/// <summary>
/// Describes the current validation status after setting a property value.
/// </summary>
public class ExceptionValidationStatus : ValidationStatus
{
internal ExceptionValidationStatus(Exception exception)
{
Exception = exception;
}
/// <summary>
/// The thrown exception. If there was no thrown exception, null.
/// </summary>
public Exception Exception { get; }
/// <inheritdoc/>
public override bool IsValid => Exception == null;
public override bool Match(ValidationMethods enabledMethods)
{
return (enabledMethods & ValidationMethods.Exceptions) != 0;
}
}
}
}

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

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
namespace Perspex.Markup.Data.Plugins
{

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

@ -0,0 +1,36 @@
using Perspex.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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);
}
}

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

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Perspex.Data;
using System.ComponentModel;
using System.Collections;
using Perspex.Utilities;
namespace Perspex.Markup.Data.Plugins
{
/// <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);
}
private class IndeiValidationChecker : ValidationCheckerBase, IWeakSubscriber<DataErrorsChangedEventArgs>
{
public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
: base(reference, name, accessor, callback)
{
var target = reference.Target as INotifyDataErrorInfo;
if (target != null)
{
if (target.HasErrors)
{
SendValidationCallback(new IndeiValidationStatus(target.GetErrors(name)));
}
WeakSubscriptionManager.Subscribe(
target,
nameof(target.ErrorsChanged),
this);
}
}
public override void Dispose()
{
base.Dispose();
var target = _reference.Target as INotifyDataErrorInfo;
if (target != null)
{
WeakSubscriptionManager.Unsubscribe(
target,
nameof(target.ErrorsChanged),
this);
}
}
public void OnEvent(object sender, DataErrorsChangedEventArgs e)
{
if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
{
var indei = _reference.Target as INotifyDataErrorInfo;
SendValidationCallback(new IndeiValidationStatus(indei.GetErrors(e.PropertyName)));
}
}
}
/// <summary>
/// Describes the current validation status of a property as reported by an object that implements <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationStatus : ValidationStatus
{
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; }
public override bool Match(ValidationMethods enabledMethods)
{
return (enabledMethods & ValidationMethods.INotifyDataErrorInfo) != 0;
}
}
}
}

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

@ -9,6 +9,7 @@ using System.Reflection;
using Perspex.Data;
using Perspex.Logging;
using Perspex.Utilities;
using System.Collections;
namespace Perspex.Markup.Data.Plugins
{
@ -86,7 +87,7 @@ namespace Perspex.Markup.Data.Plugins
if (inpc != null)
{
WeakSubscriptionManager.Subscribe(
WeakSubscriptionManager.Subscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
@ -113,7 +114,7 @@ namespace Perspex.Markup.Data.Plugins
if (inpc != null)
{
WeakSubscriptionManager.Unsubscribe(
WeakSubscriptionManager.Unsubscribe<PropertyChangedEventArgs>(
inpc,
nameof(inpc.PropertyChanged),
this);
@ -131,7 +132,7 @@ namespace Perspex.Markup.Data.Plugins
return false;
}
public void OnEvent(object sender, PropertyChangedEventArgs e)
void IWeakSubscriber<PropertyChangedEventArgs>.OnEvent(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
{

34
src/Markup/Perspex.Markup/Data/Plugins/ValidationCheckerBase.cs

@ -0,0 +1,34 @@
using System;
using Perspex.Data;
namespace Perspex.Markup.Data.Plugins
{
public abstract class ValidationCheckerBase : IPropertyAccessor
{
protected readonly WeakReference _reference;
protected readonly string _name;
private readonly IPropertyAccessor _accessor;
private readonly Action<ValidationStatus> _callback;
protected ValidationCheckerBase(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
{
_reference = reference;
_name = name;
_accessor = accessor;
_callback = callback;
}
public Type PropertyType => _accessor.PropertyType;
public object Value => _accessor.Value;
public virtual void Dispose() => _accessor.Dispose();
public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority);
protected void SendValidationCallback(ValidationStatus status)
{
_callback?.Invoke(status);
}
}
}

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

@ -50,11 +50,18 @@ 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);
foreach (var validationPlugin in ExpressionObserver.ValidationCheckers.Where(x => x.Match(reference)))
{
if (validationPlugin != null)
{
_accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
}
}
if (_accessor != null)
{

4
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -46,6 +46,10 @@
<Compile Include="Data\ExpressionParseException.cs" />
<Compile Include="Data\ExpressionSubject.cs" />
<Compile Include="ControlLocator.cs" />
<Compile Include="Data\Plugins\ExceptionValidationCheckerPlugin.cs" />
<Compile Include="Data\Plugins\IndeiValidationCheckerPlugin.cs" />
<Compile Include="Data\Plugins\ValidationCheckerBase.cs" />
<Compile Include="Data\Plugins\IValidationCheckerPlugin.cs" />
<Compile Include="Data\Plugins\PerspexPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\IPropertyAccessor.cs" />

3
src/Perspex.Base/Data/InstancedBinding.cs

@ -30,7 +30,8 @@ namespace Perspex.Data
/// The value used for the <see cref="BindingMode.OneTime"/> binding.
/// </param>
/// <param name="priority">The binding priority.</param>
public InstancedBinding(object value, BindingPriority priority = BindingPriority.LocalValue)
public InstancedBinding(object value,
BindingPriority priority = BindingPriority.LocalValue)
{
Mode = BindingMode.OneTime;
Priority = priority;

13
src/Perspex.Base/Data/ValidationMethods.cs

@ -0,0 +1,13 @@
using System;
namespace Perspex.Data
{
[Flags]
public enum ValidationMethods
{
None = 0,
Exceptions = 1,
INotifyDataErrorInfo = 2,
All = -1
}
}

30
src/Perspex.Base/Data/ValidationStatus.cs

@ -0,0 +1,30 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Perspex.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 abstract class ValidationStatus
{
/// <summary>
/// True when the data passes validation; otherwise, false.
/// </summary>
public abstract bool IsValid { get; }
/// <summary>
/// Checks if this validation status came from a currently enabled method of validation checking.
/// </summary>
/// <param name="enabledMethods">The enabled methods of validation checking.</param>
/// <returns>True if enabled; otherwise, false.</returns>
public abstract bool Match(ValidationMethods enabledMethods);
}
}

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 DataValidationChanged(PriorityValue sender, ValidationStatus status);
}
}

2
src/Perspex.Base/Perspex.Base.csproj

@ -44,6 +44,8 @@
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\BindingError.cs" />
<Compile Include="Data\ValidationMethods.cs" />
<Compile Include="Data\ValidationStatus.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" />

25
src/Perspex.Base/PerspexObject.cs

@ -401,16 +401,22 @@ namespace Perspex
GetDescription(source));
IDisposable subscription = null;
IDisposable validationSubcription = null;
subscription = source
.Where(x => !(x is ValidationStatus))
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => s_directBindings.Remove(subscription))
.Subscribe(x => DirectBindingSet(property, x));
validationSubcription = source
.OfType<ValidationStatus>()
.Subscribe(x => DataValidation(property, x));
s_directBindings.Add(subscription);
return Disposable.Create(() =>
{
validationSubcription.Dispose();
subscription.Dispose();
s_directBindings.Remove(subscription);
});
@ -459,7 +465,7 @@ namespace Perspex
{
Contract.Requires<ArgumentNullException>(property != null);
return Bind((PerspexProperty)property, source.Select(x => (object)x), priority);
return Bind(property, source.Select(x => (object)x), priority);
}
/// <summary>
@ -505,6 +511,23 @@ namespace Perspex
}
}
/// <inheritdoc/>
void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, ValidationStatus status)
{
var property = sender.Property;
DataValidation(property, status);
}
/// <summary>
/// Called when the validation state on a tracked property is changed.
/// </summary>
/// <param name="property">The property whose validation state changed.</param>
/// <param name="status">The new validation state.</param>
protected virtual void DataValidation(PerspexProperty property, ValidationStatus status)
{
}
/// <inheritdoc/>
Delegate[] IPerspexObjectDebug.GetPropertyChangedSubscribers()
{

9
src/Perspex.Base/PriorityBindingEntry.cs

@ -21,6 +21,7 @@ namespace Perspex
/// <param name="index">
/// The binding index. Later bindings should have higher indexes.
/// </param>
/// <param name="validation">The validation settings for the binding.</param>
public PriorityBindingEntry(PriorityLevel owner, int index)
{
_owner = owner;
@ -99,7 +100,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);

12
src/Perspex.Base/PriorityLevel.cs

@ -97,6 +97,7 @@ namespace Perspex
/// Adds a binding.
/// </summary>
/// <param name="binding">The binding to add.</param>
/// <param name="validation">Validation settings for the binding.</param>
/// <returns>A disposable used to remove the binding.</returns>
public IDisposable Add(IObservable<object> binding)
{
@ -164,6 +165,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>

11
src/Perspex.Base/PriorityValue.cs

@ -77,6 +77,7 @@ namespace Perspex
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <param name="validation">Validation settings for the binding.</param>
/// <returns>
/// A disposable that will remove the binding.
/// </returns>
@ -178,6 +179,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.DataValidationChanged(this, validationStatus);
}
/// <summary>
/// Called when a priority level encounters an error.
/// </summary>

31
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, ControlValidationStatus> ValidationStatusProperty =
PerspexProperty.RegisterDirect<Control, ControlValidationStatus>(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,30 @@ namespace Perspex.Controls
/// </summary>
protected IPseudoClasses PseudoClasses => Classes;
private ControlValidationStatus validationStatus = new ControlValidationStatus();
/// <summary>
/// The current validation status of the control.
/// </summary>
public ControlValidationStatus ValidationStatus
{
get
{
return validationStatus;
}
private set
{
SetAndRaise(ValidationStatusProperty, ref validationStatus, value);
}
}
/// <inheritdoc/>
protected override void DataValidation(PerspexProperty property, ValidationStatus status)
{
base.DataValidation(property, status);
ValidationStatus.UpdateValidationStatus(status);
}
/// <summary>
/// Sets the control's logical parent.
/// </summary>

27
src/Perspex.Controls/ControlValidationStatus.cs

@ -0,0 +1,27 @@
using Perspex.Data;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Perspex.Controls
{
public class ControlValidationStatus : ValidationStatus, INotifyPropertyChanged
{
private Dictionary<Type, ValidationStatus> propertyValidation = new Dictionary<Type, ValidationStatus>();
public override bool IsValid => propertyValidation.Values.All(status => status.IsValid);
public event PropertyChangedEventHandler PropertyChanged;
public override bool Match(ValidationMethods enabledMethods) => true;
public void UpdateValidationStatus(ValidationStatus status)
{
propertyValidation[status.GetType()] = status;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(""));
}
}
}

1
src/Perspex.Controls/Perspex.Controls.csproj

@ -46,6 +46,7 @@
<Compile Include="Application.cs" />
<Compile Include="Classes.cs" />
<Compile Include="ContextMenu.cs" />
<Compile Include="ControlValidationStatus.cs" />
<Compile Include="Design.cs" />
<Compile Include="DockPanel.cs" />
<Compile Include="Expander.cs" />

3
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@ -196,9 +196,6 @@
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<ItemGroup>
<WCFMetadata Include="Service References\" />
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>

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

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

@ -300,15 +300,6 @@ namespace Perspex.Markup.UnitTests.Data
Assert.False(target.SetValue("baz"));
}
[Fact]
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));
}
[Fact]
public async void Should_Handle_Null_Root()
{

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

2
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,6 +98,7 @@
<Compile Include="Data\ExpressionObserverTests_SetValue.cs" />
<Compile Include="Data\ExpressionObserverTests_Task.cs" />
<Compile Include="Data\ExpressionSubjectTests.cs" />
<Compile Include="Data\IndeiValidatorTests.cs" />
<Compile Include="DefaultValueConverterTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UnitTestSynchronizationContext.cs" />

111
tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

@ -0,0 +1,111 @@
using Perspex.Controls;
using Perspex.Markup.Xaml.Data;
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.Xaml.UnitTests.Data
{
public class BindingTests_Validation
{
public class Data : INotifyPropertyChanged
{
private string mustbeNonEmpty;
public string MustBeNonEmpty
{
get { return mustbeNonEmpty; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(nameof(value));
}
mustbeNonEmpty = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
[Fact]
public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Direct()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Perspex.Data.BindingMode.TwoWay,
ValidationMethods = Perspex.Data.ValidationMethods.None
};
target.Bind(TextBlock.TextProperty, binding);
target.Text = "";
Assert.True(target.ValidationStatus.IsValid);
}
[Fact]
public void Enabled_Validation_Should_Trigger_Validation_Change_Direct()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Perspex.Data.BindingMode.TwoWay,
ValidationMethods = Perspex.Data.ValidationMethods.All
};
target.Bind(TextBlock.TextProperty, binding);
target.Text = "";
Assert.False(target.ValidationStatus.IsValid);
}
[Fact]
public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Styled()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Perspex.Data.BindingMode.TwoWay,
ValidationMethods = Perspex.Data.ValidationMethods.None
};
target.Bind(Control.TagProperty, binding);
target.Tag = "";
Assert.True(target.ValidationStatus.IsValid);
}
[Fact]
public void Enabled_Validation_Should_Trigger_Validation_Change_Styled()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Perspex.Data.BindingMode.TwoWay,
ValidationMethods = Perspex.Data.ValidationMethods.All
};
target.Bind(Control.TagProperty, binding);
target.Tag = "";
Assert.False(target.ValidationStatus.IsValid);
}
}
}

1
tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

@ -94,6 +94,7 @@
<ItemGroup>
<Compile Include="Data\BindingTests_Source.cs" />
<Compile Include="Data\BindingTests_ElementName.cs" />
<Compile Include="Data\BindingTests_Validation.cs" />
<Compile Include="Data\MultiBindingTests.cs" />
<Compile Include="Data\BindingTests_TemplatedParent.cs" />
<Compile Include="Data\BindingTests.cs" />

2
tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs

@ -160,7 +160,7 @@ namespace Perspex.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority)
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}

2
tests/Perspex.Styling.UnitTests/TestControlBase.cs

@ -62,7 +62,7 @@ namespace Perspex.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority)
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}

2
tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs

@ -57,7 +57,7 @@ namespace Perspex.Styling.UnitTests
throw new NotImplementedException();
}
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority)
public IDisposable Bind(PerspexProperty property, IObservable<object> source, BindingPriority priority = BindingPriority.LocalValue)
{
throw new NotImplementedException();
}

Loading…
Cancel
Save