Validation

De Banane Atomic
Aller à la navigationAller à la recherche

Liens

ValidatesOnExceptions

Le Binding catch les exceptions, il est donc possible de lancer une exception si la valeur n'est pas valide.

Xaml.png
<TextBox Text="{Binding InputValue, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}">
Csharp.svg
private string _inputValue;
public string InputValue
{
    get => _inputValue;
    set 
    {
        // le test est réalisé avant l'affectation de la valeur
        // ce qui empeche l'affectation de la valeur si celle-ci n'est pas validée
        if(value.Contains("x"))
        {
            throw new ArgumentException("Message d'ereeur.");
        }

        _inputValue= value; 
    }
}

Classe ValidationRule

La validation est réalisée avant l'affectation de la valeur, celle-ci n'est setée que si elle est valide.
Desavantage: la validation se fait uniquement sur la valeur, le context n'est pas accessible.
Les paramètres ne peuvent être bindés car ce ne sont pas des dependancy properties, leurs valeurs sont donc statiques.

Csharp.svg
public class MyValidationRule : ValidationRule
{
    public string ForbiddenValue { get; set; }

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (ForbiddenValue == null)
            return ValidationResult.ValidResult;

        if (value is string input && !input.Contains("x"))
        {
            return ValidationResult.ValidResult;
        }
        return new ValidationResult(false, "Error message.");
    }
}
Xaml.png
<UserControl xmlns:local="clr-namespace:Main.Views">

<TextBox>
    <TextBox.Text>
        <Binding>
            <Binding.ValidationRules>
                <local:MyValidationRule ForbiddenValue="x" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>

ExceptionValidationRule

Xaml.png
<TextBox>
    <TextBox.Text>
        <Binding>
            <Binding.ValidationRules>
                <ExceptionValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>

IDataErrorInfo

Désaventage: la valeur est setée avant la validation.

Xaml.png
<TextBox Text="{Binding InputValue, ValidatesOnDataErrors=True}" />
Csharp.svg
public class MyViewModel : ViewModelBase, IDataErrorInfo
{
    // erreur globale liée au VM
    public string Error => string.Empty;

    // déclenché lors de l'appel du set sur une propriété
    public string this[string propertyName] => GetErrorForProperty(propertyName);

    private string GetErrorForProperty(string propertyName)
    {
        switch (propertyName)
        {
            case nameof(InputValue):
            {
                if (InputValue.Contains("x"))
                {
                    // si le retour est différent de null ou vide
                    // le programme considère qu'il y a une erreur.
                    return "Error message.";
                }
                return string.Empty;
            }
            default:
                return string.Empty;
        }

INotifyDataErrorInfo

Même concept qu'IDataErrorInfo avec une validation asynchrone.
Permet aussi de renvoyer plusieurs erreurs pour une seule propriété.

Xaml.png
<!-- ValidatesOnNotifyDataErrors est par défaut à True -->
<TextBox Text="{Binding InputValue, ValidatesOnNotifyDataErrors=True}" />
Csharp.svg
public class MyViewModel : ViewModelBase, INotifyDataErrorInfo
{
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public IEnumerable GetErrors(string propertyName)
    {
        // use lock because of multi-thread async calls
        // or a concurrent collection instead of a Dictionary
        lock (_propertyErrors)
        {
            if (_propertyErrors.ContainsKey(propertyName))
                return _propertyErrors[propertyName];
        }

        return null;
    }

    public bool HasErrors => _propertyErrors.Values.Any(errors => errors != null);

    private readonly Dictionary<string, IList<string>> _propertyErrors = new Dictionary<string, IList<string>>();

    /* propriété bindée avec sa validation async */
    private string _inputValue;
    public string InputValue
    {
        get => _inputValue;
        set
        {
            SetProperty(ref _inputValue, value);
            ValidateAsync(InputValue).ContinueWith((errorTasks) =>
            {
                lock (_propertyErrors)
                {
                    _propertyErrors[nameof(InputValue)] = errorTasks.Result;
                    ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(InputValue)));
                }
            });
        }
    }

    private async Task<IList<string>> ValidateAsync(string inputValue)
    {
        await Task.Run(() =>
        {
            Thread.Sleep(4000);
        });

        return inputValue.Contains("y") ? new List<string> { "Error message." } : null;
    }

Event

Lève un évenement en cas d'erreur de validation.

Xaml.png
<TextBox Text="{Binding InputValue, NotifyOnValidationError=True}"
         Validation.Error="Validation_OnError" />
<!-- l'evt est de type bubble, Validation.Error peut ainsi être placé dans un parent de l'abre visuel -->
Csharp.svg
private void Validation_OnError(object sender, ValidationErrorEventArgs e)
{
    Debug.WriteLine(e.Error.ErrorContent);
}

Style

Xaml.png
<Style x:Key="ErrorStyle" TargetType="FrameworkElement">
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="ToolTip"
                    Value="{Binding RelativeSource={RelativeSource Self},       
                                    Path=(Validation.Errors).CurrentItem.ErrorContent}" />
        </Trigger>
    </Style.Triggers>
</Style>

ErrorTemplate

Change la manière dont est signalé une erreur.

