Browse Source

Reimplemented property validation.

As far as it was in `property-validation-grokys` branch. Things were
messed up on master so had to merge this manually so may be problems.
pull/537/head
Steven Kirk 11 years ago
parent
commit
5c33fbc6ee
  1. 3
      samples/BindingTest/BindingTest.csproj
  2. 8
      samples/BindingTest/MainWindow.xaml
  3. 29
      samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs
  4. 3
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  5. 6
      src/Avalonia.Base/Avalonia.Base.csproj
  6. 43
      src/Avalonia.Base/AvaloniaObject.cs
  7. 17
      src/Avalonia.Base/Data/IValidationStatus.cs
  8. 44
      src/Avalonia.Base/Data/ObjectValidationStatus.cs
  9. 13
      src/Avalonia.Base/Data/ValidationMethods.cs
  10. 30
      src/Avalonia.Base/Data/ValidationStatus.cs
  11. 2
      src/Avalonia.Base/IPriorityValueOwner.cs
  12. 2
      src/Avalonia.Base/PriorityBindingEntry.cs
  13. 3
      src/Avalonia.Base/PriorityLevel.cs
  14. 2
      src/Avalonia.Base/PriorityValue.cs
  15. 3
      src/Avalonia.Controls/Avalonia.Controls.csproj
  16. 30
      src/Avalonia.Controls/Control.cs
  17. 27
      src/Avalonia.Controls/ControlValidationStatus.cs
  18. 8
      src/Avalonia.Controls/TextBox.cs
  19. 3
      src/Avalonia.Themes.Default/TextBox.xaml
  20. 22
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  21. 4
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  22. 10
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  23. 4
      src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
  24. 5
      src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs
  25. 52
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  26. 2
      src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
  27. 23
      src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs
  28. 36
      src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
  29. 17
      src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs
  30. 27
      src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
  31. 46
      src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs
  32. 34
      src/Markup/Avalonia.Markup/Data/Plugins/ValidationCheckerBase.cs
  33. 22
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  34. 4
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
  35. 3
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  36. 130
      tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs
  37. 3
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  38. 24
      tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
  39. 129
      tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs
  40. 27
      tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
  41. 172
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs

3
samples/BindingTest/BindingTest.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -88,6 +88,7 @@
<Compile Include="TestItemView.xaml.cs">
<DependentUpon>TestItemView.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModels\ExceptionPropertyErrorViewModel.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
<Compile Include="ViewModels\TestItem.cs" />
</ItemGroup>

8
samples/BindingTest/MainWindow.xaml

@ -68,6 +68,14 @@
</ContentControl>
</StackPanel>
</TabItem>
<TabItem Header="Property Validation">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200" DataContext="{Binding ExceptionPropertyValidation}">
<TextBlock FontSize="16" Text="Exception Validation"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10, EnableValidation=True}"/>
</StackPanel>
</StackPanel>
</TabItem>
<TabItem Header="Commands">
<StackPanel Margin="18" Gap="4" Width="200">
<Button Content="Button" Command="{Binding StringValueCommand}" CommandParameter="Button"/>

29
samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs

@ -0,0 +1,29 @@
// 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 ReactiveUI;
using System;
namespace BindingTest.ViewModels
{
public class ExceptionPropertyErrorViewModel : ReactiveObject
{
private int _lessThan10;
public int LessThan10
{
get { return _lessThan10; }
set
{
if (value < 10)
{
this.RaiseAndSetIfChanged(ref _lessThan10, value);
}
else
{
throw new InvalidOperationException("Value must be less than 10.");
}
}
}
}
}

3
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -68,5 +68,8 @@ namespace BindingTest.ViewModels
}
public ReactiveCommand<object> StringValueCommand { get; }
public ExceptionPropertyErrorViewModel ExceptionPropertyValidation { get; }
= new ExceptionPropertyErrorViewModel();
}
}

6
src/Avalonia.Base/Avalonia.Base.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -44,8 +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="Data\IValidationStatus.cs" />
<Compile Include="Data\ObjectValidationStatus.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" />

43
src/Avalonia.Base/AvaloniaObject.cs

