Browse Source

Added Data Annotations validation.

pull/691/head
Steven Kirk 10 years ago
parent
commit
57a611533c
  1. 2
      samples/BindingTest/BindingTest.csproj
  2. 4
      samples/BindingTest/MainWindow.xaml
  3. 14
      samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
  4. 1
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  5. 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  6. 1
      src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
  7. 81
      src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs
  8. 2
      src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
  9. 3
      src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs
  10. 2
      src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
  11. 2
      src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
  12. 1
      src/Markup/Avalonia.Markup/packages.config
  13. 5
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  14. 111
      tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs
  15. 1
      tests/Avalonia.Markup.UnitTests/packages.config

2
samples/BindingTest/BindingTest.csproj

@ -50,6 +50,7 @@
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll</HintPath>
@ -80,6 +81,7 @@
<Compile Include="TestItemView.xaml.cs">
<DependentUpon>TestItemView.xaml</DependentUpon>
</Compile>
<Compile Include="ViewModels\DataAnnotationsErrorViewModel.cs" />
<Compile Include="ViewModels\IndeiErrorViewModel.cs" />
<Compile Include="ViewModels\ExceptionErrorViewModel.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />

4
samples/BindingTest/MainWindow.xaml

@ -79,6 +79,10 @@
<TextBox Watermark="Maximum" UseFloatingWatermark="True" Text="{Binding Path=Maximum}"/>
<TextBox Watermark="Value" UseFloatingWatermark="True" Text="{Binding Path=Value}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
<TextBlock FontSize="16" Text="Data Annotations Validation"/>
<TextBox Watermark="Phone #" UseFloatingWatermark="True" Text="{Binding PhoneNumber}"/>
</StackPanel>
</StackPanel>
</TabItem>
<TabItem Header="Commands">

14
samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs

@ -0,0 +1,14 @@
// 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.ComponentModel.DataAnnotations;
namespace BindingTest.ViewModels
{
public class DataAnnotationsErrorViewModel
{
[Phone]
[MaxLength(10)]
public string PhoneNumber { get; set; }
}
}

1
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -69,6 +69,7 @@ namespace BindingTest.ViewModels
public ReactiveCommand<object> StringValueCommand { get; }
public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel();
public ExceptionErrorViewModel ExceptionDataValidation { get; } = new ExceptionErrorViewModel();
public IndeiErrorViewModel IndeiDataValidation { get; } = new IndeiErrorViewModel();
}

1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -47,6 +47,7 @@
<Compile Include="Data\ExpressionParseException.cs" />
<Compile Include="Data\ExpressionSubject.cs" />
<Compile Include="ControlLocator.cs" />
<Compile Include="Data\Plugins\DataAnnotationsValidationPlugin.cs" />
<Compile Include="Data\Plugins\ExceptionValidationPlugin.cs" />
<Compile Include="Data\Plugins\IndeiValidationPlugin.cs" />
<Compile Include="Data\Plugins\IDataValidationPlugin.cs" />

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

@ -35,6 +35,7 @@ namespace Avalonia.Markup.Data
public static readonly IList<IDataValidationPlugin> DataValidators =
new List<IDataValidationPlugin>
{
new DataAnnotationsValidationPlugin(),
new IndeiValidationPlugin(),
new ExceptionValidationPlugin(),
};

81
src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs

@ -0,0 +1,81 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
{
/// <summary>
/// Validates properties on that have <see cref="ValidationAttribute"/>s.
/// </summary>
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference, string memberName)
{
return reference.Target?
.GetType()
.GetRuntimeProperty(memberName)?
.GetCustomAttributes<ValidationAttribute>()
.Any() ?? false;
}
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
{
return new Accessor(reference, name, inner);
}
private class Accessor : DataValidatiorBase
{
private ValidationContext _context;
public Accessor(WeakReference reference, string name, IPropertyAccessor inner)
: base(inner)
{
_context = new ValidationContext(reference.Target);
_context.MemberName = name;
}
public override bool SetValue(object value, BindingPriority priority)
{
return base.SetValue(value, priority);
}
protected override void InnerValueChanged(object value)
{
var errors = new List<ValidationResult>();
if (Validator.TryValidateProperty(value, _context, errors))
{
base.InnerValueChanged(value);
}
else
{
base.InnerValueChanged(new BindingNotification(
CreateException(errors),
BindingErrorType.DataValidationError,
value));
}
}
private Exception CreateException(IList<ValidationResult> errors)
{
if (errors.Count == 1)
{
return new ValidationException(errors[0].ErrorMessage);
}
else
{
return new AggregateException(
errors.Select(x => new ValidationException(x.ErrorMessage)));
}
}
}
}
}

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

@ -13,7 +13,7 @@ namespace Avalonia.Markup.Data.Plugins
public class ExceptionValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference) => true;
public bool Match(WeakReference reference, string memberName) => true;
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)

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

@ -15,8 +15,9 @@ namespace Avalonia.Markup.Data.Plugins
/// Checks whether this plugin can handle data validation on the specified object.
/// </summary>
/// <param name="reference">A weak reference to the object.</param>
/// <param name="memberName">The name of the member to validate.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
bool Match(WeakReference reference);
bool Match(WeakReference reference, string memberName);
/// <summary>
/// Starts monitoring the data validation state of a property on an object.

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

