Проверка ввода

Применение в WPF сложных бизнес-правил к вводу данных

Брайан Нойз

Загрузить образец кода

В Microsoft Windows Presentation Foundation (WPF) имеется мощная система связывания с данными. Эта система не только является ключевой для свободного сопряжения определения UI с поддерживающей его логикой и данными через шаблон Model-View-ViewModel (MVVM), но и предоставляет эффективную и гибкую поддержку для проверки данных. Механизмы связывания с данными в WPF обеспечивают несколько вариантов оценки корректности входных данных, когда вы создаете редактируемое представление. Кроме того, поддержка в WPF шаблонов и стилей для элементов управления позволяет легко адаптировать под свои потребности отображение сообщений об ошибках ввода.

Для поддержки сложных правил и отображения пользователю ошибок, обнаруженных при проверке, вы, как правило, должны применять некую комбинацию доступных механизмов проверки. Даже в форме с простым вводом данных можно столкнуться с проблемами проверки, если бизнес-правила станут усложняться. В распространенных ситуациях часто используются как простые правила на уровне индивидуальных свойств, так и перекрестно-связанные свойства (cross-coupled properties), где результат проверки одного из свойств зависит от значения другого свойства. Однако поддержка проверки в системе связывания с данными в WPF упрощает решение этих задач.

В этой статье вы увидите, как использовать реализацию интерфейса IDataErrorInfo, ValidationRule, BindingGroup, исключения, подключаемые свойства и события, относящиеся к проверке, для решения ваших задач верификации данных. Вы также узнаете, как настраивать отображение ошибок, выявленных при проверке, с помощью собственных ErrorTemplate и ToolTip. При этом я исхожу из того, что вы уже знакомы с базовыми возможностями WPF в связывании с данными. Информацию на эту тему можно почерпнуть из статьи Джона Папы (John Papa) «Data Binding in WPF» в декабрьском номере MSDN Magazine в статье, «Привязка данных в WPF».

Обзор проверки данных

Почти всякий раз, когда вы вводите или модифицируете данные в приложении, вам нужно проверять, что эти данные допустимы, пока дело не зашло слишком далеко. Более того, вы должны давать пользователям четкие указания, когда они вводят неправильные данные, и желательно подсказывать им, как исправить ошибки в данных. Такие вещи в WPF делаются довольно легко, пока вы знаете, какой механизм следует задействовать и когда.

Когда вы используете связывание с данными в WPF для представления бизнес-данных, вы обычно применяете объект Binding, чтобы создать конвейер данных между единственным свойством целевого элемента управления и каким-либо свойством объекта — источника данных. Как правило, осуществляется двухстороннее связывание с данными, т. е. информация не только посылается из свойства источника в свойство целевого объекта для отображения, но и возвращается после изменения из целевого объекта в источник (рис. 1).

Figure 1 Data Flow in TwoWay Data Binding
Рис. 1. Поток данных при двухстороннем связывании с данными

Определить, допустимы ли данные, введенные через связанный с данными элемент управления, позволяют три механизма. Все они кратко описаны на рис. 2.

Рис. 2. Механизмы проверки

Механизм проверки Описание
Исключения Если установлено свойство ValidatesOnExceptions объекта Binding и при попытке присвоить модифицированное значение свойству объекта источника генерируется исключение, то для этой привязки (объекта Binding) будет установлена ошибка проверки
ValidationRule В классе Binding есть свойство, которое предоставляет набор экземпляров класса, производного от ValidationRule. Они должны переопределять метод Validate, вызываемый Binding при каждом изменении данных в связанном элементе управления. Если метод Validate возвращает недопустимый объект ValidationResult, для этой привязки (объекта Binding) будет установлена ошибка проверки
IDataErrorInfo Если вы реализуете интерфейс IDataErrorInfo в связанном объекте — источнике данных и установите свойство ValidatesOnDataErrors объекта Binding, то Binding будет вызывать IDataErrorInfo API, предоставляемый связанным объектом-источником. Если в результате вызовов это свойство будет возвращать ненулевые или не пустые, для этой привязки (объекта Binding) будет установлена ошибка проверки

