Руководство для начинающих. Часть 5

Предисловие и благодарности

Я программист .NET. Я много программирую на VB.NET и C#, ASP.NET/Winforms/WPF/WCF Flash Silverlight. Но когда начал писать данные статьи, я, естественно, выбрал мой любимый язык — C#. Через некоторое время я получил сообщение от одного человека с просьбой публиковать исходный код на VB.NET и C# в статьях этой серии. Я ответил, что у меня нет времени. И тогда этот человек, Роберт Рэнк (Robert Ranck), вызвался помочь с преобразованием моих исходных проектов на C# в VB.NET.

За этот и последующие проекты VB.NET следует благодарить Роберта Рэнка. Спасибо, Роберт! Ваше участие, несомненно, сделает эту серию более доступной для всех разработчиков .NET.

Также я хотел бы выразить благодарность Карлу Шифлету (Karl Shifflett) (плодовитый автор статей и блогов, также известный под псевдонимом Molenator) за его ответы на мои глупые вопросы по VB.NET. Замечу, что Карл недавно приступил к написанию серии более продвинутых статей по WPF (примеры в которых будут пока на VB.NET, но, надеюсь, появятся и на C#). У Карла должна получиться отличная серия статей, и я прошу всех поддержать его работу. Непросто заставить себя писать всю серию на одном языке, не говоря уже о двух. Первую статью Карла можно прочитать здесь. Лично мне она нравится.

Введение

Это моя пятая статья из серии статей о WPF для начинающих. На этот раз мы поговорим о привязке данных. А вот предполагаемое содержание этой серии:

В этой статье я планирую кратко остановиться на следующих вопросах:

  • Основная идея привязки данных
  • Свойство DataContext
  • Основные понятия привязки данных
  • Синтаксис привязки данных
  • Привязка к элементам интерфейса пользователя
  • Привязка к xml
  • Привязка к коллекции
  • Преобразователи значений привязки данных
  • Проверка привязки данных

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

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

На самом деле идея привязки данных не нова (нов способ их реализации в WPF), с некоторых пор привязки уже используются в ASP.NET и Winforms. Платформа WPF заимствовала лучшее из обеих платформ, что позволило создать действительно хорошую систему привязки данных. Но что собой представляют сами привязки?

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

Вот типичная схема привязки:

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

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

Это очень упрощенная версия того, что происходит при привязке данных.

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

Свойство DataContext

Прежде чем переходить к подробному изучению привязок данных, следует сказать о свойстве DataContext, присущем каждому элементу FrameworkElement. Свойство DataContext позволяет элементам наследовать от своих родительских элементов информацию об источнике данных привязки, а также другие параметры привязки, например путь. Свойству DataContext можно присвоить непосредственно объект CLR с привязками для вычисления свойств такого объекта. Либо можно присвоить контекст данных объекту DataSourceProvider.

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

В XAML свойство DataContext обычно задается в объявлении Binding. Можно использовать синтаксис элемента свойства или синтаксис атрибута. Обычно это делается так.

<Window.Resources>

  <src:LeagueList x:Key="MyList" />

...

...

</Window.Resources>

...

...

<DockPanel DataContext="{Binding Source={StaticResource MyList}}">

Задать свойство DataContext можно и в коде, используя выражения следующего вида: <элемент>.DataContext = <значение>.

Стоит рассмотреть ситуацию, когда для некоторого объекта, наследующего свойство DataContextродительского объекта, не указаны поля, которые будут использоваться для привязки, например:

<MenuItem Tag="{Binding}">

Это значит, что весь объект, использующий свойство DataContext родительского объекта, будет присвоен свойству Tag.

Основные понятия привязки данных

Прежде чем переходить к рассмотрению привязки данных, необходимо остановиться на нескольких важных моментах:

  • направление потока данных;
  • что инициирует обновление источника.

Итак, приступим.

Направление потока данных

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

  • Привязка OneWayавтоматически обновляет целевое свойство при изменении исходного, но изменения целевого свойства не передаются обратно исходному. Этот тип привязки оптимален, если связываемый элемент управления неявно доступен только для чтения.
  • Привязка TwoWayавтоматически обновляет целевое свойство при изменении исходного и наоборот. Этот тип привязки подходит для редактируемых форм или других полностью интерактивных сценариев пользовательского интерфейса.
  • Привязка OneTimeобеспечивает инициализацию целевого свойства значением исходного, но последующие изменения не передаются. Это означает, что изменение контекста данных или объекта в контексте данных не отражается в целевом свойстве. Такой тип привязки подходит для данных, в которых применяется снимок текущего состояния, или для действительно статических данных.
  • Привязка OneWayToSourceпротивоположна привязке OneWay. Она обновляет исходное свойство при изменении целевого.
  • С привязкой Default целевое свойство по умолчанию использует значение Mode.

Для задания направления потока данных используется свойство Binding.Mode. Чтобы обнаруживать изменения исходного свойства в односторонних и двусторонних привязках, в источнике должен быть реализован подходящий механизм уведомления об изменении свойств, например интерфейс INotifyPropertyChanged. Пример см. в разделе «Практическое руководство. Реализация уведомления об изменении свойства».

Уведомление об изменении — не менее важная тема, так что давайте рассмотрим ее прямо сейчас. Посмотрим на пример использования интерфейса INotifyPropertyChanged.

Для автоматического отражения динамических изменений источника привязки в ее целевых свойствах OneWay или TwoWay в классе должен быть механизм уведомления об изменениях свойства. Для этого используется интерфейс INotifyPropertyChanged.

using System.ComponentModel;

namespace SDKSample

{

  // Этот класс реализует интерфейс INotifyPropertyChanged

  // для поддержки привязок one-way и two-way

  // (в которых элемент управления обновляется при динамическом изменении

  // источника)

  public class Person : INotifyPropertyChanged

  {

      private string name;

      // Объявление события

      public event PropertyChangedEventHandler PropertyChanged;

      public Person()

      {

      }

      public Person(string value)

      {

          this.name = value;

      }

      public string PersonName

      {

          get { return name; }

          set

          {

              name = value;

              // Вызов OnPropertyChanged при обновлении свойства

              OnPropertyChanged("PersonName");

          }

      }

      // Создание метода OnPropertyChanged для порождения события

      protected void OnPropertyChanged(string name)

      {

          PropertyChangedEventHandler handler = PropertyChanged;

          if (handler != null)

          {

              handler(this, new PropertyChangedEventArgs(name));

          }

      }

  }

}

А здесь приводится версия VB.NET.

Imports System.ComponentModel

' Этот класс реализует интерфейс INotifyPropertyChanged

' для поддержки привязок one-way и two-way

' (в которых элемент управления обновляется при динамическом изменении

' источника)

Public Class Person

    Implements INotifyPropertyChanged

    Private personName As String

    Sub New()

    End Sub

    Sub New(ByVal Name As String)

        Me.personName = Name

    End Sub

    ' Объявление события

    Public Event PropertyChanged As PropertyChangedEventHandler

        Implements INotifyPropertyChanged.PropertyChanged

    Public Property Name() As String

        Get

            Return personName

        End Get

        Set(ByVal value As String)

            personName = value

            ' Вызов OnPropertyChanged при обновлении свойства

            OnPropertyChanged("Name")

        End Set

    End Property

    ' Создание метода OnPropertyChanged для порождения события

    Protected Sub OnPropertyChanged(ByVal name As String)

        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(name))

    End Sub

