Расширения привязки и xaml-разметки на примере локализации. Настройка отображения данных с привязкой данных и WPF

  • Программирование
  • Одним из ключевых моментов в разработке xaml -ориентированных приложений является использование привязок (Bindings ). Привязка - это медиатор (посредник), с помощью которого синхронизируются значения свойств между связанными объектами.

    Стоит отметить не очевидный, но важный нюанс: хотя привязка так или иначе ссылается на взаимодействующие объекты, она не удерживает их от сборки мусора!

    Наследование от класса Binding разрешено, но в целях безопасности кода переопределение метода ProvideValue , который связан с основной логикой работы, не допускается. Это так или иначе провоцирует разработчиков на применение паттерна Converter , который тесно переплетается с темой привязок.

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


    Объявлять привязки в xaml допустимо двумя образами:



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


    В простейшем случае нужно унаследоваться от класса MarkupExtension и реализовать метод ProvideValue , в котором по ключу получить нужное значение.

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

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

    Более того, на xaml -платформах Windows Phone , Windows Store и Xamarin.Forms нет возможности создавать пользовательские расширения разметки, что наталкивает на идею использования привязок в качестве расширений разметки

    Не будем ходить вокруг да около, вот то, что нам нужно:

    Public abstract class BindingExtension: Binding, IValueConverter { protected BindingExtension() { Source = Converter = this; } protected BindingExtension(object source) // set Source to null for using DataContext { Source = source; Converter = this; } protected BindingExtension(RelativeSource relativeSource) { RelativeSource = relativeSource; Converter = this; } public abstract object Convert(object value, Type targetType, object parameter, CultureInfo culture); public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
    Примечательно, что привязка является конвертером для самой себя. В результате мы получаем поведение очень похожее, как при наследовании от класса MarkupExtension , но, кроме того, остаётся возможность использовать стандартные механизмы контроля сборки мусора!

    Теперь логика для локализации выглядит проще некуда:

    Public partial class Localizing: Base.BindingExtension { public static readonly Manager ActiveManager = new Manager(); public Localizing() { Source = ActiveManager; Path = new PropertyPath("Source"); } public Localizing(string key) { Key = key; Source = ActiveManager; Path = new PropertyPath("Source"); } public string Key { get; set; } public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var key = Key; var resourceManager = value as ResourceManager; var localizedValue = resourceManager == null || string.IsNullOrEmpty(key) ? ":" + key + ":" : (resourceManager.GetString(key) ?? ":" + key + ":"); return localizedValue; } }
    public partial class Localizing { public class Manager: INotifyPropertyChanged { private ResourceManager _source; public ResourceManager Source { get { return _source; } set { _source = value; PropertyChanged(this, new PropertyChangedEventArgs("Source")); } } public string Get(string key, string stringFormat = null) { if (_source == null || string.IsNullOrWhiteSpace(key)) return key; var localizedValue = _source.GetString(key) ?? ":" + key + ":"; return string.IsNullOrEmpty(stringFormat) ? localizedValue: string.Format(stringFormat, localizedValue); } public event PropertyChangedEventHandler PropertyChanged = (sender, args) => { }; } }
    Легко добавить возможность для смены регистра букв:

    Public partial class Localizing: Base.BindingExtension { public enum Cases { Default, Lower, Upper } public static readonly Manager ActiveManager = new Manager(); public Localizing() { Source = ActiveManager; Path = new PropertyPath("Source"); } public Localizing(string key) { Key = key; Source = ActiveManager; Path = new PropertyPath("Source"); } public string Key { get; set; } public Cases Case { get; set; } public override string ToString() { return Convert(ActiveManager.Source, null, Key, Thread.CurrentThread.CurrentCulture) as string ?? string.Empty; } public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var key = Key; var resourceManager = value as ResourceManager; var localizedValue = resourceManager == null || string.IsNullOrEmpty(key) ? ":" + key + ":" : (resourceManager.GetString(key) ?? ":" + key + ":"); switch (Case) { case Cases.Lower: return localizedValue.ToLower(); case Cases.Upper: return localizedValue.ToUpper(); default: return localizedValue; } } }
    В xaml запись выглядит удобно и красиво, но есть некоторые ограничения парсеров разметки на различных платформах:


    Чтобы избавиться на WPF от обязательного префикса m: нужно поместить расширение разметки в отдельную сборку и в Properties/AssemblyInfo.cs указать следующие директивы:


    Для регулирования имени префикса на Windows Phone или Store :


    Использование расширений привязки (Binding Extensions ) на WPF не исключает обычных расширений разметки, но в некоторых случаях является даже более безопасным и простым вариантом. Также всё это не ограничивается одной лишь локализацией, а пригодно для множества других целей...

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

    Последнее обновление: 7.02.2016

    В WPF привязка (binding) является мощным инструментом программирования, без которого не обходится ни одно серьезное приложение.

    Привязка подразумевает взаимодействие двух объектов: источника и приемника. Объект-приемник создает привязку к определенному свойству объекта-источника. В случае модификации объекта-источника, объект-приемник также будет модифицирован. Например, простейшая форма с использованием привязки:

    Для определения привязки используется выражение типа:

    {Binding ElementName=Имя_объекта-источника, Path=Свойство_объекта-источника}

    То есть в данном случае у нас элемент TextBox является источником, а TextBlock - приемником привязки. Свойство Text элемента TextBlock привязывается к свойству Text элемента TextBox. В итоге при осуществлении ввода в текстовое поле синхронно будут происходить изменения в текстовом блоке.

    Работа с привязкой в C#

    Ключевым объектом при создании привязки является объект System.Windows.Data.Binding . Используя этот объект мы можем получить уже имеющуюся привязку для элемента:

    Binding binding = BindingOperations.GetBinding(myTextBlock, TextBlock.TextProperty);

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

    Также можно полностью установить привязку в коде C#:

    Public MainWindow() { InitializeComponent(); Binding binding = new Binding(); binding.ElementName = "myTextBox"; // элемент-источник binding.Path = new PropertyPath("Text"); // свойство элемента-источника myTextBlock.SetBinding(TextBlock.TextProperty, binding); // установка привязки для элемента-приемника }

    Если в дальнейшем нам станет не нужна привязка, то мы можем воспользоваться классом BindingOperations и его методами ClearBinding() (удаляет одну привязку) и ClearAllBindings() (удаляет все привязки для данного элемента)

    BindingOperations.ClearBinding(myTextBlock, TextBlock.TextProperty);

    BindingOperations.ClearAllBindings(myTextBlock);

    Некоторые свойства класса Binding :

      ElementName : имя элемента, к которому создается привязка

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

      Mode : режим привязки

      TargetNullValue : устанавливает значение по умолчанию, если привязанное свойство источника привязки имеет значение null

      RelativeSource : создает привязку относительно текущего объекта

      Source : указывает на объект-источник, если он не является элементом управления.

      XPath : используется вместо свойства path для указания пути к xml-данным

    Режимы привязки

    Свойство Mode объекта Binding, которое представляет режим привязки, может принимать следующие значения:

      OneWay : свойство объекта-приемника изменяется после модификации свойства объекта-источника.

      OneTime : свойство объекта-приемника устанавливается по свойству объекта-источника только один раз. В дальнейшем изменения в источнике никак не влияют на объект-приемник.

      TwoWay : оба объекта - применки и источник могут изменять привязанные свойства друг друга.

      OneWayToSource: объект-приемник, в котором объявлена привязка, меняет объект-источник.

      Default : по умолчанию (если меняется свойство TextBox.Text , то имеет значение TwoWay, в остальных случаях OneWay).

    Применение режима привязки:

    Обновление привязки. UpdateSourceTrigger

    Односторонняя привязка от источника к приемнику практически мгновенно изменяет свойство приемника. Но если мы используем двустороннюю привязку в случае с текстовыми полями (как в примере выше), то при изменении приемника свойство источника не изменяется мгновенно. Так, в примере выше, чтобы текстовое поле-источник изменилось, нам надо перевести фокус с текстового поля-приемника. И в данном случае в дело вступает свойство UpdateSourceTrigger класса Binding, которое задает, как будет присходить обновление. Это свойство в качестве принимает одно из значений перечисления UpdateSourceTrigger :

      PropertyChanged: источник привязки обновляется сразу после обновления свойства в приемнике

      LostFocus: источник привязки обновляется только после потери фокуса приемником

      Explicit: источник не обновляется до тех пор, пока не будет вызван метод BindingExpression.UpdateSource()

      Default: значение по умолчанию. Для большинства свойств это значение PropertyChanged . А для свойства Text элемента TextBox это значение LostFocus

    В данном случае речь идет об обновлении источника привязки после изменения приемника в режимах OneWayToSource или TwoWay . То есть чтобы у нас оба текстовых поля, которые связаны режимом TwoWay, моментально обновлялись после изменения одного из них, надо использовать значение UpdateSourceTrigger.PropertyChanged:

    Свойство Source

    Свойство Source позволяет установить привязку даже к тем объектам, которые не являются элементами управления WPF. Например, определим класс Phone:

    Class Phone { public string Title { get; set; } public string Company { get; set; } public int Price { get; set; } }

    Теперь создадим объект этого класса и определим к нему привязку:

    Свойство TargetNullValue

    На случай, если свойство в источнике привязки вдруг имеет значение null, то есть оно не установлено, мы можем задать некоторое значение по умолчанию. Например:

    В данном случае у ресурса nexusPhone не установлено свойство Title, поэтому текстовый блок будет выводить значение по умолчанию, указанное в параметре TargetNullValue.

    Свойство RelativeSource

    Свойство RelativeSource позволяет установить привязку относительно элемента-источника, который связан какими-нибудь отношениями с элементом-приемником. Например, элемент-источник может быть одним из внешних контейнеров для элемента-приемника. Либо источником и приемником может быть один и тот же элемент.

    Для установки этого свойства используется одноименный объект RelativeSource . У этого объекта есть свойство Mode , которое задает способ привязки. Оно принимает одно из значений перечисления RelativeSourceMode :

      Self: привязка осуществляется к свойству этого же элемента. То есть элемент-источник привязки в то же время является и приемником привязки.

      FindAncestor: привязка осуществляется к свойству элемента-контейнера.

    Например, совместим источник и приемник привязке в самом элементе:

    Здесь текст и фоновый цвет текстового поля связаны двусторонней привязкой. В итоге мы можем увидеть в поле числовое значение цвета, поменять его, и вместе с ним изменится и фон поля.

    Привязка к свойствам контейнера:

    При использовании режима FindAncestor, то есть привязке к контейнеру, необходимо еще указывать параметр AncestorType и передавать ему тип контейнера в виде выражения AncestorType={x:Type Тип_элемента-контейнера} . При этом в качестве контейнера мы могли бы выбрать любой контейнер в дереве элементов, в частности, в данном случае кроме Grid таким контейнером также является элемент Window.

    Свойство DataContext

    У объекта FrameworkElement , от которого наследуются элементы управления, есть интересное свойство DataContext . Оно позволяет задавать для элемента и вложенных в него элементов некоторый контекст данных. Тогда вложенные элементы могут использовать объект Binding для привязки к конкретным свойствам этого контекста. Например, используем ранее определенный класс Phone и создадим контекст данных из объекта этого класса:

    Таким образом мы задаем свойству DataContext некоторый динамический или статический ресурс. Затем осуществляем привязку к этому ресурсу.

    Привязка данных в XAML

    Совместное использование объектов в файле XAML также может осуществляться через привязку данных. По сути привязка данных (data binding) связывает между собой два свойства разных объектов. Как будет показано позже, привязки данных чаще всего применяются для связывания визуальных элементов страницы с источниками данных; кроме того, они являются важной составляющей реализации популярного архитектурного паттерна MVVM (Model-View-ViewModel) . Привязки также играют важную роль при определении шаблонов отображения объектов данных.

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

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

    Настройки кистей

    Неявный стиль TextBlock не содержит свойства Foreground. Кисть LinearGradientBrush определяется для первого из четырех элементов TextBlock, использующих эту кисть, а последующие элементы TextBlock ссылаются на эту же кисть через привязку:

    У привязки данных имеется источник (source) и приемник (target) . Приемником всегда является свойство, для которого устанавливается привязка, а источником - свойство, к которому оно привязывается. Источником приведенных привязок является элемент TextBlock с именем topTxb; приемниками - три элемента TextBlock, совместно использующих свойство Foreground. Два приемника представляют более стандартный способ выражения объекта Binding как расширения разметки XAML:

    {Binding ElementName=topTxb, Path=Foreground}

    Расширения разметки XAML всегда размещаются в фигурных скобках. В расширении разметки для Binding обычно необходимо задать пару свойств, разделяемых запятыми. Свойство ElementName обозначает имя элемента, для которого задано желаемое свойство; свойство Path предоставляет имя свойства.

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

    Последний элемент TextBlock демонстрирует выражение Binding в менее распространенном синтаксисе элементов свойств:

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

    Свойство Path класса Binding называется так потому, что оно может содержать несколько имен свойств, разделенных точками. Например, попробуйте заменить одно из значений Text в проекте следующим значением:

    Text="{Binding ElementName=topTxb, Path=FontFamily.Source}"

    Первая часть Path указывает, что нам нужны данные из свойства FontFamily. Свойству задается объект типа FontFamily, содержащий с именем Source, обозначающим название семейства шрифта. Следовательно, в TextBlock будет выведен текст «Arial».

    Попробуйте применить следующую конструкцию к любому элементу TextBlock в нашем проекте:

    Text="{Binding RelativeSource={RelativeSource Self}, Path=FontSize}"

    Здесь расширение разметки RelativeSource находится внутри расширения разметки Binding и используется для ссылки на свойство того элемента, для которого задается привязка.

    Вот вы и создали полноценные страницы для вашего приложения. Теперь вы, наверно, захотите заполнить их различными данными.

    В этой части вы узнаете:

    • Как привязать данные к пользовательскому интерфейсу.
    • Как Visual Studio может помочь вам создать привязки к данным.
    • Как отображать данные в списке.
    • Как работать с более сложными сценариями привязки.

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

    Приложение Fuel Tracker имеет три страницы с данными. Данные хранятся в основном в трёх классах. На следующем изображении показаны страницы и связанные с ними классы.

    Для отображения данных обычно используется привязка данных (data binding). Привязка данных предоставляет возможность подключения пользовательского интерфейса к источнику данных. Когда привязки созданы и источник данных изменяется, элементы пользовательского интерфейса, которые связаны с источником данных отображают изменения автоматически. Аналогично, изменения, внесённые пользователем в элемент пользовательского интерфейса отражаются в источнике данных. Например, если пользователь изменит значение в TextBox, соответствующий источник данных автоматически обновится, чтобы отразить эти изменения.

    Следующий фрагмент XAML-кода иллюстрирует синтаксис, который используется для привязки свойства Text элемента управления TextBox к свойству Name объекта-источника.

    1. < TextBox x:Name ="NameTextBox" Text ="{Binding Name, Mode=TwoWay}" />

    На следующем изображении показан пример такой привязки.

    Каждая привязка имеет свойство Mode (режим), которое определяет, как и когда обновляются данные. OneWay (односторонняя) привязка означает, что целевой (target) элемент пользовательского интерфейса обновляется, если источник изменяется. TwoWay (двухсторонняя) привязка означает, что как цель, так и источник обновляются, если кто-либо из них изменяется. Если вы используете OneWay или TwoWay привязки, то для того, чтобы привязка была уведомлена об изменениях объекта-источника, необходимо реализовать интерфейс INotifyPropertyChanged. Этот интерфейс будет более подробно обсуждаться в следующей части «Создание классов данных».

    Вы указываете объект-источник, устанавливая свойство DataContext (контекст данных). Если вы используете элемент управления ListBox, необходимо указать объект-источник, установив свойство ItemsSource. В следующем примере показано, как задать свойству DataContext панели CarHeader объект-источник, извлекаемый из статического свойства.

    1. CarHeader.DataContext = CarDataStore.Car;
    * This source code was highlighted with Source Code Highlighter .

    Свойство DataContext позволяет установить стандартную привязку для всего элемента пользовательского интерфейса, включая все его дочерние элементы. В некоторых случаях вам будет удобней установить свойство DataContext для всей страницы, а в некоторых будет удобней установить его отдельно для каждого элемента на странице. Установка DataContext на каждом уровне XAML перекрывает любые установки на более высоком уровне. Кроме того, вы можете фактически переопределить любую установку DataContext для отдельных привязок, установив для них свойство Source (источник).

    Например, в приложение Fuel Tracker каждая страница устанавливает DataContext в различное значение. Однако, на странице FillupPage DataContext уровня страницы переопределяется для панели, которая отображает название автомобиля и его фотографию. На следующем изображении показаны настройки контекста данных для приложения Fuel Tracker.

    Использование data binding builder

    Visual Studio включает в себя data binding builder (создатель привязок данных), чтобы помочь вам создать привязки данных в XAML. Хотя data binding builder может обеспечить повышение производительности, он не поддерживает все возможные сценарии. Например, он не поддерживает привязку к индексированным элементам и он не распознаёт привязки, созданные в коде. Таким образом, в некоторых случаях вам потребуется указать привязки данных вручную.

    Отображение данных в списке

    Отображение коллекции элементов в списке - одна из основных задач на телефоне. Для того, чтобы отобразить коллекцию элементов в списке с помощью привязки данных вам необходимо сделать следующее:
    1. Добавить ListBox в ваше приложение.
    2. Указать источник данных для ListBox путем привязки коллекции к свойству ItemsSource.
    3. Чтобы настроить внешний вид каждого элемента в ListBox, добавьте шаблон данных для ListBox.
    4. В шаблоне данных привяжите элементы ListBox к свойствам коллекции элементов.
    На следующем изображении показаны привязки для ListBox на странице сводной информации приложения Fuel Tracker.

    Следующий XAML-код показывает, как были указаны привязки для ListBox.

    1. < ListBox ItemContainerStyle ="{StaticResource ListBoxStyle}"
    2. ItemsSource ="{Binding FillupHistory}"
    3. Height ="380" HorizontalAlignment ="Left" Margin ="5,25,0,0"
    4. VerticalAlignment ="Top" Width ="444" >
    5. < ListBox.ItemTemplate >
    6. < DataTemplate >
    7. < StackPanel Orientation ="Horizontal" >
    8. < TextBlock Style
    9. Text ="{Binding Date, Converter={StaticResource StringFormatter}, ConverterParameter=\{0:d\} }"
    10. Width ="105" TextWrapping ="Wrap" />
    11. < TextBlock Style ="{StaticResource SummaryStyle}"
    12. Text ="{Binding FuelQuantity}" TextWrapping ="Wrap" />
    13. < TextBlock Style ="{StaticResource SummaryStyle}"
    14. Text ="{Binding DistanceDriven}" TextWrapping ="Wrap" />
    15. < TextBlock Style ="{StaticResource SummaryStyle}"
    16. Text ="{Binding PricePerFuelUnit, Converter={StaticResource StringFormatter}, ConverterParameter=\{0:c\}, ConverterCulture=en-US}" />
    17. < TextBlock Style ="{StaticResource SummaryStyle}"
    18. Text ="{Binding FuelEfficiency, Converter={StaticResource StringFormatter}, ConverterParameter=\{0:F\}}" TextWrapping ="Wrap" />
    * This source code was highlighted with Source Code Highlighter .

    В предыдущем XAML-коде, свойство ListBox.ItemsSource привязано к свойству Car.FillupHistory так, что каждый объект Fillup в коллекции истории появится в качестве отдельного пункта в ListBox. Элемент DataTemplate определяет внешний вид каждого пункта и содержит несколько элементов TextBlock, каждый из которых привязан к свойству класса Fillup .

    Этот XAML будет работать только тогда, когда объект Car впервые ассоциирован со страницей, как показано в следующем коде из SummaryPage.xaml.cs.

    1. this .DataContext = CarDataStore.Car;
    * This source code was highlighted with Source Code Highlighter .

    Совет по улучшению производительности:
    Если прокрутка в вашем ListBox не кажется плавной и отзывчивый, воспользуйтесь следующими советами:
    • Упростите элементы в ListBox .
    • Загружайте изображения в фоновом режиме.
    • Используйте виртуализацию данных.
    • Обратите внимание на использование DeferredLoadListBox или LazyListBox .
    • Не используйте вложенные списки.

    Сложные пути привязки

    В дополнение к гибкости настройки свойства DataContext на любом уровне (что позволяет переопределить настройки на более высоком уровне), можно также указать сложные пути привязки для того, чтобы «пробуриться» в свойства ссылки, такой как Car.FillupHistory . Например, следующий XAML-код из SummaryPage.xaml демонстрирует привязку к свойству Fillup.FuelEfficiency первого пункта в коллекции истории заправок.
    1. < TextBlock Text ="{Binding FillupHistory.FuelEfficiency, Converter={StaticResource StringFormatter}, ConverterParameter=\{0:F\}}" />
    * This source code was highlighted with Source Code Highlighter .

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

    Зеленые прямоугольники на левом экране Pivot показывают элементы управления, которые привязаны с использованием сложных путей. Эти пути начинаются с первого элемента (индекс 0) в коллекции Car.FillupHistory и заканчиваются различными свойствами класса Fillup . Например, поле Current MPG использует путь привязки FillupHistory.FuelEfficiency . Если вы включите в этот путь настройку DataContext страницы, весь путь привязки будет выглядеть следующим образом: CarDataStore.Car.FillupHistory.FuelEfficiency .

    Теги:

    • windows phone 7
    • создание приложения
    • от начала до конца
    Добавить метки

    Когда платформа Windows Presentation Foundation (WPF) впервые появилась в поле зрения.NET, большинство статей и демонстрационных приложений превозносили ее первоклассный механизм визуализации и возможности работы с трехмерной графикой. Хотя читать их и баловаться с ними интересно, такие примеры не отражают широких реальных возможностей WPF. Большинству из нас не нужно создавать приложения с вращающимися видеокубами, которые взрываются фейерверком, если их щелкнуть. Большинство из нас зарабатывают на жизнь, создавая программное обеспечение для отображения и правки больших объемов сложных научных или деловых данных.

    Хорошие новости состоят в том, что WPF предлагает отличную поддержку для управления отображением и правкой сложных данных. В номере журнала MSDN® Magazine за декабрь 2007 года Джон Папа (John Papa) написал статью «Привязка данных в WPF» (msdn.microsoft.com/magazine/cc163299), отлично объясняющую ключевые концепции привязки данных в WPF. Здесь я рассмотрю более сложные случаи привязки данных, отталкиваясь от того, что Джон представил в вышеупомянутом выпуске рубрики «Точки данных». По прочтении читатели будут осведомлены о различных путях реализации распространенных требований к привязке данных, наблюдаемых в большинстве бизнес-приложений.

    Привязка в коде

    Одним из крупнейших изменений, представляемых WPF для разработчиков настольных приложений, является широкое использование и поддержка декларативного программирования. Интерфейсы пользователя и ресурсы WPF можно объявлять с помощью расширяемого языка разметки приложений (XAML), стандартного языка разметки, основанного на XML. Большинство объяснений привязки данных WPF показывают, лишь как работать с привязками в XAML. Поскольку всего, что можно сделать в XAML, также можно достичь в коде, важно, чтобы профессиональные разработчики под WPF знали, как работать с привязкой данных не только декларативно, но и программно.

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

    Элементы WPF наследуют методы SetBinding и GetBindingExpression либо от FrameworkElement, либо от FrameworkContentElement. Это просто повышающие удобство работы методы, которые вызывают методы с теми же именами в служебном классе BindingOperations. В приведенном ниже коде показано, как использовать класс BindingOperations для привязки свойства Text текстового окна к свойству на другом объекте:

    Static void BindText(TextBox textBox, string property)
    {
    if (!BindingOperations.IsDataBound(textBox, textProp))
    {
    Binding b = new Binding(property);
    BindingOperations.SetBinding(textBox, textProp, b);
    }
    }

    Привязку свойства несложно отменить, использовав следующий код:

    Static void UnbindText(TextBox textBox)
    {
    DependencyProperty textProp = TextBox.TextProperty;
    if (BindingOperations.IsDataBound(textBox, textProp))
    {
    BindingOperations.ClearBinding(textBox, textProp);
    }
    }

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

    Объявление привязки данных в XAML скрывает некоторые из лежащих в основе деталей. При начале работы с привязками в коде эти детали начинают всплывать на поверхность. Одна из них заключается в том, что отношения между источником привязки и ее целью на деле поддерживаются экземпляром класса BindingExpression, а не самим классом Binding. Класс Binding («Привязка») содержит высокоуровневую информацию, которую могут совместно использовать несколько классов BindingExpression, но обеспечение связи между двумя связанными свойствами производится лежащим в основе выражением. Нижеследующий код показывает, как BindingExpression можно использовать, чтобы программно удостовериться в том, проверяется ли свойство Text («Текст») текстового окна:

    static bool IsTextValidated(TextBox textBox)
    {
    DependencyProperty textProp = TextBox.TextProperty;

    var expr = textBox.GetBindingExpression(textProp);
    if (expr == null)
    return false;

    Binding b = expr.ParentBinding;
    return b.ValidationRules.Any();
    }

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

    Работа с шаблонами

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

    Класс System.Windows.DataTemplate - лишь одна из форм шаблона в WPF. В целом, шаблон - это формочка для выпечки печенья, которую инфраструктура WPF использует, чтобы создавать визуальные элементы, помогающие в визуализации объектов, не имеющих собственного визуального представления. Когда элемент пытается отобразить объект, не имеющий такого представления, скажем, нестандартный бизнес-объект, можно указать элементу, как визуализировать объект, дав ему DataTemplate.
    DataTemplate может создать столько визуальных элементов, сколько необходимо для отображения объекта данных. Эти элементы используют привязки данных для отображения значении свойств объекта данных. Если элементу неизвестно, как отображать объект, который ему указано визуализировать, он просто вызывает метод ToString на нем и отображает результаты в TextBlock.

    Предположим, что у нас имеется простой класс, именуемый FullName, в котором хранится имя некоего лица. Необходимо отобразить список имен, в котором фамилия каждого лица выделялась бы по сравнению с прочим. Чтобы сделать это, можно создать шаблон DataTemplate, описывающий, как визуализовать объект FullName. Код, показанный наРис. 1, отображает класс FullName и фоновый код для окна, в котором отобразится список имен.

    Рис. 1. Отображение объектов FullName с помощью DataTemplate

    public class FullName
    {
    public string FirstName { get; set; }
    public char MiddleInitial { get; set; }
    public string LastName { get; set; }
    }

    public partial class WorkingWithTemplates: Window
    {
    // This is the Window"s constructor.
    public WorkingWithTemplates()
    {
    InitializeComponent();

    base.DataContext = new FullName
    {
    new FullName
    {
    FirstName = "Johann",
    MiddleInitial = "S",
    LastName = "Bach"
    },
    new FullName
    {
    FirstName = "Gustav",
    MiddleInitial = " ",
    LastName = "Mahler"
    },
    new FullName
    {
    FirstName = "Alfred",
    MiddleInitial = "G",
    LastName = "Schnittke"
    }
    };
    }
    }

    Как можно увидеть наРис. 2, в файле XAML окна имеется элемент управления ItemsControl. Он создает простой список элементов, которые пользователь не может выбирать или удалять. У элемента ItemsControl имеется наблон DataTemplate, присвоенный его свойству ItemTemplate, с помощью которого он визуализирует каждый экземпляр FullName, созданный в конструкторе окна. Можно заметить, что у большинства элементов TextBlock в DataTemplate свойство Text («Текст») привязано к представляемым ими свойствам объекта FullName.

    Рис. 2. Отображение объектов FullName с помощью DataTemplate














    При запуске этого демонстрационного приложения оно выглядит, как показано наРис. 3. Использование DataTemplate для визуализации имени позволяет легко выделить фамилию каждого лица, поскольку параметр шрифта FontWeight соответствующего TextBlock является полужирным. Этот простой пример демонстрирует суть взаимоотношений между привязкой данных WPF и шаблонами. По мере углубления в тему я буду совмещать эти функции, создавая способы визуализации сложных объектов со все более широкими возможностями.

    Рис. 3. Объекты FullName, визуализованные DataTemplate

    Работа с унаследованным DataContext

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

    Для ссылки на объект источника данных не обязательно устанавливать свойство DataContext. Если свойству DataContext элемента-предка в дереве элементов (технически говоря, логическом дереве) дано значение для его DataContext, то значение автоматически будет унаследовано каждым производным элементом в интерфейсе пользователя. Другими словами, если DataContext установлен так, чтобы ссылаться на объект Foo, то, по умолчанию, DataContext каждого элемента в окне будет ссылаться на тот же объект Foo. Любому элементу в окне можно легко дать свое значение DataContext, что заставит все элементы, производные от этого элемента, унаследовать новое значение DataContext. Это напоминает внешнее свойство в Windows Forms.

    В предыдущем разделе я рассмотрел использование DataTemplates для создания визуализаций объектов данных. Свойства элементов, созданных шаблоном наРис. 2, привязаны к свойствам объекта FullName. Эти элементы неявно привязывают к их свойству DataContext. Свойство DataContext элементов, созданных ншаблоном DataTemplate, ссылается на объект данных, для которого используется шаблон, такой как объект FullName.

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

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

    Здесь термин «наследование» используется в значении, отличающемся от его объектно-ориентированного смысла, где подкласс наследует члены родительского класса. Наследование значений свойств относится только к распространению значений вниз по дереву элементов во время выполнения. Естественно, класс может унаследовать свойство зависимости для поддержки наследования значений в объектно-ориентированном смысле.

    Работа с представлениями коллекции

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

    На представление коллекции возложено несколько задач. Оно отслеживает текущий элемент в коллекции, каковым обычно является выбранный/активный элемент в элементе управления «список». Представления коллекций также предлагают общие способы упорядочения, фильтрации и разбиения на группы элементов в списке. Несколько элементов управления могут привязывать к одному и тому же представлению вокруг коллекции, обеспечивая свою координацию друг с другом. В приведенном ниже коде показаны некоторые возможности ICollectionView:

    // Get the default view wrapped around the list of Customers.
    ICollectionView view = CollectionViewSource.GetDefaultView(allCustomers);

    // Get the Customer selected in the UI.
    Customer selectedCustomer = view.CurrentItem as Customer;

    // Set the selected Customer in the UI.
    view.MoveCurrentTo(someOtherCustomer);

    У всех элементов управления типа списка, в том числе у списка, поля со списком и представления списка, свойство IsSynchronizedWithCurrentItem должно быть установлено на true для сохранения синхронизации со свойством представления коллекции CurrentItem. Это свойство определяет абстрактный класс Selector («Селектор»). Если оно не установлено на true, то выбор элемента в списочном элементе управления не обновит CurrentItem представления коллекции, и присваивание CurrentItem нового значения не будет отображено в списочном элементе управления.

    Работа с иерархическими данными

    Реальный мир полон иерархических данных. Клиент размещает несколько заказов, молекула состоит из множества атомов, отдел состоит из множества сотрудников, а солнечная система содержит группу небесных тел. Читатели, без сомнения, знакомы с такой схемой «основной/подробности».
    WPF предоставляет различные способы работы с иерархическими структурами данных, каждый из которых подходит для своих ситуаций. По сути, альтернатива сводится либо к использованию нескольких элементов управления для отображения данных, либо к отображение нескольких уровней иерархии данных в одном элементе управления. Здесь я разберу оба эти подхода.

    Использование нескольких элементов управления для отображения данных XML

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

    На Рис. 4, основанном на описанной выше ситуации, показан упрощенный пример данных, с которыми может работать приложение, обернутых в компонент XmlDataProvider платформы WPF. Эти данные согут отображаться в интерфейсе пользователя, подобном показанному наРис. 5. Обратите внимание, что клиентов и заказы можно выбирать, но сведения о заказе существуют в форме списка только для чтения. Этому есть причина - возможность выбирать визуальный объект следует предоставлять только тогда, когда он воздействует на состояние приложения или является изменяемым.

    Рис. 4. Иерархия клиентов заказов и сведений о заказах в формате XML





















    Рис. 5 Один из способов отображения данных XML

    В коде XAML наРис. 6 описано, как использовать эти различные элементы управления для отображения только что показанных иерархических данных. Это окно не требует кода; оно существует полностью в коде XAML.

    Рис. 6. Код XAML для привязки иерархических данных XML к интерфейсу пользователя

    "{Binding Source={StaticResource xmlData},
    XPath=customers/customer}"
    Margin="4"
    >







    ItemsSource="{Binding}"
    >









    x:Name="orderSelector"
    DataContext="{Binding Path=CurrentItem}"
    IsSynchronizedWithCurrentItem="True"
    ItemsSource="{Binding XPath=order}"
    >








    Text="Order Details" />
    DataContext=
    "{Binding ElementName=orderSelector, Path=SelectedItem}"
    ItemsSource="{Binding XPath=orderDetail}">



    Product:

    (

    )





    Обратите внимание на широкое использование коротких запросов XPath для указания платформе WPF на место, где нужно получить привязанные значения. Класс Binding предоставляет свойство XPath, которому можно выделить любой запрос XPath, поддерживаемый методом XmlNode.SelectNodes. Внутренние механизмы WPF используют этот метод для исполнения запросов XPath. Увы, это значит, что поскольку XmlNode.SelectNodes не поддерживает на данный момент использование функций XPath, их не поддерживает и привязка данных WPF.

    Поле со списком клиентов и список заказов производят привязку к получающемуся набору узлов запроса XPath, выполняемого запросом DataContext корневого элемента Grid (таблица). DataContext списка автоматически возвратит CurrentItem представления коллекции, являющегося оберткой для коллекции XmlNodes, создаваемой для DataContext таблицы. Другими словами, DataContext списка - это выбранный в настоящий момент клиент. Поскольку ItemsSource списка неявно привязан к собственному DataContext (потому что не было указано другого источника) и его привязка ItemsSource исполняет запрос XPath для получения элементов из DataContext, то ItemsSource фактически привязан к списку заказов выбранного клиента.

    Помните, что при привязке к данным XML реально привязка происходит к объектам, созданным вызовом к XmlNode.SelectNodes. При неосторожности можно получить несколько элементов управления, выполняющих привязку к логически эквивалентным, но физически различным наборам XmlNodes. Это обусловлено тем, что при каждом вызове к XmlNode.SelectNodes создается новый набор узлов XmlNode, даже если каждый раз передавать тот же запрос XPath тому же узлу XmlNode. Это особая проблема привязки к данным XML, так что при привязке к бизнес-объектам ее можно спокойно игнорировать.

    Использование множества элементов управления для отображения бизнес-объектов

    Теперь предположим, что требуется провести привязку к данным из предыдущего примера, но данные существуют в виде бизнес-объектов, а не в коде XML. Как это изменит способ привязки к различным уровням иерархии данных? Насколько похожим или отличающимся будет прием?

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

    Рис. 7. Классы для создания иерархии бизнес-объектов

    public class Customer
    {
    public string Name { get; set; }
    public List Orders { get; set; }


    {
    return this.Name;
    }
    }

    public class Order
    {
    public string Desc { get; set; }
    public List OrderDetails { get; set; }

    public override string ToString()
    {
    return this.Desc;
    }
    }

    public class OrderDetail
    {
    public string Product { get; set; }
    public int Quantity { get; set; }
    }

    Код XAML окна, отображающего эти объекты, показан наРис. 8. Он очень похож на код XAML сРис. 6, но между существуют важные отличия, на которые стоит обратить внимание. Чего в коде XAML не наблюдается, так это конструктора окна, создающего объекты данных и устанавливающего DataContext, вместо установки кодом XAML ссылки на него, как на ресурс. Обратите внимание, что ни в одном из элементов управления свойство DataContext не установлено прямо. Все они наследуют то же свойство DataContext, являющееся экземпляром List.

    Рис. 8. Код XAML для привязки иерархических бизнес-объектов к интерфейсу пользователя








    />
    IsSynchronizedWithCurrentItem="True"
    ItemsSource="{Binding Path=.}"
    />




    IsSynchronizedWithCurrentItem="True"
    ItemsSource="{Binding Path=CurrentItem.Orders}"
    />



    Text="Order Details" />
    ItemsSource="{Binding Path=CurrentItem.Orders.CurrentItem.
    OrderDetails}"
    >



    Product:

    (

    )





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

    При привязке к бизнес-объектам вместо XML привязка ко вложенным уровням выбранных элементов является тривиальной задачей. Принадлежащая элементу ItemsControl привязка ItemsSource использует эту удобную функцию, дважды указывая CurrentItem в пути привязки: один раз для выбранного клиента, второй раз для выбранного заказа. Свойство CurrentItem является членом базового представления ICollectionView, являющегося оберткой источника данных, как уже говорилось выше.

    Существует еще один интересный момент, относящийся к различию в подходах к работе XML и бизнес-объекта. Поскольку пример XML привязывает к XmlElements, необходимо предоставить шаблоны DataTemplate, чтобы объяснить, как визуализировать клиентов и заказы. При привязке к специальным бизнес-объектам можно избежать этого лишнего труда, просто переопределив метод ToString классов Customer («Клиент») и Order («Заказ») и позволив WPF отобразить выходные данные этого метода для данных объектов. Этого фокуса достаточно лишь для объектов, у которых могут быть простые текстовые представления. При работе со сложными объектами данных использование этой удобного приема может не иметь смысла.

    Один элемент управления для отображения всей иерархии

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

    Заполнить представление TreeView в WPF элементами можно одним из двух способов. Первый способ - добавление элементов вручную в коде либо в коде XAML, а второй - создание их через привязку данных.

    В приведеном ниже коде XAML показано, как можно добавлять элементы TreeViewItem к представлению TreeView в коде XAML:







    Прием создания элементов в TreeView вручную имеет смысл в ситуациях, когда элемент управления всегда будет отображать небольшой и статичный набор элементов. При необходимости отображать большие объемы данных, которые могут меняться со временем, становится необходимым использование более динамического подхода. На этом этапе вариантов есть два. Можно написать код, который проходит по структуре данных, создает элементы TreeViewItem, основанные на найденных им объектах данных, и добавляет эти элементы к TreeView. В качестве альтернативы можно воспользоваться иерархическими шаблонами данных и возложить всю работу на WPF.

    Использование иерархических шаблонов данных

    То, как WPF следует визуализировать иерархические данные через иерархические шаблоны данных, можно выразить декларативно. Класс HierarchicalDataTemplate является средством, наводящим мост между сложной структурой данных и визуальным представлением этих данных. Он очень похож на нормальный DataTemplate, но также позволяет указать, откуда происходят дочерние элементы объекта данных. Также можно предоставить классу HierarchicalDataTemplate шаблон для визуализации этих дочерних элементов.

    Предположим, что теперь требуется отобразить данные, представленные наРис. 7 внутри одного элемента управления TreeView. Получившийся TreeView может выглядеть примерно так, как показано наРис. 9. Реализация этого включает использование двух HierarchicalDataTemplate и одного DataTemplate.

    Рис. 9. Изображение целой иерархии данных в TreeView

    Два иерархических шаблона отображают объекты Customer и Order. Поскольку у объектов OrderDetail нет дочерних элементов, их можно визуализировать с помощью неиерархического DataTemplate. Свойство ItemTemplate элемента TreeView использует шаблон для объектов типа Customer, поскольку объекты типа Customer и объекты данных содержатся в корневом уровне TreeView. В коде XAML, приведенном наРис. 10, показано, как собираются все части этой головоломки.

    Рис. 10. Код XAML в основе отображения TreeView




    xmlns:local="clr-namespace:VariousBindingExamples"
    ObjectType="{x:Type local:Customer}"
    MethodName="CreateCustomers"
    />





    Product:

    (

    )


    x:Key="OrderTemplate"
    ItemsSource="{Binding Path=OrderDetails}"
    ItemTemplate="{StaticResource OrderDetailTemplate}"
    >


    x:Key="CustomerTemplate"
    ItemsSource="{Binding Path=Orders}"
    ItemTemplate="{StaticResource OrderTemplate}"
    >


    ItemsSource="{Binding Path=.}"
    ItemTemplate="{StaticResource CustomerTemplate}"
    />

    Я выделяю коллекцию объектов Customer для DataContext таблицы (Grid), которая содержит представление TreeView. В коде XAML это можно проделать, используя ObjectDataProvider, являющийся удобным способом вызова метода из XAML. Поскольку DataContext наследуется вниз по дереву элементов, DataContext представления TreeView дает ссылку на этот набор объектов Customer. Именно по этой причине мы можем дать его свойству ItemsSource привязку "{Binding Path=.}", которая является способом указать, что свойство ItemsSource привязано к DataContext элемента TreeView.

    Если свойству ItemTemplate представления TreeView не было присвоено значение, то TreeView будет отображать только объекты Customer верхнего уровня. Поскольку WPF не известно, как визуализировать Customer, оно вызовет ToString на каждом Customer и отобразит этот текст для каждого элемента. У него не будет возможности выяснить, что у каждого Customer имеется список связанных с ним объектов Order, а у каждого объекта Order список объектов OrderDetail. Поскольку платформа WPF не может волшебным образом разобраться в существующей схеме данных, необходимо объяснить эту схему платформе WPF, чтобы она могла правильно визуализировать структуру данных.

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

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

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

    Работа со вводимыми пользователем данными

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

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

    Проверка ввода через ValidationRules

    В первой версии платформы WPF, являвшейся частью Microsoft® .NET Framework 3.0, имелась лишь ограниченная поддержка проверки ввода. У класса Binding имелось свойство ValidationRules, в котором могло храниться любое число классов, производных от ValidationRule. Каждое из этих правил могло содержать некоторую логику, проверяющую, является ли привязанное значение допустимым.

    В то время в WPF имелся лишь один подкласс правила ValidationRule, именуемый ExceptionValidationRule. Разработчики могли добавить это правило к свойству ValidationRules привязки, после чего оно улавливало исключения, выдаваемые в ходе обновлений источника данных, позволяя интерфейсу пользователя отображать сообщение об ошибке исключения. Полезность этого подхода к проверке ввода спорна, учитывая, что фундаментом хорошего обслуживания пользователей является предотвращение показа им ненужных технических деталей. Сообщения об ошибках в исключениях анализа данных обычно являются такими деталями для большинства пользователей, но вернемся к нашей теме.

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

    Public class Era
    {

    }

    Если необходимо позволить пользователю изменять дату начала и продолжительность эпохи, можно использовать два элемента управления «текстовое поле» и привязать их свойства Text к свойствам экземпляра Era. Поскольку пользователь может вводить в текстовое поле всё, что пожелает, нельзя быть уверенным, что введенный текст окажется преобразуемым в экземпляр DateTime или TimeSpan. В данном случае можно использовать ExceptionValidationRule для сообщения об ошибках в преобразовании данных и затем отобразить эти ошибки в интерфейсе пользователя. В коде XAML, приведенном наРис. 11, показано, как можно выполнить эту задачу.

    Рис. 11. Простой класс, представляющий эпоху времени


    Start Date:









    Duration:
    Grid.Row="3"
    Text="{Binding
    Path=Duration,

    />

    Эти два текстовых поля демонстрируют два способа, которыми ExceptionValidationRule можно добавить к свойству ValidationRules привязки в XAML. В текстовом поле Start Date («Дата начала») использован развернутый синтаксис элемента свойства, чтобы прямо добавить правило. В текстовом поле Duration («Продолжительность») использован сокращенный синтаксис, который просто устанавливает свойство привязки ValidatesOnExceptions на true. У обеих привязок свойство UpdateSourceTrigger установлено на PropertyChanged, чтобы ввод проверялся каждый раз, когда свойству текстового поля Text дается новое значение, вместо ожидания момента, когда элемент управления потеряет фокус. Снимок экрана программы показан наРис. 12.

    Рис. 12. ExceptionValidationRule отображает ошибки проверки

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

    Как показано наРис. 13, текстовое поле Duration содержит неверное значение. Содержащаяся в нем строка не преобразуема в экземпляр TimeSpan. Во всплывающем сообщении текстового поля отображается сообщение об ошибке, и маленький красный значок ошибки появляется на правой стороне элемента управления. Это поведение не является автоматическим, но его легко реализовать и подогнать под конкретный случай.

    Рис. 13. Визуализация ошибок, выявленных при проверке ввода, для пользователя




    DockPanel.Dock="Right"
    Margin="2,0"
    ToolTip="Contains invalid data"
    Width="10" Height="10"
    >











    Статический класс Validation формирует взаимоотношения между элементом управления и любыми содержащимися в нем ошибками проверки путем использования прикрепленных свойств и статических методов. На эти прикрепленные свойства можно сослаться в XAML, чтобы создать состоящие из одной разметки описания того, как интерфейс пользователя должен представлять пользователю ошибки, выявленные при проверке ввода. Код XAML наРис. 13 отвечает за объяснение того, как визуализировать сообщения об ошибках ввода для двух элементов управления текстовых полей из предыдущего примера.

    Style («Стиль») на Рис. 13 нацелен на все экземпляры текстового поля в интерфейсе пользователя Он применяет к текстовому полю три параметра. Первый, Setter («Метод установки») влияет на свойство Margin («Поля») текстового поля. Свойство Margin устанавливается на значение, которое предоставляет достаточно пространства для отображения значка ошибки с правой стороны.

    Следующее свойство Setter в Style присваивает ControlTemplate, используемый для визуализации текстового поля, когда то содержит неверные данные. Оно устанавливает присоединенное свойство Validation.ErrorTemplate на шаблон ControlTemplate, объявленный над Style. Когда класс Validation («Проверка») сообщает, что проверка выявила в текстовом поле одну или несколько ошибок, текстовое поле визуализирует сообщение с помощью этого шаблона. Именно это создает красный значок ошибки, показанный на Рис. 12.

    Style также содержит Trigger («Триггер»), отслеживающий прикрепленное к текстовому полю свойство Validation.HasError. Когда класс Validation устанавливает прикрепленное свойство HasError на true для определенного текстового поля, Trigger в Style активируется и присваивает текстовому полю всплывающее сообщение. Содержимое всплывающего сообщения привязано к сообщению об ошибке исключения, выданного при попытке преобразовать текст из текстового поля в экземпляр типа данных свойства источника данных.

    Проверка ввода через IDataErrorInfo

    С появлением Microsoft .NET Framework 3.5 поддержка проверки ввода в WPF радикально улучшилась. Подход ValidationRule полезен для простых приложений, но реальные приложения имеют дело со всей сложностью реальных данных и бизнес-правил. Кодирование бизнес-правил в объекты ValidationRule не только привязывает этот код к платформе WPF, но также и не позволяет бизнес-логике быть там, где ей и положено быть: в бизнес-объектах!

    У многих приложений имеется бизнес-слой, где все сложности обработки бизнес-правил заключены в набор бизнес-объектов. При компиляции в Microsoft .NET Framework 3.5 можно воспользоваться интерфейсом IDataErrorInfo, чтобы заставить WPF запрашивать бизнес-объекты о том, находятся ли они в допустимом состоянии или нет. Это избавляет от необходимости размещать бизнес-логику в объектах, отдельных от бизнес-слоя, и позволяет создавать бизнес-объекты, независимые от платформы интерфейса пользователя. Поскольку интерфейс IDataErrorInfo используется уже несколько лет, это также упрощает повторное использование бизнес-объектов из старых приложений Windows Forms или ASP.NET.

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

    Эти типы правил подобны общей идее бизнес-логики в том, что оба они являются экземплярами правил области. Правила области лучше всего создавать в объектах, которые сохраняют их состояние: объектах области. В коде, приведенном наРис. 14, показан класс SmartEra, предоставляющий сообщения о выявленных проверкой ошибках через интерфейс IDataErrorInfo.

    Рис. 14. IDataErrorInfo предоставляет сообщения о выявленных проверкой ошибках

    public class SmartEra
    : System.ComponentModel.IDataErrorInfo
    {
    public DateTime StartDate { get; set; }
    public TimeSpan Duration { get; set; }

    #region IDataErrorInfo Members

    public string Error
    {
    get { return null; }
    }

    public string this
    {
    get
    {
    string msg = null;
    switch (property)
    {
    case "StartDate":
    if (DateTime.Now < this.StartDate)
    msg = "Start date must be in the past.";
    break;

    case "Duration":
    if (this.Duration.Ticks == 0)
    msg = "An era must have a duration.";
    break;

    default:
    throw new ArgumentException(
    "Unrecognized property: " + property);
    }
    return msg;
    }
    }

    #endregion // IDataErrorInfo Members
    }

    Употребить поддержку проверки класса SmartEra из интерфейса пользователя WPF очень просто. Нужно лишь указать привязкам, что тем следует принять интерфейс IDataErrorInfo на объекте, к которому они привязаны. Это можно сделать одним из двух способов, как показано наРис. 15.

    Рис. 15. Употребление логики проверки


    Start Date:










    Duration:
    Grid.Row="3"
    Text="{Binding
    Path=Duration,
    UpdateSourceTrigger=PropertyChanged,
    ValidatesOnDataErrors=True,
    ValidatesOnExceptions=True}"
    />

    Подобно тому, как правило ExceptionValidationRule можно добавить к коллекции ValidationRules привязки явно или неявно, правило DataErrorValidationRule можно добавить прямо к ValidationRules привязки, или же можно установить свойство ValidatesOnDataErrors на true. Оба подхода дают один и тот же конечный эффект - система привязки запрашивает интерфейс IDataErrorInfo источника данных на предмет выявленных проверкой ошибок.

    Подводя итоги

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

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