Когда пользователь вводит или модифицирует данные через двухстороннюю привязку, происходит следующее.

  • Данные вводятся в элемент или модифицируются в нем пользователем с клавиатуры, мышью, сенсорным вводом или пером, что приводит к изменению некоего свойства этого элемента.
  • Данные при необходимости приводятся к типу свойства источника данных.
  • Задается значение свойства источника.
  • Срабатывает подключенное событие Binding.SourceUpdated.
  • Исключения перехватываются Binding, если они генерируются аксессором set свойства источника данных, и могут быть использованы для указания ошибки, обнаруженной в результате проверки.
  • Вызываются свойства IDataErrorInfo объекта источника данных, если они реализованы.
  • Указания на ошибки представляются пользователю, и срабатывает подключенное событие Validation.Error.

Как видите, в этом процессе есть несколько точек, где при проверке могут быть выявлены ошибки в зависимости от применяемого вами механизма. В списке не показано, где срабатывают ValidationRules. Я поступил так потому, что они могут срабатывать в различных точках всего процесса в зависимости от значения свойства ValidationStep, принадлежащего ValidationRule, в том числе до преобразования типа, после преобразования, после обновления свойства или фиксации измененного значения (если объект данных реализует IEditableObject). Значение по умолчанию — RawProposedValue, т. е. до преобразования типа. Точка, в которой данные преобразуются из типа свойства целевого элемента управления в тип свойства объекта — источника данных, обычно появляется неявно, никак не затрагивая ваш код, например при числовом вводе в TextBox. В процессе преобразования типа возможны исключения, что должно служить указанием на ошибку верификации для пользователя.

Если значение даже нельзя записать в свойство объекта источника, ввод явно недопустим. Если вы выбрали подключение ValidationRules, то они срабатывают в точке процесса, на которую указывает свойство ValidationStep, и могут возвращать ошибки верификации на основе логики, встроенной в них или вызываемой для них. Если аксессор set свойства объекта источника генерирует исключение, это почти всегда следует интерпретировать как ошибку при проверке — по аналогии со случаем преобразования типа.

Наконец, если вы реализуете IDataErrorInfo, то добавляете к объекту источника свойство-индексатор (indexer property), чтобы этот интерфейс вызывался для изменяемого свойства. Это позволит увидеть, не было ли ошибки при проверке, на основе строки, возвращаемой из этого интерфейса. Подробнее о каждом из этих механизмов я расскажу немного позже.

Когда должна происходить проверка — это еще одно решение, которое вы должны принять. Проверка происходит, когда Binding записывает данные в свойство нижележащего объекта источника; момент выполнения проверки указывается свойством UpdateSourceTrigger объекта Binding, которое устанавливается в PropertyChanged, как и в случае большинства других свойств. Для некоторых свойств, например TextBox.Text, значение изменяют на FocusChange, и тогда проверка происходит при потере фокуса ввода элементом управления, с помощью которого редактировались данные. Значение также можно установить в Explicit, указывающее, что проверка должна явно запускаться для данной привязки. В частности, это значение используется в BindingGroup, о котором мы поговорим позже.

При проверке, особенно в случае TextBox, вы обычно предпочтете как можно быстрее сообщать пользователю о выявленных ошибках. Для поддержки этого вы должны установить свойство UpdateSourceTrigger объекта Binding в PropertyChanged:

Text="{Binding Path=Activity.Description, UpdateSourceTrigger=PropertyChanged}

Однако на практике зачастую приходится использовать более одного механизма проверки. У каждого из них есть свои преимущества и недостатки в зависимости от того, какой тип ошибки при проверке вас интересует и где можно разместить логику проверки.

Бизнес-сценарий проверки

Чтобы сделать пример более конкретным, рассмотрим ситуацию с редактированием данных в контексте, довольно близком к реальному, и вы увидите, какую роль сможет сыграть в нем каждый из механизмов проверки. Эта ситуация и правила проверки основаны на реальном приложении, которое я написал для заказчика и в котором довольно простая форма требовала применения почти всех механизмов проверки из-за поддержки целого ряда бизнес-правил. Для его упрощенного варианта в этой статье я буду применять каждый из механизмов для демонстрации их использования, несмотря на то, что в данном случае задействовать их все явно не требуется.

Допустим, вам нужно написать приложение для поддержки технических специалистов, выезжающих по вызову клиентов к ним на дом. Для каждой операции, выполненной на выезде, специалисту нужно заполнить отчет, в котором указывается, что именно он сделал, и который связан с несколькими блоками данных. Объектная модель показана на рис. 3.

Figure 3 Object Model for the Sample Application
Рис. 3. Объектная модель приложения-примера