Xaml.png
<ControlTemplate x:Key="TextBoxErrorTemplate">
    <DockPanel>
        <Ellipse DockPanel.Dock="Right"
                 Margin="2,0"
                 ToolTip="Invalid data"
                 Width="10"
                 Height="10">
            <Ellipse.Fill>
                <LinearGradientBrush>
                    <GradientStop Color="Black" Offset="0" />
                    <GradientStop Color="Red" Offset="1" />
                </LinearGradientBrush>
            </Ellipse.Fill>
        </Ellipse>
        <AdornedElementPlaceholder />
    </DockPanel>
</ControlTemplate>

<Style TargetType="TextBox">
    <Setter Property="Margin" Value="4,4,15,4" />
    <Setter Property="Validation.ErrorTemplate" Value="{StaticResource TextBoxErrorTemplate}" />
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="ToolTip"
                    Value="{Binding RelativeSource={RelativeSource Self},
                                    Path=(Validation.Errors).CurrentItem.ErrorContent}" />
        </Trigger>
    </Style.Triggers>
</Style>

DataAnnotation

Ajouter la référence System.ComponentModel.DataAnnotations

ValidationException

MyViewModel.cs
public class MyViewModel : BindableBase
{
    private string _inputValue;

    [Required(AllowEmptyStrings = false, ErrorMessage = "InputValue is required")]
    public string InputValue
    {
        get => _inputValue;
        set
        {
            ValidateProperty(nameof(InputValue), value);
            SetProperty(ref _inputValue, value);
        }
    }

    // à placer dans une classe abstraite dont tous les VM pourront hériter
    private void ValidateProperty(string propertyName, object value)
    {
        var context = new ValidationContext(this);
        context.MemberName = propertyName;
        // lève une ValidationException en cas d'erreur de validation
        Validator.ValidateProperty(value, context);
    }

IDataErrorInfo

MyViewModel.cs
public class MyViewModel : BindableBase, IDataErrorInfo
{
    private string _inputValue;

    [Required(AllowEmptyStrings = false, ErrorMessage = "InputValue is required")]
    public string InputValue
    {
        get => _inputValue;
        set => SetProperty(ref _inputValue, value);
    }

    public string Error => string.Empty;

    public string this[string propertyName] => GetErrorForProperty(propertyName);

    private string GetErrorForProperty(string propertyName)
    {
        var validationResults = ValidateProperty(propertyName, GetType().GetProperty(propertyName)?.GetValue(this));
        return validationResults.FirstOrDefault()?.ErrorMessage;
    }

    // à placer dans une classe abstraite dont tous les VM pourront hériter
    private IList<ValidationResult> ValidateProperty(string propertyName, object value)
    {
        var context = new ValidationContext(this);
        context.MemberName = propertyName;
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateProperty(value, context, validationResults);
        return validationResults;
    }

INotifyDataErrorInfo

MyViewModel.cs
public class MyViewModel : BindableBase, INotifyDataErrorInfo
{
    private string _inputValue;

    [Required(AllowEmptyStrings = false, ErrorMessage = "InputValue is required")]
    public string InputValue
    {
        get => _inputValue;
        set
        {
            ValidateProperty(nameof(InputValue), value);
            SetProperty(ref _inputValue, value);
        }
    }

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public IEnumerable GetErrors(string propertyName)
    {
        // use lock because of multi-thread async calls, or a concurrent collection instead of a Dictionary
        lock (_propertyErrors)
        {
            if (_propertyErrors.ContainsKey(propertyName))
                return _propertyErrors[propertyName];
        }
        return null;
    }

    public bool HasErrors => _propertyErrors.Values.Any(errors => errors != null);

    private readonly Dictionary<string, IList<string>> _propertyErrors = new Dictionary<string, IList<string>>();

    // à placer dans une classe abstraite dont tous les VM pourront hériter
    private void ValidateProperty(string propertyName, object value)
    {
        var context = new ValidationContext(this);
        context.MemberName = propertyName;
        var validationResults = new List<ValidationResult>();
        Validator.TryValidateProperty(value, context, validationResults);

        lock (_propertyErrors)
        {
            _propertyErrors[propertyName] = validationResults.Select(vr => vr.ErrorMessage).ToList();
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }

Validation d'une fenêtre avec un bouton

Xaml.png
<TextBox Text="{Binding Path=..., UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"
         Validation.Error="Validation_Error" />
<Button Command="{x:Static views:ReportSaver.SaveCmd}">OK</Button>
Csharp.svg
public static RoutedCommand SaveCmd = new RoutedCommand();
private void ExecutedSaveCmd(object sender, ExecutedRoutedEventArgs e)
{
    this.DialogResult = true;
    this.Close();
}
private void CanExecuteSaveCmd(object sender, CanExecuteRoutedEventArgs e)
{
    e.CanExecute = _noOfErrorsOnScreen == 0;
    e.Handled = true;
}

private int _noOfErrorsOnScreen = 0;

private void Validation_Error(object sender, ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added)
        _noOfErrorsOnScreen++;
    else
        _noOfErrorsOnScreen--;
}

BindingGroup

Permet d'avoir le visuel de validation dans un control parent.