End Class

Чтобы реализовать интерфейс INotifyPropertyChanged, нужно объявить событие PropertyChanged и создать методOnPropertyChanged. А затем для каждого свойства, требующего уведомления об изменении, вызывать метод OnPropertyChanged при каждом обновлении свойства.

Поверьте, интерфейс INotifyPropertyChanged критически важен, изучите его как следует. Особенно если вы планируете использовать привязки в приложениях WPF.

Что инициирует обновление источника

Привязки типа TwoWay и OneWayToSource прослушивают изменения целевого свойства и передают их в исходное. Это называется обновлением источника. Например, может потребоваться, чтобы изменение текста в элементе TextBox приводило к изменению базового исходного значения. Как говорилось в предыдущем разделе, направление потока данных определяется значением свойства Binding.Mode привязки.

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

В следующей таблице приведены примеры сценариев для каждого значения Binding.UpdateSourceTrigger. Для примера используется элемент TextBox.

Значение UpdateSourceTrigger Когда обновляется исходное значение
LostFocus (значение по умолчанию для TextBox.Text) При потере фокуса элементом TextBox
PropertyChanged При вводе в элементе TextBox
Explicit При вызове приложением метода UpdateSource

Синтаксис привязки данных

В классе Binding могут использоваться различные свойства. У меня нет времени рассмотреть их все, но я попытаюсь пройтись по самым типичным элементам синтаксиса. Чаще всего привязки задаются в коде XAML, поэтому я сосредоточусь на синтаксисе XAML. Однако следует отметить, что все, что можно сделать в XAML, также можно реализовать и в коде программной части на C# или VB.NET.

Так что давайте изучим базовый синтаксис (более сложные вопросы оставим для следующих разделов).

Самая простая форма привязки — это привязка к значению существующего элемента (она также будет более подробно рассмотрена ниже). Я хотел познакомить вас с синтаксисом и вернуться назад, чтобы показать, что делать со свойствами Binding.Mode и Binding.UpdateSourceTrigger.

Это, вероятно, одна из самых простых привязок. В этом примере используются две кнопки. Свойство Background первой кнопки (btnSource) обладает значением Yellow. Вторая кнопка использует первую кнопку (btnSource) в качестве источника привязки, при этом значение Background первой кнопки (btnSource) используется для задания свойства Background второй кнопки.

<Button x:Name="btnSource" Background="Yellow" Width="150" Height="30">Yellow BUtton</Button>

<Button Background="{Binding ElementName=btnSource, Path=Background}"

          Width="150" Height="30">I am bound to be Yellow Background</Button>

Предельно просто. А теперь давайте посмотрим, как использовать свойства Binding.Mode и Binding.UpdateSourceTrigger в синтаксисе привязки.

И тут все довольно просто. Мы просто добавляем в выражение привязки дополнительное свойство и его требуемое значение.

<TextBox x:Name="txtSource" Width="150" Height="30"/>

<TextBox Width="150" Height="30" Text="{Binding ElementName=txtSource,

    Path=Text, Mode=TwoWay, UpdateSourceTrigger=LostFocus }"/>

Важное примечание

Помните, в части 2 я говорил, что привязка — это расширение разметки? Именно поэтому анализатор XAML знает, как обрабатывать разделы { }. Но это фактически всего лишь собирательное свойство, которое (при желании) можно выразить с помощью более подробного синтаксиса, приведенного ниже.

<Button Margin="10,0,0,0" Content="Im bound to btnSource, using long Binding syntax">

    <Button.Background>

        <Binding ElementName="btnSource" Path="Background"/>

    </Button.Background>

</Button>

Вы можете выбрать синтаксис, который вам больше нравится. Лично я предпочитаю синтаксис { }, однако при использовании такого синтаксиса функция Intellisense в Visual Studio не работает.

Привязка к элементам интерфейса пользователя

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

  1. Какое свойство требуется привязывать?
  2. К какому свойству требуется выполнить привязку?
  3. Должна ли это быть привязка типа OneWay, TwoWay и т. д.? Если нужна привязка типа TwoWay или OneWayToSource, является ли исходное свойство свойством зависимости? Оно должно быть таковым, чтобы передавать привязку типа TwoWay или OneWayToSource.

Когда вы определились с этим, все становится просто как дважды два. В состав демонстрационного решения входит проект BindingToUIElements, который при выполнении выглядит так:

В этом простом демонстрационном приложении иллюстрируются три различные привязки. Давайте кратко обсудим каждую.

1. Простая привязка к элементу (режим по умолчанию)

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

Код выглядит так.

<!-- Простая привязка к элементу-->

<Label Content="Simple Element Binding" Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

<StackPanel Orientation="Horizontal" Margin="10,10,10,10" Background="Gainsboro">

    <Label Content="Simple Element Binding"/>

    <Button x:Name="btnSource" Margin="10,0,0,0" Background="Pink" Content="Im btnSource"/>

    <Button Margin="10,0,0,0" Background="{Binding ElementName=btnSource,

        Path=Background}" Content="Im bound to btnSource"/>

    <Button Margin="10,0,0,0" Content="Im bound to btnSource, using long Binding syntax">

        <Button.Background>

            <Binding ElementName="btnSource" Path="Background"/>

        </Button.Background>

    </Button>

</StackPanel>

2. Более сложная привязка (режим по умолчанию)

В этом простом примере в качестве источника привязки используется свойство SelectedItem.Contentэлемента ComboBox. Значение свойства Background элемента Buttonизменяется в зависимости от значения свойства SelectedItem.Contentэлемента ComboBox.

Код выглядит так.

<Label Content="More Elaborate Binding" Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

