Проверка достоверности

91

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

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

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

К счастью, WPF предоставляет средство проверки достоверности, работающее подобно системе привязки данных. Проверка достоверности предлагает два дополнительных варианта выбора для перехвата неверных значений:

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

Однако некоторые разработчики пользуются обоими приемами. Они применяют проверку достоверности в объекте данных для защиты от небольшого набора фундаментальных ошибок и реализуют проверку достоверности привязки для перехвата более широкого диапазона ошибок пользовательского ввода.

Проверка достоверности применяется только тогда, когда значение из целевого элемента используется для обновления источника. Другими словами, в ситуациях с привязкой TwoWay или OneWayToSource.

Проверка достоверности в объекте данных

Некоторые разработчики встраивают проверку ошибок непосредственно в свои объекты данных. Например, ниже приведена модифицированная версия свойства CarTable.Cost, которая отклоняет отрицательные числа:

public Nullable<global::System.Double> Cost
{
            get
            {
                return _Cost;
            }
            set
            {
                if (value < 0)
                    throw new ArgumentException("Стоимость не может быть отрицательной");
                else
                {
                    OnCostChanging(value);
                    ReportPropertyChanging("Cost");
                    _Cost = StructuralObject.SetValidValue(value);
                    ReportPropertyChanged("Cost");
                    OnCostChanged();
                }
            }
}

Логика проверки достоверности, показанная в предыдущем примере, предотвращает отрицательные значения для цены, но не дает пользователю никакой информации о причинах проблемы. Как уже известно, WPF молча игнорирует ошибки привязки данных, которые происходят при установке и чтении свойств. В этом случае некорректное значение останется в текстовом поле — оно просто не появится в привязанном объекте данных. Чтобы изменить эту ситуацию, понадобится помощь класса ExceptionValidationRule, который рассматривается ниже.

Класс ExceptionValidationRule

Класс ExceptionValidationRule — это предварительно построенное правило проверки достоверности, которое заставляет WPF сообщать обо всех исключениях. Чтобы использовать ExceptionValidationRule, его понадобится добавить в коллекцию Binding.ValidationRules, как показано ниже:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1">
      <TextBox.Text>
           <Binding Path="Cost">
                 <Binding.ValidationRules>
                       <ExceptionValidationRule/>
                 </Binding.ValidationRules>
           </Binding>
      </TextBox.Text>
</TextBox>

Итак, что же произойдет, если проверка достоверности не прошла? Ошибки проверки достоверности записываются с использованием присоединенных свойств класса System.Windows.Controls.Validation. Для каждого нарушенного правила проверки достоверности WPF предпринимает описанные ниже шаги:

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

Индикация ошибки

Для корректной работы примера потребуется также отключить вызов исключений среды CLR (Debug --> Exceptions...).

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

Итак, в выражении привязки задайте свойство NotifyOnValidationError в True, которое будет создавать событие Validation.Error:

<Binding Path="Cost" NotifyOnValidationError="True">
   ...

Событие Error является маршрутизируемым и использует пузырьковое распространение, так что его можно обработать для многих элементов управления, присоединив обработчик событий к родительскому контейнеру, как показано ниже:

<Grid  Name="gridCarDetails" DataContext="{Binding ElementName=lstCars, Path=SelectedItem}"
                   Validation.Error="validationError">
       ...

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

private void validationError(object sender, ValidationErrorEventArgs e)
{
     if (e.Action == ValidationErrorEventAction.Added)
     {
           MessageBox.Show(e.Error.ErrorContent.ToString());
     }
}

Свойство ValidationErrorEventArgs.Error предоставляет объект ValidationError, соединяющий воедино несколько полезных деталей, в том числе исключение, вызвавшее проблему (Exception), нарушенное правило проверки достоверности (ValidationRule), ассоциированный объект Binding (BindinglnError) и любую специальную информацию, возвращенную объектом ValidationRule (ErrorContent).

Теперь при возникновении ошибки достоверности будет показано модальное окно с предупреждающим сообщением:

Специальные правила проверки достоверности

Подход с применением специального правила проверки достоверности подобен использованию специального конвертера. В этом случае определяется класс-наследник ValidationRule (из пространства имен System.Windows.Controls) и его метод Validate() переопределяется для выполнения требуемой проверки достоверности. При необходимости можно добавить свойства, принимающие другие детали, которые влияют на проверку достоверности (например, правило, определяющее, что текст может включать свойство CaseSensitive булевского типа).

Ниже приведен полный код правила проверки достоверности, ограничивающего значения некоторым диапазоном. По умолчанию минимум устанавливается в 100000, а максимум — в наибольшее число, которое умещается в тип данных Double:

public class PriceRule : ValidationRule
{
        private double min = 100000;
        private double max = Double.MaxValue;

        public double Min
        {
            get { return min; }
            set { min = value; }
        }

        public double Max
        {
            get { return max; }
            set { max = value; }
        }


        public override ValidationResult Validate(object value, System.Globalization.CultureInfo ci)
        {
            double price = 0;

            try
            {
                price = Double.Parse((string)value);
            }
            catch
            {
                return new ValidationResult(false, "Недопустимые символы.");
            }

            if ((price < Min) || (price > Max))
            {
                return new ValidationResult(false,
                  "Стоимость не входит в диапазон " + Min + " до " + Max + ".");
            }
            else
            {
                return new ValidationResult(true, null);
            }
        }
}

Как только правило проверки достоверности завершено, его можно применить к элементу, добавив в коллекцию Binding.ValidationRules. Ниже показан пример использования правила PriceRule с установкой значения Maximum в 20.000.000:

<Window x:Class="DataBinding.MainWindow"
        xmlns:myns="clr-namespace:DataBinding"
        ...
        >
   
   ...
   