@ -50,6 +50,29 @@ 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>
@ -404,13 +427,13 @@ namespace Avalonia
}
subscription = source
.Where(x => !(x is ValidationStatus))
.Where(x => !(x is IValidationStatus))
.Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => _directBindings.Remove(subscription))
.Subscribe(x => DirectBindingSet(property, x));
validationSubcription = source
.OfType<ValidationStatus>()
.Subscribe(x => DataValidation(property, x));
.OfType<IValidationStatus>()
.Subscribe(x => DataValidationChanged(property, x));
_directBindings.Add(subscription);
@ -512,10 +535,10 @@ namespace Avalonia
}
/// <inheritdoc/>
void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, ValidationStatus status)
void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, IValidationStatus status)
{
var property = sender.Property;
DataValidation(property, status);
DataValidationChanged(property, status);
}
/// <summary>
@ -523,9 +546,17 @@ namespace Avalonia
/// </summary>
/// <param name="property">The property whose validation state changed.</param>
/// <param name="status">The new validation state.</param>
protected virtual void DataValidation(AvaloniaProperty property, ValidationStatus status)
protected virtual void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
{
}
/// <summary>
/// Updates the validation status of the current object.
/// </summary>
/// <param name="status">The new validation status.</param>
protected void UpdateValidationState(IValidationStatus status)
{
ValidationStatus = ValidationStatus.UpdateValidationStatus(status);
}
/// <inheritdoc/>

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

@ -0,0 +1,17 @@
// 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

@ -0,0 +1,44 @@
// 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;
}
}

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

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

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

@ -1,30 +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;
using System.Text;
using System.Threading.Tasks;
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 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);
}
}

2
src/Avalonia.Base/IPriorityValueOwner.cs

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

2
src/Avalonia.Base/PriorityBindingEntry.cs

@ -100,7 +100,7 @@ namespace Avalonia
_owner.Error(this, bindingError);
}
var validationStatus = value as ValidationStatus;
var validationStatus = value as IValidationStatus;
if (validationStatus != null)
{

3
src/Avalonia.Base/PriorityLevel.cs

@ -97,7 +97,6 @@ namespace Avalonia
/// 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)
{
@ -170,7 +169,7 @@ namespace Avalonia
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="validationStatus">The validation status.</param>
public void Validation(PriorityBindingEntry entry, ValidationStatus validationStatus)
public void Validation(PriorityBindingEntry entry, IValidationStatus validationStatus)
{
_owner.LevelValidation(this, validationStatus);
}

2
src/Avalonia.Base/PriorityValue.cs

@ -184,7 +184,7 @@ namespace Avalonia
/// </summary>
/// <param name="priorityLevel">The priority level of the changed entry.</param>
/// <param name="validationStatus">The validation status.</param>
public void LevelValidation(PriorityLevel priorityLevel, ValidationStatus validationStatus)
public void LevelValidation(PriorityLevel priorityLevel, IValidationStatus validationStatus)
{
_owner.DataValidationChanged(this, validationStatus);
}

3
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -46,7 +46,6 @@
<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" />

30
src/Avalonia.Controls/Control.cs

@ -86,12 +86,6 @@ namespace Avalonia.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 =
AvaloniaProperty.RegisterDirect<Control, ControlValidationStatus>(nameof(ValidationStatus), c=> c.ValidationStatus);
private int _initCount;
private string _name;
private IControl _parent;
@ -114,7 +108,7 @@ namespace Avalonia.Controls
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass(IsFocusedProperty, ":focus");
PseudoClass(IsPointerOverProperty, ":pointerover");
PseudoClass(ValidationStatusProperty, status => status != null && !status.IsValid, ":invalid");
PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid");
}
/// <summary>
@ -406,27 +400,10 @@ namespace Avalonia.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(AvaloniaProperty property, ValidationStatus status)
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
{
base.DataValidation(property, status);
base.DataValidationChanged(property, status);
ValidationStatus.UpdateValidationStatus(status);
}
@ -511,6 +488,7 @@ namespace Avalonia.Controls
}
property.Changed.Merge(property.Initialized)
.Where(e => e.Sender is Control)
.Subscribe(e =>
{
if (selector((T)e.NewValue))

27
src/Avalonia.Controls/ControlValidationStatus.cs

@ -1,27 +0,0 @@
using Avalonia.Data;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.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(""));
}
}
}

8
src/Avalonia.Controls/TextBox.cs