<StackPanel Orientation="Horizontal" Margin="10,10,10,10" Background="Gainsboro">

    <Label Content="Choose a color"/>

    <ComboBox Name="cmbColor" SelectedIndex="0">

        <ComboBoxItem>Green</ComboBoxItem>

        <ComboBoxItem>Blue</ComboBoxItem>

        <ComboBoxItem>Red</ComboBoxItem>

    </ComboBox>

    <Button Margin="10,0,0,0" Background="{Binding ElementName=cmbColor,

        Path=SelectedItem.Content}" Content="Im bound to btnSource"/>

</StackPanel>

3. Привязка TwoWay с использованиемUpdateSourceTrigger

В этом простом примере используются два элемента TextBox. Свойству Binding.Mode присвоено значение TwoWay, а свойству Binding.UpdateSourceTrigger — событие PropertyChanged. Это означает, что источник привязки будет обновляться при изменении значения второго элемента TextBox.

Код выглядит так.

<!-- Использование UpdateSourceTrigger/Mode-->

<Label Content="Using UpdateSourceTrigger/Mode" Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

<StackPanel Orientation="Horizontal" Margin="10,10,10,10" Background="Gainsboro">

    <TextBlock TextWrapping="Wrap"

        Text="This uses TwoWay Binding and UpdateSourceTrigger=PropertyChanged.

        Type in one textbox then the other,and see them update each other" Width="400"/>

    <TextBox x:Name="txtSource" Width="50" Height="25" Margin="5,0,0,0"/>

    <TextBox Width="50" Height="25" Margin="5,0,0,0"

            Text="{Binding ElementName=txtSource,

            Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged }"/>

</StackPanel>

Привязка к XML

Язык XML сегодня широко используется для хранения сведений о конфигурации и обмена данными и даже для разработки интерфейса пользователя. Не забывайте, XAML — производный язык XML.

Но это не все, для чего можно использовать данные XML. К ним еще можно выполнять привязку. В XAML сделать это довольно просто: либо код XAML может содержать данные XML (что, впрочем, кажется не слишком нормальным), либо можно использовать внешние файлы XML.

В любом случае обычно используется объект XmlDataProvider в XAML или коде. Как я уже говорил, большая часть привязок будет создаваться в XAML.

В состав демонстрационного решения входит проект BindingToXML, который при выполнении выглядит так:

В двух верхних областях демонстрационного приложения используются данные XML, содержащиеся в коде XAML, а в двух нижних областях — внешний файл XML.

XmlDataProvider с использованием данных, содержащихся в коде XAML (встроенных данных XML)

Все данные XML могут находиться в файле XAML и использоваться как источник привязки. Рассмотрим эту ситуацию.

<Window x:Class="BindingToXML.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    WindowStartupLocation="CenterScreen"

    Title="Window1" Height="800" Width="800">

    <Window.Resources>

        <!-- встроенные данные XML -->

        <XmlDataProvider x:Key="xmlData" XPath="Films">

            <x:XData>

                <Films >

                    <Film Type="Horror" Year="1987">

                        <Director>Sam Raimi</Director>

                        <Title>Evil Dead II</Title>

                    </Film>

                    <Film Type="Murder" Year="1991">

                        <Director>Jonathan Demme</Director>

                        <Title>Silence Of The Lambs</Title>

                    </Film>

                    <Film Type="Sc" Year="1979">

                        <Director>Ridley Scott</Director>

                        <Title>Alien</Title>

                    </Film>

                </Films>

            </x:XData>

        </XmlDataProvider>

    </Window.Resources>

    <ScrollViewer>

        <StackPanel Orientation="Vertical">

            <!--Простая привязка XPath с использованием встроенных данных XML -->

            <Label Content="Show all Films (using inline XML data)"

                    Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10"

                    Background="Gainsboro">

                <ListBox ItemsSource="{Binding Source={StaticResource xmlData},

                    XPath=Film/Title}"/>

            </StackPanel>

            <!-- Более сложная привязка XPath с использованием встроенных данных XML -->

            <Label Content="Show Only Films After 1991 (using inline XML data)"

                    Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10"

                    Background="Gainsboro">

                <ListBox ItemsSource="{Binding Source={StaticResource xmlData},

                    XPath=*[@Year>\=1991]}"/>

            </StackPanel>

           </StackPanel>

    </ScrollViewer>

</Window>

В этом примере поставщик XmlDataProviderиспользует встроенный (в код XAML) набор данных XML. И этот объект XmlDataProviderиспользуется как источник привязки для элемента ListBox. В первом элементе ListBox показаны все жанры и названия фильмов, поскольку выбирается весь набор узлов Film/Title с использованием выражения Binding.XPath=Film/Title, то есть отображаются все названия.

Второй элемент ListBox более разборчив. В нем для обхода оси атрибутов используются запросы XPath и выбираются только те узлы, у которых Year > 1991, поэтому возвращается меньше узлов.

XmlDataProvider с использованием внешнего файла XML

Как я уже упоминал, с объектом XmlDataProvider чаще используются внешние файлы XML. Для этого свойству Source объекта XmlDataProviderприсваивается имя внешнего файла XML.

<XmlDataProvider x:Key="xmlDataSeperateFile" XPath="Resteraunts" Source="XMLFile1.xml">

</XmlDataProvider>

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

<ListBox ItemsSource="{Binding Source={StaticResource xmlDataSeperateFile}, XPath=*[@Type\=\'Mexican\']}"/>

Ниже приведен весь пример.

