Проверка данных в WPF

wpf controls bindings

Эта статья демонстрирует, как можно создать правило для проверки данных, заданных через Binding. В статье так же рассматривается проблема передачи дополнительных параметров в метод проверки.

Предположим, что мы хотим создать простое приложение, которое позволяет нам выбрать некоторое число с помощью Slider, затем ввести некоторое другое число в TextBox.

Приложение не должно допускать ввод числа не кратного тому, что выбрано с помощью Slider’а.

Пользовательский интерфейс может быть похож на этот:

Пример UI

Описание проблемы

2 числа, введенные пользователем, будут храниться в классе DivisionNumbers, который реализует следующий интерфейс:

interface IDivisionNumbers : INotifyPropertyChanged
{
  int Dividend { get; set; }
  int Divisor { get; set; }
}

В конструкторе класса MainWindow сконфигурируем объект данного класса и установим свойство DataContext для того, чтобы можно было использовать Binding для элементов UI.

public MainWindow()
{
  InitializeComponent();
  var nums = new DivisionNumbers();
  nums.Divisor = 1;
  nums.Dividend = 1;
  this.DataContext = nums;
}

Логику проверки данных, введенных в TextBox, вынесем в подкласс ValidationRule. Объект данного класса добавим в свойство ValidationRules ассоциированного с TextBox’ом Binding’а. Когда пользователь вводит некоторое число в текстовом поле, наше правило будет его проверять на кратность заданному с помощью Slider’а числу.<br />Вот XAML описывающий вышесказанное:

<TextBox>
  <TextBox.Text>
    <Binding Path="Dividend" UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <local:IsMultipleOfValidationRule />
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

Незавершенная реализация нашего правила может выглядеть так:

public class IsMultipleOfValidationRule : ValidationRule
{
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
  {
    try
    {
      int dividend = (int)Convert.ChangeType(value, typeof(int));
      int divisor = // Каким-то образом получаем делитель...
      if (dividend % divisor == 0)
        return ValidationResult.ValidResult;
      return new ValidationResult(
        false,
        "The number is not a multiple of " + divisor);
    }
    catch
    {
      return new ValidationResult( false, "Not a number." );
    }
  }
}

И тут проявляется следующая проблема. Как получить значение делителя, которое выбрал пользователь, в методе Validate?

Мы не можем добавить свойство Divisor в класс IsMultipleOfValidationRule и использовать для него Binding, так как класс ValidationRule не является подклассом класса DependencyObject.

Каким образом мы можем преодолеть эту, казалось бы, невозможную из-за технического барьера, цель наиболее элегантно?

Решение

Добавим следующий фрагмент XAML кода в описание нашего окна:

<Window.Resources>
  <FrameworkElement x:Key="DataContextBridge" />
</Window.Resources>
<Window.DataContext>
  <Binding Mode="OneWayToSource" Path="DataContext" Source="{StaticResource DataContextBridge}" />
</Window.DataContext>

Таким образом, мы создали некоторый виртуальный элемент DataContextBridge, к которому будет привязан DataContext нашего окна.

Создадим новый класс, который будет хранить некоторое число:

public class IntegerContainer : FrameworkElement
{
  public int Value
  {
    get { return (int)GetValue( ValueProperty ); }
    set { SetValue( ValueProperty, value ); }
  }

  public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register(
    "Value",
    typeof( int ),
    typeof( IntegerContainer ),
    new UIPropertyMetadata( 0 ) );
}

Теперь обновим наше правило для проверки данных с использованием IntegerContainer:

public class IsMultipleOfValidationRule : ValidationRule
{
  public IntegerContainer DivisorContainer { get; set; }

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo )
  {
    try
    {
      int dividend = (int)Convert.ChangeType( value, typeof( int ) );
      int divisor = this.DivisorContainer.Value;
      if( dividend % divisor == 0 )
        return ValidationResult.ValidResult;
      return new ValidationResult(
        false,
        "The number is not a multiple of " + divisor );
    }
    catch
    {
      return new ValidationResult( false, "Not a number." );
    }
  }
}

Теперь осталось только настроить наше правило в XAML представлении.

<TextBox>
  <TextBox.Text>
    <Binding Path="Dividend" UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <local:IsMultipleOfValidationRule>
          <local:IsMultipleOfValidationRule.DivisorContainer>
            <!-- This IntegerContainer is the "child node" of
                 the DataContextBridge element, in the virtual
                 branch attached to the Window's logical tree. -->
            <local:IntegerContainer
              DataContext="{Binding Source={StaticResource DataContextBridge}, Path=DataContext}"
              Value="{Binding Divisor}" />
          </local:IsMultipleOfValidationRule.DivisorContainer>
        </local:IsMultipleOfValidationRule>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

Готово!