@ -235,6 +235,14 @@ namespace Avalonia.Controls
HandleTextInput(e.Text);
}
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
{
if (property == TextProperty)
{
UpdateValidationState(status);
}
}
private void HandleTextInput(string input)
{
if (!IsReadOnly)

3
src/Avalonia.Themes.Default/TextBox.xaml

@ -57,4 +57,7 @@
<Style Selector="TextBox:focus /template/ Border#border">
<Setter Property="BorderBrush" Value="{StyleResource ThemeBorderDarkBrush}"/>
</Style>
<Style Selector="TextBox:invalid /template/ Border#border">
<Setter Property="BorderBrush" Value="Red"/>
</Style>
</Styles>

22
src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs

@ -4,7 +4,6 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Data;
@ -78,13 +77,13 @@ namespace Avalonia.Markup.Xaml.Data
public object Source { get; set; }
/// <summary>
/// Gets or sets the validation methods for the binding to use.
/// Gets or sets a value indicating whether the property should be validated.
/// </summary>
public ValidationMethods ValidationMethods { get; set; }
public bool EnableValidation { get; set; }
/// <inheritdoc/>
public InstancedBinding Initiate(
IAvaloniaObject target,
IAvaloniaObject target,
AvaloniaProperty targetProperty,
object anchor = null)
{
@ -99,7 +98,7 @@ namespace Avalonia.Markup.Xaml.Data
{
observer = CreateElementObserver(
(target as IControl) ?? (anchor as IControl),
pathInfo.ElementName ?? ElementName,
pathInfo.ElementName ?? ElementName,
pathInfo.Path);
}
else if (Source != null)
@ -109,7 +108,7 @@ namespace Avalonia.Markup.Xaml.Data
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContexObserver(
target,
target,
pathInfo.Path,
targetProperty == Control.DataContextProperty,
anchor);
@ -207,7 +206,8 @@ namespace Avalonia.Markup.Xaml.Data
var result = new ExpressionObserver(
() => target.GetValue(Control.DataContextProperty),
path,
update, ValidationMethods);
update,
EnableValidation);
return result;
}
@ -218,7 +218,8 @@ namespace Avalonia.Markup.Xaml.Data
.OfType<IAvaloniaObject>()
.Select(x => x.GetObservable(Control.DataContextProperty))
.Switch(),
path, ValidationMethods);
path,
EnableValidation);
}
}
@ -228,7 +229,8 @@ namespace Avalonia.Markup.Xaml.Data
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path, ValidationMethods);
path,
EnableValidation);
return result;
}
@ -236,7 +238,7 @@ namespace Avalonia.Markup.Xaml.Data
{
Contract.Requires<ArgumentNullException>(source != null);
return new ExpressionObserver(source, path, ValidationMethods);
return new ExpressionObserver(source, path, EnableValidation);
}
private ExpressionObserver CreateTemplatedParentObserver(

4
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -29,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Mode = Mode,
Path = Path,
Priority = Priority,
ValidationMethods = ValidationMethods
EnableValidation = EnableValidation,
};
}
@ -41,6 +41,6 @@ namespace Avalonia.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;
public bool EnableValidation { get; set; }
}
}

10
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
@ -46,10 +46,9 @@
<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\ExceptionValidationPlugin.cs" />
<Compile Include="Data\Plugins\IndeiValidationPlugin.cs" />
<Compile Include="Data\Plugins\IValidationPlugin.cs" />
<Compile Include="Data\Plugins\AvaloniaPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\IPropertyAccessor.cs" />
@ -61,6 +60,7 @@
<Compile Include="Data\Parsers\ExpressionParser.cs" />
<Compile Include="Data\Parsers\Reader.cs" />
<Compile Include="Data\Plugins\PropertyError.cs" />
<Compile Include="Data\Plugins\ValidatingPropertyAccessorBase.cs" />
<Compile Include="Data\PropertyAccessorNode.cs" />
<Compile Include="Data\ExpressionNode.cs" />
<Compile Include="Data\ExpressionObserver.cs" />

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