<Window x:Class="BindingToXML.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    WindowStartupLocation="CenterScreen"

    Title="Window1" Height="800" Width="800">

    <Window.Resources>

        <!-- внешние данные XML -->

        <XmlDataProvider x:Key="xmlDataSeperateFile" XPath="Resteraunts"

            Source="XMLFile1.xml">

        </XmlDataProvider>

    </Window.Resources>

    <ScrollViewer>

        <StackPanel Orientation="Vertical">

              <!-- Простая привязка XPath с использованием отдельных данных XML -->

            <Label Content="Show all Resteraunts (using seperate XML data)"

                Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10"

                    Background="Gainsboro">

                <ListBox ItemsSource="{Binding Source=

                    {StaticResource xmlDataSeperateFile},

                    XPath=Resteraunt/Name}"/>

            </StackPanel>

            <!-- Более сложная привязка XPath с использованием отдельных данных XML -->

            <Label Content="Show Only Mexican Resteraunts (using inline XML data)"

                    Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10"

                    Background="Gainsboro">

                <ListBox ItemsSource="{Binding Source={StaticResource xmlDataSeperateFile},

                    XPath=*[@Type\=\'Mexican\']}"/>

            </StackPanel>

        </StackPanel>

    </ScrollViewer>

</Window>

А здесь соответствующий файл XML (на случай, если вы заинтересуетесь его структурой).

<?xml version="1.0" encoding="utf-8" ?>

<Resteraunts >

  <Resteraunt Type="Meat">

    <Name>The MeatHouse</Name>

    <Phone>01237 78516</Phone>

    <email>yada@here.com</email>

  </Resteraunt>

  <Resteraunt Type="Veggie">

    <Name>VegHead</Name>

    <Phone>99999</Phone>

    <email>mmm-vegies@here.com</email>

  </Resteraunt>

  <Resteraunt Type="Mexican">

    <Name>Mexican't (El Mariachi)</Name>

    <Phone>464654654</Phone>

    <email>mex@here.com</email>

  </Resteraunt>

</Resteraunts>

ПРИМЕЧАНИЕ

Вы легко сможете использовать XML для определения значений привязки, но обновлять данные XML, просто присваивая свойству Binding.Modeзначение TwoWay, вам не удастся. Метод не сработает потому, что привязка данных XML — простая односторонняя и не обновляемая.

Привязка к XLINQ

Я не буду останавливаться на этом вопросе. В блоге Беатриц Коста (Beatriz Costa), известной также под псевдонимом The Binding Queen, есть хорошая запись, если вам интересно.

Привязка к коллекции

Как правило, на некотором этапе разработки приложения WPF выполняется привязка к целой коллекции. Сделать это в WPF можно очень просто. Сделать это можно по-разному. Я кратко опишу два способа, но их, естественно, больше. Просто мне нравятся именно эти.

Во-первых, НИКОГДА не нужно забывать об уведомлениях об изменениях. Вспомните, мы реализовали такие уведомления для отдельных классов с помощью интерфейса INotifyPropertyChanged. Но что делать с коллекциями, содержащими объекты?

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

В состав демонстрационного решения входит проект BindingToCollections, который при выполнении выглядит так:

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

Привязка к коллекции в коде программной части

Свойство ItemSource первого списка ListBox задается в коде так.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

using System.Collections.ObjectModel;

namespace BindingToCollections

{

    /// <summary>

    /// Логика взаимодействия для Window1.xaml

    /// </summary>

    public partial class Window1 : Window

    {

        private People people = new People();

        public Window1()

        {

            InitializeComponent();

            people.Add(new Person { PersonName = "Judge Mental" });

            people.Add(new Person { PersonName = "Office Rocker" });

            people.Add(new Person { PersonName = "Sir Real" });

            this.lstBox1.ItemsSource = people;

        }

    }

}

И на VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Data

Imports System.Windows.Documents

Imports System.Windows.Input

Imports System.Windows.Media

Imports System.Windows.Media.Imaging

Imports System.Windows.Navigation

Imports System.Windows.Shapes

Imports System.Collections.ObjectModel

    ''' <summary>

    ''' Логика взаимодействия для Window1.xaml

    ''' </summary>

    Partial Public Class Window1

        Inherits Window

        Private people As New People()

        Public Sub New()

        InitializeComponent()

        Dim _Person As New Person

        _Person.PersonName = "Judge Mental"

        people.Add(_Person)

        _Person.PersonName = "Office Rocker"

        people.Add(_Person)

        _Person.PersonName = "Sir Real"

        people.Add(_Person)

        lstBox1.ItemsSource = people

        End Sub

    End Class

Код XAML, соответствующий этой привязке, выглядит так.

<Window x:Class="BindingToCollections.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:BindingToCollections"

    WindowStartupLocation="CenterScreen"

    Title="Window1" Height="800" Width="800">

    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">

        <StackPanel Orientation="Vertical">

            <!-- ListBox Source задается в коде программной части-->

            <StackPanel Orientation="Vertical">

                <Label Content="ListBox Source Set In Code Behind"

                    Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

                <ListBox x:Name="lstBox1"/>

            </StackPanel>

        </StackPanel>

    </ScrollViewer>

</Window>

ПривязкакколлекциивкодеXAML

Свойство ItemSourceвторого списка ListBox задается в коде XAML так.

<Window x:Class="BindingToCollections.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:BindingToCollections"

    WindowStartupLocation="CenterScreen"

    Title="Window1" Height="800" Width="800">

    <Window.Resources>

        <local:People x:Key="people">

            <local:Person PersonName="Freddy Star"/>

            <local:Person PersonName="Stick Head"/>

        </local:People>

    </Window.Resources>

    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">

        <StackPanel Orientation="Vertical">

            <!-- ListBox Source задается с использованием ресурсов -->

            <StackPanel Orientation="Vertical">

                <Label Content="ListBox Source Set By Using Resources"

                    Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

                <ListBox x:Name="lstBox2"

                    ItemsSource="{Binding Source={StaticResource people}}"/>

            </StackPanel>

        </StackPanel>

    </ScrollViewer>

</Window>

Как видно из кода, экземпляр объекта People находится непосредственно в разделе ресурсов XAML, а у объявленного объекта People есть несколько дочерних объектов типа Person. Все это достигается благодаря анализатору XAML, который знает о том, что дочерние объекты следует добавлять с помощью метода Add() объекта ObserverableCollection.

Все эти примеры приведены исключительно для демонстрации привязки к коллекциям. В продуктивной системе коллекция может относиться к уровню BAL или входить в модель в схеме MVC/MVVM. Мне нужно было быстро и просто донести свою мысль.

И еще один момент, на который я хотел бы обратить внимание, — обработка элементов в списке ListBox. Строка BindingToCollection.Persons просто отображается в виде обычного текста.

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

ПРИМЕЧАНИЕ

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

Преобразователи значений привязки данных

Представьте, что значение связанных данных нужно форматировать, например использовать короткий формат даты вместо длинного (до этого мы просто использовали необработанное значение привязки). На первый взгляд невозможная задача: связанное значение должно содержать именно то, что нужно отобразить. К счастью, в WPF предусмотрен один прием. Можно использовать класс, который реализует интерфейс IValueConverter, предоставляющий новое значение привязки.

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

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

Преобразователи значений находятся между исходным значением и привязанным целевым свойством. Их единственное назначение — получать значение и предоставлять другое значение.

Интерфейс IValueConverter содержит следующие методы, которые необходимо реализовать.

object Convert(object value, Type targetType, object parameter, CultureInfo culture)

Этот метод используется для преобразования исходного объекта в новое значение, которое будет использоваться для целевого свойства.

object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

Этот метод используется для преобразования целевого объекта в исходный. Он применяется, если свойству привязки Binding.Mode присвоено значение TwoWay или OneWayToSource. Во всех остальных случаях он просто создает исключение.

Посмотрим, как можно использовать эти преобразователи значений в коде. Все очень просто. Мы используем обычное выражение Binding и указываем используемый преобразователь в свойстве Binding.Converter. Рассмотрим пример.

В состав демонстрационного решения входит проект ValueConverters, который при выполнении выглядит так:

В этом небольшом примере используются два преобразователя значений. Первый преобразует название цвета в объект Brush, который используется для раскраски объекта Rectangle. Второй использует объект DataContext, заданный явно в коде программной части, а свойству DataContext двух объектов Label присваивается новый объект DataContext. Посмотрим код.

Сначала XAML.

<Window x:Class="ValueConverters.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:ValueConverters"

    WindowStartupLocation="CenterScreen"

    Title="Window1" Height="800" Width="800">

    <Window.Resources>

        <local:WordToColorConverter x:Key="wordToColorConv"/>

        <local:DateConverter x:Key="dateConv"/>

    </Window.Resources>

    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">

        <StackPanel Orientation="Vertical">

            <!--Использование преобразователя значения для преобразования текста в цвет -->

            <Label Content="Using Vaue Converter To Convert Text To Fill Color"

                    Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10" Background="Gainsboro">

                <TextBlock TextWrapping="Wrap" Text="Using Vaue Converter. Type 'hot' or 'cold'

                        into the textbox and watch the rectangle change color" Width="400"/>

                <TextBox x:Name="txtSource" Width="50" Height="25" Margin="5,0,0,0"/>

                <Rectangle Width="50" Height="25" Margin="5,0,0,0"

                    Fill="{Binding ElementName=txtSource, Path=Text,

                    Converter={StaticResource wordToColorConv}}" />

            </StackPanel>

            <!-- Использование преобразователя значения для преобразования даты в короткий формат -->

            <Label Content="Using Vaue Converter To Convert Date To Short Date"

                    Margin="5,0,0,0" FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Vertical" Margin="10,10,10,10" Background="Gainsboro">

                <StackPanel Orientation="Horizontal">

                    <Label Content="LongDate"/>

                    <Label x:Name="lblLongDate" Content="{Binding Path=Now}"/>

                </StackPanel>

                <StackPanel Orientation="Horizontal">

                    <Label Content="ShortDate thanks to value converter"/>

                    <Label x:Name="lblShortDate" Content="{Binding Path=Now,

                        Converter={StaticResource dateConv}}"/>

                </StackPanel>

            </StackPanel>

        </StackPanel>

    </ScrollViewer>

</Window>

А здесь код программной части для этого окна.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

namespace ValueConverters

{

    /// <summary>

    /// Логика взаимодействия для Window1.xaml

    /// </summary>

    public partial class Window1 : Window

    {

        public Window1()

        {

            InitializeComponent();

            lblLongDate.DataContext = new DateTime();

            lblShortDate.DataContext = new DateTime();

        }

    }

}

И, наконец, преобразователи.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Data;

using System.Windows.Media;

using System.Globalization;

namespace ValueConverters

{

    [ValueConversion(typeof(String), typeof(SolidColorBrush))]

    public class WordToColorConverter : IValueConverter

    {

        public object Convert(object value, Type targetType,

            object parameter, CultureInfo culture)

        {

            string boundWord = value as string;

            SolidColorBrush returnBrush = null;

            switch (boundWord.ToLower().Trim())

            {

                case "hot":

                    returnBrush = new SolidColorBrush(Colors.Red);

                    break;

                case "cold":

                    returnBrush = new SolidColorBrush(Colors.Green);

                    break;

                default:

                    returnBrush = new SolidColorBrush(Colors.Yellow);

                    break;

            }

            return returnBrush;

        }

        public object ConvertBack(object value, Type targetType,

            object parameter, CultureInfo culture)

        {

            throw new Exception("Cant convert back");

        }

    }

}

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Data;

using System.Windows.Media;

using System.Globalization;

namespace ValueConverters

{

    [ValueConversion(typeof(DateTime), typeof(String))]

    public class DateConverter : IValueConverter

    {

        public object Convert(object value, Type targetType,

            object parameter, CultureInfo culture)

        {

            DateTime date = (DateTime)value;

            return date.ToShortDateString();

        }

        public object ConvertBack(object value, Type targetType,

            object parameter, CultureInfo culture)

        {

            string strValue = value.ToString();

            DateTime resultDateTime;

            if (DateTime.TryParse(strValue, out resultDateTime))

            {

                return resultDateTime;

            }

            return value;

        }

    }

}

А здесь приводится версия VB.NET для кода программной части.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Data

Imports System.Windows.Documents

Imports System.Windows.Input

Imports System.Windows.Media

Imports System.Windows.Media.Imaging

Imports System.Windows.Navigation

Imports System.Windows.Shapes

    ''' <summary>

    ''' Логика взаимодействия для Window1.xaml

    ''' </summary>

    Partial Public Class Window1

        Inherits Window

        Public Sub New()

            InitializeComponent()

            lblLongDate.DataContext = New DateTime()

            lblShortDate.DataContext = New DateTime()

        End Sub

    End Class

Преобразователи на VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows.Data

Imports System.Windows.Media

Imports System.Globalization

    <ValueConversion(GetType(String), GetType(SolidColorBrush))> _

    Public Class WordToColorConverter

        Implements IValueConverter

    Public Function Convert(ByVal value As Object, ByVal targetType As Type,

        ByVal parameter As Object, ByVal culture As CultureInfo)

            As Object Implements IValueConverter.Convert

        Dim boundWord As String = TryCast(value, String)

        Dim returnBrush As SolidColorBrush = Nothing

        Select Case boundWord.ToLower().Trim()

            Case "hot"

                returnBrush = New SolidColorBrush(Colors.Red)

                Exit Select

            Case "cold"

                returnBrush = New SolidColorBrush(Colors.Green)

                Exit Select

            Case Else

                returnBrush = New SolidColorBrush(Colors.Yellow)

                Exit Select

        End Select

        Return returnBrush

    End Function

    Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type,

        ByVal parameter As Object, ByVal culture As CultureInfo)

            As Object Implements IValueConverter.ConvertBack

        Throw New Exception("Cant convert back")

    End Function

    End Class

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows.Data