@ -16,7 +16,7 @@ namespace Avalonia.Markup.Data.Plugins
public class IndeiValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
public bool Match(WeakReference reference) => reference.Target is INotifyDataErrorInfo;
public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo;
/// <inheritdoc/>
public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor)

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

@ -44,7 +44,7 @@ namespace Avalonia.Markup.Data
{
foreach (var validator in ExpressionObserver.DataValidators)
{
if (validator.Match(reference))
if (validator.Match(reference, PropertyName))
{
accessor = validator.Start(reference, PropertyName, accessor);
}

1
src/Markup/Avalonia.Markup/packages.config

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="System.ComponentModel.Annotations" version="4.1.0" targetFramework="portable45-net45+win8" />
<package id="System.Reactive" version="3.0.0" targetFramework="portable45-net45+win8" />
<package id="System.Reactive.Core" version="3.0.0" targetFramework="portable45-net45+win8" />
<package id="System.Reactive.Interfaces" version="3.0.0" targetFramework="portable45-net45+win8" />

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

@ -44,6 +44,10 @@
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.Annotations">
<HintPath>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile7\System.ComponentModel.Annotations.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=3.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263, processorArchitecture=MSIL">
<HintPath>..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll</HintPath>
@ -92,6 +96,7 @@
<ItemGroup>
<Compile Include="ControlLocatorTests.cs" />
<Compile Include="Data\IndeiBase.cs" />
<Compile Include="Data\Plugins\DataAnnotationsValidationPluginTests.cs" />
<Compile Include="Data\Plugins\IndeiValidationPluginTests.cs" />
<Compile Include="Data\Plugins\ExceptionValidationPluginTests.cs" />
<Compile Include="Data\ExpressionNodeBuilderTests.cs" />

111
tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs

@ -0,0 +1,111 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
using Xunit;
namespace Avalonia.Markup.UnitTests.Data.Plugins
{
public class DataAnnotationsValidationPluginTests
{
[Fact]
public void Should_Match_Property_With_ValidatorAttribute()
{
var target = new DataAnnotationsValidationPlugin();
var data = new Data();
Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10)));
}
[Fact]
public void Should_Match_Property_With_Multiple_ValidatorAttributes()
{
var target = new DataAnnotationsValidationPlugin();
var data = new Data();
Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber)));
}
[Fact]
public void Should_Not_Match_Property_Without_ValidatorAttribute()
{
var target = new DataAnnotationsValidationPlugin();
var data = new Data();
Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated)));
}
[Fact]
public void Produces_Range_BindingNotificationsx()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new DataAnnotationsValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor);
var result = new List<object>();
validator.Subscribe(x => result.Add(x));
validator.SetValue(3, BindingPriority.LocalValue);
validator.SetValue(7, BindingPriority.LocalValue);
validator.SetValue(11, BindingPriority.LocalValue);
Assert.Equal(new[]
{
new BindingNotification(5),
new BindingNotification(
new ValidationException("The field Between5And10 must be between 5 and 10."),
BindingErrorType.DataValidationError,
3),
new BindingNotification(7),
new BindingNotification(
new ValidationException("The field Between5And10 must be between 5 and 10."),
BindingErrorType.DataValidationError,
11),
}, result);
}
[Fact]
public void Produces_Aggregate_BindingNotificationsx()
{
var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
var validatorPlugin = new DataAnnotationsValidationPlugin();
var data = new Data();
var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber));
var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor);
var result = new List<object>();
validator.Subscribe(x => result.Add(x));
validator.SetValue("123456", BindingPriority.LocalValue);
validator.SetValue("abcdefghijklm", BindingPriority.LocalValue);
Assert.Equal(new[]
{
new BindingNotification(null),
new BindingNotification("123456"),
new BindingNotification(
new AggregateException(
new ValidationException("The PhoneNumber field is not a valid phone number."),
new ValidationException("The field PhoneNumber must be a string or array type with a maximum length of '10'.")),
BindingErrorType.DataValidationError,
"abcdefghijklm"),
}, result);
}
private class Data
{
[Range(5, 10)]
public int Between5And10 { get; set; } = 5;
public int Unvalidated { get; set; }
[Phone]
[MaxLength(10)]
public string PhoneNumber { get; set; }
}
}
}

1
tests/Avalonia.Markup.UnitTests/packages.config

@ -2,6 +2,7 @@
<packages>
<package id="Microsoft.Reactive.Testing" version="3.0.0" targetFramework="net45" />
<package id="Moq" version="4.2.1510.2205" targetFramework="net46" />
<package id="System.ComponentModel.Annotations" version="4.1.0" targetFramework="net45" />
<package id="System.Reactive" version="3.0.0" targetFramework="net45" />
<package id="System.Reactive.Core" version="3.0.0" targetFramework="net45" />
<package id="System.Reactive.Interfaces" version="3.0.0" targetFramework="net45" />

Loading…
Cancel
Save