Browse Source

Converter for DataValidationErrors (#11282)

* Introduce ErrorConverter and DisplayErrors attached properties

- the converter can be used to change the way a message is print
- we use DisplayErrors to get the converted error messages

* Adjust FluentTheme

* [WIP] Add a sample Page for DataValidationErrors

* use a private attached property to store recent errors 

this approach gets rid of the need to DisplayErrors property

* Update samples with some additional details

* Reuse rich SetError logic in DataGrid as well

* Unify some code with OnErrorsOrConverterChanged

* Restore old behavior with null default value

* Add SetErrorConverter test

---------

Co-authored-by: Max Katz <maxkatz6@outlook.com>
UnsafeAsSpan-logical-children
Tim 3 years ago
committed by GitHub
parent
commit
8fc0be82cd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      samples/ControlCatalog/MainView.xaml
  2. 42
      samples/ControlCatalog/Pages/DataValidationPage.axaml
  3. 14
      samples/ControlCatalog/Pages/DataValidationPage.axaml.cs
  4. 45
      samples/ControlCatalog/ViewModels/DataValidationViewModel.cs
  5. 9
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  6. 99
      src/Avalonia.Controls/DataValidationErrors.cs
  7. 24
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

3
samples/ControlCatalog/MainView.xaml

@ -74,6 +74,9 @@
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<pages:DataGridPage />
</TabItem>
<TabItem Header="Data Validation">
<pages:DataValidationPage />
</TabItem>
<TabItem Header="Date/Time Picker">
<pages:DateTimePickerPage />
</TabItem>

42
samples/ControlCatalog/Pages/DataValidationPage.axaml

@ -0,0 +1,42 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:ControlCatalog.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:DataValidationViewModel"
x:Class="ControlCatalog.Pages.DataValidationPage">
<UserControl.DataContext>
<viewModels:DataValidationViewModel />
</UserControl.DataContext>
<StackPanel TextBlock.TextWrapping="Wrap">
<Label Target="Email1" Content="Email validation" />
<TextBox x:Name="Email1"
MaxWidth="400"
HorizontalAlignment="Left"
Text="{Binding DataAnnotationsSample}" />
<Label Target="Email2" Content="Email validation with custom error converter"
Margin="0, 10, 0, 0" />
<TextBox x:Name="Email2"
Text="{Binding DataAnnotationsSample}"
MaxWidth="400"
HorizontalAlignment="Left"
DataValidationErrors.ErrorConverter="{Binding Converter}" />
<Label Target="SetterException" Content="Setter exception with custom error converter and tooltip error"
Margin="0, 10, 0, 0" />
<TextBox x:Name="SetterException"
Text="{Binding ExceptionInsideSetterSample}"
MaxWidth="400"
HorizontalAlignment="Left"
DataValidationErrors.ErrorConverter="{Binding ExceptionConverter}">
<TextBox.Styles>
<Style Selector="DataValidationErrors">
<Setter Property="Theme" Value="{DynamicResource TooltipDataValidationErrors}" />
</Style>
</TextBox.Styles>
</TextBox>
</StackPanel>
</UserControl>

14
samples/ControlCatalog/Pages/DataValidationPage.axaml.cs

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages;
public partial class DataValidationPage : UserControl
{
public DataValidationPage()
{
InitializeComponent();
}
}

45
samples/ControlCatalog/ViewModels/DataValidationViewModel.cs

@ -0,0 +1,45 @@
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using MiniMvvm;
namespace ControlCatalog.ViewModels;
public class DataValidationViewModel : ViewModelBase
{
private string? _DataAnnotationsSample;
[Required]
[EmailAddress]
[MinLength(5)]
public string? DataAnnotationsSample
{
get => _DataAnnotationsSample;
set => RaiseAndSetIfChanged(ref _DataAnnotationsSample, value);
}
public Func<object, object> Converter { get; } = new Func<object, object>(o =>
{
return $"Error: {o}";
});
private string? _ExceptionInsideSetterSample;
public string? ExceptionInsideSetterSample
{
get => _ExceptionInsideSetterSample;
set
{
if (value is null || value.Length < 5)
throw new ArgumentOutOfRangeException(nameof(value), "Give me 5 or more letter please :-)");
RaiseAndSetIfChanged(ref _ExceptionInsideSetterSample, value);
}
}
public Func<object, object> ExceptionConverter { get; } = new Func<object, object>(o =>
{
return o is Exception ex ? $"Huh, there was an Exception: {ex.Message}" : "Something went really wrong!";
});
}

9
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -4160,13 +4160,8 @@ namespace Avalonia.Controls
if (editingElement != null)
{
var errorList =
binding.ValidationErrors
.SelectMany(ValidationUtil.UnpackException)
.Select(ValidationUtil.UnpackDataValidationException)
.ToList();
DataValidationErrors.SetErrors(editingElement, errorList);
DataValidationErrors.SetError(editingElement,
new AggregateException(binding.ValidationErrors));
}
}
}