@ -105,12 +105,12 @@ namespace Avalonia.Markup.Data
CurrentValue = reference;
}
protected virtual void SendValidationStatus(ValidationStatus status)
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);
_subject.OnNext(status);
}
else
{

5
src/Markup/Avalonia.Markup/Data/ExpressionNodeBuilder.cs

@ -8,7 +8,7 @@ namespace Avalonia.Markup.Data
{
internal static class ExpressionNodeBuilder
{
public static ExpressionNode Build(string expression)
public static ExpressionNode Build(string expression, bool enableValidation = false)
{
if (string.IsNullOrWhiteSpace(expression))
{
@ -16,7 +16,8 @@ namespace Avalonia.Markup.Data
}
var reader = new Reader(expression);
var node = ExpressionParser.Parse(reader);
var parser = new ExpressionParser(enableValidation);
var node = parser.Parse(reader);
if (!reader.End)
{

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

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
@ -32,11 +31,10 @@ 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<IValidationCheckerPlugin> ValidationCheckers =
new List<IValidationCheckerPlugin>
public static readonly IList<IValidationPlugin> ValidationCheckers =
new List<IValidationPlugin>
{
new IndeiValidationCheckerPlugin(),
new ExceptionValidationCheckerPlugin()
new IndeiValidationPlugin(),
};
private readonly WeakReference _root;
@ -47,23 +45,24 @@ namespace Avalonia.Markup.Data
private IDisposable _updateSubscription;
private int _count;
private readonly ExpressionNode _node;
private ValidationMethods _methods;
private bool _enableValidation;
/// <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="methods">The validation methods to enable on this observer.</param>
public ExpressionObserver(object root, string expression, ValidationMethods methods = ValidationMethods.None)
/// <param name="enableValidation">Whether property validation should be enabled.</param>
public ExpressionObserver(object root, string expression, bool enableValidation = false)
{
Contract.Requires<ArgumentNullException>(expression != null);
_root = new WeakReference(root);
_methods = methods;
_enableValidation = enableValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
}
Expression = expression;
@ -74,17 +73,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="methods">The validation methods to enable on this observer.</param>
public ExpressionObserver(IObservable<object> rootObservable, string expression, ValidationMethods methods = ValidationMethods.None)
/// <param name="enableValidation">Whether property validation should be enabled.</param>
public ExpressionObserver(
IObservable<object> rootObservable,
string expression,
bool enableValidation = false)
{
Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_rootObservable = rootObservable;
_methods = methods;
_enableValidation = enableValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
}
Expression = expression;
@ -96,12 +99,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="methods">The validation methods to enable on this observer.</param>
/// <param name="enableValidation">Whether property validation should be enabled.</param>
public ExpressionObserver(
Func<object> rootGetter,
Func<object> rootGetter,
string expression,
IObservable<Unit> update,
ValidationMethods methods = ValidationMethods.None)
bool enableValidation = false)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
@ -109,10 +112,11 @@ namespace Avalonia.Markup.Data
_rootGetter = rootGetter;
_update = update;
_methods = methods;
_enableValidation = enableValidation;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
_node = ExpressionNodeBuilder.Build(expression, enableValidation);
}
Expression = expression;
@ -167,7 +171,7 @@ namespace Avalonia.Markup.Data
{
return (Leaf as PropertyAccessorNode)?.PropertyType;
}
else if(_rootGetter != null)
else if (_rootGetter != null)
{
return _rootGetter()?.GetType();
}
@ -221,8 +225,8 @@ namespace Avalonia.Markup.Data
{
source = source.TakeUntil(_update.LastOrDefaultAsync());
}
var validationFiltered = source.Where(o => (o as ValidationStatus)?.Match(_methods) ?? true);
var subscription = validationFiltered.Subscribe(observer);
var subscription = source.Subscribe(observer);
return Disposable.Create(() =>
{
@ -262,13 +266,13 @@ namespace Avalonia.Markup.Data
if (_update != null)
{
_updateSubscription = _update.Subscribe(x =>
_updateSubscription = _update.Subscribe(x =>
_node.Target = new WeakReference(_rootGetter()));
}
}
else if (_rootObservable != null)
{
_rootObserverSubscription = _rootObservable.Subscribe(x =>
_rootObserverSubscription = _rootObservable.Subscribe(x =>
_node.Target = new WeakReference(x));
}
else

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

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

23
src/Markup/Avalonia.Markup/Data/Parsers/ExpressionParser.cs

@ -7,9 +7,16 @@ using System.Linq;
namespace Avalonia.Markup.Data.Parsers
{
internal static class ExpressionParser
internal class ExpressionParser
{
public static ExpressionNode Parse(Reader r)
private bool _enableValidation;
public ExpressionParser(bool enableValidation)
{
_enableValidation = enableValidation;
}
public ExpressionNode Parse(Reader r)
{
var nodes = new List<ExpressionNode>();
var state = State.Start;
@ -49,7 +56,7 @@ namespace Avalonia.Markup.Data.Parsers
return nodes.FirstOrDefault();
}
private static State ParseStart(Reader r, IList<ExpressionNode> nodes)
private State ParseStart(Reader r, IList<ExpressionNode> nodes)
{
if (ParseNot(r))
{
@ -66,7 +73,7 @@ namespace Avalonia.Markup.Data.Parsers
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier));
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
}
@ -99,7 +106,7 @@ namespace Avalonia.Markup.Data.Parsers
return State.End;
}
private static State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
private State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes)
{
if (ParseOpenBrace(r))
{
@ -111,7 +118,7 @@ namespace Avalonia.Markup.Data.Parsers
if (identifier != null)
{
nodes.Add(new PropertyAccessorNode(identifier));
nodes.Add(new PropertyAccessorNode(identifier, _enableValidation));
return State.AfterMember;
}
@ -119,7 +126,7 @@ namespace Avalonia.Markup.Data.Parsers
}
}
private static State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
private State ParseAttachedProperty(Reader r, List<ExpressionNode> nodes)
{
var owner = IdentifierParser.Parse(r);
@ -135,7 +142,7 @@ namespace Avalonia.Markup.Data.Parsers
throw new ExpressionParseException(r.Position, "Expected ')'.");
}
nodes.Add(new PropertyAccessorNode(owner + '.' + name));
nodes.Add(new PropertyAccessorNode(owner + '.' + name, _enableValidation));
return State.AfterMember;
}

36
src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationCheckerPlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs

@ -1,31 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// 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 Avalonia.Data;
using System;
using System.Reflection;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Validates properties that report errors by throwing exceptions.
/// </summary>
public class ExceptionValidationCheckerPlugin : IValidationCheckerPlugin
public class ExceptionValidationPlugin : IValidationPlugin
{
public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin();
/// <inheritdoc/>
public bool Match(WeakReference reference) => true;
/// <inheritdoc/>
public ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
{
return new ExceptionValidationChecker(reference, name, accessor, callback);
}
private class ExceptionValidationChecker : ValidationCheckerBase
private class ExceptionValidationChecker : ValidatingPropertyAccessorBase
{
public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
: base(reference, name, accessor, callback)
{
}
@ -38,6 +38,10 @@ namespace Avalonia.Markup.Data.Plugins
SendValidationCallback(new ExceptionValidationStatus(null));
return success;
}
catch (TargetInvocationException ex)
{
SendValidationCallback(new ExceptionValidationStatus(ex.InnerException));
}
catch (Exception ex)
{
SendValidationCallback(new ExceptionValidationStatus(ex));
@ -49,7 +53,7 @@ namespace Avalonia.Markup.Data.Plugins
/// <summary>
/// Describes the current validation status after setting a property value.
/// </summary>
public class ExceptionValidationStatus : ValidationStatus
public class ExceptionValidationStatus : IValidationStatus
{
internal ExceptionValidationStatus(Exception exception)
{
@ -60,15 +64,9 @@ namespace Avalonia.Markup.Data.Plugins
/// 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;
}
public bool IsValid => Exception == null;
}
}
}

17
src/Markup/Avalonia.Markup/Data/Plugins/IValidationCheckerPlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs

@ -1,16 +1,15 @@
using Avalonia.Data;
// 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;
using System.Text;
using System.Threading.Tasks;
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 IValidationCheckerPlugin
public interface IValidationPlugin
{
/// <summary>
@ -21,16 +20,16 @@ namespace Avalonia.Markup.Data.Plugins
bool Match(WeakReference reference);
/// <summary>
/// Starts monitering the validation state of an object for the given property.
/// 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="ValidationCheckerBase"/> subclass through which future interactions with the
/// A <see cref="ValidatingPropertyAccessorBase"/> subclass through which future interactions with the
/// property will be made.
/// </returns>
ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback);
IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback);
}
}

27
src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationCheckerPlugin.cs → src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs

@ -1,11 +1,11 @@
// 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.Collections;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Data;
using System.ComponentModel;
using System.Collections;
using Avalonia.Utilities;
namespace Avalonia.Markup.Data.Plugins
@ -13,7 +13,7 @@ namespace Avalonia.Markup.Data.Plugins
/// <summary>
/// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationCheckerPlugin : IValidationCheckerPlugin
public class IndeiValidationPlugin : IValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference)
@ -22,14 +22,14 @@ namespace Avalonia.Markup.Data.Plugins
}
/// <inheritdoc/>
public ValidationCheckerBase Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
{
return new IndeiValidationChecker(reference, name, accessor, callback);
}
private class IndeiValidationChecker : ValidationCheckerBase, IWeakSubscriber<DataErrorsChangedEventArgs>
private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber<DataErrorsChangedEventArgs>
{
public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<ValidationStatus> callback)
public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action<IValidationStatus> callback)
: base(reference, name, accessor, callback)
{
var target = reference.Target as INotifyDataErrorInfo;
@ -72,7 +72,7 @@ namespace Avalonia.Markup.Data.Plugins
/// <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 class IndeiValidationStatus : IValidationStatus
{
internal IndeiValidationStatus(IEnumerable errors)
{
@ -80,17 +80,12 @@ namespace Avalonia.Markup.Data.Plugins
}
/// <inheritdoc/>
public override bool IsValid => !Errors.OfType<object>().Any();
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; }
public override bool Match(ValidationMethods enabledMethods)
{
return (enabledMethods & ValidationMethods.INotifyDataErrorInfo) != 0;
}
}
}
}

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