Imports System.Windows.Media

Imports System.Globalization

<ValueConversion(GetType(DateTime), GetType(String))> _

    Public Class DateConverter

    Implements IValueConverter

    Public Function Convert(ByVal value As Object, ByVal targetType As Type,

        ByVal parameter As Object, ByVal culture As CultureInfo)

            As Object Implements IValueConverter.Convert

        Dim [date] As DateTime = DirectCast(value, DateTime)

        Return [date].ToShortDateString()

    End Function

    Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type,

        ByVal parameter As Object, ByVal culture As CultureInfo)

            As Object Implements IValueConverter.ConvertBack

        Dim strValue As String = value.ToString()

        Dim resultDateTime As DateTime

        If DateTime.TryParse(strValue, resultDateTime) Then

            Return resultDateTime

        End If

        Return value

    End Function

End Class

ПРИМЕЧАНИЕ

Я не рассматривал привязки MultiBinding, поскольку Джош Смит уже сделал это в своей превосходной статье.

Проверка привязки данных

Итак, мы изучили привязки к значениям, другим элементам, данным xml и т. д. и узнали, как преобразовывать значения. Это хорошо. Но при использовании привязок одна из основных проблем связана с проверкой введенных данных, особенно если свойству Binding.Mode присвоено значение TwoWay или OneWayToSource. Необходимо сделать так, чтобы в исходный объект передавались только допустимые данные. Это также необходимо для того, чтобы не отправлять бессмысленные данные в постоянное хранилище (базу данных, файл и т. д.). Так как можно реализовать необходимую проверку привязки?