<TextBox.Text>
      <Binding Path="Cost" NotifyOnValidationError="True">
              <Binding.ValidationRules>
                       <myns:PriceRule Max="20000000"/>
                       <ExceptionValidationRule/>
              </Binding.ValidationRules>
      </Binding>
</TextBox.Text>

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

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

Коллекция Binding.ValidationRules может принимать неограниченное количество правил. Когда значение фиксируется в источнике, WPF проверяет каждое правило проверки достоверности по порядку. (Вспомните, что значение в текстовом поле фиксируется в источнике, когда текстовое поле теряет фокус, если только в свойстве UpdateSourceTrigger не указано иначе.) Если все проверки достоверности прошли успешно, то WPF затем вызывает конвертер (если он есть) и применяет значение к источнику.

В случае проверки достоверности с помощью PriceRule поведение будет таким же, как и при использовании ExceptionValidationRule — текстовое поле помечается красным, устанавливаются свойства HasError и Error, генерируется событие Error. Чтобы снабдить пользователя каким-нибудь полезным откликом, потребуется добавить немного кода для настройки ErrorTemplate.

Отображение отличающегося индикатора ошибки

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

Шаблоны ошибок используют декоративный слой (adorner layer), который является слоем рисования, существующим прямо над обычным содержимым окна. Используя этот слой, можно добавлять визуальные украшения для сигнализации об ошибке, не подменяя шаблона элемента управления и не изменяя компоновки окна.

Стандартный шаблон ошибок для текстового поля работает, добавляя элемент Border красного цвета, который "парит" прямо над соответствующим текстовым полем (поле под ним остается неизменным).

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

<TextBox Margin="5" Grid.Row="2" Grid.Column="1">
                    <TextBox.Text>
                        <Binding Path="Cost" NotifyOnValidationError="True">
                            <Binding.ValidationRules>
                                <myns:PriceRule Max="20000000"/>
                                <ExceptionValidationRule/>
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                    <TextBox.Style>
                        <Style TargetType="{x:Type TextBox}">
                            <Setter Property="Validation.ErrorTemplate">
                                <Setter.Value>
                                    <ControlTemplate>
                                        <DockPanel LastChildFill="True">
                                            <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold"
                                                       Text="*"/>
                                            <Border BorderBrush="Green" BorderThickness="1">
                                                <AdornedElementPlaceholder Name="adornerPlaceholder"/>
                                            </Border>
                                        </DockPanel>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                            <Style.Triggers>
                                <Trigger Property="Validation.HasError" Value="True">
                                    <Setter Property="Foreground" Value="Red"/>
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </TextBox.Style>
</TextBox>

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

В результате рамка в этом примере размещается прямо поверх текстового поля, независимо от его размеров. Звездочка в этом примере помещается сразу справа:

Сигнализация об ошибке с помощью шаблона ошибок

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

Если нужно, чтобы шаблон ошибок появлялся над элементом (а не где-то возле него), можно поместить и содержимое, и AdornerElementPlaceholder в одну ячейку Grid. В качестве альтернативы можно вообще опустить AdornerElementPlaceholder, но тогда утрачивается возможность точного позиционирования содержимого по отношению к лежащему ниже элементу.

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

<DockPanel LastChildFill="True">
            <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="14" FontWeight="Bold"
                       ToolTip="{Binding ElementName=adornerPlaceholder, 
                                Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
            Text="*"/>
          ...

Теперь при перемещении курсора мыши над звездочкой будет отображаться сообщение об ошибке.

В качестве альтернативы можно отображать сообщение об ошибке в ToolTip из Border или самого TextBox, так что сообщение об ошибке появится, когда пользователь поместит курсор мыши над любой частью элемента управления. Этот трюк можно выполнить без помощи специального шаблона ошибок; все, что понадобится — это триггер на элементе управления TextBox, который отреагирует на присваивание Validation.HasError значения true и применит ToolTip с сообщением об ошибке. Вот пример:

...
   
  <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
                 <Setter Property="Foreground" Value="Red"/>
                 <Setter Property="ToolTip" 
                         Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
         </Trigger>
</Style.Triggers>
Вывод сообщения об ошибке проверки достоверности во всплывающей подсказке

Получение списка ошибок

В определенный момент может потребоваться список отложенных ошибок в текущем окне (или заданном контейнере из этого окна). Эта задача довольно проста, все, что нужно сделать — это пройтись по дереву элементов и проверить свойство Validation.HasError каждого элемента.

Код следующей процедуры демонстрирует пример, который целенаправленно ищет недопустимые данные в объектах TextBox. В нем используется рекурсия для прохода по всей иерархии элементов. При этом информация об ошибках накапливается в единственном сообщении, которое отображается пользователю:

private void cmdGetExceptions_Click(object sender, RoutedEventArgs e)
{
            StringBuilder sb = new StringBuilder();
            GetErrors(sb, gridCarDetails);
            string message = sb.ToString();
            if (message != "") MessageBox.Show(message);
}

private void GetErrors(StringBuilder sb, DependencyObject obj)
{
            foreach (object child in LogicalTreeHelper.GetChildren(obj))
            {
                TextBox element = child as TextBox;
                if (element == null) continue;

                if (Validation.GetHasError(element))
                {
                    sb.Append(element.Text + " найдена ошибка:\r\n");
                    foreach (ValidationError error in Validation.GetErrors(element))
                    {
                        sb.Append("  " + error.ErrorContent.ToString());
                        sb.Append("\r\n");
                    }
                }

                GetErrors(sb, element);
            }
}
Поиск множества ошибок
Пройди тесты
Лучший чат для C# программистов