@ -0,0 +1,46 @@
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);
}
}
}

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

@ -1,34 +0,0 @@
using System;
using Avalonia.Data;
namespace Avalonia.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);
}
}
}

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

@ -17,10 +17,12 @@ namespace Avalonia.Markup.Data
{
private IPropertyAccessor _accessor;
private IDisposable _subscription;
private bool _enableValidation;
public PropertyAccessorNode(string propertyName)
public PropertyAccessorNode(string propertyName, bool enableValidation)
{
PropertyName = propertyName;
_enableValidation = enableValidation;
}
public string PropertyName { get; }
@ -54,13 +56,21 @@ namespace Avalonia.Markup.Data
if (accessorPlugin != null)
{
_accessor = accessorPlugin.Start(reference, PropertyName, SetCurrentValue);
foreach (var validationPlugin in ExpressionObserver.ValidationCheckers.Where(x => x.Match(reference)))
_accessor = ExceptionValidationPlugin.Instance.Start(
reference,
PropertyName,
accessorPlugin.Start(reference, PropertyName, SetCurrentValue),
SendValidationStatus);
if (_enableValidation)
{
if (validationPlugin != null)
foreach (var validationPlugin in ExpressionObserver.ValidationCheckers)
{
_accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
}
if (validationPlugin.Match(reference))
{
_accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
}
}
}
if (_accessor != null)

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" }, names);
Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, 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" }, names);
Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
}
[Fact]