Основные блоки данных, заполняемые ими, — это объект Activity, в том числе свойства Title, ActivityDate, ActivityType (раскрывающийся список предопределенных типов операций) и Description. Им также нужно связать свою операцию с одним из трех вариантов. А именно: выбрать (1) Customer, для которого была выполнена работа, из списка клиентов, (2) Objective компании, к которому относится данная работа, из списка задач (objectives) или (3) вручную заполнить Reason, если данная работа не применима к Customer либо Objective.

Вот правила проверки, которые приложение должно вводить в действие:

  • Title и Description — обязательные поля;
  • ActivityDate должен содержать дату в диапазоне плюс-минус семь дней от текущей;
  • если выбран ActivityType Install, поле Inventory обязательно для заполнения и должно указывать детали, использованные специалистом. Элементы в поле Inventory нужно вводить как список, разделяемый запятыми; при этом ожидается, что элементы будут представлять собой номера деталей в соответствии с номенклатурой;
  • должен быть указан минимум один Customer, Objective или Reason.

Эти требования могут показаться весьма простыми, но последние два особенно сложны, так как они указывают на перекрестную зависимость между свойствами. Выполняемое приложение с данными, часть из которых неправильна (такие данные выделяются красным прямоугольником), показано на рис. 4.

Figure 4 A Dialog Showing ToolTips and Invalid Data
Рис. 4. Диалог, показывающий подсказки и неправильные данные

Проверка на основе исключений

Простейшая форма проверки — обрабатывать исключение, генерируемое в процессе присваивания значения целевому свойству, как ошибку при проверке. Исключение может вызываться в результате преобразования типа — еще до того, как Binding будет присваивать значение целевому свойству. Оно может неявно генерироваться в аксессоре set целевого свойства или возвращаться после вызова какого-либо бизнес-объекта, где исключение генерируется ниже по стеку.

Чтобы использовать этот механизм, вы просто устанавливаете свойство ValidatesOnExceptions объекта Binding в true:

Text="{Binding Path=Activity.Title, ValidatesOnExceptions=True}"

Когда исключение генерируется при попытке изменить свойство объекта источника (Activity.Title в данном случае), для соответствующего элемента управления будет установлена ошибка проверки. По умолчанию элемент управления с ошибкой проверки обводится красным прямоугольником (рис. 5).

Figure 5 A Validation Error
Рис. 5. Ошибка при проверке

Поскольку исключения могут происходить в процессе преобразования типов, вы должны устанавливать свойство ValidatesOnExceptions в объектах Bindings ввода всякий раз, когда есть хоть крошечный шанс неудачного завершения попытки преобразования типа, даже если поддерживающее свойство (backing property) просто присваивает значение переменной-члену.

Например, вы используете TextBox как элемент ввода для свойства DateTime. Если пользователь вводит строку, которую нельзя преобразовать, ValidatesOnExceptions — единственный способ, которым ваш Binding мог бы указать на ошибку, так как вызов до свойства объекта источника просто не дошел бы.

Если вам нужно делать что-то специфическое в представлении, в котором присутствуют неправильные данные, например отключать какую-то команду, то вы можете связать с элементом управления подключенное событие Validation.Error. Вам также понадобится установить свойство NotifyOnValidationError объекта Binding в true:

<TextBox Name="ageTextBox" 
  Text ="{Binding Path=Age, 
    ValidatesOnExceptions=True, 
    NotifyOnValidationError=True}" 
    Validation.Error="OnValidationError".../>

Проверка с использованием ValidationRule

В некоторых ситуациях вам может потребоваться проверка на уровне UI и более сложная логика определения корректности ввода. Рассмотрим в приложении-примере правило проверки для поля Inventory. Если данные введены, они должны быть списком номеров деталей, разделенным запятыми, и при этом соответствовать определенному шаблону. ValidationRule легко адаптируется под такой вариант. ValidationRule может вызвать string.Split, чтобы преобразовать ввод в строковый массив, а затем с помощью регулярного выражения проверить, отвечают ли индивидуальные части заданному шаблону. Для этого вы можете определить ValidationRule так, как показано на рис. 6.

Рис. 6. ValidationRule для проверки строкового массива

