Parent Child ValidationRule
Asked Answered
H

1

1

I have a complicated scenario using ValidationRules that I need some help with. I have a User Control that is roughly organized like this:

Parent (ItemsControl)
    Child 1
        Property 1
        Property 2
    Child 2
        Property 1
        Property 2

When Child 1.Property 1 is changed, I need to perform validation on it. However, the validation rule requires the value of Child 1.Property 1 as well as the Property 1 values for all its siblings (variable number) to perform the validation. I could put a ValidationRule on the Parent ItemsControl, but I need the Control bound to Child1.Property1 to show the error. Currently when I place the validation on the parent the error is displayed on the parent, not the child. I've also considered using BindingGroups but I want the validation to fire automatically when a property is changed. There isn't, to my knowledge, a way to automatically force Validation to fire for a BindingGroup.

Is there a way to accomplish what I am trying to do?

Hygrothermograph answered 3/8, 2012 at 20:32 Comment(1)
The validation is at the object level. What I do is the ctor pass the collection to all the members so the member can walk the collection.Tunstall
I
0

This automatically turns all the object's data annotation attributes into ValidationRules, and can be applied once in the app on all TextBoxes:

using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

using DaValidationResult = System.ComponentModel.DataAnnotations.ValidationResult;
using WinValidationResult = System.Windows.Controls.ValidationResult;

public sealed class DataAnnotationsBehavior
{
  public static bool GetValidateDataAnnotations(DependencyObject obj) =>
    (bool)obj.GetValue(ValidateDataAnnotationsProperty);
  public static void SetValidateDataAnnotations(DependencyObject obj, bool value) =>
    obj.SetValue(ValidateDataAnnotationsProperty, value);

  public static readonly DependencyProperty ValidateDataAnnotationsProperty =
      DependencyProperty.RegisterAttached("ValidateDataAnnotations", typeof(bool), 
        typeof(DataAnnotationsBehavior), new PropertyMetadata(false, OnPropertyChanged));

  private static void OnPropertyChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
  {
    var boolean = (bool)e.NewValue;
    if (!(d is TextBox textBox))
      throw new NotSupportedException(
        @$"The behavior " +
        "'{typeof(DataAnnotationsBehavior)}' can only be applied " +
        "on elements of type '{typeof(TextBox)}'.");

    var bindingExpression =
      textBox.GetBindingExpression(TextBox.TextProperty);

    if (boolean)
    {
      var dataItem = bindingExpression.DataItem;
      if (bindingExpression.DataItem == null)
        return;

      var type = dataItem.GetType();
      var prop = type.GetProperty(bindingExpression.ResolvedSourcePropertyName);
      if (prop == null)
        return;

      var allAttributes = prop.GetCustomAttributes(typeof(ValidationAttribute), true);
      foreach (var validationAttr in allAttributes.OfType<ValidationAttribute>())
      {
        var context = new ValidationContext(dataItem, null, null) 
        { MemberName = bindingExpression.ResolvedSourcePropertyName };

        bindingExpression
          .ParentBinding
          .ValidationRules
          .Add(new AttributesValidationRule(context, validationAttr));
      }
    }
    else
    {
      var das =
        bindingExpression
          .ParentBinding
          .ValidationRules
          .OfType<AttributesValidationRule>()
          .ToList();

      if (das != null)
        foreach (var da in das)
          bindingExpression.ParentBinding.ValidationRules.Remove(da);
    }
  }

  abstract class DaValidationRule : ValidationRule
  {
    public ValidationContext ValidationContext { get; }

    public DaValidationRule(ValidationContext validationContext)
    {
      ValidationContext = validationContext;
    }
  }

  class AttributesValidationRule : DaValidationRule
  {
    public ValidationAttribute ValidationAttribute { get; }

    public AttributesValidationRule(ValidationContext validationContext, 
      ValidationAttribute attribute)
      : base(validationContext) =>
      ValidationAttribute = attribute;

    public override WinValidationResult Validate(object value, CultureInfo cultureInfo)
    {
      var result = ValidationAttribute.GetValidationResult(value, ValidationContext);

      return result == DaValidationResult.Success
        ? WinValidationResult.ValidResult
        : new WinValidationResult(false, result.ErrorMessage);
    }
  }
}

Usage:

<Window.DataContext>
  <local:ViewModel />
</Window.DataContext>
<Window.Resources>
  <ResourceDictionary>
    <Style TargetType="TextBox">
      <Setter 
        Property="local:DataAnnotationsBehavior.ValidateDataAnnotations" 
        Value="True"/>
      <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
          <Setter 
            Property="ToolTip" 
            Value="{Binding (Validation.Errors)[0].ErrorContent,
              RelativeSource={RelativeSource Self}}"/>
        </Trigger>
      </Style.Triggers>
    </Style>
  </ResourceDictionary>
</Window.Resources>
<StackPanel DataContext="{Binding Model}">
  <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>

I made the DaValidationRule class abstract because I think I might want to expand that in the future so it also covers IValidationAttribute and maybe other scenarios.

For the version that covers TextBoxes within DataGridTextColumn in edit mode, and for full code, see this.

Inseparable answered 5/12, 2019 at 20:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.