3
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
<PropertyGroup>
@ -91,6 +91,7 @@
<ItemGroup>
<Compile Include="ClassesTests.cs" />
<Compile Include="LayoutTransformControlTests.cs" />
<Compile Include="TextBoxTests_ValidationState.cs" />
<Compile Include="UserControlTests.cs" />
<Compile Include="DockPanelTests.cs" />
<Compile Include="EnumerableExtensions.cs" />

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

@ -0,0 +1,130 @@
// 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 Avalonia.Markup.Xaml.Data;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class TextBoxTests_ValidationState
{
[Fact]
public void Setter_Exceptions_Should_Set_ValidationState()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new TextBox();
var binding = new Binding(nameof(ExceptionTest.LessThan10));
binding.Source = new ExceptionTest();
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);
}
}
[Fact(Skip = "TODO: Not yet passing")]
public void Unconvertable_Value_Should_Set_ValidationState()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new TextBox();
var binding = new Binding(nameof(ExceptionTest.LessThan10));
binding.Source = new ExceptionTest();
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);
}
}
[Fact]
public void Indei_Should_Set_ValidationState()
{
using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
{
var target = new TextBox();
var binding = new Binding(nameof(ExceptionTest.LessThan10));
binding.Source = new IndeiTest();
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);
}
}
private class ExceptionTest
{
private int _lessThan10;
public int LessThan10
{
get { return _lessThan10; }
set
{
if (value < 10)
{
_lessThan10 = value;
}
else
{
throw new InvalidOperationException("More than 10.");
}
}
}
}
private class IndeiTest : INotifyDataErrorInfo
{
private int _lessThan10;
private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
public int LessThan10
{
get { return _lessThan10; }
set
{
if (value < 10)
{
_lessThan10 = value;
_errors.Remove(nameof(LessThan10));
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(LessThan10)));
}
else
{
_errors[nameof(LessThan10)] = new[] { "More than 10" };
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(LessThan10)));
}
}
}
public bool HasErrors => _lessThan10 >= 10;
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public IEnumerable GetErrors(string propertyName)
{
IList<string> result;
_errors.TryGetValue(propertyName, out result);
return result;
}
}
}
}

3
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.1.0\build\net20\xunit.runner.visualstudio.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
@ -97,6 +97,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\ExpressionSubjectTests.cs" />
<Compile Include="Data\IndeiValidatorTests.cs" />
<Compile Include="DefaultValueConverterTests.cs" />

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