К счастью, корпорация Microsoft знала об этой проблеме и предоставила нам все необходимое для создания правильных привязок данных с возможностями проверки. Далее мы рассмотрим три основных способа выполнения проверки.

В состав демонстрационного решения входит проект Validation, который при выполнении выглядит так:

Изменение внешнего вида при проверке

В проект Validation я включил несколько стилей. Этой области мы еще не касались. Но другого выхода у нас нет. На данный момент вам всего лишь нужно знать, что недопустимый ввод в элементе TextBox приведет к изменению внешнего вида соответствующего элемента за счет стиля или шаблона. И еще один момент. Всплывающая подсказка TextBox.ToolTip отображает сообщение проверки правильности. Это также реализуется с помощью более «умной» привязки.

<Setter Property="ToolTip"

Value="{Binding RelativeSource={RelativeSource Self},

Path=(Validation.Errors)[0].ErrorContent}"/>

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

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

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

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace Validation

{

    public class TestClass

    {

        public int Age { get; set; }

        public DateTime StartDate { get; set; }

        public TestClass()

        {

            StartDate = DateTime.Now;

        }

    }

}

А здесь приводится версия VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Public Class TestClass

    Private m_Age As Integer = 1

    Private m_StartDate As DateTime

    Public Property Age() As Integer

        Get

            Return m_Age

        End Get

        Set(ByVal value As Integer)

            m_Age = value

        End Set

    End Property

    Public Property StartDate() As DateTime

        Get

            Return m_StartDate

        End Get

        Set(ByVal value As DateTime)

            m_StartDate = value

        End Set

    End Property

    Public Sub New()

        m_StartDate = DateTime.Now

    End Sub

End Class

Зададим привязку в коде программной части следующим образом (с помощью свойства DataContext).

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

namespace Validation

{

    /// <summary>

    /// Логика взаимодействия для Window1.xaml

    /// </summary>

    public partial class Window1 : Window

    {

        public Window1()

        {

            InitializeComponent();

            this.DataContext = new TestClass();

            txtIDataErrorInfoAge.DataContext = new Person();

            txtIDataErrorInfoName.DataContext = new Person();

        }

    }

}

И на VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Data

Imports System.Windows.Documents

Imports System.Windows.Input

Imports System.Windows.Media

Imports System.Windows.Media.Imaging

Imports System.Windows.Navigation