public class InventoryValidationRule : ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    if (InventoryPattern == null)
      return ValidationResult.ValidResult;

    if (!(value is string))
      return new ValidationResult(false, 
     "Inventory should be a comma separated list of model numbers as a string");

    string[] pieces = value.ToString().Split(‘,’);
    Regex m_RegEx = new Regex(InventoryPattern);

    foreach (string item in pieces) {
      Match match = m_RegEx.Match(item);
      if (match == null || match == Match.Empty)
        return new ValidationResult(
          false, "Invalid input format");
    }

    return ValidationResult.ValidResult;
  }

  public string InventoryPattern { get; set; }
}

Свойства ValidationRule можно задавать из XAML в момент использования, что дает дополнительную гибкость. Это правило проверки игнорирует значение, которые нельзя преобразовать в строковый массив. Если же правило может выполнить string.Split, то потом используется RegEx для проверки каждой строки из списка на соответствие шаблону, заданному через свойство InventoryPattern.

Если возвращается ValidationResult с флагом правильности, установленным в false, то предоставленное вами сообщение об ошибке можно показать через UI пользователю. Один из недостатков ValidationRules состоит в том, что вам нужно подключить расширенный элемент Binding в XAML, как показано ниже:

<TextBox Name="inventoryTextBox"...>
  <TextBox.Text>
    <Binding Path="Activity.Inventory" 
             ValidatesOnExceptions="True" 
             UpdateSourceTrigger="PropertyChanged" 
             ValidatesOnDataErrors="True">
      <Binding.ValidationRules>
        <local:InventoryValidationRule 
          InventoryPattern="^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

В этом примере мой Binding будет вызывать ошибки проверки, даже если происходит исключение. Дело в том, что свойство ValidatesOnExceptions устанавливается в true. Кроме того, я поддерживаю проверку через IDataErrorInfo, так как свойство ValidatesOnDataErrors тоже устанавливается в true (об этом чуть позже).

Если с одним свойством связано несколько ValidationRule, каждое из этих правил может задавать для свойства ValidationStep разные или одинаковые значения. Правила в одном ValidationStep оцениваются в порядке их объявления. Правила в предшествующих ValidationStep, очевидно, выполняются до правил в более поздних ValidationStep. Но вот что вовсе не очевидно, так это то, что, если ValidationRule возвращает ошибку, ни одно из последующих правил не оценивается. Поэтому первая же ошибка проверки будет единственной.

Проверка с применением IDataErrorInfo

Интерфейс IDataErrorInfo требует предоставлять в реализации одно свойство и один индексатор:

public interface IDataErrorInfo {
  string Error { get; }
  string this[string propertyName] { get; }
}

Свойство Error указывает ошибку для объекта в целом, а индексатор — ошибки на уровне его индивидуальных свойств. И свойство Error, и индексатор работают одинаково: возвращают ненулевую или непустую строку с ошибкой проверки. Кроме того, возвращаемая строка может быть задействована для отображения ошибки пользователю, как я покажу позднее.

Когда вы работаете с индивидуальными элементами управления, связанными с индивидуальными свойствами объекта — источника данных, самая важная часть интерфейса — индексатор. Свойство Error применяется только в ситуациях, где, например, объект отображается в DataGrid или в BindingGroup. В этом случае свойство Error указывает ошибку на уровне строки, а индексатор — ошибку на уровне ячейки.

Реализация IDataErrorInfo имеет один крупный недостаток: по своей природе индексатор требует написания огромной конструкции switch-case, где на каждое свойство в объекте требуется по одному блоку case, а вы должны проверять совпадения строк с образцами и возвращать строки с ошибками. Более того, ваша реализация IDataErrorInfo не вызывается, пока свойству объекта не присвоено значение. Если на INotifyPropertyChanged.PropertyChanged вашего объекта подписаны другие объекты, они будут уже уведомлены об изменении и могут начать работать, исходя из данных, которые ваша реализация IDataErrorInfo собирается объявить некорректными. Если это вызывает проблему в вашем приложении, вам придется генерировать исключения в аксессорах set свойств в случае некорректных данных.

Сильная сторона IDataErrorInfo в том, что он упрощает работу с перекрестно-связанными свойствами. Например, помимо использования ValidationRule для проверки формата ввода в поле Inventory, вспомните о требовании заполнения поля Inventory, когда в качестве ActivityType выбирается Install. У ValidationRule нет доступа к другим свойствам в объекте, связанном с данными. Он просто передает значение, присваиваемое свойству, к которому подключен Binding. Чтобы удовлетворить это требование, когда свойство ActivityType получает значение, вам нужно вызывать проверку свойства Inventory и сообщать о недопустимом результате, если в качестве ActivityType выбран Install, а значение Inventory пустое.