@ -1,15 +1,11 @@
// 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 Avalonia.Data;
using Avalonia.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 Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
@ -53,10 +49,10 @@ namespace Avalonia.Markup.UnitTests.Data
public void Setting_Non_Validating_Triggers_Validation()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new ExceptionValidationCheckerPlugin();
var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
ValidationStatus status = null;
IValidationStatus status = null;
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
validator.SetValue(5, BindingPriority.LocalValue);
@ -68,27 +64,25 @@ namespace Avalonia.Markup.UnitTests.Data
public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new ExceptionValidationCheckerPlugin();
var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus status = null;
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 ExceptionValidationCheckerPlugin();
var validatorPlugin = new ExceptionValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus status = null;
IValidationStatus status = null;
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
validator.SetValue(-5, BindingPriority.LocalValue);

129
tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs

@ -0,0 +1,129 @@
// 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.Linq;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Data;
using Avalonia.Markup.Data;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data
{
public class ExpressionObserverTests_Validation
{
[Fact]
public void Exception_Validation_Sends_ValidationUpdate()
{
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.SetValue(-5);
Assert.True(validationMessageFound);
}
[Fact]
public void Disabled_Indei_Validation_Does_Not_Subscribe()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
observer.Subscribe(_ => { });
Assert.Equal(0, data.SubscriptionCount);
}
[Fact]
public void Enabled_Indei_Validation_Subscribes()
{
var data = new IndeiTest { MustBePositive = 5 };
var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
var sub = observer.Subscribe(_ => { });
Assert.Equal(1, data.SubscriptionCount);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
}
public class ExceptionTest : INotifyPropertyChanged
{
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));
}
}
private class IndeiTest : INotifyDataErrorInfo
{
private int _mustBePositive;
private Dictionary<string, IList<string>> _errors = new Dictionary<string, IList<string>>();
private EventHandler<DataErrorsChangedEventArgs> _errorsChanged;
public int MustBePositive
{
get { return _mustBePositive; }
set
{
if (value >= 0)
{
_mustBePositive = value;
_errors.Remove(nameof(MustBePositive));
_errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
}
else
{
_errors[nameof(MustBePositive)] = new[] { "Must be positive" };
_errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
}
}
}
public bool HasErrors => _mustBePositive >= 0;
public int SubscriptionCount { get; private set; }
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged
{
add
{
_errorsChanged += value;
++SubscriptionCount;
}
remove
{
_errorsChanged -= value;
--SubscriptionCount;
}
}
public IEnumerable GetErrors(string propertyName)
{
IList<string> result;
_errors.TryGetValue(propertyName, out result);
return result;
}
}
}
}

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