Imports System.Windows.Shapes

    ''' <summary>

    ''' Логика взаимодействия для Window1.xaml

    ''' </summary>

    Partial Public Class Window1

        Inherits Window

        Public Sub New()

        InitializeComponent()

        Me.DataContext = New TestClass

        txtIDataErrorInfoAge.DataContext = New Person

        txtIDataErrorInfoName.DataContext = New Person

        End Sub

    End Class

Затем можно использовать проверку на основе исключений в XAML.

<Window x:Class="Validation.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:Validation"

    Title="Window1" Height="800" Width="800"

    WindowStartupLocation="CenterScreen">

    <Window.Resources>

        <!--Стиль TextBox проверки на основе исключений и правила проверки -->

        <Style x:Key="textStyleTextBox" TargetType="TextBox">

            <Setter Property="Foreground" Value="#333333" />

            <Style.Triggers>

                <Trigger Property="Validation.HasError" Value="true">

                    <Setter Property="ToolTip"

                        Value="{Binding RelativeSource={RelativeSource Self},

                        Path=(Validation.Errors)[0].ErrorContent}"/>

                </Trigger>

            </Style.Triggers>

        </Style>

        <!--Шаблон элемента управления для проверки на основе правила проверки -->

        <ControlTemplate x:Key="validationTemplate">

            <DockPanel>

                <TextBlock Foreground="Red" FontSize="20">!</TextBlock>

                <AdornedElementPlaceholder/>

            </DockPanel>

        </ControlTemplate>

    </Window.Resources>

    <ScrollViewer HorizontalScrollBarVisibility="Auto"

            VerticalScrollBarVisibility="Auto">

        <StackPanel Orientation="Vertical">

            <!-- Проверка на основе исключений -->

            <Label Content="Exception Based Validitaion" Margin="5,0,0,0"

                FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10"

                    Background="Gainsboro">

                <TextBlock TextWrapping="Wrap" Text="Exception Based Validitaion,

                        type an non integer value" Width="400"/>

                <TextBox Name="txtException" Style="{StaticResource textStyleTextBox}"

                         Width="120" Height="25" Margin="5,0,0,0"

                         Text="{Binding Path=Age,

                         UpdateSourceTrigger=PropertyChanged,

                         ValidatesOnExceptions=True}" />

            </StackPanel>

        </StackPanel>

    </ScrollViewer>

</Window>

Следует отметить, что Binding.ValidatesOnExceptions=True, поэтому для создания сообщения проверки, отображаемого во всплывающей подсказкеTextBox.Tooltip, будет использоваться правило проверки ExceptionValidationRule, встроенное в WPF.

Проверка на основе пользовательских правил проверки

Хотя пользовательские правила проверки похожи на встроенное правило ExceptionValidationRule, в их случае мы используем собственное правило для создания сообщений проверки. Ниже приведен пример пользовательского правила проверки, которое проверяет, что введенное значение имеет тип DateTime и относится к будущему времени.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows.Controls;

using System.Globalization;

namespace Validation

{

    class FutureDateValidationRule : ValidationRule

    {

        public override ValidationResult Validate(object value, CultureInfo cultureInfo)

        {

            DateTime date;

            try

            {

                date = DateTime.Parse(value.ToString());

            }

            catch (FormatException)

            {

                return new ValidationResult(false, "Value is not a valid date.");

            }

            if (DateTime.Now.Date > date)

            {

                return new ValidationResult(false, "Please enter a date in the future.");

            }

            else

            {

                return ValidationResult.ValidResult;

            }

        }

    }

}

А здесь приводится версия VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows.Controls

Imports System.Globalization

    Public Class FutureDateValidationRule

        Inherits ValidationRule

        Public Overloads Overrides Function Validate(ByVal value As Object,

            ByVal cultureInfo As CultureInfo) As ValidationResult

            Dim [date] As DateTime

            Try

                [date] = DateTime.Parse(value.ToString())

            Catch generatedExceptionName As FormatException

                Return New ValidationResult(False, "Value is not a valid date.")

            End Try

            If DateTime.Now.[Date] > [date] Then

                Return New ValidationResult(False, "Please enter a date in the future.")

            Else

                Return ValidationResult.ValidResult

            End If

        End Function

    End Class

Зададим привязку в коде программной части следующим образом (с помощью свойства DataContext).

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Documents;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Imaging;

using System.Windows.Navigation;

using System.Windows.Shapes;

namespace Validation

{

    /// <summary>

    /// Логика взаимодействия для Window1.xaml

    /// </summary>

    public partial class Window1 : Window

    {

        public Window1()

        {

            InitializeComponent();

            this.DataContext = new TestClass();

            txtIDataErrorInfoAge.DataContext = new Person();

            txtIDataErrorInfoName.DataContext = new Person();

        }

    }

}

И на VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.Windows

Imports System.Windows.Controls

Imports System.Windows.Data

Imports System.Windows.Documents

Imports System.Windows.Input

Imports System.Windows.Media

Imports System.Windows.Media.Imaging

Imports System.Windows.Navigation

Imports System.Windows.Shapes

    ''' <summary>

    ''' Логика взаимодействия для Window1.xaml

    ''' </summary>

    Partial Public Class Window1

        Inherits Window

        Public Sub New()

        InitializeComponent()

        Me.DataContext = New TestClass

        txtIDataErrorInfoAge.DataContext = New Person

        txtIDataErrorInfoName.DataContext = New Person

        End Sub

    End Class

Затем можно использовать это пользовательское правило проверки FutureDateValidationRule в коде XAML.

<Window x:Class="Validation.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:Validation"

    Title="Window1" Height="800" Width="800"

    WindowStartupLocation="CenterScreen">

    <Window.Resources>

        <!--Стиль TextBox проверки на основе исключений и правила проверки -->

        <Style x:Key="textStyleTextBox" TargetType="TextBox">

            <Setter Property="Foreground" Value="#333333" />

            <Style.Triggers>

                <Trigger Property="Validation.HasError" Value="true">

                    <Setter Property="ToolTip"

                        Value="{Binding RelativeSource={RelativeSource Self},

                        Path=(Validation.Errors)[0].ErrorContent}"/>

                </Trigger>

            </Style.Triggers>

        </Style>

        <!--Шаблон элемента управления для проверки на основе правила проверки -->

        <ControlTemplate x:Key="validationTemplate">

            <DockPanel>

                <TextBlock Foreground="Red" FontSize="20">!</TextBlock>

                <AdornedElementPlaceholder/>

            </DockPanel>

        </ControlTemplate>

    </Window.Resources>

    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">

        <StackPanel Orientation="Vertical">

            <!-- Проверка на основе правила проверки -->

            <Label Content="ValidationRule Based Validitaion" Margin="5,0,0,0"

                    FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10" Background="Gainsboro">

                <TextBlock TextWrapping="Wrap" Text="ValidationRule Based Validitaion,

                        type a future date" Width="400"/>

                 <TextBox Name="txtStartDate"

                        Validation.ErrorTemplate="{StaticResource validationTemplate}"

                        Style="{StaticResource textStyleTextBox}" Width="150" Height="25"

                        Margin="5,0,0,0">

                    <TextBox.Text>

                          <!-- Поскольку требуется предоставить дочерний объект ValidationRule,

                             необходимо использовать синтаксис элемента свойства -->

                        <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">

                            <Binding.ValidationRules>

                                <local:FutureDateValidationRule />

                            </Binding.ValidationRules>

                        </Binding>

                    </TextBox.Text>

                </TextBox>

            </StackPanel>

        </StackPanel>

    </ScrollViewer>

</Window>

Нужно отметить, что, поскольку требуется добавить новое правило FutureDateValidationRule в свойствоBinding.ValidationRules, необходимо использовать синтаксис элемента свойства.

Использование интерфейса IDataErrorInfo в .NET 3.5

В выпуске платформы .NET 3.5 добавлены язык LINQ и несколько усовершенствований WPF. Одним из таких усовершенствований стал новый интерфейс IDataErrorInfo, который изменяет место, где выполняется проверка. Она переносится из отдельных классов проверки в сами бизнес-объекты.

Рассмотрим пример простого класса, который реализует интерфейс IDataErrorInfo.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.ComponentModel;

namespace Validation

{

    /// <summary>

    /// Это новый метод .NET 3.5, где у каждого бизнес-объекта свои средства проверки

    /// с помощью интерфейса IDataErrorInfo

    /// </summary>

    public class Person : IDataErrorInfo

    {

        public int Age { get; set; }

        public string Name { get; set; }

        public Person()

        {

            this.Age = 0;

            this.Name = "sacha";

        }

        #region IDataErrorInfo Members

        public string Error

        {

            get

            {

                return null;

            }

        }

        /// <summary>

        /// Проверяет, было ли свойство изменено, и предоставляет

        /// сообщение об ошибке на основе некоторых правил

        /// </summary>

        /// <param name="name">The property that changed</param>

        /// <returns>a error message string</returns>

        public string this[string name]