99
src/Avalonia.Controls/DataValidationErrors.cs

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Reactive;
@ -18,6 +18,8 @@ namespace Avalonia.Controls
[PseudoClasses(":error")]
public class DataValidationErrors : ContentControl
{
private static bool s_overridingErrors;
/// <summary>
/// Defines the DataValidationErrors.Errors attached property.
/// </summary>
@ -29,10 +31,24 @@ namespace Avalonia.Controls
/// </summary>
public static readonly AttachedProperty<bool> HasErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, bool>("HasErrors");
/// <summary>
/// Defines the DataValidationErrors.ErrorConverter attached property.
/// </summary>
public static readonly AttachedProperty<Func<object, object>?> ErrorConverterProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, Func<object, object>?>("ErrorConverter");
/// <summary>
/// Defines the DataValidationErrors.ErrorTemplate property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> ErrorTemplateProperty =
AvaloniaProperty.Register<DataValidationErrors, IDataTemplate>(nameof(ErrorTemplate));
/// <summary>
/// Stores the original, not converted errors passed by the control
/// </summary>
private static readonly AttachedProperty<IEnumerable<object>?> OriginalErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, IEnumerable<object>?>("OriginalErrors");
private Control? _owner;
@ -56,6 +72,12 @@ namespace Avalonia.Controls
ErrorsProperty.Changed.Subscribe(ErrorsChanged);
HasErrorsProperty.Changed.Subscribe(HasErrorsChanged);
TemplatedParentProperty.Changed.AddClassHandler<DataValidationErrors>((x, e) => x.OnTemplatedParentChange(e));
ErrorConverterProperty.Changed.Subscribe(OnErrorConverterChanged);
}
private static void OnErrorConverterChanged(AvaloniaPropertyChangedEventArgs e)
{
OnErrorsOrConverterChanged((Control)e.Sender);
}
private void OnTemplatedParentChange(AvaloniaPropertyChangedEventArgs e)
@ -74,15 +96,17 @@ namespace Avalonia.Controls
private static void ErrorsChanged(AvaloniaPropertyChangedEventArgs e)
{
if (s_overridingErrors) return;
var control = (Control)e.Sender;
var errors = (IEnumerable<object>?)e.NewValue;
var hasErrors = false;
if (errors != null && errors.Any())
hasErrors = true;
// Update original errors
control.SetValue(OriginalErrorsProperty, errors);
control.SetValue(HasErrorsProperty, hasErrors);
OnErrorsOrConverterChanged(control);
}
private static void HasErrorsChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
@ -100,8 +124,35 @@ namespace Avalonia.Controls
}
public static void SetError(Control control, Exception? error)
{
SetErrors(control, UnpackException(error));
SetErrors(control, UnpackException(error)?
.Select(UnpackDataValidationException)
.Where(e => e is not null)
.ToArray()!);
}
private static void OnErrorsOrConverterChanged(Control control)
{
var converter = GetErrorConverter(control);
var originalErrors = control.GetValue(OriginalErrorsProperty);
var newErrors = (converter is null ?
originalErrors :
originalErrors?.Select(converter)
.Where(e => e is not null))?
.ToArray();
s_overridingErrors = true;
try
{
control.SetCurrentValue(ErrorsProperty, newErrors!);
}
finally
{
s_overridingErrors = false;
}
control.SetValue(HasErrorsProperty, newErrors?.Any() == true);
}
public static void ClearErrors(Control control)
{
SetErrors(control, null);
@ -111,30 +162,36 @@ namespace Avalonia.Controls
return control.GetValue(HasErrorsProperty);
}
private static IEnumerable<object>? UnpackException(Exception? exception)
public static Func<object, object?>? GetErrorConverter(Control control)
{
return control.GetValue(ErrorConverterProperty);
}
public static void SetErrorConverter(Control control, Func<object, object>? converter)
{
control.SetValue(ErrorConverterProperty, converter);
}
private static IEnumerable<Exception>? UnpackException(Exception? exception)
{
if (exception != null)
{
var aggregate = exception as AggregateException;
var exceptions = aggregate == null ?
new[] { GetExceptionData(exception) } :
aggregate.InnerExceptions.Select(GetExceptionData).ToArray();
var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList();
if (filtered.Count > 0)
{
return filtered;
}
var exceptions = exception is AggregateException aggregate ?
aggregate.InnerExceptions :
(IEnumerable<Exception>)new[] { exception };
return exceptions.Where(x => !(x is BindingChainException)).ToArray();
}
return null;
}
private static object GetExceptionData(Exception exception)
private static object? UnpackDataValidationException(Exception exception)
{
if (exception is DataValidationException dataValidationException &&
dataValidationException.ErrorData is object data)
return data;
if (exception is DataValidationException dataValidationException)
{
return dataValidationException.ErrorData;
}
return exception;
}

24
tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

@ -64,6 +64,30 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Setter_Exceptions_Should_Be_Converter_If_Error_Converter_Set()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
DataContext = new ExceptionTest(),
[!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay),
Template = CreateTemplate()
};
DataValidationErrors.SetErrorConverter(target, err => "Error: " + err);
target.ApplyTemplate();
target.Text = "20";
IEnumerable<object> errors = DataValidationErrors.GetErrors(target);
Assert.Single(errors);
var error = Assert.IsType<string>(errors.Single());
Assert.StartsWith("Error: ", error);
}
}
[Fact]
public void Setter_Exceptions_Should_Set_DataValidationErrors_HasErrors()
{

Loading…
Cancel
Save