@ -1,18 +1,13 @@
// 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 Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
using System;
using System.Collections.Generic;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
using Xunit;
using System.Collections;
namespace Avalonia.Markup.UnitTests.Data
{
@ -74,10 +69,10 @@ namespace Avalonia.Markup.UnitTests.Data
public void Setting_Non_Validating_Does_Not_Trigger_Validation()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new IndeiValidationCheckerPlugin();
var validatorPlugin = new IndeiValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
ValidationStatus status = null;
IValidationStatus status = null;
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
validator.SetValue(5, BindingPriority.LocalValue);
@ -89,27 +84,25 @@ namespace Avalonia.Markup.UnitTests.Data
public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new IndeiValidationCheckerPlugin();
var validatorPlugin = new IndeiValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus status = null;
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 IndeiValidationCheckerPlugin();
var validatorPlugin = new IndeiValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
ValidationStatus status = null;
IValidationStatus status = null;
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
validator.SetValue(-5, BindingPriority.LocalValue);

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

@ -1,111 +1,153 @@
using System;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.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 Avalonia.Markup.Xaml.UnitTests.Data
{
public class BindingTests_Validation
{
public class Data : INotifyPropertyChanged
[Fact]
public void Disabled_Validation_Should_Trigger_Validation_Change_On_Exception()
{
private string mustbeNonEmpty;
public string MustBeNonEmpty
var source = new ValidationTestModel { MustBePositive = 5 };
var target = new TestControl { DataContext = source };
var binding = new Binding
{
get { return mustbeNonEmpty; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException(nameof(value));
}
mustbeNonEmpty = value;
}
}
Path = nameof(source.MustBePositive),
Mode = BindingMode.TwoWay,
public event PropertyChangedEventHandler PropertyChanged;
// Even though EnableValidation = false, exception validation is enabled.
EnableValidation = false,
};
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
target.Bind(TestControl.ValidationTestProperty, binding);
target.ValidationTest = -5;
Assert.False(target.ValidationStatus.IsValid);
}
[Fact]
public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Direct()
public void Enabled_Validation_Should_Trigger_Validation_Change_On_Exception()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var source = new ValidationTestModel { MustBePositive = 5 };
var target = new TestControl { DataContext = source };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Avalonia.Data.BindingMode.TwoWay,
ValidationMethods = Avalonia.Data.ValidationMethods.None
Path = nameof(source.MustBePositive),
Mode = BindingMode.TwoWay,
EnableValidation = true,
};
target.Bind(TextBlock.TextProperty, binding);
target.Text = "";
Assert.True(target.ValidationStatus.IsValid);
target.Bind(TestControl.ValidationTestProperty, binding);
target.ValidationTest = -5;
Assert.False(target.ValidationStatus.IsValid);
}
[Fact]
public void Enabled_Validation_Should_Trigger_Validation_Change_Direct()
public void Passed_Validation_Should_Not_Add_Invalid_Pseudo_Class()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var control = new TestControl();
var model = new ValidationTestModel { MustBePositive = 1 };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Avalonia.Data.BindingMode.TwoWay,
ValidationMethods = Avalonia.Data.ValidationMethods.All
Path = nameof(model.MustBePositive),
Mode = BindingMode.TwoWay,
EnableValidation = true,
};
target.Bind(TextBlock.TextProperty, binding);
target.Text = "";
Assert.False(target.ValidationStatus.IsValid);
control.Bind(TestControl.ValidationTestProperty, binding);
control.DataContext = model;
Assert.DoesNotContain(control.Classes, x => x == ":invalid");
}
[Fact]
public void Disabled_Validation_Should_Not_Trigger_Validation_Change_Styled()
public void Failed_Validation_Should_Add_Invalid_Pseudo_Class()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var control = new TestControl();
var model = new ValidationTestModel { MustBePositive = 1 };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Avalonia.Data.BindingMode.TwoWay,
ValidationMethods = Avalonia.Data.ValidationMethods.None
Path = nameof(model.MustBePositive),
Mode = BindingMode.TwoWay,
EnableValidation = true,
};
target.Bind(Control.TagProperty, binding);
target.Tag = "";
Assert.True(target.ValidationStatus.IsValid);
control.Bind(TestControl.ValidationTestProperty, binding);
control.DataContext = model;
control.ValidationTest = -5;
Assert.Contains(control.Classes, x => x == ":invalid");
}
[Fact]
public void Enabled_Validation_Should_Trigger_Validation_Change_Styled()
public void Failed_Then_Passed_Validation_Should_Remove_Invalid_Pseudo_Class()
{
var source = new Data { MustBeNonEmpty = "Test" };
var target = new TextBlock { DataContext = source };
var control = new TestControl();
var model = new ValidationTestModel { MustBePositive = 1 };
var binding = new Binding
{
Path = nameof(source.MustBeNonEmpty),
Mode = Avalonia.Data.BindingMode.TwoWay,
ValidationMethods = Avalonia.Data.ValidationMethods.All
Path = nameof(model.MustBePositive),
Mode = BindingMode.TwoWay,
EnableValidation = true,
};
target.Bind(Control.TagProperty, binding);
target.Tag = "";
Assert.False(target.ValidationStatus.IsValid);
control.Bind(TestControl.ValidationTestProperty, binding);
control.DataContext = model;
control.ValidationTest = -5;
Assert.Contains(control.Classes, x => x == ":invalid");
control.ValidationTest = 5;
Assert.DoesNotContain(control.Classes, x => x == ":invalid");
}
private class TestControl : Control
{
public static readonly StyledProperty<int> ValidationTestProperty
= AvaloniaProperty.Register<TestControl, int>(nameof(ValidationTest), 1, defaultBindingMode: BindingMode.TwoWay);
public int ValidationTest
{
get
{
return GetValue(ValidationTestProperty);
}
set
{
SetValue(ValidationTestProperty, value);
}
}
protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
{
if (property == ValidationTestProperty)
{
UpdateValidationState(status);
}
}
}
private class ValidationTestModel
{
private int mustBePositive;
public int MustBePositive
{
get { return mustBePositive; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
mustBePositive = value;
}
}
}
}
}

Loading…
Cancel
Save