        {

            get

            {

                string result = null;

                // По сути, для каждого проверяемого свойства необходим один из этих блоков

                switch (name)

                {

                    case "Age":

                        if (this.Age < 0 || this.Age > 150)

                        {

                            result = "Age must not be less than 0 or greater than 150.";

                        }

                        break;

                    case "Name":

                        if (this.Name == string.Empty)

                        {

                            result = "Name can't be empty";

                        }

                        if (this.Name.Length > 5)

                        {

                            result = "Name can't be more than 5 characters";

                        }

                        break;

                }

                return result;

            }

        }

        #endregion

    }

}

А здесь приводится версия VB.NET.

Imports System

Imports System.Collections.Generic

Imports System.Linq

Imports System.Text

Imports System.ComponentModel

''' <summary>

''' Это новый метод .NET 3.5, где у каждого бизнес-объекта свои средства проверки

''' с помощью интерфейса IDataErrorInfo

''' </summary>

Public Class Person

    Implements IDataErrorInfo

    Private m_Age As Integer

    Private m_Name As String

    Public Property Age() As Integer

        Get

            Return m_Age

        End Get

        Set(ByVal value As Integer)

            m_Age = value

        End Set

    End Property

    Public Property Name() As String

        Get

            Return m_Name

        End Get

        Set(ByVal value As String)

            m_Name = value

        End Set

    End Property

    Public Sub New()

        Me.Age = 0

        Me.Name = "sacha"

    End Sub

#Region "IDataErrorInfo Members"

    Public ReadOnly Property [Error]() As String Implements System.ComponentModel.IDataErrorInfo.Error

        Get

            Return Nothing

        End Get

    End Property

    ''' <summary>

    ''' Проверяет, было ли свойство изменено, и предоставляет

    ''' сообщение об ошибке на основе некоторых правил

    ''' </summary>

    ''' <param name="Name">The property that changed</param>

    ''' <returns>a error message string</returns>

    Default Public ReadOnly Property Item(ByVal Name As String)

            As String Implements System.ComponentModel.IDataErrorInfo.Item

        'Default Public ReadOnly Property Item(ByVal name As String) As String

        Get

            Dim result As String = Nothing

            ' По сути, для каждого проверяемого свойства необходим один из этих блоков

            Select Case Name

                Case "Age"

                    If Me.Age < 0 OrElse Me.Age > 150 Then

                        result = "Age must not be less than 0 or greater than 150."

                    End If

                    Exit Select

                Case "Name"

                    If Me.Name = String.Empty Then

                        result = "Name can't be empty"

                    End If

                    If Me.Name.Length > 5 Then

                        result = "Name can't be more than 5 characters"

                    End If

                    Exit Select

            End Select

            Return result

        End Get

    End Property

#End Region

End Class

По существу, этот интерфейс позволяет проверять измененное свойство с помощью синтаксиса public string this[string name]. В XAML все немного по-другому. Для привязки больше не нужно использовать отдельный класс проверки, можно просто использовать сокращенный синтаксис XAML с фигурными скобками { } либо более подробный синтаксис.

<Window x:Class="Validation.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:Validation"

    Title="Window1" Height="800" Width="800"

    WindowStartupLocation="CenterScreen">

    <Window.Resources>

        <!--Стиль TextBox проверки на основе исключений и правила проверки -->

        <Style x:Key="textStyleTextBox" TargetType="TextBox">

            <Setter Property="Foreground" Value="#333333" />

            <Style.Triggers>

                <Trigger Property="Validation.HasError" Value="true">

                    <Setter Property="ToolTip"

                        Value="{Binding RelativeSource={RelativeSource Self},

                        Path=(Validation.Errors)[0].ErrorContent}"/>

                </Trigger>

            </Style.Triggers>

        </Style>

    </Window.Resources>

    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">

        <StackPanel Orientation="Vertical">

            <!-- Проверка с использованием интерфейса IDataErrorInfo -->

            <Label Content="IDataErrorInfo Based Validitaion" Margin="5,0,0,0"

                    FontSize="14" FontWeight="Bold" />

            <StackPanel Orientation="Horizontal" Margin="10,10,10,10" Background="Gainsboro">

                <TextBlock TextWrapping="Wrap" Text="IDataErrorInfo Based Validitaion,

                    type a number below 0 or above 150 " Width="400"/>

                <!-- Область ввода возраста -->

                <Label Content="Age"/>

                <TextBox Name="txtIDataErrorInfoAge" Style="{StaticResource textStyleTextBox}"

                         Width="60" Height="25" Margin="5,0,0,0"

                         Text="{Binding Path=Age,

                         UpdateSourceTrigger=PropertyChanged,

                         ValidatesOnExceptions=True,

                         ValidatesOnDataErrors=True}" />

                <!-- Область ввода имени -->

                <Label Content="Name"/>

                <TextBox Name="txtIDataErrorInfoName" Style="{StaticResource textStyleTextBox}"

                         Width="60" Height="25" Margin="5,0,0,0"

                         Text="{Binding Path=Name,

                         UpdateSourceTrigger=PropertyChanged,

                         ValidatesOnDataErrors=True}" />

            </StackPanel>

        </StackPanel>

    </ScrollViewer>

</Window>

Обратите внимание, что в этот раз используется выражение Binding.ValidatesOnDataErrors=True. Это означает, что будет включено правило DataErrorValidationRule, использующее реализацию интерфейса IDataErrorInfo.

ПРЕДУПРЕЖДЕНИЕ. Остерегайтесь вкладок

Недавно я получил довольно интересное письмо от Карла Шифлета (Karl Shifllet), предупреждающее об исчезновении ошибки проверки при использовании вкладок в WPF. Следует ознакомиться с записью в блоге Карла, посвященной этому вопросу.

На этом закончим

Можно много еще говорить о привязках данных. Я, например, не касался группировки, сортировки, фильтрации и т. д. Также я опустил некоторые особенности и синтаксис привязки данных. Оставим эти вопросы как упражнения для читателя. Данная статья дает лишь общее представление о привязке данных. Если она вам понравилась, проголосуйте и оставьте комментарии. Тогда, может, прочитаете и следующую статью этой серии. Спасибо!

Ссылки

  1. MSDN. Общие сведения о связывании данных
  2. MSDN. Класс Multibinding
  3. MSDN. Класс Binding
  4. Проверка данных в .NET 3.5
  5. Привязка к данным XML с помощью XMLDataProvider и запросов XPath

Другие полезные источники

  1. Блог Беатриц Коста (Beatriz "The Binding Queen" Costa)
  2. Джош Смит. Пошаговое руководство по WPF. Часть 3. Привязка данных

С уважением,

Саша Барбер.