Для этого вам нужен IDataErrorInfo, так чтобы вы могли проверять и Inventory, и ActivityType при оценке Inventory:

public string this[string propertyName] {
  get { return IsValid(propertyName); }
}

private string IsValid(string propertyName) {
  switch (propertyName) {
    ...
    case "Inventory":
      if (ActivityType != null && 
        ActivityType.Name == "Install" &&  
        string.IsNullOrWhiteSpace(Inventory))
        return "Inventory expended must be entered for installs";
      break;
}

Кроме того, нужно получить Inventory Binding, чтобы запустить проверку, когда изменится значение свойства ActivityType. Обычно Binding запрашивает реализацию IDataErrorInfo или вызывает ValidationRules, только если это свойство изменяется в UI. В данном случае мне требуется инициировать повторную оценку Binding, даже если свойство Inventory не менялось, но изменилось связанное свойство ActivityType.

Существует два способа получения Inventory Binding для обновления после того, как изменяется значение свойства ActivityType. Первый и самый простой способ — публикация события PropertyChanged для Inventory при задании значения свойства ActivityType:

ActivityType _ActivityType;
public ActivityType ActivityType {
  get { return _ActivityType; }
  set { 
    if (value != _ActivityType) {
      _ActivityType = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("ActivityType"));
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Inventory"));
    }
  }
}

Это вызывает обновление и повторную оценку допустимости этого Binding.

Второй способ — подключение события Binding.SourceUpdated, связанного с ActivityType ComboBox или одним из его родительских элементов, и инициация обновления Binding из обработчика этого события в отделенном коде:

<ComboBox Name="activityTypeIdComboBox" 
  Binding.SourceUpdated="OnPropertySet"...

private void OnPropetySet(object sender, 
  DataTransferEventArgs e) {

  if (activityTypeIdComboBox == e.TargetObject) {
    inventoryTextBox.GetBindingExpression(
      TextBox.TextProperty).UpdateSource();
  }
}

Программный вызов UpdateSource, принадлежащего Binding, заставляет его записать текущее значение из связанного целевого элемента в свойство источника, запустив цепочку проверок так, будто пользователь только что отредактировал содержимое элемента управления.

Применение BindingGroup для перекрестно-связанных свойств

Функциональность BindingGroup была добавлена в Microsoft .NET Framework 3.5 SP1. BindingGroup специально разработан для выполнения проверки по целой группе привязок одновременно. Например, вы могли бы разрешить пользователю заполнять всю форму и ждать до тех пор, пока он не нажмет кнопку Submit или Save, после чего запускать проверку этой формы и сообщать сразу обо всех выявленных ошибках. В приложении-примере есть требование предоставлять минимум один Customer, Objective или Reason. BindingGroup также позволяет оценивать подмножество формы.

Чтобы задействовать BindingGroup, нужен набор элементов управления с обычными привязками, которые совместно используют общий элемент-предок (ancestor element). В приложении-примере Customer ComboBox, Objective ComboBox и Reason TextBox — все находятся в одном Grid. BindingGroup является свойством объекта FrameworkElement. У него есть свойство-набор ValidationRules, с помощью которого вы можете заполнять один или более объектов ValidationRule. В следующем фрагменте XAML показано подключение BindingGroup в приложении-примере:

<Grid>...
<Grid.BindingGroup>
  <BindingGroup>
    <BindingGroup.ValidationRules>
      <local:CustomerObjectiveOrReasonValidationRule 
        ValidationStep="UpdatedValue" 
        ValidatesOnTargetUpdated="True"/>
    </BindingGroup.ValidationRules>
  </BindingGroup>
</Grid.BindingGroup>
</Grid>

Здесь я добавил в набор экземпляр CustomerObjectiveOrReasonValidationRule. Свойство ValidationStep позволяет в какой-то мере контролировать значение, передаваемое в правило. UpdatedValue подразумевает использование значения, уже записанного в объект — источник данных. Вы также можете выбирать значения для ValidationStep, позволяющие использовать необработанный ввод, значение после преобразования типа или «фиксированное» («committed») значение, которое подразумевает реализацию интерфейса IEditableObject для транзакционного изменения свойств вашего объекта.

Флаг ValidatesOnTargetUpdated вызывает оценку правила всякий раз, когда целевому свойству присваивается значение через привязки. Сюда же относится и начальная установка свойства, поэтому вы немедленно получаете указания на ошибки проверки, если начальные данные недопустимы, а также когда пользователь изменяет значения в элементах управления, являющихся частью BindingGroup (и при этом допускает ошибки).

ValidationRule, подключенный к BindingGroup, работает немного иначе, чем ValidationRule, подключенный к единственной привязке (одному объекту Binding). На рис. 7 представлен ValidationRule, подключенный к BindingGroup, показанному в предыдущем примере кода.

Рис. 7. ValidationRule для BindingGroup

public class CustomerObjectiveOrReasonValidationRule : 
  ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    BindingGroup bindingGroup = value as BindingGroup;
    if (bindingGroup == null) 
      return new ValidationResult(false, 
        "CustomerObjectiveOrReasonValidationRule should only be used with a BindingGroup");

    if (bindingGroup.Items.Count == 1) {
      object item = bindingGroup.Items[0];
      ActivityEditorViewModel viewModel = 
        item as ActivityEditorViewModel;
      if (viewModel != null && viewModel.Activity != null && 
        !viewModel.Activity.CustomerObjectiveOrReasonEntered())
        return new ValidationResult(false, 
          "You must enter one of Customer, Objective, or Reason to a valid entry");
    }
    return ValidationResult.ValidResult;
  }
}

В ValidationRule, подключенном к единственному Binding, передается единственное значение из свойства источника данных, установленного как путь привязки. В случае применения BindingGroup в ValidationRule предается значение, которое является самим BindingGroup. Он содержит набор Items, заполняемый DataContext включающего элемента (containing element) — в данном случае Grid.

Для приложения-примера я использую шаблон MVVM, поэтому DataContext представления является самим ViewModel. Набор Items содержит лишь единственную ссылку на ViewModel. Из ViewModel я могу получить его свойство Activity. Класс Activity в данном случае имеет метод верификации, проверяющий, был ли введен хотя бы один Customer, Objective или Reason, и это избавляет меня от дублирования этой логики в ValidationRule.

Как и в случае других ValidationRule, о которых я рассказывал ранее, если вас устраивают переданные данные, вы возвращаете ValidationResult.ValidResult. В ином случае вы конструируете новый ValidationResult с флагом допустимости, установленным в false, и строковое сообщение с указанием на проблему, которое потом можно вывести на экран.

Однако установки флага ValidatesOnTargetUpdated недостаточно для автоматического срабатывания объектов ValidationRule. Концепции явного запуска проверки для целой группы элементов управления, обычно после нажатия на форме кнопки Submit или Save, удовлетворяет BindingGroup. В некоторых ситуациях пользователи не хотят, чтобы их отвлекали указаниями на ошибки проверки, пока они не закончат весь процесс редактирования, поэтому BindingGroup проектировали с учетом этого подхода.

В приложении-примере мне нужно обеспечивать немедленную обратную связь с пользователем по результатам проверки любого изменения, внесенного им в форму. Чтобы сделать это с помощью BindingGroup, пришлось бы подключать соответствующее событие изменения в каждом элементе ввода, входящем в группу, и уже из обработчика этих событий запускать оценку BindingGroup. В моем приложении это потребует подключения события ComboBox.SelectionChanged в двух ComboBox и события TextBox.TextChanged в TextBox. Все это можно свести к единственному методу-обработчику в отделенном коде:

private void OnCommitBindingGroup(
  object sender, EventArgs e) {

  CrossCoupledPropsGrid.BindingGroup.CommitEdit();
}

Заметьте, что на экране проверки по умолчанию выводится красный прямоугольник вокруг FrameworkElement, в котором размещается BindingGroup, такой как Grid в приложении-примере рис. 4. Это обозначение можно изменить, используя подключаемые свойства Validation.ValidationAdornerSite и Validation.ValidationAdornerSiteFor. По умолчанию индивидуальные элементы управления также обводятся красными границами, если при проверке их данных обнаружены ошибки. В приложении-примере я отключаю эти границы, присваивая null для ErrorTemplate через Styles.

При использовании BindingGroup в .NET Framework 3.5 SP1 вы можете столкнуться с проблемами корректного отображения ошибок проверки при начальной загрузке формы, даже если вы установили свойство ValidatesOnTargetUpdated объекта ValidationRule. Обходной путь, который я нашел, заключается в следующем: нужно слегка «покачать» одно из связанных свойств в BindingGroup. В приложении-примере я добавляю и удаляю в обработчике события Loaded пробел в конце любого текста, изначально отображаемого в TextBox:

string originalText = m_ProductTextBox.Text;
m_ProductTextBox.Text += " ";
m_ProductTextBox.Text = originalText;

Это вызывает срабатывание BindingGroup ValidationRules, так как одно из включенных свойств было изменено, а это приводит к проверке каждого Binding. Это поведение исправлено в .NET Framework 4.0, поэтому в ней такие ухищрения не нужны: просто устанавливайте свойство ValidatesOnTargetUpdated в true для правил проверки.

Отображение ошибок проверки

Как уже упоминалось, по умолчанию элемент управления с ошибкой проверки в WPF обводится красным прямоугольником. Вам скорее всего захочется изменить способ отображения ошибок. Более того, сообщение об ошибке, сопоставленное с ошибкой проверки, по умолчанию не выводится. Общее требование — показывать сообщение об ошибке в ToolTip, только когда имеются ошибки проверки. Настроить отображение ошибки проверки довольно легко, используя комбинацию Styles и набора подключаемых свойств, сопоставленных с процессом проверки.

Добавить ToolTip, отображающий текст об ошибке, — задача тривиальная. Вам нужно лишь определить Style, который будет применяться к элементу ввода и присваивать свойству ToolTip этого элемента управления текст ошибки проверки, если таковая будет обнаружена. При этом вам придется применять два подключаемых свойства: Validation.HasError и Validation.Errors. Style, ориентированный на тип TextBox и настраивающий значение ToolTip, показан ниже:

<Style TargetType="TextBox">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" 
             Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding 
            Path="(Validation.Errors).CurrentItem.ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}" />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>

Как видите, Style просто содержит триггер свойства (property trigger) для подключаемого свойства Validation.HasError. Свойство HasError будет установлено в true, когда Binding обновит свойство объекта источника и механизм проверки сгенерирует ошибку. Ошибка может исходить из исключения, ValidationRule или вызова IDataErrorInfo. После этого Style использует подключаемое свойство Validation.Errors, которое будет содержать набор строк ошибок, если ошибки при проверке имели место. Вы можете использовать свойство CurrentItem этого типа-набора, чтобы просто извлекать первую строку из набора. Или разработать такой объект, который был бы связан с этим набором через механизм привязки данных и отображал бы значение свойства ErrorContent для каждого элемента в списочном элементе управления.

Чтобы изменить исходное отображение ошибок проверки для какого-либо элемента управления, нужно присвоить подключаемому свойству Validation.ErrorTemplate новый шаблон для этого элемента, который будет настраивать его должным образом. В приложении-примере вместо отображения красных границ справа от каждого элемента управления с ошибкой появляется небольшой кружок с градиентной заливкой красным цветом. Для этого определяется шаблон элемента управления, который будет использоваться в качестве ErrorTemplate.

<ControlTemplate x:Key="InputErrorTemplate">
  <DockPanel>
    <Ellipse DockPanel.Dock="Right" Margin="2,0" 
             ToolTip="Contains invalid data"
             Width="10" Height="10">
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

Чтобы подключить этот шаблон к элементу управления, достаточно установить свойство Validation.ErrorTemplate для данного элемента управления, что вновь делается через Style:

<Style TargetType="TextBox">
  <Setter Property="Validation.ErrorTemplate" 
    Value="{StaticResource InputErrorTemplate}" />
  ...
</Style>

Заключение

В этой статье я показал, как использовать три WPF-механизма проверки привязок данных. Вы видели, как работать с исключениями, объектами ValidationRule и интерфейсом IDataErrorInfo для проверки одного свойства, а также свойств, правила проверки которых зависят от текущих значений других свойств элемента управления. Вы также узнали, как с помощью BindingGroup оценивать сразу несколько Binding и как настраивать отображение ошибок, выходя за рамки предлагаемого по умолчанию в WPF.

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

Брайен Нойз (Brian Noyes)  *— главный архитектор в IDesign (idesign.net), региональный директор корпорации Майкрософт и обладатель звания Microsoft MVP. Автор статей, часто выступает на Microsoft Tech·Ed, DevConnections, DevTeach и других конференциях по всему миру. С ним можно связаться через его блог на briannoyes.net. *

Выражаю благодарность за рецензирование статьи эксперту:  Сэм Бент (Sam Bent)