Шаблоны и привязка данных

advertisement
Шаблоны элементов управления.......................................... 4
Логические и визуальные деревья................................4
Что собой представляют шаблоны ...............................9
Типы шаблонов .............................................................10
Классы Chrome .............................................................11
Разбиение элементов управления ..............................12
Создание шаблонов элементов управления ..............15
Простая кнопка..........................................................15
Привязка шаблонов ......................................................16
Триггеры шаблонов ......................................................18
Организация ресурсов шаблонов ...............................21
Рефакторизация шаблона элемента управления Button
Использование шаблонов со стилями ........................24
Автоматическое применение шаблонов .................26
Обложки, выбираемые пользователем ......................28
Создание более сложных шаблонов .......................30
Шаблоны, состоящие из множества частей ............32
Шаблоны элементов управления в ItemsControl ........33
Изменение полосы прокрутки ..................................36
Создание специального окна ......................................45
Привязка данных .................................................................. 61
Основы привязки данных .............................................61
Привязка к свойству элемента .................................62
Ошибки привязки ......................................................66
Создание привязки в коде ...........................................67
Множественные привязки ........................................68
Направление привязки .................................................72
OneWayToSource ......................................................74
22
Default ........................................................................76
Обновления привязки...................................................77
Привязка объектов, не являющихся элементами ......80
Source ........................................................................81
RelativeSource ...........................................................82
DataContext ................................................................84
Привязка пользовательских объектов к базе данных 86
Построение компонента доступа к данным ................87
Построение объекта данных ....................................92
Отображение привязанного объекта .......................96
Обновление базы данных ......................................104
Уведомление об изменениях .................................105
Привязка к коллекции объектов ................................107
Отображение и редактирование элементов коллекции
Привязка к выражению LINQ .....................................114
Преобразование данных ............................................119
Форматирование строк конвертером значений ....120
Создание объектов с конвертером значений .......127
Применение условного форматирования .................131
Оценка множественных свойств ...............................133
Проверка достоверности ...........................................135
Проверка достоверности в объекте данных .........137
Объекты данных и проверка достоверности ........138
ExceptionValidationRule ...........................................138
DataErrorValidationRule ...........................................140
Специальные правила проверки достоверности ..144
Реакция на ошибки проверки достоверности .......148
109
Получение списка исключений ..............................149
Отображение отличающегося индикатора ошибки152
Шаблоны данных, представления данных, поставщики данных 157
Кратко о привязке данных ..........................................158
Шаблоны данных ........................................................160
Отделение и многократное использование шаблонов164
Усовершенствованные шаблоны ..............................166
Варьирование шаблонов ...........................................171
Селекторы шаблонов .................................................172
Шаблоны и выбор .......................................................176
Селекторы стилей ......................................................181
Изменение компоновки элемента .............................184
Представления данных ..............................................184
Извлечение объекта представления .....................186
Фильтрация коллекций ...........................................186
Фильтрация объекта DataTable .............................189
Сортировка .................................................................190
Группирование............................................................191
Создание представлений декларативным образом 195
Навигация в представлении ......................................196
Поставщики данных ........................................................... 199
Объект ObjectDataProvider .....................................200
Обработка ошибок ..................................................201
Асинхронная поддержка .........................................201
Поставщик XmlDataProvider ...................................202
Библиографический список ............................................... 203
Шаблоны элементов управления
В прошлом разработчикам Windows-приложений приходилось делать выбор между
удобством и гибкостью элементов управления. Для получения максимального удобства они
могли использовать заранее заготовленные элементы управления. Эти элементы управления
работали достаточно хорошо, но предлагали очень мало возможностей для настройки и почти всегда имели фиксированный визуальный внешний вид. Иногда некоторые элементы
управления позволяли прорисовывать часть элемента управления путем реагирования на обратный вызов. Но базовые элементы управления — кнопки, текстовые поля, кнопки-флажки,
окна списков и т.д. — были полностью заблокированы.
В результате разработчикам, которым хотелось, чтобы все выглядело немного более
стильно, приходилось создавать специальные элементы управления с нуля. Это создавало
проблему, и не только потому, что написание необходимой логики для рисования вручную
требовало много времени и усилий, но и потому, что разработчикам специальных элементов
управления также нужно было самостоятельно реализовать даже базовые функциональные
возможности, например, выделение текста в текстовом поле или обработка нажатия клавиш
в кнопке. И даже после создания специальных элементов управления их вставка в существующее приложение предполагала выполнение большого объема редактирования, часто вынуждавшего вносить в код различные изменения.
В WPF проблема с настройкой элементов управления решена благодаря стилям и
шаблонам. Успех этих функциональных возможностей заключается в способе реализации
элементов управления, который в WPF значительно отличается. В предыдущих технологиях
разработки пользовательских интерфейсов, таких как Windows Forms, часто применяемые
элементы управления на самом деле не реализовались в коде .NET. Вместо этого классы
элементов управления Windows Forms упаковывали базовые элементы из API-интерфейса
Win32, которые являются жёстко заданными и неизменными. В WPF, как уже говорилось
выше, каждый элемент управления создается в чистом коде .NET. Это дает WPF возможность предоставлять механизмы, позволяющие проникать в эти элементы и настраивать или
даже полностью изменять их.
Логические и визуальные деревья
Ранее было уделено достаточно много времени рассмотрению модели содержимого
окна — другими словами тому, как можно вставлять элементы внутрь других элементов для
создания целого окна.
В качества примера возьмем чрезвычайно простое окно с двумя кнопками, показанное
на Рис. 1.
Рис. 1 Простое окно с тремя элементами
Создается такое окно путем вставки внутрь элемента Window элемента управления
StackPanel, размещения в нем двух элементов управления Button и добавления внутри каждого из этих элементов управления Button какого-то содержимого по выбору (в данном случае это две строки текста). Код разметки приведен ниже:
<Window x:Class="SimpleWindow.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SimpleWindow" Height="338" Width="356">
<StackPanel Margin="5">
<Button Padding="5" Margin="5" Click="cmd_Click">Первая
кнопка</Button>
<Button Padding="5" Margin="5" Click="cmd_Click">Кнопка номер
два</Button>
</StackPanel>
</Window>
Набор элементов, которые были добавлены, называется логическим деревом и показан на Рис. 2. Разработчикам WPF-приложений большую часть времени придется проводить именно за созданием таких логических деревьев и написанием необходимого для них
кода обработки событий. Фактически все рассмотренные выше функциональные возможности работают через логические деревья.
Однако если есть потребность переделать или настроить элементы, от логического
дерева пользы мало. Очевидно, что можно просто взять и заменить весь элемент каким-то
другим элементом (например, текущий элемент управления Button можно было бы заменить
специальным классом FancyButton), но это требует приложения дополнительных усилий и
чревато нарушением интерфейса приложения или его кода. Поэтому WPF предлагает дополнительное визуальное дерево.
Визуальное дерево представляет собой расширенную версию логического дерева. В
нем элементы разбиваются на еще более мелкие фрагменты. Другими словами, вместо аккуратно инкапсулированного черного прямоугольника вроде элемента управления Button в нем
будут отображаться визуальные компоненты этой кнопки, а именно — граница, придающая
кнопкам их отличительный затененный фон (представленная классом ButtonChrome), находящийся внутри контейнер (ContentPresenter) и блок с текстом кнопки (представленный
классом TextBlock). Для примера на Рис. 3 показано визуальное дерево для окна на Рис. 1.
Рис. 2 Логическое дерево окна
Рис. 3 Визуальное дерево окна
Все эти детали сами являются элементами — т.е., каждая отдельная деталь в элементе
управления Button представлена классом, который наследуется от FrameworkElement.
Визуальное дерево позволяет разработчикам пользовательских интерфейсов делать
две следующих полезных вещи:
 Изменять один из элементов в визуальном дереве с помощью стилей. Можно выбирать конкретный подлежащий изменению элемент с помощью свойства
Style.TargetType, а можно даже использовать триггеры и делать так, чтобы изменения вносились автоматически при изменении свойств того или иного элемента
управления. Однако есть детали, которые очень трудно или вообще невозможно
изменять.
 Создавать для элемента управления новый шаблон. В таком случае для построения
визуального дерева в точности тем образом, которым хочет разработчик, будет использоваться шаблон элемента управления.
Отметим, что WPF предоставляет два класса для работы с логическими и визуальными деревьями, а именно— класс System.Windows.LogicalTreeHelper и класс System.Windows.Media.VisualTreeHelper.
Класс LogicalTreeHelper уже демонстрировался выше, где он позволял подключать
обработчики событий в WPF-приложении с не скомпилированным, загружаемым динамически XAML-документом. Он предлагает относительно скудный набор методов, которые перечислены в Таблица 1. Хотя иногда эти методы и бывают полезными, в большинстве случаев
вместо них будут использоваться методы конкретного элемента FrameworkElement.
Таблица 1 Методы класса LogicalTreeHelper
Имя
FindLogicalNode()
BringIntoView()
GetParent()
GetChildren()
Описание
Отыскивает определенный элемент по имени, начиная поиск с указанного элемента и опускаясь далее вниз по логическому дереву.
Прокручивает элемент так, чтобы он стал видимым (если он находится
в поддерживающем прокручивание контейнере и в текущий момент не
виден). Метод FrameworkElement.BringIntoView() делает то же самое.
Извлекает родительский элемент указанного элемента.
Извлекает дочерний элемент указанного элемента. Как показывалось
выше, разные элементы поддерживают разные модели содержимого.
Например, панели поддерживают множество дочерних элементов, а
элементы управления содержимым — только один дочерний элемент.
Однако метод GetChildren() работает с элементами любого типа.
КлассVisualTreeHelper
предоставляет
несколько
похожих
методов—
GetChildrenCount(), GetChild() и GetParent() — вместе с небольшим набором методов, которые предназначены для выполнения низкоуровневого рисования.
Класс VisualTreeHelper также еще представляет собой интересный способ для изучения визуального дерева в приложении. С помощью его метода GetChild() можно разворачивать визуальное дерево любого окна и отображать его для рассмотрения. Это хорошее средство получения информации, и требует оно всего лишь небольшого фрагмента рекурсивного
кода.
На Рис. 4 показана одна из возможных реализаций, предполагающая отображение
всего визуального дерева в отдельном окне, начиная с любого предоставленного объекта. В
данном примере еще одно окно используется окном для отображения своего визуального дерева.
Рис. 4 Представление визуального дерева
Здесь окно с именем Window1 содержит элемент Border, который, в свою очередь, содержит AdornerDecorator. Класс AdornerDecorator обеспечивает поддержку для прорисовки
содержимого в декоративном слое, который представляет собой невидимую область, накладываемую поверх содержимого элемента. WPF использует этот слой для прорисовки деталей
вроде меток фокусировки и индикаторов перетаскивания. Внутри AdornetDecorator находится ContentPresenter. который вмещает содержимое окна. Это содержимое включает StackPanel с двумя элементами управления Button, каждый из которых состоит из класса ButtonChrome (который прорисовывает стандартный внешний вид кнопки) и класса ContentPresenter (хранящего содержимое кнопки). И, наконец, внутри класса ContentPresenter каждой кнопки находится класс TextBlock, который упаковывает текст, видимый в окне.
Ниже приведен весь код для окна VisualTreeDisplay.
public partial class VisualTreeDisplay : System.Windows.Window
{
public VisualTreeDisplay()
{
InitializeComponent();
}
public void ShowVisualTree(DependencyObject element)
{
// Clear the tree.
treeElements.Items.Clear();
// Start processing elements, begin at the root.
ProcessElement(element, null);
}
private void ProcessElement(DependencyObject element, TreeViewItem previousItem)
{
// Create a TreeViewItem for the current element.
TreeViewItem item = new TreeViewItem();
item.Header = element.GetType().Name;
item.IsExpanded = true;
// Check whether this item should be added to the root of the
tree
//(if it's the first item), or nested under another item.
if (previousItem == null)
{
treeElements.Items.Add(item);
}
else
{
previousItem.Items.Add(item);
}
// Check if this element contains other elements.
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
// Process each contained element recursively.
ProcessElement(VisualTreeHelper.GetChild(element, i),
item);
}
}
}
После добавления этого дерева в проект данный код можно будет использовать и из
любого другого окна для отображения его визуального дерева:
Проникать в визуальное дерево других приложений можно с помощью утилиты
Snoop, которая является частью Expression Blend и доступна отдельно по адресу
http://www.blois.us/Snoop/. Эта утилита позволяет изучать визуальное дерево любого WPFприложения, которое выполняется в текущий момент, а также увеличивать масштаб на любом элементе, наблюдать за маршрутизируемыми событиями по мере их возникновения и
изучать и даже изменять свойства элементов.
Что собой представляют шаблоны
Такой взгляд на визуальное дерево вызывает несколько интересных вопросов. Например, как элемент управления преобразуется из логического дерева в расширенное представление визуального дерева?
Оказывается, что у каждого элемента управления есть встроенный способ отображения, определяющий то, как он должен визуализироваться (в виде группы более базовых элементов). Этот способ отображения называется шаблоном элемента управления (control template) и определяется с помощью блока XAML-разметки.
Ниже приведена упрощенная версия шаблона для общего класса Button, без объявлений пространств имен XML, атрибутов, устанавливающих свойства вложенных элементов,
и триггеров, определяющих поведение кнопки при отключении, наведении на нее фокуса
или выполнении на ней щелчка кнопкой мыши.
<ControlTemplate …>
<mwt:ButtonChrome …>
<ContentPresenter …/>
</mwt:ButtonChrome>
<ControlTemplate.Triggers>
…
</ControlTemplate.Triggers>
</ControlTemplate>
Хотя классы ButtonChrome и ContentPresenter еще не рассматривались, очевидно, что
данный шаблон элемента управления предоставляет расширение, которое демонстрировалось в визуальном дереве. Класс ButtonChrome определяет стандартные визуальные объекты
кнопки, в то время как класс ContentPresenter удерживает любое содержимое, которое было
предоставлено. При желании разработать совершенно новую кнопку, нужно будет просто
создать новый шаблон элемента управления и использовать вместо класса ButtonChrome чтонибудь другое — например, собственный специальный класс или класс рисования, подобный
тем, что рассматривались выше.
Отметим, что класс ButtomChrome унаследован от класса Decorator. Это означает, что
он предназначен для добавления графического украшения вокруг другого элемента, в данном случае — вокруг содержимого кнопки.
Триггеры отвечают за изменение внешнего вида кнопки при наведении на нее фокуса,
выполнении на ней щелчка и ее отключении. В этих триггерах нет ничего особенного. Вместо того чтобы выполнять ответственную работу самостоятельно, триггеры фокуса и щелчка
просто изменяют значение соответствующего свойства класса ButtonChrome. который отвечает за предоставление визуальных объектов для кнопки:
<Trigger Property="UIElement.IsKeyboardFocused">
<Setter Property="mwt:ButtonChrome.RenderDefaulted" TargetName="Chrome">
<Setter.Value>
<s:Boolean>True</s:Boolean>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
<Trigger Property="ToggleButton.IsChecked">
<Setter Property="mwt:ButtonChrome.RenderPressed" TargetName="Chrome">
<Setter.Value>
<s:Boolean>True</s:Boolean>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
Первый триггер делает так, чтобы при получении кнопкой фокуса для свойства RenderDefaulted устанавливалось значение true, а второй— так, чтобы при выполнении щелчка
на кнопке значение true устанавливалось для свойства RenderPressed. И в том и в другом случае соответствующая настройка осуществляется самим классом ButtonChrome. Графические
изменения, которые имеют место, являются слишком сложными для того, чтобы быть представленными несколькими устанавливающими значения для свойств операторами.
Оба объекта Setter в данном примере используют свойство TargetName для воздействия на конкретный фрагмент шаблона элемента управления. Такой прием возможен только
при работе с шаблоном элемента управления. Т.е., написать триггер стиля, использующий
свойство TargetName для получения доступа к объекту ButtonChrome нельзя, потому что имя
"Chrome" не находится в области действия стиля.
Триггерам не всегда нужно использовать свойство TargetName. Например, триггер для
свойства IsEnabled просто регулирует цвет переднего плана любого текстового содержимого
в кнопке. Этот триггер выполняет свою работу путем установки значения для прикрепленного свойства TextElement. Foreground без помощи класса ButtonChrome:
<Trigger Property="UIElement.lsEnabled">
<Setter Property="TextElement.Foreground">
<Setter.Value>
<SolidColorBrush>#FFADADAD</SolidColorBrush>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
</Trigger>
С точно таким же разделением обязанностей придется иметь дело и при создании своих собственных шаблонов элементов управления. Если всю работу можно будет выполнить
непосредственно с помощью триггеров, тогда, возможно, создавать специальные классы и
добавлять код не придется. В противном случае, например, при необходимости обеспечить
более сложное визуальное размещение, возможно, доведется создавать собственный специальный класс Chrome. Класс ButtonChrome сам по себе не предусматривает никакой
настройки — он предназначен для визуализации стандартного, соответствующего теме
внешнего вида кнопки на Windows XP и Windows Vista.
Типы шаблонов
В WPF существуют три типа шаблонов, все из которых наследуются от базового класса FrameworkTemplate. Помимо шаблонов элементов управления (представленных классом
ControlTemplate), еще также имеются шаблоны данных (представленные классом DataTemplate и HierarchicalDataTemplate) и более специализированные шаблоны панели для ItemsControl (ItemsPanelTemplate). Шаблоны данных применяются для извлечения данных из
объекта и их отображения в элементе управления содержимым или отдельных элементах в
элементе управления типа списка. Шаблоны данных чрезвычайно полезны в сценариях привязки данных и будут рассмотрены ниже. В некоторой степени шаблоны данных и шаблоны
элементов управления похожи. Например, и те и другие позволяют вставлять дополнитель-
ные элементы, применять форматирование и т.д. Отличие заключается в том, что шаблоны
данных применяются для добавления элементов внутри существующего элемента управления. Заготовленные характеристики этого элемента управления не изменяются. А шаблоны
элементов управления, в свою очередь, представляют собой гораздо более серьезный подход,
позволяющий полностью переписывать модель содержимого элемента управления.
И, наконец, шаблоны панелей применяются для управления компоновкой элементов в
элементе управления типа списка (т.е. элементе управления, который наследуется от класса
ItemsControl). Например, их можно использовать для создания окна списка так, чтобы элементы в нем размещались не стандартным образом в виде одной строки слева направо, а
справа налево и вниз. Более подробно шаблоны панелей будут рассмотрены ниже. Естественно, можно комбинировать шаблоны разного типа в одном и том же элементе управления. Например, при желании создать гибкий элемент управления типа списка, связать его с
данными определенного типа, разместить в нем элементы каким-то нестандартным способом
и заменить типичную для него границу на что-нибудь привлекательное, потребуется создать
собственный шаблон (или даже несколько шаблонов) данных, собственный шаблон панели и
собственный шаблон элемента управления.
Классы Chrome
Класс ButtonChrome определен в пространстве имен Microsoft.Windows.Themes, в котором содержится относительно небольшой набор похожих классов, визуализирующих базовые детали Windows. Помимо класса ButtonChrome там также доступны классы BulletChrome
(предназначенный для кнопок типа флажков и переключателей), ScrollChrome (предназначенный для полос прокрутки), ListBoxChrome и SystemDropShadowChrome. Это самый нижний уровень API-интерфейса общедоступных элементов управления. Уровнем выше пространство имен System.Windows.Controls.Primitives включает ряд базовых элементов управления, которые можно использовать отдельно, но которые чаще всего упаковываются в более
полезные элементы управления. К их числу относятся элементы ScrollBar, ResizeGrip (предназначенный для изменения размеров окна), Thumb (перетаскиваемая кнопка на полосе прокрутки), TickBar (необязательный набор отметок на полосе) и т.д. По сути, пространство
имен System.Windows.Controls.Primitives предлагает базовые элементы, которые можно использовать во множестве различных элементов управления, и которые сами по себе не очень
полезны, в то время как пространство имен Microsoft.Windows.Themes содержит базовую логику рисования для визуализации этих деталей.
Хотя шаблоны элементов управления часто выполняют прорисовку поверх классов
Chrome, они не всегда должны делать это. Например, элемент ResizeGrip (предназначенный
для создания сетки из точек в правом нижнем углу допускающего изменение размера окна)
является достаточно простым для того, чтобы его шаблон мог использовать классы рисования, такие как Path.DrawingBrush и LinearGradientBrush. Ниже приведен пример кода разметки такого шаблона.
<ControlTemplate TargetType="ResizeGrip" …>
<Grid Background=" {TemplateBinding Panel.Background} " SnapsToDevicePixels="True">
<Path Margin="0, 0,2,2" Data="M9,0L11, 0 11, 11 0, 11 0, 9 3, 9
3, 6 6,6 6,3 9,3z" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Path.Fill>
<DrawingBrush ViewboxUnits="Absolute" TileMode="Tile"
Viewbox="0,0,3,3" Viewport="0,0,3,3" ViewportUnits="Absolute">
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Geometry="M0,0L2,0 2,2
0,2z">
<GeometryDrawing.Brush>
<LinearGradientBrush EndPoint="l,0.75" StartPoint="0,0 .25">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.3"
Color="#FFFFFFFF" />
<GradientStop Offset="0.75"
Color="#FFBBC5D7" />
<GradientStop Offset="l" Color="#FF6D83A9" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</GeometryDrawing.Brush>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Path.Fill>
</Path>
</Grid>
</ControlTemplate>
В заготовленном шаблоне элемента управления можно часто увидеть параметр SnapsToDevicePixels (который также может быть полезен и в специальных шаблонах). Как уже
отмечалось выше, этот параметр исключает вероятность размещения из-за независимости
WPF от параметров экранного разрешения однопиксельных строк "между" пикселями, что
приводит к созданию размытой двухпиксельной строки.
Разбиение элементов управления
Когда создается шаблон элемента управления, новый шаблон заменяет существующий полностью. Это обеспечивает высочайшую степень гибкости, но при этом создаёт новые сложности. В большинстве случаев перед созданием собственной адаптированной версии требуется просмотреть стандартный шаблон, который использует данный элемент
управления. В некоторых случаях специальный шаблон элемента управления может полностью повторять стандартный шаблон, за исключением какого - то одного незначительного
изменения.
В документации WPF код XAML стандартных шаблонов элементов управления не
приведен. Однако всю необходимую информацию можно получить программно. В целом
нужно сделать следующее: извлечь шаблон элемента управления из его свойства Template
(которое является частью класса Control) и затем сериализировать его в XAML с помощью
класс XamlWriter. На Рис. 5 показан пример с программой, которая отображает список всех
элементов управления WPF и позволяет просматривать шаблон каждого из них.
Рис. 5 Просмотр шаблонов элементов управления WPF
Основой для создания такого приложения является рефлексия — API-интерфейс .NET
для исследования типов. Когда в этом приложении впервые загружается главное окно, сначала выполняется сканирование всех типов в базовой сборке PresentationFramework.dll (в которой как раз и находится определение класса Control), затем эти типы добавляются в коллекцию, где они сортируются по имени, после чего эта коллекция привязывается к списку.
private void Window_Loaded(object sender, EventArgs e)
{
Type controlType = typeof(Control);
List<Type> derivedTypes = new List<Type>();
// Search all the types in the assembly where the Control
class is defined.
Assembly assembly = Assembly.GetAssembly(typeof(Control));
foreach (Type type in assembly.GetTypes())
{
// Only add a type of the list if it's a Control, a concrete class, and public.
if (type.IsSubclassOf(controlType) && !type.IsAbstract &&
type.IsPublic)
{
derivedTypes.Add(type);
}
}
// Sort the types by type name.
derivedTypes.Sort(new TypeComparer());
// Show the list of types.
lstTypes.ItemsSource = derivedTypes;
}
Всякий раз, когда из списка выбирается какой-нибудь элемент управления, в текстовом поле справа сразу же появляется соответствующий шаблон. Этот шаг требует приложения дополнительных усилий. Первая трудность состоит в том, что шаблон элемента
управления имеет значение null до тех пор, пока этот элемент управления не будет фактиче-
ски отображен в окне. Поэтому с помощью рефлексии код пытается создать экземпляр элемента управления и добавить его в текущее окно. Вторая трудность состоит в преобразовании активного объекта ControlTemplate в XAML-разметку. О выполнении этой задачи заботится статический метод XamlWriter.Save(), хотя код использует объекты XmlWriter и
XmlWriterSettings для обеспечения наличия в XAML отступов, повышающих степень удобочитаемости XAML. Весь этот код размещается в блоке обработки исключений, который решает проблемы, возникающие из-за элементов управления, которые нельзя создавать или
добавлять в элементе управления Grid.
private void lstTypes_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
try
{
// Get the selected type.
Type type = (Type)lstTypes.SelectedItem;
// Instantiate the type.
ConstructorInfo info =
type.GetConstructor(System.Type.EmptyTypes);
Control control = (Control)info.Invoke(null);
// Add it to the grid (but keep it hidden).
control.Visibility = Visibility.Collapsed;
grid.Children.Add(control);
// Get the template.
ControlTemplate template = control.Template;
// Get the XAML for the template.
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
StringBuilder sb = new StringBuilder();
XmlWriter writer = XmlWriter.Create(sb, settings);
XamlWriter.Save(template, writer);
// Display the template.
txtTemplate.Text = sb.ToString();
// Remove the control from the grid.
grid.Children.Remove(control);
}
catch (Exception err)
{
txtTemplate.Text = "<< Error generating template: " +
err.Message + ">>";
}
}
}
Несложно расширить это приложение так, чтобы иметь возможность отредактировать
шаблон в текстовом поле, преобразовывать его обратно в объект ControlTemplate (с помо-
щью XamlReader) и затем назначить этот объект элементу управления для того, чтобы увидеть результат. Однако тестирование и совершенствование шаблонов элементов управления
гораздо легче осуществлять, позволяя им отображаться в реальном окне, о чем более подробно будет рассказываться ниже.
Создание шаблонов элементов управления
Теперь рассмотрим процесс создания собственного шаблона на примере простой специальной кнопки.
Как уже упоминалось, базовый элемент управления Button использует класс ButtonChrome для прорисовки своего специфического фона и границы. Одной из причин того, почему Button использует ButtonChrome. а не графические примитивы WPF, является то, что
внешний вид стандартной кнопки зависит от нескольких явных характеристик (того, не отключена ли она, не расположен ли на ней фокус или не выполняется ли на ней щелчок) и
других менее заметных факторов (например, текущей темы Windows). Реализовать такую
логику с помощью одних только триггеров было бы неудобно.
Однако в WPF больше беспокоятся о том, чтобы создать привлекательные оригинальные элементы управления, которые бы сочетались с остальной частью пользовательского интерфейса. Поэтому создавать классы вроде ButtonChrome может быть и не нужно. Вместо
этого можно воспользоваться графическими элементами, описанными выше и навыками работы с анимацией, которая будет показана ниже, и создать самодостаточный шаблон элемента управления безо всякого кода.
Простая кнопка
Применяется специальный шаблон элемента управления просто путем установки для
элемента управления свойства Template. Хотя и можно определять внутристроч- ный шаблон
(путем вставки шаблона внутри дескриптора элемента управления), такой подход редко бывает подходящим. Всё дело в том, что практически всегда требуется использовать шаблон
повторно для создания множества экземпляров одного и того же элемента управления. Для
получения такого дизайна необходимо определить шаблон элемента управления как ресурс и
сослаться на него с помощью ссылки StaticResource, как показано ниже:
<Button Template="{StaticResource ButtonTemplate}" >Templated Button</Button>
Такой подход не только упрощает создание целого набора специализированных кнопок, но и обеспечивает возможность изменения шаблона элемента управления в будущем без
нарушения остальной части пользовательского интерфейса.
Конкретно в данном примере ресурс ButtonTemplate размещается в коллекции Resources содержащего окна. Однако в реальном приложении разработчик, скорее всего, предпочтет использовать ресурсы приложения.
Ниже приведена базовая структура шаблона элемента управления:
<Window.Resources>
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
</ControlTemplate>
</Window.Resources>
Отметим, что в каждом шаблоне элемента управления свойство TargetType устанавливается так, чтобы оно явно указывало на то, что этот шаблон предназначен для определённого типа элементов управления, в данном примере - кнопок. В случае стиля придерживаться
такого соглашения всегда неплохо. В случае элементов управления содержимым, таких как
кнопка, это также является обязательным, поскольку иначе не будет работать ContentPresenter.
Чтобы создать шаблон для простой кнопки, необходимо нарисовать свою собственную границу и фон, а затем разместить внутри кнопки подходящее содержимое. Двумя
возможными кандидатами для рисования границы являются классы Rectangle и Border. Ниже
приведен пример, в котором класс Border используется для объединения закругленного
оранжевого контура с бросающимся в глаза красным фоном и белым текстом:
<ControlTemplate x:Key="ButtonTempiate" TargetType="{x:Type Button}">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
Background="Red" TextBlock.Foreground="White">
</Border>
</ControlTemplate>
Это решает задачу с фоном, но все равно еще нужно как-то отобразить содержимое
кнопки. Выше говорилось, что у класса Button в шаблоне элемента управления имеется элемент ContentPresenter. Элемент ContentPresenter является обязательным для всех элементов
управления — он представляет собой маркер типа "вставить содержимое здесь", показывающий WPF. где следует размещать содержимое:
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
Background="Red" TextBlock.Foreground="White">
<ContentPresenter RecognizesAccessKey="True"></ContentPresenter>
</Border>
</ControlTemplate>
В данном случае ContentPresenter устанавливает для свойства RecognizesAccessKey
значение true. Это не является обязательным, но гарантирует, что кнопка будет поддерживать клавиши доступа (access key) — подчеркнутые буквы, которые можно использовать для быстрой инициации кнопки. В данном случае при наличии у кнопки текста
вроде "Click _Ме" пользователь сможет инициировать кнопку путем нажатия комбинации
клавиш <Alt+M>. Если не установить для свойства RecognizesAccessKey значение true, эта
деталь будет игнорироваться, и все символы подчеркивания будут восприниматься как
обычные символы подчеркивания и отображаться в виде части содержимого кнопки.
Привязка шаблонов
В приведенном примере все равно еще остался один небольшой нерешенный вопрос.
Сейчас в дескрипторе, который был добавлен для кнопки, для свойства Margin указано значение 10. а для свойства Padding — 5. Свойство Margin берется в расчет элементом StackPanel, а вот свойство Padding игнорируется, в результате чего содержимое кнопки остается
«размазанным» по сторонам. Проблема здесь заключается в том, что свойство Padding не
имеет никакого эффекта до тех пор. пока на него не будет специальным образом обращено
внимание в шаблоне. Другими словами, извлечение значения Padding и его использование
для вставки вокруг содержимого какого-нибудь дополнительного пространства зависит от
шаблона.
В WPF есть средство, предназначенное как раз для этой цели, и называется оно привязками шаблона (template bindings). С помощью привязок шаблон может извлекать значение
из элемента управления, к которому он применяется. В рассматриваемом примере с помощью привязки можно извлечь значение из свойства Padding и использовать его для создания
поля вокруг ContentPresenter:
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
Background="Red" TextBlock.Foreground="White" Name="Border">
<ContentPresenter RecognizesAccessKey="True"
Margin="{TemplateBinding Padding}"></ContentPresenter>
</Border>
</ControlTemplate.Triggers>
</ControlTemplate>
Это позволит добиться желаемого эффекта, т.е. добавления между границей и содержимым небольшого пространства. Полученная в результате новая кнопка показана на Рис. 6.
Рис. 6 Кнопка с привязкой шаблона
Привязки шаблонов похожи на обычные привязки данных, которые будут рассмотрены ниже, но являются более простыми, поскольку предназначены специально для использования в шаблонах элементов управления. Они поддерживают только привязку данных
только в одном направлении, т.е., могут передавать информацию из элемента управления в
шаблон, но не наоборот и не могут применяться для прорисовывания информации из свойства класса, который наследуется от класса Freezable. В ситуации, где привязки шаблонов не
работают, вместо них могут запросто использоваться многофункциональные привязки данных.
Отметим, что привязки шаблонов поддерживают инфраструктуру мониторинга изменений WPF, которая встроена во все свойства зависимостей. Это означает, что в случае изменения какого-нибудь свойства в элементе управления шаблон принимает это во внимание
автоматически. Эта деталь особенно полезна, когда используются анимации, которые за короткий промежуток времени успевают многократно изменять значение свойства.
Единственный способ, которым можно узнать, какие привязки шаблона необходимы
— это просмотреть используемый по умолчанию шаблон элемента управления. Если вернуться к шаблону элемента управления для класса Button, то можно увидеть, что привязка в
нем выглядит точно так же. как и в приведенном специальном шаблоне, т.е. подразумевает
извлечение указанного для кнопки значения Padding и его преобразование в поле вокруг
ContentPresenter. Также можно отметить, что стандартный шаблон кнопки включает еще несколько привязок (HorizontalAlignment. VerticalAlignment и Background), которых в простом
специализированном шаблоне нет. Это означает, что в случае установки для кнопки этих
свойств, в таком шаблоне они не будут иметь никакого эффекта.
Во многих случаях пропуск привязок в шаблоне не представляет проблемы. Свойство
вообще не нужно привязывать, если его не планируется использовать или если не требуется,
чтобы оно изменяло шаблон. Например, в том, что текущая простая кнопка устанавливает
для свойства Foreground текста значение white и игнорирует любое значение, устанавливаемое для свойства Background, есть смысл, поскольку цвет переднего плана и цвет фона являются важной частью внешнего вида этой кнопки.
Существует еще одна причина, по которой привязки шаблона может быть лучше не
использовать: элемент управления может не поддерживать их адекватным образом. Например, если нужно устанавливать фон для свойства Background кнопки, то можно заметить, что
при нажатии кнопки этот фон обрабатывается несогласованным образом. То же самое касается и специального шаблона, показанного в рассмотренном выше примере. Хотя поведение
при наведении курсора и нажатии кнопки мыши в нем еще не определено, после добавления
этих деталей разработчик наверняка захочет иметь полный контроль над цветами и тем, как
они должны изменяться в различных состояниях.
Триггеры шаблонов
Если испробовать созданную выше кнопку на практике, большого практического
смысла это не принесет. По сути, это будет просто красный прямоугольник с закругленными
углами, никак не реагирующий видимым образом на наведение курсора и щелчок кнопкой
мыши. Кнопка будет отображаться, но она будет абсолютно инертной.
Эту проблему можно устранить, добавив в шаблон элемента управления соответствующие триггеры. О триггерах уже рассказывалось выше, при рассмотрении стилей.
Там отмечалось, что триггеры могут применяться для изменения одного или более свойств
при изменении другого свойства. В случае кнопки может потребоваться реакция на изменение как минимум двух свойств, а именно— isMouseOver и IsPressed. Ниже показана переделанная версия шаблона элемента управления, в которой изменяются цвета при изменении
соответствующих свойств:
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
Background="Red" TextBlock.Foreground="White" Name="Border">
<Grid>
<ContentPresenter RecognizesAccessKey="True"
Margin="{TemplateBinding Padding}"></ContentPresenter>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="DarkRed" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Border" Property="Background" Value="IndianRed" />
<Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Здесь присутствует еще одно изменение, позволяющее данному шаблону работать.
Элементу Border было дано имя, которое используется при установке значения для свойства
TargetName в каждом обьекте Setter. Благодаря этому Setter может обновлять указанные в
шаблоне свойства Background и BorderBrush. Использование имен является самым простым
способом сделать так, чтобы обновлялась только одна конкретная часть шаблона. Конечно,
можно было бы и создать правило, учитывающее тип элемента и влияющее на все элементы
Border (поскольку известно, что в шаблоне кнопки присутствует только одна единственная
граница), но такой подход является более простым и гибким на случай, если вдруг позже
возникнет необходимость изменить шаблон.
В кнопке и в большинстве других органов упралвения обязательно должен присутствовать ещё один элемент. Речь идет об индикаторе фокусировки. Изменить существующую границу так, чтобы она включала эффект фокусировки, нельзя, но можно легко добавить другой показывающий его элемент и с помощью триггера сделать так, чтобы этот элемент отображался или скрывался на основании значения свойства Button.IsKeyboardFocused.
Хотя существует много различных способов для организации эффекта фокусировки, в следующем примере просто добавляется прозрачный элемент Rectangle с пунктирной границей.
Элемент Rectangle не способен удерживать дочернее содержимое, поэтому нужно сделать
так, чтобы он перекрывал все остальное содержимое. Легче всего это сделать, упаковав Rec-
tangle и ContentPresenter в состоящий из одной ячейки элемент управления Grid, т.е. разместив оба этих элемента в одной и той же ячейке.
Ниже приведен переделанный код шаблона с поддержкой эффекта фокусировки:
<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
<Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
Background="Red" TextBlock.Foreground="White" Name="Border">
<Grid>
<Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
StrokeThickness="1" StrokeDashArray="1 2"
SnapsToDevicePixels="True" ></Rectangle>
<ContentPresenter RecognizesAccessKey="True"
Margin="{TemplateBinding Padding}"></ContentPresenter>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="DarkRed" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Border" Property="Background" Value="IndianRed" />
<Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Объект Setter отыскивает элемент, который ему нужно изменить, с помощью свойства
TargetName. В данном примере оно указывает на прямоугольник FocusCue.
Такой прием сокрытия или отображения элементов в ответ на триггер является полезным компоновочным блоком во многих шаблонах. Его можно использовать для замены
визуальных аспектов элемента управления при изменении его состояния на что-нибудь абсолютно другое.
На Рис. 7 показаны три кнопки, использующие данный переделанный шаблон. На
второй кнопке в текущий момент находится фокус (представленный в виде пунктирного
прямоугольника), в то время как на третью наведен курсор мыши. Триггер, используемый
последней кнопкой, будет показан ниже.
Рис. 7 Применение триггеров шаблона
Для полного завершения образа этой кнопки осталось добавить дополнительный
триггер, изменяющий фон кнопки (и. возможно, цвет текста) при получении свойством IsEnabled кнопки значения false:
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="TextBlock.Foreground"
Value="Gray" />
<Setter TargetName="Border" Property="Background" Value="MistyRose" />
</Trigger>
Для гарантии того, что это правило будет иметь преимущественное значение по сравнению с любыми конфликтующими параметрами триггеров, его следует определить в конце
списка триггеров. В таком случае, даже если у свойства IsMouseOver тоже будет значение
true, первым все равно будет обрабатываться триггер свойства IsEnabled, и кнопка будет
оставаться неактивной.
Очевидно, что между шаблонами и стилями существует определенная схожесть. И те
и другие позволяют изменять внешний вид элемента, обычно по всему приложению. Однако
область действия стилей является куда более ограниченной. Они способны только подстраивать свойства элемента управления, но не заменять его совершенно новым визуальным деревом, состоящим из различных элементов.
Показанная простая кнопка уже включает функциональные возможности, воспроизвести которые с помощью одних лишь стилей невозможно. Хотя использовать стили для установки фона кнопки и можно было бы, это бы усложнило корректировку фона при нажатии
кнопки, поскольку во встроенном шаблоне кнопки уже имеется триггер для такой цели. Добавить представляющий эффект фокуса прямоугольник тоже было бы не просто.
Однако шаблоны еще также предоставляют возможность и для создания множества
других, более экзотических типов кнопок, создание которых с помощью стилей является вообще немыслимым. Например, вместо того чтобы применять прямоугольную границу, можно создать кнопку, имеющую форму наподобие эллипса и использующую путь для прорисовки более сложной фигуры. Все, что для этого нужно — классы рисования, которые рассматривались выше. В остальной код разметки — и даже триггеры, переключающие фон с
одного состояния в другое — придется внести относительно небольшое количество изменений.
Организация ресурсов шаблонов
Применение шаблонов элементов управления требует принятия решения о том, насколько широко должно распространяться действие этих шаблонов и должны ли они применяться автоматически или явно.
Первое подразумевает продумывание места использования шаблонов. Например,
необходимо решить, должны ли шаблоны применяться только в каком-то определенном окне
или во всём приложении. В большинстве ситуаций шаблоны элементов управления подходят
для множества окон, а то и для всего приложения. Чтобы не определять их более одного раза, можно определить их в коллекции Resources класса Application, как соответствующем
разделе.
Однако при таком решении возникает другая проблема. Часто шаблоны элементов
управления разделяются между приложениями. Вполне возможно, чтобы в одном приложении использовались шаблоны, которые разрабатывались отдельно. Однако приложение может иметь только один файл App.xaml и одну коллекцию Application.Resources. Именно поэтому ресурсы лучше определять в отдельных словарях ресурсов. Это дает возможность вводить их в действие в конкретных окнах или во всем приложении, а также позволяет комбинировать стили, поскольку любое приложение может иметь множество словарей ресурсов.
Добавить словарь ресурсов в Visual Studio можно, щелкнув правой кнопкой мыши на проекте
в окне Solution Explorer и выбрав сначала команду Add -> New Item (Добавить -> Новый элемент), а затем— вариант Resource Dictionary (WPF) (Словарь ресурсов (WPF)) (см. Рис. 8).
Рис. 8 Добавление словаря ресурсов
О словарях ресурсов уже рассказывалось выше. Добавление словаря ресурсов добавляет в приложение новый XAML-файл с приблизительно таким содержимым:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTemplate x:Key="ButtonTemplate" TargetType="Button">
</ControlTemplate>
</ResourceDictionary>
Хотя вариант объединения всех шаблонов в одном единственном файле словаря ресурсов и является допустимым, опытные разработчики предпочитают создавать для каждого
шаблона элемента управления отдельный словарь ресурсов. Дело в том, что шаблон элемента управления может очень быстро становиться довольно сложным и начинать задействовать
ряд других связанных ресурсов. Хранение таковых в одном месте, но в отдельности от
остальных элементов управления считается хорошей организацией.
Чтобы использовать словарь ресурсов, нужно просто добавить его в коллекцию Resources конкретного окна или (что встречается чаще) приложения. Делается это с помощью
коллекции MergedDictionaries. Например, в случае, если шаблон кнопки находится в файле с
именем Button. xaml в подпапке проекта под названием Resources, в файле App.xaml можно
использовать такой код разметки:
<Application x:Class="ControlTemplates.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Menu.xaml"
>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="Resources\CustomWindowChrome.xaml"></ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Рефакторизация шаблона элемента управления Button
По мере усовершенствования и расширения шаблона элемента управления может обнаружиться, что он упаковывает ряд различных деталей вроде специальных фигур, геометрических объектов и кистей. Такие детали будет лучше извлечь из шаблона и определить в
виде отдельных ресурсов. Одной из причин для такого шага может послужить упрощение
повторного использования этих кистей в ряде связанных элементов управления. Например,
может возникнуть необходимость создать специальные элементы управления Button, CheckBox и RadioButton. использующие похожий набор цветов. Упростить эту задачу можно будет, создав для кистей отдельный словарь ресурсов (с именем Brushes. xaml) и объединив их
в один словарь ресурсов дтя каждого из элементов управления (таких как Button.xaml.
CheckBox.xaml и RadioButton.xaml).
Для демонстрации примера используем код разметки, представленный ниже. В нем
представлен весь словарь ресурсов для кнопки, включающий и те ресурсы, что использует
шаблон элемента управления, а также сам шаблон элемента управления и правило стиля,
применяющее шаблон элемента управления к каждой кнопке в приложении. Это тот порядок, который следует соблюдать всегда, поскольку ресурс можно использовать только после
того, как он уже определен.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlTemplates.GradientButton"
>
<!-- Resources used by the template. -->
<RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
x:Key="HighlightBackground">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Blue" Offset=".4" />
</RadialGradientBrush>
<RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
x:Key="PressedBackground">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Blue" Offset="1" />
</RadialGradientBrush>
<SolidColorBrush Color="Blue"
x:Key="DefaultBackground"></SolidColorBrush>
<SolidColorBrush Color="Gray"
x:Key="DisabledBackground"></SolidColorBrush>
<RadialGradientBrush RadiusX="1" RadiusY="5" GradientOrigin="0.5,0.3"
x:Key="Border">
<GradientStop Color="White" Offset="0" />
<GradientStop Color="Blue" Offset="1" />
</RadialGradientBrush>
<!-- The button control template. -->
<ControlTemplate x:Key="GradientButtonTemplate" TargetType="{x:Type
Button}">
<Border Name="Border" BorderBrush="{StaticResource Border}" BorderThickness="2"
CornerRadius="2" Background="{StaticResource DefaultBackground}"
TextBlock.Foreground="White">
<Grid>
<Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
StrokeThickness="1" StrokeDashArray="1 2" SnapsToDevicePixels="True">
</Rectangle>
<ContentPresenter Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"></ContentPresenter>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource HighlightBackground}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource PressedBackground}" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="FocusCue" Property="Visibility"
Value="Visible"></Setter>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border" Property="Background"
Value="{StaticResource DisabledBackground}"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!-- The style that applies the button control template. -->
<Style TargetType="{x:Type Button}">
<Setter Property="Control.Template"
Value="{StaticResource GradientButtonTemplate}"></Setter>
</Style>
</ResourceDictionary>
На Рис. 9 показана определяемая этим шаблоном кнопка. В этом примере при наведении пользователем на кнопку курсора мыши используется градиентная заливка. Однако градиентная заливка всегда размещается по центру кнопки. При желании создать более экзотический эффект, например, градиентную заливку, которая следует за курсором мыши, потребуется использовать анимацию или писать код.
Рис. 9 Кнопки с градиентной заливкой
Использование шаблонов со стилями
У такого дизайна есть одно ограничение. Шаблон элемента управления, по сути,
жестко кодирует ряд деталей вроде цветовой схемы. Это означает, что при желании использовать в кнопке ту же самую комбинацию элементов (Border, Grid, Rectangle и ContentPresenter) и организовать их тем же самым образом, но при этом предоставить другую
цветовую схему, придется создавать новую копию шаблона, ссылающуюся на другие ресурсы кисти.
Это необязательно будет проблемой, однако может действительно ограничить возможность повторного использования данного шаблона элемента управления. Если шаблон
содержит сложную комбинацию элементов, которую точно нужно будет применять многократно с разными деталями форматирования (каковым чаше всего оказываются цвета и
шрифты), тогда лучше извлечь эти детали из шаблона и поместить их в стиль.
Чтобы это получилось, шаблон придется переделать. Вместо того чтобы использовать
жестко закодированные цвета, нужно будет извлечь эту информацию из свойств элемента
управления с помощью привязок шаблона. Ниже приведен пример упрощенного шаблона
для показанной ранее специализированной кнопки. В этом шаблоне некоторые детали, а
именно — поле фокуса и граница с закругленными углами и толщиной в 2 единицы — воспринимаются как фундаментальные и не изменяющиеся. Кисти фона и границы, однако, являются конфигурируемыми. Единственный оставшийся триггер отвечает за отображение поля фокуса:
<ControlTemplate x:Key="CustomButtonTemplate" TargetType="{x:Type
Button}">
<Border Name="Border" BorderThickness="2" CornerRadius="2"
Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding
BorderBrush}">
<Grid>
<Rectangle Name="FocusCue" Visibility="Hiddenn
Stroke=nBlack" StrokeThickness="l" StrokeDashArray="l 2" SnapsToDevicePixels="True"></Rectangle>
<ContentPresenter Margin="{TemplateBinding Padding}"
RecognizesAccessKey="True"></ContentPresenter>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="FocusCue" Property="Visibility"
Value="Visible"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Связанный стиль применяет этот шаблон элемента управления, устанавливает цвета
границы и фона и добавляет триггеры для изменения фона в зависимости от состояния кнопки:
<Style x:Key="CustomButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Control.Template" Value="{StaticResource
CustomButtonTemplate}"></Setter>
<Setter Property="BorderBrush" Value="{StaticResource Border}"></Setter>
<Setter Property="Background" Value="{StaticResource DefaultBackground}"></Setter>
<Setter Property="TextBlock.Foreground" Value="White"></Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource
HighlightBackground}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{StaticResource
PtessedBackground}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value=" {StaticResource
DisabledBackground)"></Setter>
</Trigger>
</Style.Triggers>
</Style>
Отметим, что если установить триггеры и в шаблоне элемента управления, и в стиле,
предпочтение будет отдаваться триггерам стиля.
Чтобы использовать этот новый шаблон, нужно установить не свойство Template, а
свойство Style:
<Button Margin="10" Padding="5" Style="{StaticResource CustomButtonStyle}"> A Simple Button with a Custom Template </Button>
Теперь можно создать новые стили, использующие тот же шаблон, но привязываемые
к другим стилями для применения новой схемы цветов.
У такого подхода есть одно важное ограничение. Использовать свойство Setter.TargetName в этом стиле нельзя, поскольку он не содержит шаблона элемента управления,
а просто ссылается на него. Поэтому возможности стиля и триггеров являются несколько
ограниченными. Он не могут проникать глубоко в визуальное дерево для изменения того или
иного аспекта вложенного элемента. Вместо этого стилю нужно устанавливать свойство
элемента управления, а вложенному в этот элемент управления элементу — привязывать это
свойство с помощью привязки шаблона.
Обе указанных проблемы, а именно — необходимость определять поведение элемента
управления в стиле с помощью триггеров и отсутствие возможности воздействовать на конкретные элементы — можно обойти путем создания специального шаблона. Например, можно создать класс, унаследованный от класса Button и включающий такие дополнительные
свойства, как HighlightBackground, DisabledBackgroundи PressedBackground, а затем выполнить привязку к этим свойствам в шаблоне элемента управления и просто установить для
них значения в стиле безо всяких триггеров. Однако такой подход имеет свои недостатки. Он
вынуждает применять в пользовательском интерфейсе другой элемент управления (например, не просто Button, a CustomButton). Это усложняет процесс разработки приложения.
Обычно со специальных шаблонов элементов управления на специальные элементы
управления переходят в одном из следующих случаев.

Если элемент управления требует серьезных изменений в его функциональности. Например, имеется специальная кнопка, и эта кнопка добавляет новые функциональные возможности, которые требуют использования новых свойств и методов.

Если планируется сделать элемент управления доступным в отдельной сборке
библиотеки классов, чтобы его можно было использовать и настраивать для множества других приложений. В таком случае требуется более высокий уровень стандартизации, чем возможен при применении одних только шаблонов элементов управления.
Автоматическое применение шаблонов
В текущем примере каждая кнопка сама отвечает за
подключение к соответствующему шаблону с помощью
свойства Template или Style. Такой подход имеет смысл, если шаблон элемента управления применяется для создания
определенного эффекта в определенном месте приложения.
Но он менее удобен, если специальный внешний вид требуется обеспечить для каждой кнопки во всем приложении. В
таком случае, скорее всего, возникнет необходимость сделать так, чтобы все кнопки в приложении получили новый
шаблон автоматически. Добиться такого эффекта можно путем применения шаблона со стилем.
Данная возможность заключается в использовании типизированного стиля, влияющего на элементы соответствующего
типа автоматически и устанавливающего свойство Template.
Ниже приведен пример стиля, который нужно поместить в
коллекцию ресурсов словаря ресурсов для придания кнопкам нового вида:
<Style TargetType="{x:Type Button}">
<Setter Property="Control.Template"
Value="{StaticResource ButtonTemplace}"></Setter>
</Style>
Этот код будет работать, поскольку имя ключа в стиле не указывается, а это значит, что вместо него будет использоваться тип элемента (в данном случае - Button).
Следует помнить, что от данного стиля все равно
можно отказаться, создав кнопку и явным образом установив для ее свойства Style значение null:
<Button Style="{x:Null}" ...> </Button>
Возможности этого подхода впечатляют. Он позволяет взять существующее приложение WPF и полностью изменить внешний вид всех его элементов управления, ни разу
не коснувшись пользовательского интерфейса. Все, что он
требует сделать — это просто добавить в проект словари ре-
сурсов и затем объединить их в коллекцию Application. Resources. Такая комбинация стилей и шаблонов элементов
управления предоставляет возможность, не прилагая никаких особых усилий и не занимаясь написанием кода, придавать определенный внешний вид, то есть изменять "обложку" любого приложения.
Обложки, выбираемые пользователем
В некоторых приложениях бывает необходимо, чтобы шаблоны сменялись динамически, как правило, в ответ
на предпочтения пользователя. Добиться такого эффекта довольно легко, но документации на эту тему практически нет.
Основным способом является загрузка нового словаря во
время выполнения и его использование для замены текущего словаря ресурсов. (Заменять все ресурсы вовсе необязательно, главное заменить те, что используются для обложки.)
Секрет заключается в извлечении объекта ResourceDictionary, который компилируется и вставляется в
приложение как ресурс. Для загрузки необходимых ресурсов
класс ResourceManager.
Например, предположим, что создано два ресурса,
определяющие альтернативные версии одного и того шаблона элемента управления Button. Один хранится в файле с
именем GradientButton.xaml, а другой — в файле с именем
GradientButtonVariant.xaml. Оба файла, для наилучшей организации, находятся в текущем проекте в подпапке Resources.
Теперь можно создать простое окно, использующее
один из этих ресурсов, с помощью такой коллекции Resources:
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="Resources/GradientButton.xaml"></Resour
ceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
После этого можно переключиться на другой словарь
ресурсов с помощью следующего кода:
ResourceDictionary resourceDictionary = new ResourceDictionary();
resourceDictionary.Source = new
Uri(
"Resources/GradientButtonVariant.xaml",
UriKind.Relative);
this.Resources.MergedDictionaries[0] = resourceDictionary;
Этот код загружает словарь ресурсов GradientButtonVariant и помещает его в первый слот в коллекции
MergedDictionaries. Он не очищает ни коллекцию
MergedDictionaries, ни любые другие ресурсы окна, поскольку может понадобиться установить связь с другими
словарями ресурсов. Новую запись в коллекцию MergedDictionaries он тоже не добавляет, поскольку тогда может возникнуть конфликт между ресурсами, имеющими одинаковые имена, но находящимися в разных коллекциях.
При желании изменить обложку для всего приложения, можно было бы применить точно такой же подход, но
только использовать словарь ресурсов приложения. Это словарь ресурсов можно было бы обновить с помощью следующего кода:
Application.Current.Resources.MergedDictionaries[0] =
newDictionary;
С помощью синтаксиса URI можно также загрузить и
словарь ресурсов, определенный в другой сборке:
ResourceDictionary newDictionary = new ResourceDictionary();
newDictionary.Source = new Uri(
"ControlTemplateLibrary;component/GradientButtonVariant.xaml",
UriKind.Relative) ;
this.Resources.MergedDictionaries[0] = newDictionary;
Когда загружается новый словарь ресурсов, все кнопки автоматически обновляются и начинают использовать
новый шаблон. Можно также включить в виде части обложки базовые стили.
В этом примере предполагается, что ресурсы GradientButton.xaml и GradientButtonVariant.xaml для автоматического изменения кнопок используют стиль, указывающий, к
какому типу элементов должен быть применен новый шаблон. Как известно, существует и другой подход — перейти
на новый шаблон можно и путем установки свойства Template или Style объектов Button. В случае выбора такого подхода следует обязательно использовать ссылку DynamicResource. а не StaticResource. Если применять ссылку StaticResource, шаблон кнопки не будет обновляться при переключении обложек.
Создание более сложных шаблонов
в предыдущем разделе было показано, как можно создать базовый шаблон для кнопки. С помощью всего лишь
нескольких простых триггеров показана возможность создать относительно красивую кнопку, не реализуя заново
никаких ключевых функциональных возможностей кнопки
(чего было бы просто невозможно избежать в Windows
Forms). Лучше всего то, что такие специальные кнопки поддерживают все поведение обычных кнопок — по ним можно
перемещаться с помощью клавиши <Таb>, на них можно
щелкать для инициации события, для них можно использовать клавиши доступа и т.д. Замечательно и то, что шаблон
кнопки может использоваться многократно по всему приложению и при этом все равно быть замененным каким-то совершенно новым дизайном буквально в мгновение ока.
Что же тогда еще нужно знать, прежде чем начинать
создавать обложки для всех базовых элементов управления
WPF? Для получения желаемого привлекательного внешнего вида, возможно, придется потратить дополнительное
время на изучение связанных с рисованием деталей WPF.
Также не обойтись и без творческого чутья. Можно удивиться, но фигуры и кисти, о которых уже рассказывалось
выше, можно использовать для создания сложных кнопок с
эффектами стеклянного размытия и мягкой подсветки. Эта
возможность состоит просто в умелом комбинировании различных слоев фигур и использовании для каждого из них
своей градиентной кисти. Добиться получения эффектов подобного рода легче всего, изучив примеры шаблонов элементов управления, которые создали другие.
Еще одним секретом повышения привлекательности
специальных элементов управления являются анимированные эффекты, о которых будет рассказываться ниже.
Например, с помощью анимации можно создать кнопку, которая будет не резко изменять свой цвет при выполнении на
ней щелчка или отпускания кнопки мыши, а постепенно переходить с одного цвета на другой.
И, наконец, помимо приобретения общих навыков
рисования также понадобится получить дополнительную
информацию о том, как создаются сложные шаблоны, состоящие из множества частей.
Шаблоны, состоящие из множества частей
Шаблон кнопки может состоять из нескольких относительно простых частей. Однако во многих случаях шаблоны все-таки выглядят сложнее. Ниже перечислены некоторые из характеристик более сложных шаблонов.
 Они включают элементы управления Button, которые инициируют конкретные встроенные команды.
Каждая кнопка присоединяется в соответствующей
команде с помощью свойства Command.
 Они используют именованные специальным образом элементы, имена у которых обычно начинаются с
PART_. При создании специального шаблона следует
обязательно проверять наличие всех этих именованных элементов, поскольку класс элемента управления
наверняка включает код, который манипулирует
непосредственно этими элементами (например, присоединяет обработчики событий).
 Они включают вложенные элементы управления,
которые могут иметь свои собственные шаблоны.
 Если они наследуются от класса ContentControl,
они включают ContentPresenter. в котором будет размещаться содержимое. Если они наследуются от
класса ItemsControl, то включают ItemsPresenter, указывающий, где будет размещаться содержащая ряд
элементов панель. Прокручиваемое содержимое, которое будет отображаться внутри элемента управления ScrollViewer, представлено классом ScrollContentPresenter.
 Они
содержат
статические
свойства
SystemBrushes, SystemParameters и SystemFonts для
использования переменных среды (вроде текущей
цветовой схемы, стандартной высоты полосы прокрутки и т.д.).
 Довольно часто они предполагают организацию
элементов с помощью элемента управления Grid (хотя для более точного выравнивания различных элементов может применяться и элемент управления
Canvas).
Шаблоны элементов управления в ItemsControl
Для освоения приемов по созданию шаблонов элементов управления рассмотрим более сложный пример. Допустим, что требуется переделать такой элемент управления,
как ListBox. Далее будет показано, как можно изменить его
внешний вид, поменять в нем эффект выбора элемента и заменить используемую в нем полосу прокрутки.
Первое, что нужно сделать в этом примере, это создать для элемента управления ListBox шаблон и (если
необходимо) стиль, который будет применять этот шаблон
автоматически. Здесь оба эти компонента объединяются в
один:
<Style TargetType="{x:Type ListBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type
ListBox}">
<Border
Name="Border"
Background="{StaticResource
ListBoxBackgroundBrush}"
BorderBrush="{StaticResource StandardBorderBrush}"
BorderThickness="1"
CornerRadius="3">
<ScrollViewer
Focusable="False">
<ItemsPresenter Margin="2"></ItemsPresenter>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Данный стиль состоит из двух кистей, отвечающих за
прорисовку границы и фона. Сам шаблон представляет собой упрощенную версию стандартного шаблона элемента
управления ListBox, но вместо класса ListBoxChrome использует более простой класс Border. Внутри Border находится ScrollViewer, обеспечивающий возможность прокручивания списка, и ItemsPresenter. содержащий все элементы списка. (В настоящий момент в этом шаблоне не хватает триггера, который бы изменял его внешний вид при переходе в неактивное состояние.)
Этот шаблон больше всего примечателен тем, что он
не позволяет делать — конфигурировать внешний вид отдельных элементов в списке. Без этой возможности выбранный элемент всегда выделяется стандартным голубым фоном. Изменить это поведение можно, добавив шаблон для
элемента управления ListBoxItem, который представляет собой элемент управления содержимым и упаковывает содержимое каждого отдельного элемента в списке.
Применить шаблон элемента управления ListBoxItem.
как и шаблон элемента управления ListBox, можно с помощью стиля, ориентированного на элементы определенного
типа. Ниже показан базовый шаблон, в котором каждый
элемент упаковывается в невидимой границе. Поскольку
ListBoxItem является элементом управления содержимым,
для размещения содержимого элемента внутри него исполь-
зуется ContentPresenter. Помимо всего этого, здесь также
присутствуют триггеры, реагирующие на наведение на элемент курсора мыши или выполнение на нем щелчка.
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type
ListBoxItem}">
<Border
Name="Border" BorderThickness="2"
CornerRadius="3"
Padding="1"
SnapsToDevicePixels="True">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource
HoverBorderBrush}"/>
<Setter TargetName="Border" Property="TextBlock.FontSize" Value="20" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="{StaticResource SelectedBackgroundBrush}"/>
<Setter TargetName="Border" Property="TextBlock.Foreground" Value="{StaticResource SelectedForegroundBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Вместе эти два шаблона позволяют создать необычное окно списка, увеличивающее размер элемента, на котором в текущий момент находится курсор мыши, как показано на Рис. 10. Изменение полосы прокрутки будет описано
ниже.
Рис. 10 Окно списка с двумя шаблонами
Изменение полосы прокрутки
В рассматриваемом примере остался не рассмотренным один аспект окна списка, а именно— отображаемая в
его правой части полоса прокрутки. Эта полоса прокрутки
является частью элемента управления ScrollViewer, который, в свою очередь, является частью шаблона элемента
управления ListBox. Хотя в приведенном примере шаблон
элемента управления ListBox и был переопределен заново,
элемент ScrollViewer элемента управления ScrollBar никак
не изменялся.
Чтобы настроить эту деталь, можно было бы создать
новый шаблон ScrollViewer для использования с ListBox. а
затем указать ему на специальный шаблон ScrollBar. Однако
существует и более простой подход. Можно просто создать
стиль, ориентированный на элементы определенного типа и
изменяющий нужным образом все встречающиеся ему элементы управления ScrollBar. Это позволит избежать приложения дополнительных усилий, требуемых для создания
шаблона ScrollViewer.
Элемент управления ScrollBar является удивительно
сложным. Он фактически состоит из ряда более мелких частей, как показано на Рис. 11.
Рис. 11 Компоненты полосы прокрутки
За фон полосы прокрутки отвечает класс Track, который обычно представляет собой затененный прямоугольник,
растянутый по длине полосы прокрутки. По краям полосы
прокрутки расположены кнопки, позволяющие передвигаться на один шаг вверх или вниз (или на один шаг влево или
вправо). Эти кнопки являются экземплярами класса RepeatButton, который унаследован от класса ButtonBase. Главное
отличие между классом RepeatButton и обычным классом
ButtonBase состоит в том, что при удержании кнопки мыши
нажатой на RepeatButton событие Click постоянно генерируется.
Посредине полосы прокрутки находится бегунок
Thumb, который представляет текущую позицию в прокручиваемом содержимом. Пустое пространство по обеим сторонам бегунка, фактически состоит из еще двух объектов
RepeatButton, но только прозрачных. При щелчке на любом
из них полоса прокрутки прокручивает всю страницу (под
которой в данном случае подразумевается объем пространства, умещающегося в видимом окне прокручиваемого
содержимого). Это как раз и дает возможность быстрого перехода через прокручиваемое содержимое путем выполнения щелчка с одной или с другой стороны бегунка.
Ниже приведен шаблон для вертикальной полосы
прокрутки.
<ControlTemplate x:Key="VerticalScrollBar" TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition MaxHeight="18"/>
<RowDefinition Height="*"/>
<RowDefinition MaxHeight="18"/>
</Grid.RowDefinitions>
<RepeatButton Grid.Row="0" Height="18"
Style="{StaticResource ScrollBarLineButtonStyle}"
Command="ScrollBar.LineUpCommand" >
<Path
Fill="{StaticResource
GlyphBrush}"
Data="M 0 4 L 8 4 L 4 0
Z"></Path>
</RepeatButton>
<Track Name="PART_Track" Grid.Row="1"
IsDirectionReversed="True" ViewportSize="0">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageUpCommand"
Style="{StaticResource ScrollBarPageButtonStyle}">
</RepeatButton>
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumbStyle}">
</Thumb>
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageDownCommand"
Style="{StaticResource ScrollBarPageButtonStyle}">
</RepeatButton>
</Track.IncreaseRepeatButton>
</Track>
<RepeatButton
Grid.Row="3" Height="18"
Style="{StaticResource ScrollBarLineButtonStyle}"
Command="ScrollBar.LineDownCommand">
<Path
Fill="{StaticResource
GlyphBrush}"
Data="M 0 0 L 4 4 L 8 0
Z"></Path>
</RepeatButton>
</Grid>
</ControlTemplate>
После изучения составной структуры полосы прокрутки (которая была показана на Рис. 11), этот шаблон является довольно простым. Главное обратить в нем внимание
на описанные ниже моменты.
 Вертикальная полоса прокрутки состоит из трехстрочной сетки. В верхней и нижней строке находятся кнопки, отображаемые по ее краям в виде стрелок.
Они имеют фиксированный размер, равный 18 единицам. Остальную часть пространства занимает
средняя строка, в которой содержится бегунок.
 Для кнопок RepeatButton на обоих концах используется один и тот же стиль. Единственное, что отличается, так это свойство Content, в котором содержится отвечающий за прорисовывание стрелки объект Path, а все потому, что на верхней кнопке стрелка
должна указывать вверх, а на нижней — вниз. Для
краткости эти стрелки были представлены с помощью путевого мини-языка, о котором рассказывалось
выше. Остальные детали, вроде заливки фона и отображаемой вокруг стрелки окружности, определяются
в шаблоне элемента управления, который задается в
ScrollButtonLineStyle.
 Обе кнопки соединяются с соответствующей командой в классе ScrollBar (а именно— командами
LineUpCommand и LineDownCommand). Именно это
и позволяет им выполнять их работу. Когда предоставляется кнопка, соединенная с такой командой, ее
имя. внешний вид или используемый ею конкретный
класс не имеют никакого значения.
 Имя элемента Track выглядит как PART_Track.
Для успешного подключения кода класса ScrollBar
нужно обязательно использовать именно такое имя.
 Для свойства Track.ViewPortSize устанавливается
значение 0. Это специфическая деталь реализации в
данном шаблоне. Она гарантирует постоянный размер бегунка (Thumb). (Обычно размер бегунка устанавливается пропорционально содержимому, из-за
чего в случае прокручивания содержимого, занимающего большую часть окна, размер бегунка значительно увеличивается.)
 Track упаковывает два объекта RepeatButton (стиль
которых определяется отдельно) и один объект
Thumb. Опять-таки, все эти кнопки подключаются к
соответствующим функциональным возможностям с
помощью команд.
Также в этом шаблоне используется имя ключа, которое специально идентифицирует его как вертикальную
полосу прокрутки. Как рассказывалось выше. установка
имени ключа на стиле гарантирует, что он не будет применяться автоматически, даже если также было установлено
свойство TargetType. В этом примере такой подход применяется потому, что данный шаблон подходит только для
полос прокрутки с вертикальной ориентацией. В другом,
ориентированном на элементы определенного типа, стиле
используется триггер, автоматически применяющий данный
шаблон элемента управления в случае, если для свойства
ScrollBar.Orientation устанавливается значение Vertical.
<Style TargetType="{x:Type ScrollBar}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle"
Value="true"/>
<Style.Triggers>
<Trigger Property="Orientation" Value="Vertical">
<Setter Property="Width" Value="18"/>
<Setter Property="Height" Value="Auto"
/>
<Setter Property="Template" Value="{StaticResource VerticalScrollBar}" />
</Trigger>
</Style.Triggers>
</Style>
Хотя из тех же самых базовых фрагментов очень просто можно было создать и горизонтальную полосу прокрутки, в данном примере это не делается (здесь сохраняется
обычная вертикальная полоса прокрутки).
Осталось только добавить стили для форматирования
различных объектов RepeatButton и бегунка (Thumb). Предлагаемые здесь стили являются относительно скромными,
но действительно меняют стандартный внешний вид полосы
прокрутки. Во-первых, бегунок (Thumb) отображается в
форме эллипса:
<Style x:Key="ScrollBarThumbStyle"
TargetType="{x:Type Thumb}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Margin" Value="1,0,1,0"
/>
<Setter Property="Background" Value="{StaticResource StandardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource StandardBorderBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type
Thumb}">
<Ellipse Stroke="{StaticResource
StandardBorderBrush}"
Fill="{StaticResource
StandardBrush}"></Ellipse>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Во-вторых, стрелки на обоих концах прорисовываются внутри хорошо скругленных окружностей. Эти окружности определяются в шаблоне элемента управления, в то время как стрелки берутся из содержимого RepeatButton и
вставляются в шаблон элемента управления с помощью
ContentPresenter:
<Style x:Key="ScrollBarLineButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type
RepeatButton}">
<Grid Margin="1">
<Ellipse Name="Border" StrokeThickness="1" Stroke="{StaticResource StandardBorderBrush}"
Fill="{StaticResource
StandardBrush}"></Ellipse>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"></ContentPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="Border" Property="Fill" Value="{StaticResource PressedBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Объекты RepeatButton. отображаемые поверх дорожки (Track), не изменяются. Для них просто используется
прозрачный фон, так чтобы сквозь него проглядывала дорожка:
<Style x:Key="ScrollBarPageButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type
RepeatButton}">
<Border Background="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
В отличие от обычной полосы прокрутки, в данном
шаблоне никакой фон бегунку не назначается, что и Делает
его прозрачным. Благодаря этому сквозь него проглядывает
слегка затененная градиентная заливка окна списка. Окончательный внешний вид окна списка показан на Рис. 10.
Создание специального окна
Выше рассказывалось о том, как можно создать окно
специальной формы, используя вместо стандартной оконной
рамки элементы рисования WPF. Хотя описанный там подход работал достаточно хорошо, он вынуждал вручную изменять стиль каждого окна с помощью имеющей определенную форму границы, области заголовка, кнопок закрытия
и т.д. В этом разделе будет показано, как приведённую выше
разметку можно преобразовать в шаблон элемента управления, подходящий для использования в любом окне.
Первое, что нужно сделать — это взглянуть на шаблон элемента управления, используемый по умолчанию для
класса Window. По большей части этот шаблон является довольно простым, но включает одну ранее не встречавшуюся
деталь — элемент AdornerDecorator. Этот элемент создает
поверх остального клиентского содержимого окна специ-
альную область рисования, называемую декоративным слоем (adorner layer). Элементы управления WPF могут использовать этот слой для прорисовывания содержимого, которое
должно отображаться как наложенное поверх элементов.
Таким содержимым могут быть небольшие графические индикаторы, показывающие фокус, ошибки проверки флагов и
направление операций перетаскивания. При создании специального окна, нужно обязательно удостовериться в присутствии такого слоя для того, чтобы применяющие его
элементы управления продолжали функционировать.
С учетом этого базовую структуру, которой должен
пользоваться шаблон элемента управления, предназначенный для окна, можно идентифицировать следующим образом:
<ControlTemplate TargetType="{x:Type
Window}">
<Border Width="Auto" Height="Auto"
Name="windowFrame"
BorderBrush="#395984"
BorderThickness="1"
CornerRadius="0,20,30,40" >
<Border.Background>
<LinearGradientBrush >
<GradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Color="#E7EBF7" Offset="0.0"/>
<GradientStop Color="#CEE3FF" Offset="0.5"/>
</GradientStopCollection>
</GradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition
Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Margin="1" Padding="5"
Text="{TemplateBinding Title}" FontWeight="Bold"
MouseLeftButtonDown="titleBar_MouseLeftButtonDown"></TextBlock
>
<Button Style="{StaticResource
CloseButton}" HorizontalAlignment="Right" Margin="0,5,25,0"
Click="cmdClose_Click"></Button>
<Border Background="#B5CBEF"
Grid.Row="1">
<AdornerDecorator>
<ContentPresenter/>
</AdornerDecorator>
</Border>
<ContentPresenter Grid.Row="2"
Margin="10"
HorizontalAlignment="Center"
Content="{TemplateBinding
Tag}"></ContentPresenter>
<ResizeGrip
Name="WindowResizeGrip" Grid.Row="2" Margin="0,0,10,7"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Visibility="Collapsed"
IsTabStop="false"/>
<Rectangle Grid.Row="1"
Grid.RowSpan="3"
Cursor="SizeWE" Fill="Transparent"
Width="5" VerticalAlignment="Stretch" HorizontalAlignment="Right"
MouseLeftButtonDown="window_initiateResizeWE"
MouseLeftButtonUp="window_endResize"
MouseMove="window_Resize"></Rectangle>
<Rectangle Grid.Row="2"
Cursor="SizeNS" Fill="Transparent"
Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"
MouseLeftButtonDown="window_initiateResizeNS"
MouseLeftButtonUp="window_endResize"
MouseMove="window_Resize"></Rectangle>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="ResizeMode" Value="CanResizeWithGrip">
<Setter TargetName="WindowResizeGrip" Property="Visibility"
Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Элементом наивысшего уровня в этом шаблоне является объект Border, отвечающий за границу окна. Внутри
него находится элемент управления Grid с тремя строками.
Содержимое элемента управления Grid поделено следующим образом:
 В верхней строке содержится строка заголовка, состоящая из обычного элемента TextBlock, который
отображает заголовок и кнопку закрытия окна. Заголовок окна извлекается из свойства Window.Title
с помощью привязки шаблона.
 В средней строке содержится вложенный объект
Border с остальным содержимым окна. Это содержимое вставляется с помощью элемента ContentPresenter. Элемент ContentPresenter упаковывается в элемент AdornerDecorator, который гарантирует размещение декоративного слоя поверх содержимого элементов.
 В третьей строке содержится еще один элемент
ContentPresenter. Однако этот элемент не использует стандартную привязку для извлечения своего
содержимого из свойства Window.Content. Вместо
этого он извлекает его явным образом из свойства
Window.Tag. Как правило, таким содержимым является обычный текст, но оно может включать и
содержимое любого элемента, которое хочет использовать разработчик.
 В третьей строке еще также находится элемент захвата и изменения размера. Триггер отображает
этот элемент тогда, когда для свойства Window.ResizeMode устанавливается значение CanResizeWithGrip.
Здесь не показаны две детали: стиль для элемента захвата и изменения размера (который просто создает небольшой узор из точек для использования в качестве такого
элемента) и кнопка для закрытия окна (с небольшим знаком
X на красном квадратном фоне). В этой разметке также отсутствуют и детали форматирования вроде градиентной кисти, закрашивающей фон, и свойств, создающих аккуратно
закругленную граничную каемку.
Шаблон окна применяется с помощью простого стиля. Этот стиль также устанавливает значения для трех главных свойств класса Window, которые делают его прозрачным. что дает возможность создать границу и фон окна с
помощью элементов управления WPF
<Style x:Key="CustomWindowChrome" TargetType="{x:Type Window}">
<Setter Property="AllowsTransparency" Value="True"></Setter>
<Setter Property="WindowStyle" Value="None"></Setter>
<Setter Property="Background" Value="Transparent"></Setter>
<Setter Property="Template” Value="{StaticResource CustomWindowTemplate}">
</Setter>
</Style>
На этом этапе можно уже переходить к использованию специального окна. Например, можно создать окно,
устанавливающее стиль и добавляющее некоторое базовое
содержимое, подобное показанному ниже:
<Window x:Class="ControlTemplates.CustomWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/
xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/200
6/xaml"
Title="CustomWindowTest" Height="300"
Width="300"
Style="{StaticResource CustomWindowChrome}"
Tag="This is a custom footer"
ResizeMode="CanResizeWithGrip"
>
<StackPanel Margin="10">
<TextBlock Margin="3">This is a
test.</TextBlock>
<Button Margin="3" Padding="3">OK</Button>
</StackPanel>
</Window>
Рис. 12 Специальное окно, использующее шаблон
На Рис. 12 можно видеть результат. Здесь имеется
только одна проблема. В настоящий момент у данного окна
отсутствует большая часть требуемого для окон поведения.
Например, это окно нельзя перетаскивать по рабочему столу, нельзя изменять его размер и пользоваться кнопкой для
его закрытия. Для выполнения этих действий необходим
код.
Добавить необходимый код можно двумя способами:
путем расширения данного примера до специального унаследованного от Window класса или путем создания класса
отделенного кода для словаря ресурсов. Подход со специальным элементом управления предусматривает более
удобную инкапсуляцию и позволяет расширить общедоступный интерфейс окна (например, добавить полезные методы и свойства для применения в приложении). Однако
подход с классом отделенного кода является относительно
упрощенной альтернативой и позволяет расширить возможности шаблона элемента управления, не изменив при этом
используемых приложением базовых классов элементов
управления. В данном примере будет продемонстрирован
именно этот подход.
Создание файла отделённого кода включает в себя
создание типизированного класса, унаследованного от класса ResourceDictionary, с именем, совпадающим с именем
словаря ресурсов. Создав файл кода, добавить код обработки событий уже нетрудно. Единственной проблемой является то, что этот код будет выполняться в объекте словаря ресурсов, а не внутри объекта окна. А это означает, что для
получения доступа к текущему окну нельзя использовать
ключевое слово this. К счастью, существует удобная альтернатива: свойство FrameworkElement. TemplatedParent.
Например, чтобы сделать окно перетаскиваемым,
необходимо перехватить событие мыши в строке заголовка
и инициировать перетаскивание. Ниже показана переделанная версия TextBlock. в которой при выполнении пользователем щелчка кнопкой мыши подключается обработчик
событий:
<TextBlock Margin="1" Padding="5"
Text="{TemplateBinding Title}" FontWeight="Bold"
MouseLeftButtonDown="titleBar_MouseLeftButtonDown"></TextBlock
>
Теперь в класс отделенного кода для словаря ресурсов можно добавить следующий обработчик событий:
private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Window win = (Window)
((FrameworkElement)sender).TemplatedParent;
win.DragMove();
}
Чтобы сделать окно допускающим изменение размера, необходимо в его правой и нижней части добавить два
невидимых прямоугольника. Эти прямоугольники могут получать события мыши и вызывать обработчики событий, отвечающие за изменения размера окна.
Ниже приведен код разметки, необходимый для того,
чтобы сконфигурировать элемент управления Grid в шаблоне так, чтобы он поддерживал возможность изменения
размера окна:
<Rectangle Grid.Row="1"
Grid.RowSpan="3"
Cursor="SizeWE" Fill="Transparent"
Width="5" VerticalAlignment="Stretch" HorizontalAlignment="Right"
MouseLeftButtonDown="window_initiateResizeWE"
MouseLeftButtonUp="window_endResize"
MouseMove="window_Resize"></Rectangle>
<Rectangle Grid.Row="2"
Cursor="SizeNS" Fill="Transparent"
Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Bottom"
MouseLeftButtonDown="window_initiateResizeNS"
MouseLeftButtonUp="window_endResize"
MouseMove="window_Resize"></Rectangle>
</Grid>
Далее показаны обработчики событий, отвечающие
за изменение размера окна. Булевское поле is Re sizing следит за переходом в режим изменения размера окна, а поле
resizeType — за направлением, в котором изменяется этот
размер.
private bool isResizing = false;
[Flags()]
private enum ResizeType
{
Width, Height
}
private ResizeType resizeType;
private void window_initiateResizeWE(object sender, System.Windows.Input.MouseEventArgs e)
{
isResizing = true;
resizeType = ResizeType.Width;
}
private void window_initiateResizeNS(object sender, System.Windows.Input.MouseEventArgs e)
{
isResizing = true;
resizeType = ResizeType.Height;
}
private void window_endResize(object
sender, System.Windows.Input.MouseEventArgs e)
{
isResizing = false;
// Make sure capture is released.
Rectangle rect = (Rectangle)sender;
rect.ReleaseMouseCapture();
}
private void window_Resize(object sender, System.Windows.Input.MouseEventArgs e)
{
Rectangle rect = (Rectangle)sender;
Window win = (Window)rect.TemplatedParent;
if (isResizing)
{
rect.CaptureMouse();
if (resizeType ==
ResizeType.Width)
{
double width =
e.GetPosition(win).X + 5;
if (width > 0) win.Width =
width;
}
if (resizeType ==
ResizeType.Height)
{
double height =
e.GetPosition(win).Y + 5;
if (height > 0) win.Height
= height;
}
}
}
И, наконец, ниже приведен похожий код, отвечающий за обработку щелчка на кнопке закрытия окна:
private void cmdClose_Click(object
sender, RoutedEventArgs e)
{
Window win = (Window)
((FrameworkElement)sender).TemplatedParent;
win.Close();
}
using
using
using
using
using
using
using
Полностью программный текст приведён ниже:
System;
System.Collections.Generic;
System.Text;
System.Windows;
System.Windows.Controls;
System.Windows.Input;
System.Windows.Shapes;
namespace ControlTemplates
{
public partial class CustomWindowChrome :
ResourceDictionary
{
public CustomWindowChrome()
{
InitializeComponent();
}
private bool isResizing = false;
[Flags()]
private enum ResizeType
{
Width, Height
}
private ResizeType resizeType;
private void window_initiateResizeWE(object sender, System.Windows.Input.MouseEventArgs e)
{
isResizing = true;
resizeType = ResizeType.Width;
}
private void window_initiateResizeNS(object sender, System.Windows.Input.MouseEventArgs e)
{
isResizing = true;
resizeType = ResizeType.Height;
}
private void window_endResize(object
sender, System.Windows.Input.MouseEventArgs e)
{
isResizing = false;
// Make sure capture is released.
Rectangle rect = (Rectangle)sender;
rect.ReleaseMouseCapture();
}
private void window_Resize(object sender, System.Windows.Input.MouseEventArgs e)
{
Rectangle rect = (Rectangle)sender;
Window win = (Window)rect.TemplatedParent;
if (isResizing)
{
rect.CaptureMouse();
if (resizeType ==
ResizeType.Width)
{
double width =
e.GetPosition(win).X + 5;
if (width > 0) win.Width =
width;
}
if (resizeType ==
ResizeType.Height)
{
double height =
e.GetPosition(win).Y + 5;
if (height > 0) win.Height
= height;
}
}
}
private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Window win = (Window)
((FrameworkElement)sender).TemplatedParent;
win.DragMove();
}
private void cmdClose_Click(object
sender, RoutedEventArgs e)
{
Window win = (Window)
((FrameworkElement)sender).TemplatedParent;
win.Close();
}
}
}
Показанный в примере шаблон может быть применен
к любому обычному окну WPF. Конечно, для придания данному окну внешнего вида, достаточно привлекательного для
современного приложения, все равно потребуется приложить немало дополнительных усилий. Однако этот пример
демонстрирует последовательность шагов, которые необходимо выполнить, чтобы создать сложный шаблон элемента
управления, и показывает, как можно добиться результатов,
для достижения которых в предыдущих каркасах пользовательских интерфейсов требовалось разрабатывать специальные элементы управления.
Привязка данных
Привязка данных — это технология извлечения информации из объекта и отображения ее в пользовательском
интерфейсе приложения без написания программного кода,
который выполняет всю эту работу. Часто "толстые" клиенты используют двунаправленную привязку данных, что добавляет возможности перемещения информации из пользовательского интерфейса обратно в некоторый объект с минимальным кодированием, либо вообще без оного. Поскольку многие Windows-приложения связаны с данными (и все
они а определенное время нуждаются во взаимодействии с
данными), привязка данных находится в центре внимания
такой технологии пользовательских интерфейсов, как WPF.
Разработчики, пришедшие к WPF с опытом работы в
Windows Forms. найдут в привязке данных WPF много схожего с тем, к чему они привыкли. Как и в Windows Forms,
привязка данных WPF позволяет создавать привязки, которые извлекают информацию практически из любого свойства любого элемента. WPF также включает набор списочных элементов управления, которые могут обрабатывать целые коллекции информации и позволяют осуществлять
навигацию по этой информации. Однако произошли и существенные изменения в способах привязки данных, которая
происходит автоматически, появилась некоторая впечатляющая новая функциональность, а также возможности тонкой настройки. Многие концепции остались прежними, но
код изменился.
В данном разделе будут рассмотрены способы применения привязки данных WPF.
Основы привязки данных
В своем простейшем виде привязка данных — это
отношение, которое указывает WPF извлечь некоторую информацию из объекта-источника и использовать ее для ус-
тановки свойства целевого объекта. Целевое свойство всегда
является свойством зависимостей и обычно принадлежит
элементу WPF. В конце концов, конечная цель привязки
данных WPF — отобразить некоторую информацию в пользовательском интерфейсе. Однако исходный объект может
быть почти любым, начиная с другого элемента WPF и заканчивая объектом данных ADO.NET (вроде DataTable и
DataRow) либо собственным объектом данных. Вначале рассмотрим привязку данных на примере самого простейшего
подхода (привязка типа "элемент к элементу"), а затем рассмотрим, как использовать привязку данных с объектами
других типов.
Привязка к свойству элемента
Простейший сценарий привязки данных состоит в
том, что исходный объект является элементом WPF, а исходное свойство — свойством зависимостей. Это объясняется тем, что свойства зависимостей имеют встроенную
поддержку уведомлений об изменениях. В результате, при
изменении значения свойства зависимостей исходного объекта, привязанное свойство в целевом объекте обновляется
автоматически. Это именно то, что необходимо, и для этого
не требуется строить никакой дополнительной инфраструктуры. Такая привязка может использоваться для автоматизации способа взаимодействия элементов так, что когда пользователь модифицирует элемент управления, то другой элемент обновится автоматически. Это существенное преимущество, которое позволит существенно сэкономить время на
написание рутинного кода.
Чтобы понять, как можно привязать один элемент к
другому, рассмотрим простое окно, представленное на Рис.
13. Оно содержит два элемента управления— Slider и TextBlock с единственной строкой текста. Если перетащить бе-
гунок вправо, размер шрифта текста немедленно увеличится. Если сдвинуть его влево, размер шрифта уменьшится.
Рис. 13 Демонстрация привязки к свойству элемента
Ясно, что было бы совсем нетрудно запрограммировать такое поведение в коде. Для этого всего лишь понадобилось бы реагировать на событие ValueChanged и копировать текущее значение бегунка в TextBlock. Однако привязка данных позволяет сделать это еще проще.
При использовании привязки данных нет необходимости вносить никаких изменений в исходный объект (в
данном случае — Slider). Достаточно просто сконфигурировать его, чтобы он принимал правильный диапазон значений. Для этого можно использовать следующую разметку:
<Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10" TickFrequen-
cy="1" IsSnapToTickEnabled="True" TickPlacement="TopLeft"></Slider>
Привязка задается в элементе TextBlock. Вместо
установки FontSize с применением литерального значения
используется выражение привязки, как показано ниже:
<TextBlock Margin="10" Name="lblSampleText"
FontSize="{Binding ElementName=sliderFontSize, Path=Value}"
Text="Simple Text"> </TextBlock>
Выражения привязки данных используют расширение разметки XAML (отсюда и фигурные скобки). Выражение начинается со слова Binding, поскольку создаётся экземпляр класса System.Windows.Data.Binding. Хотя можно
сконфигурировать объект Binding несколькими способами, в
данной ситуации нужно установить всего два свойства: ElementName, указывающее элемент-источник, и Path, указывающее привязываемое свойство элемента-источника.
Одним из замечательных возможностей привязки
данных является то, что целевой объект обновляется автоматически, независимо от того, как модифицируется источник. В данном примере источник может модифицироваться
единственным способом— взаимодействием пользователя с
бегунком. Однако рассмотрим слегка измененную версию
этого примера, в который добавлено несколько кнопок, каждая из которых применяет заранее установленное значение
бегунка. На Рис. 14 показано новое окно.
Рис. 14 Демонстрация изменения связанного свойства
Когда пользователь щёлкает на кнопке Set to Large
(Установить крупным), запускается следующий код:
private void cmd_SetLarge(object sender,
RoutedEventArgs e)
{
sliderFontSize.Value = 30;
}
Этот код устанавливает значение бегунка, что, в свою
очередь, вызывает изменение размера шрифта текста через
привязку данных. Это то же самое, как если перемещение
бегунока происходило вручную.
Тем не менее, следующий код так не работает:
private void cmd_SetLarge(object sender, RoutedEventArgs e)
{
lblSampleText.FontSize = 30;
}
Ошибки привязки
WPF не возбуждает исключений, чтобы известить вас
о проблемах привязки данных. Если специфицировать несуществующий элемент или свойство, то никакого указания
на такую ошибку не будет — вместо этого данные просто не
появятся в целевом свойстве.
На первый взгляд может показаться, что это чревато
кошмаром при отладке. К счастью, WPF выводит трассировочную информацию, в которой детализирована работа
средств привязки. Эта информация появляется в окне Output
в Visual Studio при отладке приложения.
WPF также игнорирует любые исключения, которые
генерируются при попытке прочесть свойство - источник, и
молча "проглатывает" исключения, которые происходят, когда данные не могут быть приведены к типу данных целевого свойства.
Однако есть еще один способ справиться с этими
проблемами — можно указать WPF, чтобы при возникновении ошибок изменялся внешний вид элемента -источника,
тем самым сигнализируя о проблеме. Например, это позволит пометить некорректный ввод пиктограммой с восклицательным знаком или подчеркнуть его красным.
Последний код устанавливает шрифт текста напрямую. В результате позиция бегунка не будет обновлена,
чтобы соответствовать новому значению шрифта. Хуже того, это разрушит привязку размера шрифта, заменив ее литеральным значением. Если после этого передвинуть бегунок, то текстовый блок вообще больше не изменится.
Интересно то, что все-таки существует способ передачи значения в обоих направлениях: от источника к цели и
от цели к источнику. Способ заключается в установке свойства Mode объекта Binding. Ниже приведен пример объявле-
ния двунаправленной привязки данных, которая позволит
применять изменения как к источнику, так и к цели и получать автоматическое эквивалентное обновление противоположной стороны:
<TextBox Text="{Binding ElementName=lblSampleText, Path=Text, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Width="100"></TextBox>
В данном примере нет причин использовать двунаправленную привязку (которая требует больше накладных
расходов), поскольку можно решить ту же проблему правильным кодом. Тем не менее, рассмотрим вариант данного
примера, включающий текстовое поле, в котором пользователь может точно указывать размер шрифта. Текстовое поле
нуждается в двунаправленной привязке, чтобы оно и применяло пользовательские изменения, и отображало значение
размера шрифта, когда пользователь изменит его с другого
конца. Этот пример будет рассмотрен ниже.
Создание привязки в коде
При построении окна обычно лучше объявлять выражение привязки в коде разметки XAML. используя расширение Binding. Однако привязку можно также создать и в
программном коде.
Вот как можно создать привязку TextBlock. показанную в предыдущем примере:
Binding binding = new Binding();
binding.Source = sliderFontSize;
binding.Path = new PropertyPath("Value");
binding.Mode = BindingMode.TwoWay;
IblSampleText.SetBinding(TextBlock.TextProperty,
binding);
Можно также удалить привязку в коде программы,
используя два статических метода класса BindingOperations.
Метод ClearBinding() принимает ссылку на свойство зависимостей, которое имеет привязку, подлежащую удалению,
в то время как ClearAllBindings() удаляет все привязки данных элемента: BindingOperations.ClearAllBindmgs
(IblSair.pieText) ;
И ClearBinding(), и ClearAllBindings() используют метод ClearValue(), который каждый элемент наследует от базового класса DependencyObject. Метод ClearValue() просто
удаляет локальное значение свойства (которое в данном
случае является выражением привязки).
Привязка на основе разметки намного более распространена, чем программная привязка, потому что она проще
и требует меньше работы.
Множественные привязки
Хотя предыдущий пример включает только одну
привязку, однако это не предел. При желании можно установить TextBlock, чтобы он дублировал содержимое текстового поля, текущий цвет переднего плана и фона из отдельного списка цветов, и так далее. Ниже показан пример.
<TextBlock Margin="3" Name="lblSampleText"
FontSize="{Binding ElementName=sliderFontSize, Path=Value}" Grid.Row="4"
Text="{Binding ElementName=txtContent, Path=Text}"
Foreground="{Binding ElementName=lstColors, Path=SelectedItem.Tag}">
</TextBlock>
На Рис. 15 демонстрируется трижды привязанный
TextBlock.
Рис. 15 TextBlock, использующий множественную привязку
Привязки данных можно связывать в цепочку.
Например, можно создать выражение привязки для свойства
TextBox.Text, которое связано со свойством TextBlock.FontSize, содержащим выражение привязки к свойству
Slider.Value. В этом случае, когда пользователь перетащит
бегунок в новое положение, значение из Slider будет передано в TextBlock, а затем — из TextBlock в TextBox. Хотя
все это работает гладко, более ясный подход заключается в
привязке элементов управления как можно ближе к используемым ими данным.
Возможны ситуации, когда целевое свойство должно
зависеть более чем от одного источника, например, если
требуется иметь две равнозначных привязки для установки
свойства. На первый взгляд, это кажется невозможным. Однако это ограничение можно обойти несколькими путями.
Простейший подход — изменить режим привязки
данных. Как было сказано выше, свойство Mode позволяет
изменять способ работы привязки — так, чтобы значения не
просто выталкивались из источника к цели, но также и
наоборот — от цели к источнику. Используя эту технику,
можно создать множественные выражения привязки, устанавливающие одно и то же свойство. При этом действительным является значение, установленное последним свойством.
Чтобы понять, как это работает, рассмотрим вариант
примера с бегунком, добавив в него текстовое поле, в котором можно указывать нужный размер шрифта. В этом примере (показанном на Рис. 14) пользователь может установить свойство TextBlock.FontSize двумя способами — перетаскивая бегунок или вводя размер шрифта в текстовом поле. Все элементы управления синхронизированы, так что
если вводится новую цифру в текстовое поле, то размер
шрифта текста-примера изменяется и бегунок перемещается
в соответствующую позицию.
Как уже было сказано, к свойству TextBlock.FontSize
можно применить только одну привязку данных. Имеет
смысл оставить его привязанным непосредственно к бегунку:
<TextBlock Margin="10" Text="Simple Text"
Name="lblSampleText" FontSize="{Binding ElementName=sliderFontSize, Path=Value,
Mode=TwoWay}" > </TextBlock>
Хотя нельзя добавить другую привязку к свойству
FontSize, можно привязать новый элемент управления—
TextBox— к свойству TextBlock. FontSize. Вот необходимый
для этого код разметки:
<TextBox Text="{Binding ElementName=lblSampletext, Path=FontSize,
Mode=TwoWay}"> </TextBox>
Теперь всякий раз при изменении TextBlock.FontSize
текущее значение будет вставляться в текстовое поле. Более
того, можно редактировать значение в текстовом поле, указывая нужный размер. Отметим, что для того, чтобы этот
пример работал, свойство TextBox.Text должно использовать двунаправленную привязку, чтобы данные передавались в обоих направлениях. В противном случае текстовое
поле сможет отображать значение TextBlock. FontSize. но не
сможет изменять его.
С этим примером связано несколько особенностей.
Поскольку свойство Slider.Value имеет тип double, то при
перетаскивании бегунка возникает дробное значение размера шрифта. Можно ограничить бегунок только целочисленными значениями, установив его свойство TickFrequency в 1
(или некоторый другой числовой интервал) и установив
свойство IsSnapToTickEnabled в true.
Текстовое поле допускает ввод букв и других нецифровых символов. Если будет введён любой из них, то значение текстового поля нельзя будет интерпретировать как
число. В результате привязка данных молча потерпит неудачу, и размер шрифта будет установлен в 0. Другой подход
заключается в обработке нажатий клавиш в текстовом поле,
чтобы предотвратить вообще неправильный ввод, либо использовать проверку достоверности привязки данных, о которой будет сказано ниже.
Изменения, проведенные в текстовом поле, не применяются до тех пор, пока оно не утратит фокус (например,
когда нажимается клавиша <ТаЬ> для перехода к другому
элементу управления). Если такое поведение по каким-либо
причинам не подходит, то можно инициировать непрерыв-
ное обновление, используя для этого свойство UpdateSourceTrigger объекта Binding, как будет описано ниже.
Отметим, что показанное решение — не единственный способ подключения текстового поля. Также можно
сконфигурировать текстовое поле, чтобы оно изменяло
свойство Slider.Value вместо TextBlock.FontSize:
<TextBox Text="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}">
</TextBox>
Теперь изменение текста инициирует изменение положения бегунка, которое затем применит новый размер
шрифта к тексту. Такой подход работает, только если используется двунаправленная привязка.
Как демонстрирует этот пример, двунаправленные
привязки обеспечивают замечательную гибкость. Можно
использовать их для применения изменений от источника к
цели и от цели к источнику. Можно также применять их в
комбинации, создавая необыкновенно сложные окна, лишенные кода.
Обычно решение о том, куда поместить выражение
привязки, диктуется логикой модели кодирования. В предыдущем примере, возможно, имело бы больше смысла поместить привязку в свойство TextBox.Text вместо Slider.Value,
поскольку текстовое поле — это необязательное дополнение
к завершенному примеру, а не центральный ингредиент, на
который полагается бегунок. Также имело бы больше смысла привязать текстовое поле непосредственно к свойству
TextBlock.FontSize, а не к Slider.Value. Очевидно, все эти
решения субъективны, зависят от стиля кодирования и полностью определяются разработчиком.
Направление привязки
Выше была продемонстрирована работа с однонаправленной и двунаправленной привязками. На самом деле
WPF позволяет при установке свойства Binding. Mode применять одно из пяти значений перечисления System.Windows.Data.BindingMode. В табл. 16.1 приведен полный список.
Таблица 2 Значения перечисления BindinigMode
Имя
Oneway
Описание
Целевое свойство обновляется при изменении свойства-источника.
TwoWay
Целевое свойство обновляется при изменении свойства-источника, и свойствоисточник обновляется при изменении целевого свойства.
OneTime
Целевое свойство устанавливается изначально на основе значения свойстваисточника. Однако с этого момента изменения игнорируются (если только привязка не установлена на совершенно другой
объект,
или
вызывается
BindingExpression.UpdateTarget()). Обычно можно использоать этот метод для сокращения накладных расходов, если известно, что исходное свойство не будет
изменяться.
OneWaySource Подобно OneWav, только наоборот. Исходное свойство обновляется при изменении целевого свойства, но целевое свойство никогда не обновляется.
Default
Тип привязки зависит от целевого свойства. Оно будет либо TwoWay (для устанавливаемых пользователем свойств, таких как ТехtBox.Text), либо OneWay (для
всех прочих). Все привязки используют
этот подход, если явно не указан другой.
На Рис. 16 продемонстрирована разница между способами привязки свойств.
Рис. 16 Различные способы привязки двух свойств
OneWayToSource
Возникает логичный вопрос, зачем нужны два свойства — и OneWay, и OneWayToSource? В конце концов, оба
значения создают однонаправленную привязку, которая работает одинаково. Единственное отличие состоит в том. где
помещается выражение привязки. По сути, OneWayToSource
позволяет поменять местами источник и цель, поместив выражение в тот объект, который обычно будет служить источником привязки.
Наиболее распространенная причина для применения
такого трюка — установка свойства, не являющегося свойством зависимостей. Как было показано выше, выражения
привязки могут применяться только для установки свойств
зависимостей. Однако, используя OneWayToSource, можно
преодолеть это ограничение, если свойство, применяющее
значение, само является свойством зависимостей.
Эта техника не слишком часто применяется при выполнении привязки "элемент к элементу", поскольку почти
все свойства элементов являются свойствами зависимостей.
Единственным исключением является установка внутристрочных (inline) элементов, которые можно использовать
для построения документов, как будет показано ниже.
Например, рассмотрим следующий код разметки, создаю-
щий FlowDocument— идеальный выбор для отображения
симпатично форматированных регионов статического содержимого:
<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph>This is a paragraph
one.</Paragraph>
<Paragraph>This is paragraph
two.
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
FlowDocument помещается внутри прокручиваемого
содержимого (который является лишь одним из нескольких
возможный контейнеров), и предоставляет два маленьких
параграфа с небольшим объемом текста.
Теперь посмотрим, что случится, если нужно привязать некоторый текст в параграфе к другому свойству. Первый шаг — поместить текст, который необходимо изменить,
в оболочку объекта Run, представляющего любую небольшую единицу текста внутри FlowDocument. Следующий шаг
— можно попытаться установить текст Run, используя выражение привязки:
<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph>This Is a paragraph
one.</Paragraph>
<Paragraph>
<Run Text="{Binding ElementName=txtParagraph, Path=Text}"
Name="runParagraphTwo"></Run>
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
В этом примере попытки Run пытается извлечь свой
текст из текстового поля по имени txtParagraph. К сожалению, этот код не будет работать, потому что Run.Text не является свойством зависимостей, так что оно не знает, что
делать с выражением привязки. Решение состоит в удалении
выражения привязки из Run и помещении его вместо этого в
текстовое поле:
<Run Name="runParagraphTwo"></Run>
<TextBlock Margin="5">Content for second paragraph:</TextBlock>
<TextBox Margin="5" MinLines="2" TextWrapping="Wrap" Name="txtParagraph" Text="{Binding
ElementName=runParagraphTwo, Path=Text,
Mode=OneWayToSource}"></TextBox>
Теперь текст автоматически копируется из текстового
поля в Run. Конечно, можно также использовать двунаправленную привязку в текстовом поле, но это повлечет за собой
некоторый объем дополнительных накладных расходов. Это
может оказаться лучшим выбором, если есть некоторый
начальный текст в Run, и необходимо, чтобы он сначала появлялся в привязанном текстовом поле.
Default
Изначально кажется логичным предположить, что все
привязки являются однонаправленными, если явно не указать другое. Однако на самом деле это не так. Чтобы продемонстрировать этот факт, вернемся к примеру с привязанным текстовым полем, который позволяет редактировать
текущий размер шрифта. Если удалить настройку
Mode=TwoWay, этот пример будет работать как раньше. Это
объясняется тем, что WPF использует разные установки по
умолчанию для Mode, в зависимости от привязываемого
свойства.
Часто установок по умолчанию вполне достаточно.
Однако можно представить пример с текстовым полем, доступным только для чтения, который пользователь не может
изменить. В этом случае можно слегка сократить накладные
расходы, установив режим однонаправленной привязки.
В качестве общего эмпирического правила — никогда не помещает явно установить режим. Даже в случае текстового поля стоит подчеркнуть, что нужна двунаправленная привязка, включив свойство Mode.
Обновления привязки
В примере, приведенном на Рис. 14 (который привязывает TextBox.Text к TextBlock.FontSize), есть еще одна
особенность. При изменении размера шрифта отображаемого текста вводом его в текстовое поле ничего не происходит. И не происходит до тех пор, пока пользователь не
перейдет в другой элемент управления, применяющий изменения. Это поведение отличается от того поведения, которое
наблюдается у элемента управления — бегунка. Там новый
размер шрифта немедленно применяется при перемещении
этого бегунка. Нет необходимости переносить фокус.
Чтобы понять, в чем разница, нужно присмотреться к
выражениям привязки, используемым этими двумя элементами управления. Когда применяется привязка OneWay или
TwoWay, измененное значение распространяется от источника к цели немедленно. В случае бегунка присутствует выражение однонаправленной привязки в TextBlock. Таким
образом, изменения свойства Slider.Value немедленно применяются к свойству TextBlock.FontSize. То же поведение
имеет место в примере с текстовым полем — изменения в
источнике (TextBlock.FontSize) немедленно затрагивают
Цель (TextBox.Text).
Однако изменения в обратном направлении — от цели к источнику — не обязательно происходят немедленно.
Вместо этого их поведением управляет свойство Binding.UpdateSourceTrigger, принимающее одно из начений в
таблице Таблица 3. Когда текст берется из текстового поля
и используется для обновления свойства TextBlock.FontSize,
наблюдается пример обновления "цель-источник", которое
использует поведение UpdateSourceTrigger.LostFocus.
Таблица 3 Значения перечисления UpdateSourceTrigger
Имя
Описание
PropertyChanged Источник обновляется немедленно после
изменения целевого свойства.
LostFocus
Источник обновляется, когда изменяется
целевое свойство, а целевой элемент теряет фокус.
Explicit
Источник не обновляется, пока не будет
вызван метод
BindingExpression.UpdateSource ().
Default
Поведение обновления определяется метаданными целевого свойства. Для большинства свойств поведением по умолчанию является PropertyChanged, хотя свойство TextBox.Text имеет поведение по
умолчанию LostFocus.
Отметим, что значения в Таблица 3 не оказывают
влияния на то, как обновляется цель. Они просто управляют
тем. как обновляется источник в привязке TwoWay или
OneWayToSource.
Зная это, можно усовершенствовать пример с текстовым полем, чтобы изменения применялись к размеру шриф-
та, когда пользователь осуществляет ввод в текстовом поле.
Вот как это сделать:
<TextBox Text="{Binding ElementName=lblSampleText,
Path=Text, UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}" Width="100" ></TextBox>
Поведением по умолчанию свойства TextBox.Text является LostFocus, просто потому что текст в текстовом поле
будет непрерывно изменяться во время ввода пользователя,
требуя множественных обновлений. В зависимости от того,
как элемент управления — источник обновляет себя, режим
обновления PropertyChanged может создать впечатление
"замедленного" поведения приложения. Вдобавок это может
вызвать обновление объекта-источника до завершения редактирования, что может вызвать проблемы при проверке
достоверности ввода.
Поведение UpdateSourceTrigger.Explicit часто представляет собой приемлемый компромисс, хотя требует
написания определенной порции кода. Например, в примере
с текстовым полем можно добавить кнопку Apply (Применить), щелчок на которой обновит размер шрифта. Затем
следует
воспользоваться
методом
BindingExpression.UpdateSource() для инициации немедленного обновления. Разумеется, это порождает два вопроса: что собой
представляет объект BindingExpression и как его получить?
BindingExpression — это небольшой объект, который
содержит в себе две вещи: объект Binding (представлен
свойством BindingExpression.ParentBinding) и объект, исходящий от источника (BindingExpression.DataItem). Вдобавок
объект BindingExpression предоставляет два метода для
инициирования немедленного обновления одной части привязки: UpdateSource() и UpdateTarget().
Для получения объекта BindingExpression используется метод GetBindingExpression(), который наследует каждый элемент от класса FrameworkElement, и передаётся це-
левое свойство данной привязки. Ниже приведен пример,
изменяющий размер шрифта в TextBlock на основе текущего
содержимого текстового поля:
BindingExpression binding = txtFontSize.GetBindingExpression(TextBox.TextProperty
);
binding.UpdateSource();
Привязка объектов, не являющихся элементами
До сих пор рассматривались выражения привязки,
связывающей два элемента. Но в управляемых данными
приложениях намного чаще приходится создавать выражения привязки, которые извлекают данные из невизуального
объекта. Единственное требование состоит в том, чтобы информация, которую необходимо отобразить, хранилась в
общедоступных свойствах. Инфраструктура привязки данных WPF не может обращаться к приватной информации
объектов или непосредственно к общедоступным полям.
Привязываясь к объекту, не являющемуся элементом,
необходимо отказаться от свойства Binding.ElementName и
использовать вместо него одно из описанных ниже свойств:
 Source. Это — ссылка, указывающая на объектисточник. Другими словами — на объект, поставляющий данные.
 RelativeSource. Указывает на объект-источник, используя объект RelativeSource, позволяющий базировать ссылку на текущем элементе. Это специализированный инструмент, удобный для написания шаблонов элементов управления и шаблонов
данных.
 DataContext. Если источник не специфицируется
через свойство Source или RelativeSource, то WPF
выполняет поиск по дереву элементов, начиная с
текущего элемента. При этом проверяется свойство DataContext каждого элемента и используется
первый элемент, у которого оно не равно null.
Свойство DataContext чрезвычайно удобно, если
нужно привязать несколько свойств одного и того
же объекта к разным элементам, поскольку можно
установить свойство DataContext у более высокоуровневого объекта-контейнера вместо целевого
элемента.
Рассмотрим указанные свойства более подробно.
Source
Свойство Source достаточно просто. Единственный
нюанс состоит в том, что объект данных должен быть доступным, чтобы можно было привязать его. Как будет показано, для получения объекта данных можно использовать
несколько подходов. Можно извлечь его из ресурса, сгенерировать программно или же получить его с помощью поставщика данных.
Простейший вариант — установить Source на некоторый статический объект, который постоянно доступен.
Например, можно создать статический объект в коде и использовать его. Или же применить элемент библиотеки
классов .NET. как показано ниже:
<TextBlock Text="{Binding Source={x:Static SystemFonts.IconFontFamily},
Path=Source}"></TextBlock>
Выражение привязки получает объект FontFamily,
представленный
статическим
свойством
SystemFonts.IconFontFamily. Затем оно устанавливает свойство
Binding.Path в свойство FontFamily.Source, что даст имя семейства шрифтов. Результатом является единственная строка текста. В Windows Vista и Windows 7 появляется имя
шрифта Segoe UI.
Другой вариант — привязать объект, который был
заранее создан в виде ресурса.
Например, следующая разметка создает объект
FontFamily. указывающий на шрифт Calibri:
<Window.Resources>
<FontFamily
x:Key="CustomFont">Calibri</FontFamily>
</Window.Resources>
TextBlock, привязывающийся к этому ресурсу:
<TextBlock Text="{Binding
Source={StaticResource CustomFont},
Path=Source}"></TextBlock>
RelativeSource
Свойство RelativeSource позволяет указать на объектисточник на основе его отношения к целевому объекту.
Например, можно использовать свойство RelativeSource для
привязки элемента к самому себе или к родительскому элементу, находящемуся на неизвестное число шагов вверх по
дереву элементов.
Для установки свойства Binding.RelativeSource используется объект RelativeSource. Это делает синтаксис несколько более изощренным, потому что нужно создавать
объект Binding и создавать внутри него вложенный объект
RelativeSource. Один из вариантов предусматривает применение синтаксиса установки свойства вместо расширения
разметки Binding. Например, следующий код создает объект
Binding для TextBlock. Объект Binding использует RelativeSource. который ищет родительское окно и отображает
его заголовок.
<TextBlock>
<TextBlock.Text> <Binding
Path="Title">
<Binding.RelativeSource>
<RelativeSource
Mode="FindAncestor" AncestorType="{x:Type Window}" />
</Binding.RelativeSource>
</Binding>
</TextBlock.Text>
</TextBlock>
Объект RelativeSource использует режим FindAncestor, который заставляет его искать вверх по дереву элементов, пока не будет найден тип элемента, определенный
свойством AncestorType.
Более распространенный способ записи этой привязки заключается в комбинации ее в одну строку с использованием расширений разметки Binding и RelativeSource. как
показано ниже:
<TextBlock Text="{Binding Path=Title,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}} }">
</TextBlock>
Режим FindAncestor — лишь один из четырех возможных при создании объекта RelativeSource. Все четыре
режима перечислены в Таблица 4.
Таблица 4 Значения перечисления RelativeSourceMode
Имя
Self
FindAncestor
Описание
Выражение привязывает к другому свойству того же элемента.
Выражение привязывает к родительскому
элементу. WPF будет производить поиск
вверх по дереву элементов, пока не найдет
нужного родителя. Чтобы специфицировать родителя, необходимо также установить свойство AncestorType для указания
типа родительского элемента, который
нужно найти. Необязательно можно использовать свойство AncestorLevel, чтобы
пропустить определенное число вхождений указанного элемента.
PreviousData
Выражение привязывает к предыдущему
элементу данных в привязанном к данным
списке. Это используется в элементе
списка.
TemplatedParent Выражение привязывает к элементу, к которому применен шаблон. Этот режим работает, только если привязка расположена
внутри шаблона элемента управления или
шаблона данных.
На первый взгляд свойство RelativeSource выглядит
как излишнее усложнение разметки. Логичным решением
было бы привязаться непосредственно к нужному источнику, используя свойство Source или ElementName. Однако это не всегда возможно — обычно потому, что разметка объекта-источника и целевого объекта — это разные
фрагменты разметки. Такое случается, когда создаются
шаблоны элементов управления и шаблоны данных.
DataContext
В некоторых случаях у вас будет несколько элементов, привязанных к одному объекту. Например, рассмотрим
следующую группу элементов TextBlock, каждый из которых использует похожее выражение привязки для извлечения различных деталей о шрифте пиктограммы по умолчанию, включая промежуток между строками, стиль и вес первой его гарнитуры (и то, и другое будет просто Regular).
Можно использовать свойство Source для каждого такого
элемента, но это приведет к довольно длинной разметке:
<StackPanel DataContext="{x:Static SystemFonts.IconFontFamily}">
<TextBlock Text="{Binding
Source={x:Static SystemFonts.IconFontFamily},
Path=Source}">
</TextBlock>
<TextBlock Text="{Binding
Source={x:Static SystemFonts.IconFontFamily},
Path=LineSpacing}"></TextBlock>
<TextBlock Text="{Binding
Source={x:Static SystemFonts.IconFontFamily},
Path=FamilyTypefaces[0].Style}"></TextBlock>
<TextBlock Text="{Binding
Source={x:Static SystemFonts.IconFontFamily},
Path=FamilyTypefaces[0].Weight}"></TextBlock>
</StackPanel>
В этой ситуации яснее и более гибко будет определить источник привязки, используя свойство FrameworkElement.DataContext. В данном примере имеет смысл установить свойство DataContext объекта StackPanel, содержащего
элементы TextBlock. Также необходимо установить свойство DataContext на еще более высоком уровне — например,
на уровне всего окна.
Можно установить свойство DataContext элемента
таким же образом, как устанавливается свойство Binding.Source. Другими словами, можно просто применить
объект встроенным образом, извлекая его из статического
свойства или из ресурса, как показано ниже:
<StackPanel DataContext="{x:Static SystemFonts.IconFontFamily}">
Теперь можно упростить выражения привязки, исключив информацию об источнике:
<TextBlock Margin="5" Text="{Binding
Path=Source}"></TextBlock>
Когда информация об источнике пропущена в выражении привязки, WPF проверяет свойство DataContext этого
элемента. Если оно равно null, то WPF выполняет поиск по
дереву элементов, пытаясь найти первый контекст данных,
который не равен null. Если контекст данных найден, он используется для привязки. Если нет, то выражение привязки
не применяет никакого значения к целевому свойству.
Этот пример демонстрирует, как можно создать базовую привязку к объекту, не являющемуся элементом. Однако чтобы использовать эту технику в реальном приложении,
понадобится немного более высокая квалификация.
Привязка пользовательских объектов к базе
данных
Когда разработчики слышат термин привязка данных,
то они часто думают об одном специфическом ее приложении — получении информации из базы данных и отображении на экране с минимальным объемом кода либо вообще
без него.
Как было показано, привязка данных в WPF —
намного более широкое понятие. Даже если приложение никогда не вступает в контакт с базой данных, все же сохраняется вероятность применения привязки данных для автоматизации взаимодействия элементов между собой или трансляции объектной модели в наглядное представление. Однако
необходимо знать много подробностей о привязке объектов,
для чего мы рассмотрим традиционный пример, запрашивающий и обновляющий таблицу базы данных. Прежде чем
обратиться к этому, нужно рассмотреть специальный компонент доступа к данным и объект данных, используемый в
примере.
Построение компонента доступа к данным
В профессиональных приложениях код работы с базой данных встроен в класс отделенного кода окна, но инкапсулирован в выделенном классе. Для еще лучшего разбиения на компоненты эти классы доступа к данным могут
быть вообще исключены из приложения и скомпилированы
в отдельный компонент DLL. Это справедливо, в частности,
при написании кода доступа к базе данных, поскольку этот
код имеет тенденцию быть чрезвычайно чувствительным к
производительности.
Независимо от того, как планируется использовать
привязку данных, код доступа к данным всегда должен
находиться в отдельных классах. Такой подход — единственный путь обеспечения возможности эффективного сопровождения, оптимизации, диагностики проблем и повторного использования кода работы с данными. При создании
класса доступа к данным необходимо должны следовать нескольким базовым правилам, которые описаны ниже.
 Открывать и закрывать соединения быстро. Следует открывать соединение с базой данных при
каждом вызове метода и закрывать перед завершением метода. Таким образом, соединения не останутся открытыми по неосторожности. Один из
способов гарантировать закрытие соединения в
надлежащее время — использовать блок using.
 Реализовать обработку ошибок. Следует использовать обработку ошибок, чтобы гарантировать закрытие соединений даже в случае возникновения
исключений.
 Следовать практике не поддерживающего состояние дизайна. Передавать всю необходимую для
метода информацию в его параметрах и возвращать все извлеченные данные через значение воз-
врата. Это позволит избежать усложнения во многих сценариях.
 Хранить строку подключения в одном месте. В
идеале таким местом должен быть конфигурационный файл приложения.
Компонент базы данных, показанный в следующем
примере, извлекает табличную информацию о продуктах из
базы данных Store — базы данных примеров, описывающей
фиктивный магазин IBuySpy, которая поставляется вместе с
рядом программных продуктов Microsoft. На Рис. 17 показаны две таблицы из базы данных Store и их схемы.
Рис. 17 Две таблицы базы данных Store
Класс доступа к данным весьма прост — он предоставляет только один метод, позволяющий извлечь одну запись о продукте. Ниже представлен базовый эскиз.
public class StoreDB
{
// Получить строку подключения из текущего конфигурационного файла
private string connectionString = Properties.Settings.Default.Store;
public Product GetProduct(int ID)
{}
}
Запрос выполняется хранимой процедурой по имени
GetProduct. Строка соединения не кодируется жестко —
вместо этого она извлекается из конфигурационного файла
config приложения.
Когда другие окна нуждаются в данных, они вызывают метод StoreDB.GetProduct () для извлечения объекта
Product. Объект Product — это специальный объект, имеющий единственное назначение — предоставлять информацию из единственной строки таблицы Products. Он будет
рассмотрен ниже.
Есть несколько способов сделать экземпляр StoreDB
доступным окну приложения:
 Окно может создавать экземпляр StoreDB всякий
раз, когда ему понадобится доступ к базе данных.
 Можно изменить методы класса StoreDB, сделав
их статическими.
 Можно создать единственный экземпляр StoreDB
и сделать его доступным через статическое свойство другого класса.
Первые два варианта вполне уместны, но оба они
ограничивают гибкость. Первый вариант исключает кэширование объектов данных для использования в нескольких
окнах. Даже если нет необходимости применять кэширование, все же стоит так проектировать приложение, чтобы
можно было легко реализовать кэширование в будущем.
Аналогично, второй подход предполагает, что в программе
не будет никаких специфичных для экземпляра состояний,
которые можно было бы удерживать в классе StoreDB. Хотя
это хороший принцип проектирования, все же можно запоминать некоторые детали (такие как строка подключения).
Если преобразовать класс StoreDB для использования статических методов, становится сложнее обращаться к разным
экземплярам базы данных Store в разных хранилищах данных.
В конечном итоге третий вариант оказывается наиболее гибким. Он предотвращает дизайн "коммутатора", за-
ставляя все окна работать через единственное свойство. Ниже приведен пример, который открывает доступ к экземпляру StoreDB всему классу Application:
public partial class App : System.Windows.Application
{
private static StoreDb storeDb = new
StoreDb();
public static StoreDb StoreDb
{
get { return storeDb; }
}
private static StoreDb2 storeDbDataSet
= new StoreDb2();
public static StoreDb2 StoreDbDataSet
{
get { return storeDbDataSet; }
}
}
В рамках данного учебного пособия важным является
вопрос о том, каким образом объекты данных могут быть
привязаны к элементам WPF. Действительный процесс,
имеющий дело с созданием и наполнением этих объектов
данных (наряду с другими деталями реализации, например:
кэширует ли StoreDB данные между несколькими вызовами
метода, использует ли хранимые процедуры вместо встроенных запросов, извлекает ли данные из локального XMLфайла при отсутствии связи с базой, и т.д.), не является
предметом рассмотрения. Однако просто чтобы получить
представление о том, что происходит, ниже показан полный
код.
public class StoreDb
{
private string connectionString =
StoreDatabase.Properties.Settings.Default.Store;
public Product GetProduct(int ID)
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetProductByID", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@ProductID", ID);
try
{
con.Open();
SqlDataReader reader =
cmd.ExecuteReader(CommandBehavior.SingleRow);
if (reader.Read())
{
// Create a Product object
that wraps the
// current record.
Product product = new Product((string)reader["ModelNumber"],
(string)reader["ModelName"], (decimal)reader["UnitCost"],
(string)reader["Description"],
(string)reader["ProductImage"]);
return product;
}
else
{
return null;
}
}
finally
{
con.Close();
}
}
Отметим, что в таком виде метод GetProduct() не
включает никакого кода обработки исключений, так что все
они возникают в вызывающем коде. Это разумное дизайнерское решение, но можно решить перехватывать исключения
в GetProduct(), выполнять необходимую очистку или протоколирование и затем повторно возбуждать исключение, чтобы известить вызывающий код о возникновении проблемы.
Такой шаблон дизайна называется caller inform (информирование вызывающего).
Построение объекта данных
Объект данных — это пакет информации, который
будет отображаться в пользовательском интерфейсе. Любой
класс работает только при условии наличия у него общедоступных свойств (поля и приватные свойства не поддерживаются). Вдобавок, если необходимо применить этот объект
для внесения изменений через двунаправленную привязку,
то такие свойства не могут быть доступными только для
чтения.
Ниже приведен код объекта Product, используемый
StoreDB.
public class Product : INotifyPropertyChanged
{
private string modelNumber;
public string ModelNumber
{
get { return modelNumber; }
set {
modelNumber = value;
OnPropertyChanged(new PropertyChangedEventArgs("ModelNumber"));
}
}
private string modelName;
public string ModelName
{
get { return modelName; }
set {
modelName = value;
OnPropertyChanged(new PropertyChangedEventArgs("ModelName"));
}
}
private decimal unitCost;
public decimal UnitCost
{
get { return unitCost; }
set { unitCost = value;
OnPropertyChanged(new PropertyChangedEventArgs("UnitCost"));
}
}
private string description;
public string Description
{
get { return description; }
set { description = value;
OnPropertyChanged(new PropertyChangedEventArgs("Description"));
}
}
private string categoryName;
public string CategoryName
{
get { return categoryName; }
set { categoryName = value; }
}
private string productImagePath;
public string ProductImagePath
{
get { return productImagePath; }
set { productImagePath = value; }
}
public Product(string modelNumber,
string modelName,
decimal unitCost, string description)
{
ModelNumber = modelNumber;
ModelName = modelName;
UnitCost = unitCost;
Description = description;
}
public Product(string modelNumber,
string modelName,
decimal unitCost, string description,
string productImagePath)
:
this(modelNumber, modelName,
unitCost, description)
{
ProductImagePath = productImagePath;
}
public Product(string modelNumber,
string modelName,
decimal unitCost, string description, string categoryName,
string productImagePath) :
this(modelNumber, modelName,
unitCost, description)
{
CategoryName = categoryName;
ProductImagePath = productImagePath;
}
public override string ToString()
{
return ModelName + " (" + ModelNumber + ")";
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}
Отображение привязанного объекта
Финальный шаг — создание объекта Product с последующей привязкой к элементам управления. Хотя можно
создать объект Product и сохранить его в ресурсах или статическом свойстве, оба подхода не имеют особого смысла.
Вместо этого рекомендуется использовать StoreDB для создания соответствующего объекта во время выполнения с
последующей привязкой к окну приложения.
Отметим, что хотя декларативный подход без кода
кажется более элегантным, существует немало веских причин добавлять немного кода к окнам, привязанным к данным. Например, если запрашивается базу данных, то, вероятно, есть необходимость поддерживать в коде подключение
к ней, чтобы решать, как обрабатывать исключения и информировать пользователя о проблемах.
Рассмотрим простое окно, показанное на рис. 16.7.
Оно позволяет пользователю указывать код продукта, и за-
тем отображает соответствующий продукт в Grid (в нижней
части окна).
Рис. 18 Окно запроса продукта
Во время проектирования этого окна разработчик не
имеет доступа к объекту Product, который поставит данные
во время выполнения. Однако можно создавать привязки без
указания источника данных. Для этого нужно просто указать свойство класса Product, которое будет использовать
каждый элемент.
Ниже приведен код разметки для отображения объекта Product.
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="Auto"></RowDefinition>
<RowDefinition
Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition
Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition
Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Margin="7">Product
ID:</TextBlock>
<TextBox Name="txtID" Margin="5"
Grid.Column="1">356</TextBox>
<Button Click="cmdGetProduct_Click" Margin="5" Padding="2" Grid.Column="2">Get Product</Button>
</Grid>
<Border Grid.Row="1" Padding="7" Margin="7"
Background="LightSteelBlue">
<Grid Name="gridProductDetails">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="Auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition
Height="Auto"></RowDefinition>
<RowDefinition
Height="Auto"></RowDefinition>
<RowDefinition
Height="Auto"></RowDefinition>
<RowDefinition
Height="Auto"></RowDefinition>
<RowDefinition
Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Margin="7">Model Number:</TextBlock>
<TextBox Margin="5" Grid.Column="1"
Text="{Binding Path=ModelNumber}"></TextBox>
<TextBlock Margin="7"
Grid.Row="1">Model Name:</TextBlock>
<TextBox Margin="5" Grid.Row="1"
Grid.Column="1" Text="{Binding
Path=ModelName}"></TextBox>
<TextBlock Margin="7" Grid.Row="2">Unit
Cost:</TextBlock>
<TextBox Margin="5" Grid.Row="2"
Grid.Column="1" Text="{Binding
Path=UnitCost}"></TextBox>
<TextBlock Margin="7,7,7,0"
Grid.Row="3">Description:</TextBlock>
<TextBox Margin="7" Grid.Row="4"
Grid.Column="0" Grid.ColumnSpan="2"
VerticalScrollBarVisibility="Visible" TextWrapping="Wrap"
Text="{Binding Path=Description}"></TextBox>
</Grid>
</Border>
</Grid>
Чтобы создать этот пример, нужно начать с построения логики доступа к данным. В данном случае метод
StoreDB.GetProducts() извлекает список всех продуктов базы
из данных, используя хранимую процедуру GetProducts.
Объект Product создается для каждой записи и добавляется в
обобщенную коллекцию List.
Ниже приведен код метода GetProducts().
public ICollection<Product> GetProducts()
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetProducts", con);
cmd.CommandType = CommandType.StoredProcedure;
ObservableCollection<Product> products = new ObservableCollection<Product>();
try
{
con.Open();
SqlDataReader reader =
cmd.ExecuteReader();
while (reader.Read())
{
// Create a Product
object that wraps the
// current record.
Product product = new Product((string)reader["ModelNumber"],
(string)reader["ModelName"], (decimal)reader["UnitCost"],
(string)reader["Description"],
(string)reader["CategoryName"],
(string)reader["ProductImage"]);
// Add to collection
products.Add(product);
}
}
finally
{
con.Close();
}
return products;
}
Когда выполняется щелчок на кнопке Get Products
(Извлечь продукты), код обработки событий вызывает метод
GetProducts() и применяет его в качестве ItemsSource для
списка. Коллекция также сохраняется как переменная-член
класса окна для облегчения доступа из любой части кода.
private List<Product> products;
private void cmdGetProducts_Click(object sender, RoutedEventArgs e)
{
products = App.StoreDB.GetProducts();
lstProducts.ItemsSource = products;
}
Это успешно наполняет список объектами Product.
Однако список не знает, как отображать объект продукта,
поэтому он просто вызывает метод ToString(). Поскольку
этот метод не был переопределен в классе Product, это даст
не впечатляющий результат — отображение полного квалифицированного имени класса для каждого элемента списка.
Эту проблему можно решить тремя способами.
 Установить свойство DisplayMemberPath списка.
 Переопределить метод ToString() для возврата более полезной информации. Например, можно вернуть строку с номером модели и наименованием
модели каждого продукта. Такой подход обеспечивает возможность отображения более одного
свойства в списке. Однако и в этом случае попрежнему слабо контролируется представление
данных.
 Применить шаблон данных. Таким образом можно
отобразить любое размещение значений свойств
(наряду с фиксированным текстом).
Решив, как будет отображать информацию в списке,
необходимо реализовать следующую задачу: отображение
подробностей текущего выбранного элемента списка в сетке, которая появляется под этим списком. С этой задачей
можно справиться, реагируя на событие SelectionChanged и
вручную изменяя контекст данных сетки, но есть более
быстрый способ, который вообще не требует никакого кода.
Для этого нужно просто должны установить выражение
привязки для свойства Grid.DataContent, которое извлечет
выбранный в списке объект Product, как показано ниже:
<Grid DataContext="{Binding ElementName=lstProducts, Path=SelectedItem}”>
</Grid>
Когда появляется окно, в списке ничего не выбрано.
Свойство ListBox.SelectedItem равно null, и потому Grid.
DataContext также равно null, так что никакой информации
не появляется. Как только пользователь выберет элемент в
списке, контекст данных устанавливается в соответствующий объект и вся информация тут же появляется.
Очевидно, что для полноты примера необходим
фрагмент кода, который будет обрабатывать нажатие кнопки «Get products». Необходимый код представлен ниже:
private void cmdGetProduct_Click(object
sender, RoutedEventArgs e)
{
int ID;
if (Int32.TryParse(txtID.Text, out
ID))
{
try
{
gridProductDetails.DataContext = App.StoreDb.GetProduct(ID);
}
catch
{
MessageBox.Show("Error contacting database.");
}
}
else
{
MessageBox.Show("Invalid ID.");
}
}
Обновление базы данных
Вам не нужно ничего делать дополнительно для того,
чтобы включить обновления базы данных в этом примере.
Было сказано выше, свойство TextBox.Text использует двустороннюю привязку по умолчанию. В результате объект
Product модифицируется, когда редактируется содержимое
текстовых полей.
Можно зафиксировать изменения в базе данных в
любой момент. Все, что нужно для этого — добавить метод
updateProduct() в класс StoreDB и кнопку Update (Обновить)
в окно. При щелчке на ней код может получить текущий
объект Product из контекста данных и использовать его для
фиксации обновления:
private void cmdUpdateProduct_Click(object
sender, RoutedEventArgs e)
{
Product product = (Product)gridProductDetails.DataContext;
try {
App.StoreDB.UpdateProduct(product);
}
catch
{
MessageBox.Show("Ошибка подключения к базе данных.");
}
}
С этим примером связана одна потенциальная проблема. Когда пользователь щелкает на кнопке Update, фокус
переходит к этой кнопке и все незафиксированные изменения в текстовых полях применяются к объекту Product. Од-
нако если сделаеть кнопку Update кнопкой по умолчанию
(установив свойство IsDefault в true), появится другая возможность. Пользователь может внести изменения в одно из
полей и нажать клавишу < Enter>, чтобы запустить процесс
обновления без фиксации последнего изменения. Во избежание такой ситуации можно явно передать фокус прежде,
чем выполнить любой код базы данных:
FocusManager.SetFocusedElement(this, (Button)sender);
Уведомление об изменениях
Пример с привязкой Product работает так хорошо потому, что каждый объект product фактически фиксирован —
он никогда не изменяется за исключением ситуации, когда
пользователь редактирует текст в одном из привязанных
текстовых полей.
Для простых сценариев, когда есть заинтересованность в первую очередь в отображении содержимого и разрешении пользователю редактировать его, такое поведение
приемлемо. Однако несложно представить другую ситуацию, когда привязанный объект Product может модифицироваться где-то в другом месте кода. Например, кнопка Increase Price (Увеличить цену) могла бы выполнять следующий код:
product.UnitCost *= 1.1M;
Запустив этот код, можно обнаружить, что даже несмотря на то, что объект Product изменен, в текстовом поле
остается старое значение. Это происходит потому, что текстовое поле не имеет никакой возможности узнать о том,
что отображаемое значение было изменено.
Для решения этой проблемы применяются три подхода, которые описаны токе.
 Можно сделать каждое свойство класса Product свойством зависимостей. Хотя такой подход заставляет
WPF выполнять нужную работу, он имеет больше
смысла в элементах — классах, имеющих визуальное
представление в окне. Это не слишком естественный
подход к классам данных вроде Product.
 Можно инициировать событие для каждого свойства.
В данном случае событие должно иметь имя
ИмяСвойстзаChanged (например, UmtCostChanged).
Генерирование события при изменении свойства
полностью на совести разработчика.
 Можно
реализовать
интерфейс
System.ComponentModel.INotifyPropertyChanged, что потребует единственного события по имени PropertyChanged. Необходимо затем инициировать это событие всякий раз, когда свойство изменяется, и указывать, какое именно свойство изменилось, передавая
его имя в строке. При этом также генерирование события при изменении свойств возлагается на разработчика, но уже не надо определять отдельное событие для каждого свойства.
Первый подход полагается на инфраструктуру
свойств зависимостей WPF, в то время как второй и третий
строятся на событиях. Обычно при создании объекта данных
использоуется третий подход. Это простейший вариант для
не-элементных классов.
Ниже приведено определение измененного класса
Product, использующего интерфейс INotifyPropertyChanged,
с кодом реализации события PropertyChanged:
public class Product : INotifyPropertyChanged
{
public event PropertyChangedEventHandler
PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}
Теперь просто нужно инициировать событие PropertyChanged во всех установщиках значений свойств:
private decimal unitCost;
public decimal UnitCost
{
get { return unitCost; }
set { unitCost = value;
OnPropertyChanged(new
PropertyChangedEventArgs("UnitCost"));
}
}
Если использовать эту версию класса Product в
предыдущем примере, то поведение станет тем, которое
ожидается. При изменении текущего объекта Product новая
информация появится в текстовом поле немедленно.
Привязка к коллекции объектов
Привязка к единственному объекту довольно проста.
Но все становится намного интереснее, когда необходимо
привязать некоторую коллекцию объектов — например, все
продукты в таблице.
Хотя каждое свойство зависимостей поддерживает
привязку одного значения, привязка коллекций требует несколько более интеллектуального элемента. В WPF все
классы, унаследованные от ItemsControl, способны отображать целый список элементов. Возможностью привязки
данных обладают ListBox, ComboBox и ListView (а также
Menu и TreeView — для иерархических данных).
Может показаться, что WPF предлагает ограниченный перечень списочных элементов управления, тем не менее, эти элементы предоставляют почти неограниченные
возможности по отображению данных. Это связано с тем,
что списочные элементы управления поддерживают шаблоны данных, которые позволяют непосредственно управлять
отображением элементов списка. Подробно шаблоны данных будут разобраны ниже.
Чтобы поддержать привязку коллекций, класс ItemsControl определяет три ключевых свойства, перечисленные
в Таблица 5.
Таблица 5 Свойства класса ItemControl для привязки данных
Имя
ItemsSource
Описание
Указывает на коллекцию, содержащую все объекты, которые будут показаны в списке.
DisplayMemberPath Идентифицирует свойство, которое
будет применяться для создания
отображаемого текста каждого элемента коллекции.
ItemTemplate
Принимает шаблон данных, который
будет использован для создания визуального представления каждого
элемента. Это свойство намного
мощнее, чем DisplayMemberPath.
Здесь возникает логичный вопрос — какой именно тип
коллекции можно поместить в свойство ItemSource? Правильный ответ: практически любой. Все, что для этого понадобится — это поддержка интерфейса IEnumerable, которую обеспечивают массивы, все типы коллекций и многие другие специализированные объекты, служащие оболочками для групп
элементов. Однако поддержка, которую предоставляется базовым интерфейсом IEnumerable, ограничена привязкой только для чтения. Если необходимо редактировать коллекцию
(например, разрешить пользователям вставку и удаление), то
понадобится немного более сложная инфраструктура.
Отображение и редактирование элементов коллекции
Рассмотрим окно, показанное на Рис. 19, которое
отображает список продуктов. Когда пользователь выбирает
продукт, информация о нем появляется в нижней части окна,
где он можете редактировать ее.
Рис. 19 Отображение коллекции продуктов
Чтобы создать этот пример, нужно начать с построения
логики доступа к данным. В данном случае метод
StoreDB.GetProducts() извлекает список всех продуктов базы
из данных. Объект Product создается для каждой записи и добавляется в обобщенную коллекцию List.
Ниже приведен код метода GetProducts().
public ICollection<Product> GetProducts()
{
DataSet ds = StoreDb2.ReadDataSet();
ObservableCollection<Product> products = new ObservableCollection<Product>();
foreach (DataRow productRow in
ds.Tables["Products"].Rows)
{
products.Add(new Product((string)productRow["ModelNumber"],
(string)productRow["ModelName"], (decimal)productRow["UnitCost"],
(string)productRow["Description"],
(string)productRow["CategoryName"],
(string)productRow["ProductImage"]));
}
return products;
}
Когда выполняется щелчок на кнопке Get Products
(Извлечь продукты), код обработки событий вызывает метод
GetProducts() и применяет его в качестве ItemsSource для
списка. Коллекция также сохраняется как переменная-член
класса окна для облегчения доступа из любой части кода.
private ICollection<Product> products;
private void cmdGetProducts_Click(object
sender, RoutedEventArgs e)
{
products =
App.StoreDb.GetProducts();
lstProducts.ItemsSource = products;
}
Приведённый код успешно наполняет список объектами Product. Однако список не знает, как отображать объект
продукта, поэтому он просто вызывает метод ToString(). Поскольку этот метод не был переопределен в классе Product, это
даст не впечатляющий результат — отображение полного
квалифицированного имени класса для каждого элемента
списка.
Эту проблему можно решить тремя способами.
 Установить свойство DisplayMemberPath списка.
 Переопределить метод ToString() для возврата более
полезной информации. Например, можно вернуть
строку с номером модели и наименованием модели
каждого продукта. Такой подход обеспечивает возможность отображения более одного свойства в
списке. Однако и в этом случае по-прежнему слабо
контролируется представление данных.
 Применить шаблон данных. Этим способом можно
отобразить любое размещение значений свойств
(наряду с фиксированным текстом).
Решив, как будет отображатьсяся информация в списке, необходимо решить следующую задачу: отображение подробностей текущего выбранного элемента списка в сетке,
которая появляется под этим списком. С этой задачей можно
справиться, реагируя на событие SelectionChanged и вручную
изменяя контекст данных сетки, но есть более быстрый способ, который вообще не требует никакого кода. Для этого
необходимо просто установить выражение привязки для свойства Grid.DataContent, которое извлечет выбранный в списке
объект Product, как показано ниже:
<Grid DataContext="{Binding ElementName=lstProducts, Path=SelectedItem}">
</Grid>
Когда появляется окно, в списке ничего не выбрано.
Свойство ListBox.SelectedItem равно null, и потому Grid.
DataContext также равно null, так что никакой информации не
появляется. Как только выбирается элемент в списке, контекст
данных устанавливается в соответствующий объект и вся информация тут же появляется.
Данный пример уже полностью функционален. В нём
уже можно редактировать продукты, перемещаться по списку
и затем, вернувшись к предыдущей записи, убедиться, что изменения были успешно зафиксированы.
Фактически, можно изменить значение, которое затрагивает отображаемый в списке текст. Если модифицировать
наименование модели и перейти на другой элемент управления, то соответствующая позиция в списке автоматически обновится.
Конечно, чтобы завершить этот пример с точки зрения
приложения, еще нужно добавить некоторый код. Например,
может понадобиться метод UpdateProducts(), принимающий
коллекцию продуктов и выполняющий соответствующие операторы. Поскольку обычный объект .NET не предоставляет
никаких возможностей отслеживания изменений, в этой ситуации можно подумать о применении DataSet из ADO.NET.
Альтернативно можно пожелать заставить пользователей обновлять записи по одной за раз. Вставка и удаление элементов коллекций
Одно из ограничений предыдущего примера заключается в том, что он не может показать изменения, которые были внесены в коллекцию. Он замечает измененные объекты
Products, но не может обновить список, если был добавлен новый элемент или елемент был удалён в коде.
Например, предположим, что добавлена кнопку Delete
(Удалить), которая выполняет следующий код:
private void cmdDeleteProduct_Click(object sender, RoutedEventArgs e)
{
products.Remove((Product)IstProducts.Selectedltem);
}
Удаленный элемент исключается из коллекции, но
упорно остается видимым в привязанном списке.
Чтобы включить отслеживание изменений коллекции,
необходимо использовать коллекцию, реализующую интерфейс INotifyCollectionChanged. Большинство обобщенных
коллекций этого не делают, включая List<T>. Фактически
WPF включает единственную коллекцию, реализующую INotifvCollectionChanged — это класс ObservableCollection.
Можно унаследовать собственную коллекцию от ObservableCollection, чтобы настроить по своему усмотрению ее
работу, хотя это и не обязательно.
Теперь, если программно удалить или добавить программно элемент в коллекцию, список будет соответствующим образом обновлен. Конечно, придется по-прежнему создавать код доступа к данным, который происходит перед модификацией коллекции — например, код, удаляющий запись о
продукте из лежащей в основе базы данных.
Привязка к выражению LINQ
Одной из причин перехода от .NET 3.0 к .NET 3.5 является поддержка языка интегрированных запросов (Language
Integrated Query — LINQ), предлагающий синтаксис запросов
общего назначения, который работает с широким разнообразием источников данных и тесно интегрирован в язык С#.
L1NQ работает с любым источником, для которого есть соответствующий поставщик LINQ. Используя поддержку, включенную в .NET 3.5, можно использовать одинаково структурированные запросы LINQ для извлечения данных из коллекции, находящейся в памяти, из файла XML или из базы данных SQL Server. Как и другие языки запросов, LINQ позволя-
ет фильтровать, сортировать и трансформировать извлекаемые данные.
Например, предположим, что есть коллекция объектов
Product по имени products, и необходимо создать вторую коллекцию, содержащую только те продукты, цена которых превышает $100. Используя процедурный код, можно написать
что-то вроде следующего:
// Получить полный список продуктов.
List<Product> products =
App.StoreDB.GetProducts();
// Создать вторую коллекцию с нужными продуктами.
List<Product> matches = new List<Product>();
foreach (Product product in products)
{
if (product.UnitCost >= 100) {
matches.Add(product);
}
}
Используя LINQ, эту же задачу можно решить более
удобным способом:
// Получить полный список продуктов.
List<Product> products =
App.StoreDB.GetProducts();
// Создать вторую коллекцию с подходящими продуктами.
IEnumerable<Product> matches =
from product in products
where product.UnitCost >= 100
select product;
Этот пример использует API-интерфейс LINQ to Collections, т.е. применяет выражение LINQ для выполнения запроса данных из находящейся в памяти коллекции. Выраже-
ния LINQ используют набор новых ключевых слов языка,
включая from, in и select. Эти ключевые слова LINQ составляют неотъемлемую часть языка С#.
Более детальная информация по LINQ доступна в центре
разработчиков
.NET
по
адресу
http://msdn.microsoft.com/ru-ru/netframework/aa904594.aspx.
Огромный каталог примеров LINQ доступен по адресу на
http://msdn.microsoft.com/en-us/vcsharp/aa336746.aspx.
Центром LINQ является интерфейс IEnumerable<T>.
Независимо от того, какие используются источники данных,
каждое выражение LINQ возвращает объект, реализующий
IEnumerable<T>. Поскольку интерфейс IEnumerable<T> расширяет IEnumerable, можно привязать его к окну WPF, как это
делается с обычной коллекцией:
IstProducts.ItemsSource = matches;
Таким образом, нужно рассмотреть лишь несколько
нюансов.
Преобразование IEnumerable<T> в обычную коллекцию
В отличие от классов ObservableCollection и DataTable,
интерфейс IEnumerable<T> не предусматривает способов добавления или удаления элементов. Если нужна такая функциональность, сначала придется преобразовать объект IEnumerable<T> в массив или коллекцию List, используя для этого метод ТоАггау() или ToList().
Ниже приведен пример использования ToList() для
преобразования результатов запроса LINQ в строго типизированную коллекцию List объектов Product:
List<Product> productMatches = matches.ToList();
Метод ToList() вызывает немедленное выполнение выражения LINQ. В результате получается обычная коллекция, с
которой можно работать привычным способом. Например,
можно поместить ее в оболочку ObservableCollection, чтобы
получать события уведомлений, так что любые изменения в
ней будут немедленно отражаться в связанных элементах
управления:
ObservableCollection<Product> productMatchesTracked = new ObservableCollection<Product>(productMatches);
Затем можно привязать коллекцию productMatchesTracked к элементу управления в окне приложения.
Отложенное выполнение
LINQ использует отложенное выполнение. В противоположность тому, что можно ожидать, результатом выражения LINQ не является непосредственно коллекция. Вместо
этого возникает специализированный объект LINQ, обладающий способностью извлекать данные по мере необходимости,
а не в тот момент, когда создается выражение LINQ.
В этом примере соответствующим объектом является
экземпляр WhereIterator — приватного класса, вложенного
внутрь класса System.Linq.Enumerable:
matches = from product in products
where product.UnitCost >= 100 select product;
В зависимости от используемого специфического запроса выражение LINQ может возвращать разные объекты.
Например, выражение объединения, комбинирующее данные
из двух разных коллекций, будет возвращать экземпляр приватного класса UnionIterator. Или же, если упростить запрос,
исключив конструкцию where, то получится простой итератор
SelectIterator. На самом деле разработчику даже не нужно
знать конкретный класс итератора, используемый кодом, потому что он взаимодействует с его результатами через интерфейс IEnumerable<T>.
Объекты итераторов LINQ добавляют дополнительный
слой между определением выражения LINQ и его выполнением. Как только начинается выполнение итерации по объекту
WhereIterator, он извлекает необходимые ему данные. Напри-
мер, если разработчик пишет блок foreach, который проходит
по коллекции соответствующих запросу объектов, это действие вызывает оценку выражения LINQ. То же самое происходит, когда привязывается объект IEnumerable<T> к окну
WPF — в этом случае инфраструктура привязки данных WPF
выполняет итерацию по его содержимому.
Отложенное выполнение и LINQ to SQL
Концепцию отложенного выполнения важно понимать,
когда используется источник данных, который может быть не
доступным. В примерах, которые показывались выше, выражения LINQ работали с находящейся в памяти коллекцией,
отсюда важность знания о том, когда именно вычисляется
выражение. Однако это не касается случая, когда применяется
LINQ to SQL для выполнения оперативного (just-in-time) запроса, адресованного базе данных. В такой ситуации перечисление результатов объекта IEnumerable<T> заставит .NET
установить соединение с базой данных и выполнить запрос.
Это очевидно рискованное действие — если сервер базы данных недоступен или не может ответить, то исключение возникнет тогда, когда оно менее всего ожидается. По этой причине принято использовать выражения LINQ двумя более
ограниченными способами.
 После извлечения данных следует применять LINQ
to Collections для фильтрации результатов. Это
удобно, если нужно получить несколько разных
представлений одних и тех же результатов.
 Использовать LINQ to SQL для получения нужных
данных. Это избавит от написания низкоуровневого
кода доступа к данным. Рекомендуется применять
метод ToList() для принудительного немедленного
выполнения запроса и возврата обычной коллекции.
Обычно создание компонента базы данных, использующего LINQ to SQL и возвращающего результирующий объ-
ект IEnumerable<T> не является удачным решением. Если
разработчик допускает подобное, то теряется контроль над
тем, когда именно будет выполнен запрос, и как будут обработаны потенциальные ошибки, а также над количеством выполнений запроса.
LINQ to SQL - сама по себе обширная тема. Этот API
предлагает гибкий, свободный от SQL способ извлечения информации из базы данных и помещения их в спроектированные объекты. Документация по LINQ to SQL расположена по
адресу http://msdn2.microsoft.com/en-us/library/bb425822.aspx.
Преобразование данных
При обычной привязке информация путешествует от
источника к цели без каких-либо изменений. Это кажется логичным, но такое поведение не всегда подходит. Часто источник данных может применять некоторое низкоуровневое
представление, которое не нужно отображать непосредственно в пользовательском интерфейсе. Например, можно захотеть, чтобы числовые коды заменялись читабельными для человека строками, числа представлялись в укороченном виде,
даты отображались в длинном формате и т.д. Если так, то нужен какой-то способ преобразования этих значений в корректную отображаемую форму. И если применяется двунаправленная привязка, также понадобится обратная операция
— взять введенные пользователем данные и преобразовать в
представление, подходящее для хранения в соответствующем
объекте данных.
WPF позволяет делать то и другое посредством создания (и использования) класса конвертера значений. Такой
конвертер значений отвечает за преобразование исходных
данных непосредственно перед их отображением в целевом
элементе и (в случае двунаправленной привязки) преобразования нового целевого значения непосредственно перед его
применением к источнику.
Конвертеры значений — исключительно удобная часть
инфраструктуры привязки данных WPF. Их можно использовать несколькими удобными способами, которые перечислены
ниже.
 Для форматирования данных к строчному представлению. Например, можно преобразовывать
число в строку валюты. Это — наиболее очевидное
применение конвертеров значений, но, конечно же,
не единственное.
 Для создания специфических типов объектов WPF.
Например, можно прочитать блок двоичных данных и создать объект BitmapImage, который можно
привязать к элементу Image.
 Для условного изменения свойства элемента на основе привязанных данных. Например, можно создать конвертер значений, который изменяет цвет
фона элемента для выделения значений из определенного диапазона.
Форматирование строк конвертером значений
Конвертеры значений — замечательный инструмент
для форматирования чисел, которые нужно отображать в виде
тексте. Например, рассмотрим свойство Product.UnitCost из
предыдущего примера. Оно сохраняется в виде десятичного
числа, и в результате при отображении в текстовом поле пользователь видит значение вроде 3.9900. Такой формат не только отображает больше десятичных знаков, чем вам нужно, но
также он пропускает символ валюты. Более интуитивно понятное представление должно выглядеть как $3.99, что показано на Рис. 20.
Рис. 20 Отображение форматированных величин
Рис. 16.10. Отображение форматированных денежных
величин
Для создания конвертера значений потребуется выполнить четыре шага.
1. Создать класс, реализующий IValueConverter.
2. Добавить атрибут ValueConversion в объявление
класса и специфицировать исходный и целевой типы
данных.
3. Реализовать метод Convert(), преобразующий данные из исходного формата в отображаемый формат.
4. Реализовать метод ConvertBack(), выполняющий обратное преобразование значения из отображаемого
формата в его "родной" формат.
На Рис. 21 можно видеть это в работе.
В случае преобразования десятичного значения в денежное можно использовать метод Decimal.ToString(). чтобы
получить нужное отформатированное строковое представление.
Рис. 21 Преобразования привязанных данных
Для этого достаточно специфицировать строку денежного формата "С", как показано ниже:
string currencyText = decimalPrice.ToString(“С”)
;
Этот код использует установки культуры, примененные в текущем потоке. Компьютер, настроенный для региона
English (United States), работает с установкой локали en-US и
отображает валюту знаком доллара ($). Компьютер, сконфигурированный для другой локали, может отображать другой
символ валюты. Если это не то, что нужно (например, необходимо, чтобы появлялся знак числа), то можно специфицировать культуру, используя перегрузку метода ToString(), показанную ниже:
CultureInfo culture = new Culturelnfo("en-US");
string currencyText = decimalPrice.ToString("C",
culture);
Подробнее обо всех доступных строках формата можно
прочитать в справочной системе Visual Studio. В Таблица 6 и
Таблица 7 перечислены наиболее часто используемые опции,
которые можно применять для числовых данных и дат соответственно.
Таблица 6 Строки формата числовых данных
Тип
Строка
формата
С
Валюта
Научный
(экспо- E
тенциальный)
Процентный
P
Финансовый деся- F?
тичный
Пример
$1,234.50 Отрицательные
значения представляются
в скобках: ($1,234.50).
Знак валюты специфичен
для локали.
1.234.50Е+004
45.6%
Зависит от количества
установленных десятичных разрядов. F3 форматирует значения в виде
123.400, F0 форматирует
значения как 123
Таблица 7 Строки формата даты и времени
Тип
Короткая дата
Длинная дата
Длинная дата
короткое время
Строка Формат
формата
d
M/d/yyyy.
Например:
10/30/2007
D
dddd, мммм dd, уууу.
Например: Monday, January
30, 2008
и f
dddd, ММММ dd, уууу
HH:mm
aa.
Например:
Monday, January 30, 2008
10:00 AM
dddd, MMMM dd, yyyy
HH:mm:ss aa. Например:
Monday, January 30, 2008
10:00:23 AM
Сортируемый
s
yyyy-MM-dd
HH:mm:ss.
стандарт ISO
Например:
2008-01-30
10:00:23
Месяц и день
H
MMMM dd. Например: January 30
Общий
G
M/d/yyyy HH:mm:ss aa (зависит от локальных установок). Например: 10/30/2008
10:00:23 AM
Обратное преобразование от отображаемого формата к
числовому немного сложнее. Методы Parse() и TryParse() типа
Decimal — логичный способ выполнить эту работу, но обычно
они не могут справиться со строками, включающими символы
валюты. Решение заключается в применении перегруженных
версии методов Parse() и TryParse(), которые принимают значение System.Globalization.NumberStyles. Если применить
NumberStyles.Any, то можно успешно избавиться от символа
валюты, если таковой присутствует.
Ниже приведен полный код конвертера значений, который имеет дело со значениями цены, хранимыми в свойстве
Product.UnitCost.
[ValueConversion(typeof(decimal),
typeof(string))]
public class PriceConverter : IValueConverter
{
Длинная дата
длинное время
и F
public object Convert(object value, Type
targetType, object parameter, CultureInfo culture)
{
decimal price = (decimal)value;
return price.ToString("c", culture);
}
public object ConvertBack(object value,
Type targetType, object parameter, CultureInfo
culture)
{
string price = value.ToString();
decimal result;
if (Decimal.TryParse(price, System.Globalization.NumberStyles.Any, culture, out
result))
{
return result;
}
return value;
}
}
Чтобы ввести в действие этот конвертер, надо начать с
отображения пространства имен проекта на префикс пространства имен XML, который можно применять в коде разметки. Приведем пример, в котором используется префикс
пространства имен и предполагается, что конвертер значений
находится в пространстве имен DataBinding:
xmlns:local="clr-namespace:DataBinding"
Обычно этот атрибут добавляется к дескриптору
<Wmdows>, который содержит всю разметку окна приложения.
Теперь нужно просто создать экземпляр класса
PriceConverter и присвоить его свойству Converter привязки.
Чтобы сделать это, понадобится синтаксис, приведенный ниже:
<TextBlock Margin="7" Grid.Row="2">Unit
Cost:</TextBlock>
<TextBox Margin="5" Grid.Row="2"
Grid.Column="1">
<TextBox.Text>
<Binding Path="UnitCost" NotifyOnValidationError="true">
<Binding.Converter>
<local:PriceConverter></local:PriceConverter>
</Binding.Converter>
<Binding.ValidationRules>
<local:PositivePriceRule
Max="999.99" ></local:PositivePriceRule>
<ExceptionValidationRule></ExceptionValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Во многих случаях один и тот же конвертер используется для множества привязок. В этом случае не имеет смысла
создавать по экземпляру конвертера для каждой привязки.
Вместо этого можно создать один объект конвертера в коллекции Resources, как показано далее:
<Window.Resources>
<local:PriceConverter
x:Key="PriceConverter"></local:PriceConverter>
</Window.Resources>
Затем можно указывать на него в привязке, используя
ссылку StaticResource:
<TextBox Margin="5" Grid.Row="2"
Grid.Column="1" Text="{Binding Path=UnitCost,
Converter=StaticResource PriceConverter}">
</TextBox>
Создание объектов с конвертером значений
Конвертеры значений незаменимы, когда необходимо
хотите преодолеть зазор между способом сохранения данных
в разрабатывемых классах и способом их отображения в окне.
Например, предположим, что есть графические данные, хранящиеся в виде массива байт в поле базы данных. Можно
преобразовать
двоичные
данные
в
объект
System.Windows.Media.Imaging.BitmapImage и сохранить его как
часть объекта данных. Однако такой дизайн может не подойти
по нескольким причинам.
Например, может понадобиться гибкость для создания
более одного объектного представления изображения. В этом
случае имеет смысл хранить оригинальную двоичную информацию в объекте данных и преобразовывать ее в WPF - объект
BitmapImage с помощью конвертера значений.
Таблица Products из базы данных Store не включает
двоичных графических данных, но содержит поле Product Image, хранящее имя файла с изображением продукта. Такое решение не является удачным по нескольким причинам. Вопервых, изображение может быть недоступным в зависимости
от расхода памяти при работе приложения. Во-вторых, нет
смысла в расходе дополнительной памяти для хранения изображения, если нет необходимости его показывать.
Поле Product Image включает имя файла, но не полный
путь к этому файлу изображения, что дает определенную гибкость для размещения таких файлов в любом подходящем месте. Перед конвертером значений стоит задача создания URI,
указывающего на файл изображения, на основе поля Product
Image и каталога, который будет использоваться для хранения
таких файлов. Каталог хранится в специальном свойстве по
имени ImageDirectory, которое по умолчанию указывает на
текущий каталог. Ниже приведен полный код ImagePathConverter, выполняющий преобразование.
public class ImagePathConverter : IValueConverter
{
private string imageDirectory = Directory.GetCurrentDirectory();
public string ImageDirectory
{
get { return imageDirectory; }
set { imageDirectory = value; }
}
public object Convert(object value, Type
targetType, object parameter,
System.Globalization.CultureInfo culture)
{
string imagePath =
Path.Combine(ImageDirectory,
(string)value);
return new BitmapImage(new
Uri(imagePath));
}
public object ConvertBack(object value,
Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Чтобы использовать этот конвертер, следует начать с
добавления его в Resources. В этом примере свойство ImageDirectory не установлено, а это значит, что ImagePathConverter
по умолчанию работает с текущим каталогом:
<Window.Resources>
<local:ImagePathConverter
x:Key="ImagePathConverter"></local:ImagePathConverter>
</Window.Resources>
Теперь можно создать выражение привязки, использующее этот конвертер значений:
<Image Margin="5" Grid.Row="2"
Grid.Column="1" Stretch="None" HorizontalAlignment="Left"
Source="{Binding
Path=ProductImagePath, Converter={StaticResource
ImagePathConverter}}">
</Image>
Это работает, поскольку свойство Image.Source ожидает объекта ImageSource, а класс Bitmaplmage наследуется от
ImageSource.
Результат показан на Рис. 22.
Рис. 22 Отображение привязанного изображения
Этот пример можно усовершенствовать несколькими
способами. Во-первых, попытка создать BitmapImage. указывающий на несуществующий файл, вызовет исключение, которое возникнет при установке свойств DataContext, ItemsSource или Source. В качестве альтернативы можно добавить
свойства в класс ImagePathConverter, которые позволят
настроить это поведение. Например, можно добавить свойство
DefaultImage, которое будет принимать BitmapImage. Тогда
ImagePathConverter сможет вернуть изображение по умолчанию в случае возникновения исключений.
Также можно отметить, что этот конвертер поддерживает только однонаправленное преобразование. Это
объясняется тем, что невозможно изменить объект BitmapImage и применять его для обновления пути к изображению. Однако можно применить альтернативный подход. Вместо возврата BitmapImage из ImagePathConverter, можно просто вернуть полностью квалифицированный URI из метода
Convert(), как показано ниже:
return new Uri(imagePath);
Это работает успешно, поскольку элемент Image использует конвертер типа для трансляции Uri в объект ImageSource, который ему в действительности нужен. Если применить такой подход, то можно затем разрешить пользователю выбирать новый путь к файлу, из которого можно извлечь
имя файла. Полученное имя файла можно применять для обновления пути к изображению, хранящегося в объекте данных.
Применение условного форматирования
Некоторые из наиболее интересных конвертеров значений не предназначены для форматирования данных для целей презентации. Вместо этого они служат для форматирования некоторых других связанных с внешним видом аспектов элемента на основе правила данных.
Например, предположим, что необходимо выделить
самые дорогостоящие продукты, окрасив фон другим цветом.
Эту логику легко инкапсулировать в следующем конвертере
значений:
public class PriceToBackgroundConverter :
IValueConverter
{
public decimal MinimumPriceToHighlight
{
get;
set;
}
public Brush HighlightBrush
{
get;
set;
}
public Brush DefaultBrush
{
get;
set;
}
public object Convert(object value, Type
targetType, object parameter,
System.Globalization.CultureInfo culture)
{
decimal price = (decimal)value;
if (price >= MinimumPriceToHighlight)
return HighlightBrush;
else
return DefaultBrush;
}
public object ConvertBack(object value,
Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Этот конвертер значений спроектирован с учетом возможности повторного использования. Вместо жесткого кодирования цветов для выделения, эти цвета специфицированы в
XAML кодом, который использует этот конвертер:
<local:PriceToBackgroundConverter
x:Key="PriceToBackgroundConverter"
DefaultBrush="{x:Null}" HighlightBrush="Orange" MinimumPriceToHighlight="10">
</local:PriceToBackgroundConverter>
Вместо цветов используются кисти, так что можно более совершенные эффекты выделения, применяя градиенты и
фоновые изображения. И если необходимо сохранить стандартный, прозрачный фон (чтобы использовался фон родительского элемента), следует просто установить свойство DefaultBrush или HighlightBrush в Null, как показано выше.
Для использования этого конвертера необходимо
установить цвет некоторого элемента, подобного Border, который содержит все прочие элементы:
<Border Background="LightSteelBlue" Grid.Row="2"
Margin="7">
<Border DataContext="{Binding ElementName=lstProducts, Path=SelectedItem}" Background="{Binding Path=UnitCost, Converter={StaticResource PriceToBackgroundConverter}}"
Padding="7" >
</Border>
Оценка множественных свойств
С помощью конвертеров значений можно реализовать
еще одну возможность — оценку нескольких отдельных полей и использование их для создания единственного конвертированного значения. Например, можно использовать это для
объединения различных кусочков информации (таких как поля FirstName и LastName), выполнения вычислений (вроде
умножения UnitPrice Ha UnitsInStock) и применения форматирования, которое принимает во внимание несколько деталей
(наподобие выделения цветом наиболее дорогостоящих продуктов в определенной категории).
Чтобы осуществить эту возможность, необходимы два
компонента:
 MultiBinding, определяющий привязку (вместо
обычного объекта Binding):
 конвертер, реализующий интерфейс IMultiValueConverter (вместо IValueConverter).
MultiBinding группирует последовательность объектов
Binding. Ниже приведен пример, где MultiBinding использует
два свойства в объекте данных.
<TextBlock>Total Stock Value:</TextBlock>
<TextBox>
<TextBox.Text>
<MultiBinding Converter="{StaticResource ValueInStockConverter}">
<Binding
Path="UnitCost"></Binding>
<Binding
Path="UnitsInStock"></Binding>
</MultiBinding>
</TextBox.Text>
</TextBox>
Интерфейс IMultiValueConverter определяет методы
Convert() и ConvertBack() — аналогичные интерфейсу IValueConverter. Главное отличие в том, что вместо единственного
значения передается массив значений. Эти значения помещаются в том же порядке, как они определены в разметке. Таким
образом, в предыдущем примере следует ожидать появления
UnitCost первым, а за ним— UnitsInStock.
Вот как выглядит код ValuelnStockConverter:
public class ValueInStockConverter : IMultiValueConverter
{
public object Convert(object[] values,
Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
// Return the total value of all the
items in stock.
decimal unitCost = (decimal)values[0];
int unitsInStock = (int)values[1];
return unitCost * unitsInStock;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Проверка достоверности
Еще одной ключевой составляющей сценария двунаправленной привязки является проверка достоверности. Другими словами, это логика, перехватывающая некорректные
значения и отвергающая их. Встраивать проверку достоверности можно непосредственно в элементы управления (например, реагируя на ввод в текстовом поле и отклоняя недопустимые символы), но такой низкоуровневый подход ограничивает гибкость.
WPF предоставляет средство проверки достоверности,
работающее подобно системе привязки данных. Проверка достоверности предлагает два дополнительных варианта выбора
для перехвата неверных значений.
• Можно инициировать ошибки в объекте данных. Чтобы известить WPF об ошибке, достаточно сгенерировать исключение из процедуры установки свойства. Обычно WPF игнорирует любые исключения, которые возбуждаются при
установке свойств, но можно сконфигурировать его для отображения более полезной визуальной индикации. Другой выбор — реализовать интерфейс IDataErrorInfo в классе данных,
что даст возможность обозначать ошибки, не генерируя исключений.
• Можно определить проверку достоверности на уровне
привязки. Это обеспечивает гибкость для использования одной и той же проверки достоверности, независимо от элемента управления вводом. Также поскольку проверка достоверности определяется в отдельном классе, то можно легко повторно использовать ее с множеством привязок, которые
имеют дело с однотипными данными.
Обычно используется первый подход, если объекты
данных уже обладают встроенной логикой проверки достоверности в своих процедурах установки свойств, и можно использовать преимущества этой логики. Второй подход применяется при первом определении логики проверки достоверности и необходимо обеспечить многократно использование её в
разных контекстах с разными элементами управления. Однако
возможно применение обоих приёмов. В этом случае проверка
достоверности в объекте данных используется для защиты от
небольшого набора фундаментальных ошибок, и реализуется
проверка достоверности привязки для перехвата более широкого диапазона ошибок пользовательского ввода.
Проверка достоверности в объекте данных
Некоторые разработчики встраивают проверку ошибок
непосредственно в свои объекты данных. Например, ниже
приведена модифицированная версия свойства Product. UnitPrice, которое отклоняет отрицательные числа:
public decimal UnitCost
{
get { return unitCost; }
set
{
if (value < 0)
throw new ArgumentException("UnitCost не может бь:ть отрицательным.");
else
{
unitCost = value;
OnPropertyChanged(new PropertyChangedEventArgs("UnitCost"));
}
}
}
Логика проверки достоверности, показанная в предыдущем примере, предотвращает отрицательные значения цены, но не дает пользователю никакой информации о причинах
проблемы. Как было сказано выше, WPF молча игнорирует
ошибки привязки данных, которые происходят при установке
и считывании свойств. В этом случае некорректное значение
останется в текстовом поле — оно просто не появится в привязанном объекте данных. Чтобы изменить эту ситуацию,
необходимо использовать класс ExceptionValidationRule, который будет описан ниже.
Объекты данных и проверка достоверности
Размещение логики проверки достоверности в объекте
данных имеет определенные преимущества — например, позволяет всегда перехватывать все ошибки, вызваны ли они неправильным пользовательским вводом, ошибкой программы,
либо получены в результате вычислений на основе неверных
данных.
Однако здесь есть и недостаток — усложнение объектов данных и перемещение кода проверки достоверности,
предназначенного для интерфейсной части приложения,
вглубь модели данных заднего плана.
При неаккуратном применении проверка достоверности свойств может помешать совершенно корректному использованию объекта данных. Это может также привести к
несоответствию и на самом деле к составным ошибкам данных. Иногда подобные проблемы решаются созданием еще
одного слоя объектов — например, в сложной системе разработчики могут построить развитую модель бизнес-объектов,
находящуюся поверх слоя примитивных объектов данных. В
данном примере классы StoreDB и Product спроектированы
как часть компонента доступа к данным заднего плана. В этом
контексте класс Product служит просто упаковкой, позволяющей передавать информацию от одного слоя кода к другому.
По этой причине код проверки достоверности в действительности не относится к классу Product.
ExceptionValidationRule
ExceptionValidationRule—это предварительно построенное правило проверки достоверности, которое принуждает
WPF сообщать обо всех исключениях. Чтобы использовать
ExceptionValidationRule, необходимо должны добавить его в
коллекцию Binding.ValidationRules, как показано далее:
<TextBox Margin="5" Grid.Row="2"
Grid.Column="1">
<TextBox.Text>
<Binding Path="UnitCost">
<Binding.Converter>
<local:PriceConverter></local:PriceConverter>
</Binding.Converter>
<Binding.ValidationRules>
<ExceptionValidationRule></ExceptionValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
В этом примере используется и конвертер значений, и
правило проверки достоверности. Обычно проверка достоверности выполняется перед преобразованием значений, но в
этом отношении ExceptionValidationRule — специальный случай. Он перехватывает исключения, возникающие в любом
месте, в том числе исключения, возникающие при невозможности приведения введенного значения к корректному типу
данных, исключения, сгенерированные установщиком
свойств, и исключения, сгенерированные конвертером значений.
Итак, что же произойдет, если проверка достоверности
не прошла? Ошибки проверки достоверности записываются с
применением
прикрепленных
свойств
класса
System.Windows.Controls.Validation. Для каждого нарушенного
правила проверки достоверности WPF предпринимает описанные ниже шаги.
 Устанавливает прикрепленное свойство Validation.HasError в true на привязанном элементе (в данном случае — элементе управления TextBox).
 Создает объект ValidationError с подробностями об
ошибке (возвращенными из метода ValidationRule.Validate()) и добавляет его в прикрепленную
коллекцию Validation.Errors.
 Если свойство Binding.NotifyOnValidationError установлено в true. WPF инициирует в элементе событие
Validation.Error.
Визуальное представление элемента управления также
изменяется при возникновении ошибки. WPF автоматически
переключает шаблон, используемый элементом управления.
когда его свойство Validation.HasError принимает значение
true, на шаблон, определенный в свойстве Validation.ErrorTemplate. В текстовом поле новый шаблон окрашивает контур рамки в красный цвет.
В большинстве случаев есть необходимость как-то
усилить индикацию ошибки и выдать определенную информацию об ошибке, послужившей причиной проблемы, можно
использовать код, обрабатывающий событие Error, или же
применить шаблон элемента управления, который обеспечивает другую визуальную индикацию. Но прежде чем использовать любой из этих вариантов, стоит рассмотреть другой
способ, которым WPF позволяет перехватывать ошибки —
использование IDataErrorInfo в объектах данных и написание
собственных специальных правил проверки достоверности.
DataErrorValidationRule
Многие разработчики предпочитают не генерировать
исключения для индикации ошибок ввода. На то есть несколько причин, среди которых следующие: ошибка пользовательского ввода не является исключительной ситуацией,
ошибочные условия могут зависеть от взаимодействия значений множества свойств, к тому же иногда стоит сохранить
некорректные значения для дальнейшей обработки вместо того, чтобы немедленно отклонять их.
В технологии Windows Forms разработчики могут использовать интерфейс IDataErrorInfo (из пространства имен
System.ComponentModel), чтобы избегать исключений с сохранением кода проверки достоверности в классе данных. Интерфейс IDataErrorInfo был изначально спроектирован для
поддержки сеточных отображаемых элементов управления,
таких как DataGridView, но также он работает и с решениями
общего назначения для сообщений об ошибках.
Интерфейс IDataErrorInfo требует наличия двух членов:
строкового свойства Error и строкового индексатора. Свойство Error предоставляет общую строку описания всего объекта (которая может выглядеть как "Invalid Data" ("Неверные
данные")). Строковый индексатор принимает имя свойства и
возвращает соответствующую детальную информацию об
ошибке. Например, если передать "UnitCost" строковому индексатору, то можно получить ответ вроде "The UnitCost cannot be negative" ("Значение UnitCost не можут быть отрицательным"). Ключевая идея состоит в том, что свойства устанавливаются нормально, без возражений, а индексатор позволяет пользовательскому интерфейсу проверить неверные данные. Все логика обработки ошибок для целого класса централизована в одном месте.
Ниже приведена пересмотренная версия класса Product,
реализующего IDataErrorInfo. Хотя можно использовать IDataErrorInfo для вывода сообщений проверки достоверности
для широкого диапазона связанных с этой проверкой проблем,
данная логика проверяет на наличие ошибок только одно
свойство — ModeNumber.
public class Product : INotifyPropertyChanged, IDataErrorInfo
{
private string modelNumber;
public string ModelNumber
{
get { return modelNumber; }
set {
modelNumber = value;
OnPropertyChanged(new PropertyChangedEventArgs("ModelNumber"));
}
}
public string this[string propertyName] {
get
{
if (propertyName == "ModelNumber")
{
bool valid = true;
foreach (char с in ModelNumber)
{
if
(!Char.IsLetterOrDigit(с))
{
valid = false;
break;
}
}
if (!valid)
return "The ModelNumber
can only contain letters and numbers.";
}
return null;
}
}
// WPF не использует это свойство.
public string Error
{
get { return null; }
}
}
Чтобы заставить WPF использовать интерфейс IDataErrorInfo и применять его для проверки ошибок при модификации свойства, необходимо добавить DataErrorValidationRule к коллекции правил Binding.ValidationRules, как показано ниже:
<TextBox Margin="5" Grid.Row="2"
Grid.Column="1">
<TextBox.Text>
<Binding Path="UnitCost">
<Binding.Converter>
<local:PriceConverter></local:PriceConverter>
</Binding.Converter>
<Binding.ValidationRules>
<ExceptionValidationRule></ExceptionValidationRule>
<DataErrorValidationRule></DataErrorValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Отметим, что можно комбинировать оба подхода, создавая объект данных, который генерирует исключения для
некоторых типов ошибок и использует IDataErrorInfo для сообщения о других. Достаточно лишь позаботиться о примене-
нии
как
ExceptionValidationRule,
DataErrorValidationRule.
так
и
Специальные правила проверки достоверности
Подход с применением специального правила проверки
достоверности подобен применению специального конвертера. Для этого определяется класс-наследник ValidationRule из
пространства имен System.Windows.Controls и переопределяется его метод Validate() для выполнения требуемой проверки
достоверности. Если необходимо, можно добавить свойства,
принимающие другие детали, которые могут повлиять на проверку достоверности.
Ниже приведено полное правило проверки достоверности, ограничивающее десятичные значения некоторым диапазоном. По умолчанию минимум устанавливается в 0, а максимум — в наибольшее число, которое умещается в десятичный
тип данных, поскольку это правило предназначено для работы
с денежными значениями. Однако для максимальной гибкости
обе эти детали являются конфигурируемыми через свойства.
public class PositivePriceRule : ValidationRule
{
private decimal min = 0;
private decimal max = Decimal.MaxValue;
public decimal Min
{
get { return min; }
set { min = value; }
}
public decimal Max
{
get { return max; }
set { max = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
decimal price = 0;
try
{
if (((string)value).Length > 0)
// Allow number styles with
currency symbols like $.
price = Decimal.Parse((string)value, System.Globalization.NumberStyles.Any);
}
catch (Exception e)
{
return new ValidationResult(false, "Illegal characters.");
}
if ((price < Min) || (price > Max))
{
return new ValidationResult(false,
"Not in the range " + Min + "
to " + Max + ".");
}
else
{
return new ValidationResult(true, null);
}
}
}
Отметим, что логика проверки достоверности использует перегруженную версию метода Decimal. Parse(), принимающего значение из перечисления NumberStyles. Это связано
с тем, что проверка достоверности всегда выполняется перед
преобразованием. Если применяется и средство проверки достоверности, и конвертер к одному полю, то нужно обеспечить успех проверки достоверности при наличии символа валюты. Успех или неудача логики проверки достоверности
определяется возвращенным объектом ValidationResult. Свойство IsValid указывает на успех проверки достоверности, и
если проверка достоверности не прошла, то свойство ErrorContent предоставляет объект, описывающий проблему. В
данном примере содержимое ошибки — строка, которая будет
отображена в пользовательском интерфейсе, что является
наиболее распространенным подходом.
Правило проверки достоверности можно применить к
элементу, добавив его в коллекцию Binding.ValidationRules.
Вот пример, использующий PositivePriceRule и устанавливающий Maximum равным 999.99:
<TextBlock Margin="7" Grid.Row="2">Unit
Cost:</TextBlock>
<TextBox Margin="5" Grid.Row="2"
Grid.Column="1">
<TextBox.Text>
<Binding Path="UnitCost" NotifyOnValidationError="true">
<Binding.Converter>
<local:PriceConverter></local:PriceConverter>
</Binding.Converter>
<Binding.ValidationRules>
<local:PositivePriceRule
Max="999.99" ></local:PositivePriceRule>
<ExceptionValidationRule></ExceptionValidationRule>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
Часто определяется отдельный объект правила проверки достоверности для каждого элемента, использующего один
и тот же тип правила. Это объясняется тем, что можно решить
подкорректировать свойства проверки достоверности (такие
как минимум и максимум в PositivePriceRule) для разных элементов индивидуально. Если известно, что нужно использовать абсолютно одинаковые правила проверки достоверности
для более одной привязки, можно определить правило в виде
ресурса и просто указывать на него в каждой привязке, используя расширение разметки StaticResource.
Коллекция Binding.ValidationRules может принимать
неограниченное количество правил. Когда значение фиксируется в источнике, WFP проверяет каждое правило проверки
достоверности по порядку. Если все проверки достоверности
прошли успешно, то WPF затем вызывает конвертер и применяет значение к источнику.
Когда выполняется проверка достоверности с помощью
PositivePriceRule, поведение будет таким же, как и при использовании ExceptionValidationRule — текстовое поле будет
очерчено красным, установлены свойства HasError и Error, и
инициировано событие Error. Чтобы снабдить пользователя
каким-то полезным откликом, потребуется добавить немного
кода, чтобы настроить ErrorTemplate.
Реакция на ошибки проверки достоверности
В приведённом выше примере единственное указание
на ошибку, которое получит пользователь— это красный контур вокруг текстового поля. Чтобы представить больше информации, можно обработать событие Error, возникающее при
всякой установке или очистке ошибки. Однако сначала необходимо
убедиться,
что
свойство
Binding.NotifyOnValidationError установлено в true:
<Binding Path="UnitCost" NotifyOnValidationError="true">
Событие Error — маршрутизируемое, использует передачу вверх, так что можно обработать его для многих элементов управления, присоединив обработчик событий к родительскому контейнеру, как показано ниже:
<Grid Name="gridProductDetails" Validation.Error="validationError">
Далее представлен код, реагирующий на это событие и отображающий окно сообщения с информацией об ошибке.
private void validationError(object sender, ValidationErrorEventArgs e)
{
if (e.Action == ValidationErrorEventAction.Added)
{
MessageBox.Show(e.Error.ErrorContent.ToString());
}
}
Свойство ValidationErrorEventArgs.Error предоставляет
объект ValidationError, соединяющий воедино несколько по-
лезных объектов, в том числе исключение, вызвавшее проблему (Exception), нарушенное правило проверки достоверности (ValidationRule), ассоциированный объект Binding (BindingInError) и любую специальную информацию, возвращенную объектом ValidationRule (ErrorContent).
Если используются собственные специальные правила
проверки достоверности, то информацию об ошибке рекомендуется помещать в свойство ValidationError.ErrorContent. Если применяется ExceptionValidationRule, то свойство ErrorContent вернет свойство Message соответствующего исключения.
Отметим один важный момент. Если исключение произошло по причине того, что тип данных не может быть приведен к соответствующему значению, то ErrorContent работает, как ожидается, и сообщает о проблеме.
Однако если установщик свойства в объекте данных
генерирует исключение, то это исключение помещается в
оболочку TargetInvocationException, и ErrorContent предоставит текст из свойства TargetInvocationException.Message, которое будет выглядеть как менее полезное предупреждение
"Exception has been thrown by the target of an invocation" ("Исключение возбуждено целевым объектом вызова").
Таким образом, если используются собственные установщики свойств для генерации исключений, то придется добавлять код, проверяющий свойство InnerException объекта
TargetInvocationException. Если оно не равно null, можно извлечь оригинальный объект исключения и использовать его
свойство
Message
вместо
свойства
ValidationError.ErrorContent.
Получение списка исключений
В определенный момент разработчику может понадобиться список отложенных ошибок в текущем окне или заданном контейнере из этого окна. Для решения этой задачи
достаточно пройтись по дереву элементов и проверить свойство Validation.HasError каждого элемента.
Код следующей процедуры демонстрирует пример, который целенаправленно ищет недопустимые данные в объектах TextBox. Он использует рекурсивный код для прохода по
всей иерархии элементов. При этом информация об ошибках
накапливается в единственном сообщении, которое отображается пользователю.
private void GetErrors(StringBuilder sb,
DependencyObject obj)
{
foreach (object child in LogicalTreeHelper.GetChildren(obj))
{
// Ignore strings and dependency
objects that aren't elements.
TextBox element = child as TextBox;
if (element == null) continue;
if (Validation.GetHasError(element))
{
sb.Append(element.Text + "
has errors:\r\n");
foreach (ValidationError error in Validation.GetErrors(element))
{
sb.Append(" " + error.ErrorContent.ToString());
sb.Append("\r\n");
}
}
// Check the children of this
object.
GetErrors(sb, element);
}
}
private bool FormHasErrors(out string
message)
{
StringBuilder sb = new StringBuilder(); GetErrors(sb, gridProductDetails); message
= sb.ToString (); return message != "";
}
private void cmdOK_Click(object sender,
RoutedEventArgs e)
{
string message;
if (FormHasErrors(message))
{
// Ошибки есть.
MessageBox.Show(message);
}
else
{
// Ошибок нет. Можно продолжать работу {например, // зафиксировать изменения в источнике данных)
}
}
Отображение отличающегося индикатора ошибки
Чтобы получить максимальную отдачу от проверки достоверности WPF, можно создать собственный шаблон ошибок, который будет помечать ошибки соответствующим образом. На первый взгляд это может показаться довольно низкоуровневым способом сообщать об ошибках — в конце концов, стандартные шаблоны элементов управления предоставляют возможность настройки композиции элемента за считанные минуты. Однако шаблон ошибок не похож на обычный шаблон элемента управления.
Шаблоны ошибок используют декоративный слой
(adorner layer), который является слоем рисования, существующим прямо над обычным содержимым окна. Используя этот
слой, можно добавлять визуальные украшения для сигнализации об ошибке, не подменяя шаблона элемента управления и
не изменяя компоновки окна. Стандартный шаблон ошибок
для текстового поля работает, добавляя элемент Border красного цвета, который "парит" прямо над соответствующим текстовым полем (оно под ним остается неизменным), можно использовать шаблон ошибок для добавления других деталей
вроде изображений, текста либо иного рода графических деталей, привлекающих внимание к проблеме.
Соответствующий пример показан ниже. Он определяет шаблон ошибки, использующий зеленую рамку, и добавляет звездочку рядом с элементом управления с неправильным
вводом. Шаблон помещен в оболочку правила стиля, так что
оно автоматически применяется ко всем текстовым полям текущего окна.
<Style TargetType="{x:Type TextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right"
Foreground="Red" FontSize="14" FontWeight="Bold"
ToolTip="{Binding ElementName=adornerPlaceholder,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
>*</TextBlock>
<Border BorderBrush="Green" BorderThickness="1">
<AdornedElementPlaceholder
Name="adornerPlaceholder"></AdornedElementPlaceholder>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
AdornedElementPlaceholder служит элементом, заставляющим работать эту технику. Он представляет сам элемент
управления, который существует в слое элементов. Используя
AdornedElementPlaceholder, можно упорядочить содержимое
относительно лежащего ниже текстового поля.
В результате рамка в этом примере размещается прямо
поверх текстового поля, независимо от его размеров. Звездочка в этом примере помещается прямо справа (как показано на
Рис. 23). Лучше всего то, что содержимое нового шаблона
ошибки накладывается поверх существующего содержимого,
не вызывая никаких изменений в слое оригинального окна.
Рис. 23 Использование шаблона ошибки
Этот шаблон ошибки обдалает одним недостатком —
он не предоставляет никакой дополнительной информации об
ошибке. Чтобы показать эти детали, нужно извлечь их с помощью привязки данных. Хорошим подходом может быть такой: взять содержимое первой ошибки и использовать его для
текста всплывающей подсказки индикатора ошибки. Ниже
приведен шаблон, который именно это и делает.
<ControlTemplate>
<DockPanel
LastChildFill="True">
<TextBlock DockPanel.Dock="Right"
Foreground="Red" FontSize="14" FontWeight="Bold"
ToolTip="{Binding ElementName=adornerPlaceholder,
Path=AdornedElement.(Validation.Errors)[0].Error
Content}"
>*</TextBlock>
<Border BorderBrush="Green"
BorderThickness="1">
<AdornedElementPlaceholder
Name="adornerPlaceholder"></AdornedElementPlaceh
older>
</Border>
</DockPanel>
</ControlTemplate>
Свойство Path выражения привязки требует долее подробного рассмотрения. Источником этого выражения привязки является AdornedElementPlaceholder, определенный в
шаблоне элемента управления:
ToolTip="{Binding ElementName=adornerPlaceholder
Класс AdornedElementPlaceholder представляет ссылку
на элемент внизу (в данном случае — объект TextBox с ошибкой) через свойство по имени AdornedElement:
ToolTip="{Binding ElementName=adornerPlaceholder,
Path=AdornedElement.(Validation.Errors)[0].Error
Content}"
Чтобы извлечь действительную ошибку, нужно проверить свойство Validation. Errors этого элемента. Однако необходимо поместить свойство Validation.Errors в скобки, чтобы
указать, что это — прикрепленное (attached) свойство. Также
необходимо использовать индексатор для извлечения объекта
ValidationError из коллекции и затем извлечь значение свойства Error.
Теперь можно видеть сообщение об ошибке при перемещении курсора мыши над звездочкой.
В качестве альтернативы можно можете решить отобразить сообщение об ошибке в ToolTip из Border или самого
TextBox, так что сообщение об ошибке появится, когда пользователь поместит курсор мыши над любой частью элемента
управления. Можно реализовать такую возможность без помощи специального шаблона ошибок: все. что понадобится —
это триггер на элементе управления TextBox, который отреагирует на присваивание Validation.HasError значения true и
применит ToolTip с сообщением об ошибке. Вот пример соответствующего триггера:
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
Полученный результат показан на Рис. 24.
Рис. 24 ToolTip с информацией об ошибке
Шаблоны данных, представления данных, поставщики данных
Выше была представлена основная информация о привязках данных в WPF, т.е. о том, как можно извлекать информацию из объекта и отображать ее в окне с помощью небольшого количества кода или вообще без него. Также там было
рассказано и о том, как можно делать эту информацию редактируемой, как можно ее форматировать, преобразовывать в
необходимое представление и использовать вместе с ней более совершенные функции вроде проверки допустимости. Однако это еще далеко не все, что каждому разработчику приложений предоставляет WPF.
В данном разделе будут рассмотрены три компонента,
с помощью которых можно создавать еще лучше привязанные
окна. Сначала будут рассмотрены шаблоны данных, которые
позволяют настраивать способ отображения каждого пункта в
ItemsControl. Шаблоны данных являются ключом к преобразованию обычного списка в многофункциональное средство
представления данных со специальным форматированием,
графическим содержимым и дополнительными элементами
управления WPF. Затем будут рассмотрены представления
данных, которые способны незаметным образом координировать коллекции привязанных данных. Используя представления данных, можно добавлять логику навигации для реализации фильтров, сортировки и группирования. Затем будут рассмотрены поставщики данных, позволяющих извлекать информацию из источника данных с помощью гораздо меньшего
объема кода.
Кратко о привязке данных
В большинстве сценариев с привязками данных привязки выполняются не с одним объектом, а с целой коллекцией или объектом DataTable. На Рис. 25 показан уже рассмотренный пример — форма со списком продуктов. Когда пользователь выбирает в этом списке какой-то продукт, справа появляются все касающиеся этого продукта детали.
Рис. 25 Окно просмотра коллекции продуктов
Шаги, которые необходимо выполнить для создания
формы такого типа, описывались выше. В данном разделе
будет показано, как можно видоизменять список элементов:
фильтровать его, сортировать и создавать более детальное
представление каждого элемента данных.
Начнём рассмотрение с последней задачи, а именно
задачи создания более детального представления каждого
элемента данных. Свойство элемента управления DisplayMemberPath указывает на свойство каждого элемента данных, который должен отображаться в списке. Если его
убрать, список будет просто вызывать на каждом объекте
метод ToString() для извлечения его строкового представления. Предположим, что требуется использовать комбинацию свойств из привязанного объекта, расположить их
определенным образом или отобразить визуальное представление, которое является более сложным, чем простая строка.
Для решения этой задачи необходимо создать шаблон данных.
Шаблоны данных
Шаблон данных (data template) — это часть XAMLразметки, которая определяет, как должен отображаться
привязанный объект данных. Шаблоны данных поддерживают элементы управления двух типов:
 Элементы управления содержимым, которые поддерживают шаблоны данных через свойство ContentTemplate. Такие шаблоны применяются для
отображения любого содержимого, которое было
помещено в свойство Content.
 Списковые элементы управления (т.е. те, что унаследованы от класса ItemsControl), которые поддерживают шаблоны данных через свойство
ItemTemplate. Такие шаблоны применяются для
отображения каждого элемента из коллекции (или
каждой строки из объекта DataTable), указанной в
свойстве ItemsSource.
Возможность создания шаблонов для элементов
управления списками на самом деле подразумевает использование шаблонов элементов управления содержимым.
Именно поэтому каждый элемент в списке упаковывается в
элемент управления содержимым, как, например, элемент
ListBoxItem помещается в оболочку элемента управления
ListBox, элемент ComboBoxItem— в ComboBox, и т.д. Любой шаблон, который указывается для свойства ItemTemplate
списка, применяется как ContentTemplate для каждого элемента в этом списке.
Шаблон данных — это обычный блок XAMLразметки. Как и любой другой блок XAML-разметки. шаблон данных может включать любую комбинацию элементов.
Он также должен содержать одно или более выражений
привязки, извлекающих отображаемую информацию. (Если
не включить никаких выражений привязки данных, все элементы в списке будут выглядеть одинаково, и тогда от такого списка будет мало толку.)
Увидеть, как работает шаблон данных, лучше всего,
начав с базового списка, в котором никакой такой шаблон не
используется. Например, возьмем окно списка, которое показывалось ранее:
<ListBox Name="lstProducts" Margin="5"
DisplayMemberPath="ModelName" </ListBox>
Точно такого же эффекта можно добиться и добавив в
это окно списка шаблон данных:
<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding
Path=ModelName}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Когда этот список привязывается к коллекции продуктов (путем установки свойства ItemsSource), для каждого
объекта Product создается один элемент ListBoxItem. Свойству ListBoxItem.Content присваивается подходящий объект
Product, а свойству ListBoxItem.ContentTemplate — показанный ранее шаблон данных, который извлекает значение из
свойства Product.ModelName и отображает его в TextBlock.
Пока что результаты не впечатляют. Но этим переключением на шаблон данных был открыт путь к неограниченным возможностям для представления данных любым
вообразимым образом. Ниже приведен пример, в котором
каждый элемент упаковывается в границе с закругленными
углами, отображается состоящая из двух частей информация, и для выделения номера модели используется форматирование с полужирным начертанием.
<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="0" Background="White">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
Background="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=Background
}" CornerRadius="4">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold"
Text="{Binding Path=ModelNumber}"></TextBlock>
<TextBlock Grid.Row="1"
Text="{Binding Path=ModelName}"></TextBlock>
</Grid>
</Border>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Когда этот список привязывается, для каждого продукта создается отдельный объект Border. Внутри элемента
Border находится элемент управления Grid с состоящей из
двух частей информацией, как показано на Рис. 26.
Рис. 26 Отображение списка при помощи шаблона данных
Отделение и многократное использование шаблонов
Шаблоны, подобно стилям, часто объявляют ресурсом окна или приложения, вместо того, чтобы определять в
использующем их списке. Такое отделение зачастую оказывается более понятным, особенно при использовании длинных, сложных либо многочисленных шаблонов в одном и
том же элементе управления, а также дает возможность повторно использовать шаблоны в более чем одном списке или
элементе управления содержимым, если нужно, чтобы данные в разных частях пользовательского интерфейса выглядели абсолютно одинаково.
Для того чтобы это работало, требуется всего лишь
определить шаблон данных в коллекции ресурсов и присвоить ему имя ключа. Ниже показан пример, в котором извлекается шаблон, приводившийся в предыдущем примере.
<Window.Resources>
<DataTemplate
x:Key="ProductDataTemplate">
<Grid Margin="0" Background="White">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
Background="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=Background
}" CornerRadius="4">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold" Text="{Binding
Path=ModelNumber}"></TextBlock>
<TextBlock Grid.Row="1"
Text="{Binding Path=ModelName}"></TextBlock>
</Grid>
</Border>
</Grid>
</DataTemplate>
</Window.Resources>
Теперь этот шаблон данных можно использовать с
помощью ссылки StaticResource:
<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource ProductDataTemplate}">
При желании автоматически использовать один и тот
же шаблон данных в элементах управления разного типа
можно применить еще один интересный прием, а именно—
установить свойство DataTemplate.DataType так, чтобы оно
указывало на тип привязанных данных, для которых должен
использоваться данный шаблон. Например, предыдущий
пример можно было бы изменить, удалив ключ и указав, что
данный шаблон предназначен для привязанных объектов
Product, где бы они ни находились:
<Window.Resources>
<DataTemplate DataType="{x:Type local:Product}">
Здесь предполагается, что пространство имен XML было
определено с именем local и отображено на пространство
имен проекта при помощи следующего текста:
xmlns:data="clrnamespace:StoreDatabase;assembly=StoreDatabase"
В этом случае данный шаблон будет использоваться с
любым списком или элементом управления в этом окне, который привязывается к объектам Product. Задавать параметр
ItemTemplate не требуется.
Отметим, что для шаблонов данных привязка данных
не является обязательной. Т.е., использовать свойство ItemsSource для заполнения шаблонного списка не нужно. В приведенных выше примерах объекты Product допускается добавлять как декларативно (в XAML- разметке), так и программно (путем вызова метода ListBox.Items.Add()). И в том
и в другом случае шаблон данных будет работать одинаково.
Усовершенствованные шаблоны
Шаблоны данных могут быть самодостаточными.
Наряду с базовыми элементами вроде объекта TextBlock и
выражений привязки данных, в них также могут использоваться и более сложные элементы управления, а также при-
соединяться обработчики событий, выполняться преобразование данных в различные представления, применяться эффекты анимации и т.д.
Чтобы показать, насколько мощными могут быть
шаблоны данных, рассмотрим несколько небольших примеров. При привязке данных можно использовать объекты
IValueConverter для преобразования этих данных в более полезное представление. Возьмем, например, объект ImagePathConverter, который демонстрировался выше. Он принимает имя файла изображения и использует его для создания объекта BitmapImage с соответствующим содержимым,
который затем может быть привязан непосредственно к элементу Image.
Этот объект ImagePathConverter можно применять
для создания следующего шаблона данных, который отображает изображение для каждого продукта:
<Window.Resources>
<DataTemplate DataType="{x:Type data:Product}">
<Border Margin="3" BorderThickness="1"
BorderBrush="SteelBlue"
CornerRadius="4">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold"
Text="{Binding Path=ModelNumber}"></TextBlock>
<TextBlock Grid.Row="1"
Text="{Binding Path=ModelName}"></TextBlock>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
В результате использования данного шаблона получается гораздо более интересный список (Рис. 27).
Рис. 27 Шаблон данных с изображением
Другим полезным приемом является размещение
элементов управления прямо внутри шаблона. Например, на
Рис. 28 показан список категорий. Напротив каждой из этих
категорий отображается кнопка View (Просмотр), которую
можно использовать для отображения еще одного окна с перечнем продуктов, подпадающих под конкретную категорию.
Рис. 28 Список с элементами управления
Особенность данного примера заключается в обработке выполняемых на кнопках щелчков. Очевидно, что все кнопки нужно соединить с одним и тем же обработчиком событий, а
сам обработчик — определить внутри шаблона. Однако то, на каком элементе был выполнен
щелчок, нужно определить из списка. Одним из возможных решений является сохранение в
свойстве Tag кнопки дополнительной идентификационной информации, как показано ниже:
<DataTemplate>
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Text="{Binding
Path=CategoryName}"></TextBlock>
<Button Grid.Column="1" Padding="2"
Click="cmdView_Clicked" Tag="{Binding}">View
...</Button>
</Grid>
</DataTemplate>
Далее это свойство Tag можно извлечь в обработчике событий:
private void cmdView_Clicked(object sender, RoutedEventArgs e)
{
Button cmd = (Button)sender;
DataRowView row = (DataRowView)cmd.Tag;
lstCategories.SelectedItem = row;
MessageBox.Show("You chose category #" + row["CategoryID"].ToString() + ": " +
(string)row["CategoryName"]);
}
Данную информацию можно использовать и для выполнения еще одного действия,
например, для запуска другого окна с перечнем продуктов и передачи ему значения CategorylD,
с помощью которого оно затем могло бы выполнять фильтрацию, чтобы отображать только те
продукты, которые соответствуют выбранной категории. Если нужна вся информация о выбранном элементе данных, можно захватить весь объект данных, опустив свойство Path при
определении привязки:
<Button HorizontalAlignment="Right" Padding="l" Click="cmdView_Clicked"
Tag="{Binding}">View ...</Button>
Теперь обработчик событий будет получать объект Product (если выполнить привязку к
коллекции объектов Products). В случае привязки к DataTable он вместо этого будет получать
объект DataRowView, который можно использовать для извлечения значений всех полей точно
таким же образом, как и объект DataRow.
Подход с передачей всего объекта дает еще одно преимущество: он упрощает обновление информации о выбранном в списке элементе. В текущем примере на кнопке можно щелкать в любом элементе, независимо от того, выбран он в настоящий момент или нет. Это чревато возникновением путаницы, потому что пользователь может выбрать в списке один элемент,
а щелкнуть на кнопке View в совсем другом элементе. При возвращении им в окно списка выбранным останется все равно первый элемент, несмотря на то, что на самом деле в предыдущей
операции участие принимал второй элемент. Чтобы устранить возможность возникновения такой путаницы, лучше сделать так, чтобы при щелчке на кнопке View, выбор перемещался на
соответствующий элемент списка, как показано ниже:
Button cmd = (Button)sender;
Product product = (Product)cmd.Tag;
IstCategories.SelectedItem = product;
Возможен и еще один вариант: можно сделать так, чтобы кнопка View отображалась
только в выбранном элементе. Такой подход подразумевает изменение или замену шаблона,
используемого в данном списке.
Варьирование шаблонов
Одним из ограничений шаблонов является то, что для всего списка допускается использование только одного шаблона. А ведь во многих ситуациях будет необходимо иметь возможность представить разные элементы данных разным образом.
Достичь этой цели можно несколькими способами. Ниже перечислены некоторые
наиболее распространенные из них.
 Использование триггера данных. С помощью такого триггера можно сделать так,
чтобы значение свойства в шаблоне изменялось в зависимости от значения соответствующего свойства в привязанном объекте данных. Триггеры данных работают
почти так же, как и триггеры свойств. Единственное отличие заключается в том, что
они не требуют наличия свойств зависимостей.
 Использование конвертера значений. Класс, реализующий IValueConverter, может
преобразовывать значение из привязанного объекта в значение, пригодное для установки форматирующего свойства в шаблоне.
 Использование селектора шаблонов. Селектор шаблонов анализирует привязанный
объект данных и выбирает один из нескольких отдельных шаблонов.
Подход с триггерами данных является самым простым. В целом требуется всего лишь
установить свойство одного из элементов в шаблоне так, чтобы оно изменялось на основании
значения соответствующего свойства в элементе данных. Например, можно было бы сделать
так, чтобы фон специальной границы, которая окружает каждый элемент списка, изменялся на
основании значения в свойстве СаtegoryName соответствующего объекта Product. Ниже приведен пример, в котором буквы в наименованиях продуктов категории Tools выделены полужирным.
<DataTemplate x:Key="DefaultTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=CategoryName}" Value="Tools">
<Setter Property="ListBoxItem.Foreground" Value="Red"></Setter>
</DataTrigger>
</DataTemplate.Triggers>
<Grid Margin="0" Background="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=Background
}">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
CornerRadius="4" Background="White">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock
Text="{Binding Path=ModelNumber}"></TextBlock>
<TextBlock Grid.Row="1"
Text="{Binding Path=ModelName}"></TextBlock>
</Grid>
</Border>
</Grid>
</DataTemplate>
Поскольку объект Product реализует интерфейс INotifyPropertyChanged, любые изменения применяются немедленно. Например, в случае изменения свойства CategoryName для переноса продукта из категории Tools в другую категорию его текст в списке мгновенно изменяется соответствующим образом.
Такой подход полезен, но и ограничен по своей природе. Он не позволяет изменять никаких сложных деталей шаблона, а только корректировать в нем свойство отдельных элементов (или элементов контейнера). Как демонстрировалось выше, триггеры могут выполнять
проверку только на предмет равенства — никаких других, более сложных условий сравнения
они не поддерживают. Это означает, что применять такой подход, например, для выделения
цен, превышающих определенное значение, нельзя. А при необходимости в выборе между радом возможностей (например, применении к каждой категории продуктов своего цвета фона),
потребуется писать один триггер для каждого возможного значения, что слишком неудобно.
Другой вариант — создать один шаблон, способный корректировать свое поведение в
соответствии с привязанным объектом. Для реализации этой возможности обычно требуется
применение конвертера значений, анализирующего свойство в привязанном объекте и возвращающего более подходящее значение. Например, в текущем примере можно было бы создать
конвертер CategoryToColorConverter, анализирующий категорию продукта и возвращающий
соответствующий объект Color. Тогда в шаблоне привязку можно было выполнить прямо к
свойству CategoryName, как показано ниже:
<Border Margin="5" BorderThickness="l" BorderBrush="SteelBlue" CornerRadius="4"
Background="{Binding Path=CategoryName, Converter=StaticResource CategoryToColorConverter}">
Подход с конвертером значений, как и подход с триггерами, тоже не позволяет вносить
серьезных изменений, например, заменять часть шаблона чем-то совершенно другим. Однако
он позволяет реализовывать более сложную логику форматирования, а также основывать одно
единственное свойство форматирования на нескольких свойствах из привязанного объекта
данных.
Селекторы шаблонов
Еще одним, даже более эффективным вариантом является обеспечение разных элементов совершенно разными шаблонами. К сожалению, сделать это декларативным образом невозможно. Вместо этого нужно создать специальный класс, унаследованный от DataTemplateSelector. В обязанности этого класса входит анализ каждого элемента данных и выбор подходящего шаблона. Эта работа выполняется в методе SelectTemplate(), который обязательно
нужно переопределить.
Ниже показан элементарный селектор шаблонов, делающий выбор между двумя шаблонами:
public class ProductByCategoryTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
Product product = (Product)item;
Window window = Application.Current.MainWindow;
if (product.CategoryName == "Travel")
{
return (DataTemplate)window.FindResource("TravelProductTemplate");
}
else
{
return (DataTemplate)window.FindResource("DefaultProductTemplate");
}
}
}
В этом примере продукты, находящиеся в категории Travel, получают один шаблон. а
все остальные продукты— другой. Оба шаблона обязательно должны быть определены в коллекции Resources окна с именами TravelProductTemplate и DefaultProductTemplate.
Данный селектор шаблонов работает, но не идеален. Одна из проблем состоит в том, что
код зависит от деталей, находящихся в разметке, что означает наличие зависимости, которая не
будет принудительно учитываться во время компиляции и, следовательно, может быть легко
нарушена. Другая проблема состоит в том, что в данном селекторе шаблонов значение, которое
он должен отыскивать (каковым в этом случае является название категории), жестко закодировано, что ограничивает возможности повторного использования этого селектора.
Поэтому лучше будет создать селектор шаблонов с одним или несколькими свойствами,
позволяющими указывать детали наподобие критерия, который должен применяться для оценки элементов данных, и шаблонов, которые должны использоваться. Ниже показан другой, тоже простой, но при этом еще и чрезвычайно гибкий селектор шаблонов. Он способен анализировать любой объект данных, отыскивать указанное свойство и сравнивать его значение с другим значением для определения того, какой из двух шаблонов выбрать. Все необходимые детали (свойство, которое он должен отыскивать, значение, с которым он должен сравнивать значение этого свойства, и шаблоны, между которыми он должен выбирать) указаны в виде
свойств. Метод SelectTemplate() использует рефлексию для поиска нужного свойства.
Код этого селектора выглядит так:
public class SingleCriteriaHighlightTemplateSelector : DataTemplateSelector
{
public DataTemplate DefaultTemplate
{
get;
set;
}
public DataTemplate HighlightTemplate
{
get;
set;
}
public string PropertyToEvaluate
{
get;
set;
}
public string PropertyValueToHighlight
{
get;
set;
}
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
Product product = (Product)item;
Type type = product.GetType();
PropertyInfo property = type.GetProperty(PropertyToEvaluate);
if (property.GetValue(product, null).ToString() == PropertyValueToHighlight)
{
return HighlightTemplate;
}
else
{
return DefaultTemplate;
}
}
}
Для того чтобы этот селектор работал, необходимо создать два подлежащих применению стиля, а также создать и инициализировать экземпляр класса SingleCriteriaHighlightTemplateSelector.
Ниже показаны два похожих шаблона, которые отличаются только цветом фона, форматированием полужирным начертанием и одной дополнительной строкой текста:
<Window.Resources>
<DataTemplate x:Key="DefaultTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=CategoryName}" Value="Tools">
<Setter Property="ListBoxItem.Foreground" Value="Red"></Setter>
</DataTrigger>
</DataTemplate.Triggers>
<Grid Margin="0" Background="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=Background
}">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
CornerRadius="4" Background="White">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock
Text="{Binding Path=ModelNumber}"></TextBlock>
<TextBlock Grid.Row="1"
Text="{Binding Path=ModelName}"></TextBlock>
</Grid>
</Border>
</Grid>
</DataTemplate>
<DataTemplate x:Key="HighlightTemplate">
<Grid Margin="0" Background="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=Background
}">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
Background="LightYellow"
CornerRadius="4">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold"
Text="{Binding Path=ModelNumber}"></TextBlock>
<TextBlock Grid.Row="1" FontWeight="Bold"
Text="{Binding Path=ModelName}"></TextBlock>
<TextBlock Grid.Row="2" FontStyle="Italic" HorizontalAlignment="Right">*** Great for vacations ***</TextBlock>
</Grid>
</Border>
</Grid>
</DataTemplate>
</Window.Resources>
При создании экземпляра SingleCriteriaHighlightTemplateSelector нужно просто указать
ему на эти два шаблона. Экземпляр класса SingleCriteriaHighlight TemplateSelector также можно
создать как ресурс (что удобно, если его планируется использовать повторно в других местах)
или определить прямо вместе с элементом управления ListBox. как показано ниже:
<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplateSelector>
<local:SingleCriteriaHighlightTemplateSelector
DefaultTemplate="{StaticResource DefaultTemplate}"
HighlightTemplate="{StaticResource HighlightTemplate}"
PropertyToEvaluate="CategoryName"
PropertyValueToHighlight="Travel"
>
</local:SingleCriteriaHighlightTemplateSelector>
</ListBox.ItemTemplateSelector>
Здесь селектор SingleCriteriaHighlightTemplateSelector отыскивает в привязанном элементе данных свойство Category, и если в нем содержится текст Travel, использует шаблон
HighlightTemplate. Результат показан на Рис. 29.
Рис. 29 Список, использующий селектор шаблонов
Процесс выбора шаблона выполняется один раз, при первой привязке списка. Это является проблемой, если отображаются редактируемые данные и при редактировании возможен
перенос элемента данных из категории одного шаблона в другой. В такой ситуации необходимо заставить WPF применить шаблоны заново, а сделать это "мягким путем" невозможно. Грубый же подход подразумевает удаление селектора шаблонов и повторное его назначение:
DataTemplateSelector selector = lstProducts.ItemTemplateSelector;
lstProducts.ItemTemplateSelector = null;
lstProducts.ItemTemplateSelector = selector;
Можно сделать так, чтобы этот код выполнялся автоматически в ответ на определенные
изменения за счет обработки таких событий, как PropertyChanged, DataTable.RowChanged и более общее событие Binding.SourceUpdated.
Шаблоны и выбор
В текущем примере имеется одна небольшая, но очень неприятная особенность. Проблема состоит в том, что ни в одном из показанных шаблонов не учитывается выбор.
Если в списке выбирается какой-нибудь элемент, WPF автоматически устанавливает
значения для свойств Foreground и Background содержащего этот элемент контейнера (каковым
в данном случае является объект ListBoxItem). Для свойства Foreground в качестве значения
устанавливается белый цвет, а для свойства Background — голубой. Свойство Foreground подразумевает использование функции наследования свойств, из-за чего любые элементы, которые
были добавлены в шаблон, автоматически получают новый белый цвет, если только явно не
был указан какой-то другой цвет. Свойство Background не предполагает использования такой
функции, но по умолчанию имеет значение Transparent. Из-за этого, при наличии, например,
прозрачной границы, новый голубой фон проглядывает сквозь нее. В противном случае продолжает применяться тот цвет, который был указан.
Эта смесь может изменить все форматирование самым неожиданным образом. На Рис.
30 показан пример.
Рис. 30 Проблема форматирования выделенного элемента
Очевидно, что можно жестко закодировать все цвета, дабы избежать этой проблемы, но
тогда возникнет другая проблема. Единственным показателем того, что элемент выбран, будет
служить отображаемый вокруг изогнутой границы голубой фон.
Следовательно, более правильным решением будет изменить шаблон или предоставить
для выбранных элементов совершенно новый шаблон. В конце концов, при выборе элемента
вполне может потребоваться произвести различные изменения.
Однако изменение шаблона выбранного элемента является не стать простой задачей,
можно было бы ожидать. Класс ItemsControl не предоставляет свойства SelectedItemDataTemplate. Класс DataTemplateSelector, о котором рассказывалось выше, здесь тоже не поможет, поскольку он генерирует шаблоны только тогда, когда список привязывается впервые, и, следовательно, непригоден для изменения шаблона на этапе, когда пользователь выбирает элемент или
отменяет свой выбор.
Решение проблемы заключается в следующем. В списке без шаблонов для изменения
выбранного элемента можно использовать триггеры стилей. Эти стили могут изменять внешний вид контейнера, в котором содержатся все элементы списка. (В случае элемента управления ListBox таким контейнером является ListBoxItem, в случае элемента управления ComboBox — это ComboBoxItem и т.д.)
Эти стили можно применить двумя способами. Можно применить их по типу ко всем
элементам
управления
ListBoxItem,
а
можно
воспользоваться
свойством
ListBox.ItemContainerStyle, которое позволяет устанавливать применяемый стиль так, чтобы он
влиял на каждый объект ListBoxItem, который создается для этого списка. Оба подхода работают одинаково хорошо.
<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch">
<ListBox.ItemContainerStyle>
<Style>
<Setter Property="Control.Padding" Value="0"></Setter>
<Style.Triggers>
<Trigger Property="ListBoxItem.IsSelected" Value="True">
<Setter Property="ListBoxItem.Background" Value="DarkRed"
/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Этот триггер применяет к выбранному элементу темно-красный фон. К сожалению, в
случае списка, использующего шаблоны, данный код не дает желаемого эффекта. Дело в том,
что эти шаблоны включают элементы с разным цветом фона, который отображается поверх
темно-красного фона. Если не сделать все прозрачным (и не разрешить красному цвету просачиваться по всему шаблону), отображаться будет только красная каемка вокруг области поля
шаблона.
Возможное решение состоит в явной привязке фона в части шаблона к значению свойства ListBoxItem.Background. В этом есть смысл, поскольку теперь возникла задача выбора
правильного цвета фона для выделения выбранного элемента. Главное — удостовериться в
том, что он будет появляться именно в нужном месте.
Требуемая для реализации этого решения разметка является несколько запутанной. Все
дело в том, что использовать обычное выражение привязки и просто реализовать привязку к
свойству в текущем объекте данных (каковым в данном случае является объект Product) нельзя.
Вместо этого необходимо извлечь фон из контейнера элемента (каковым в данном случае является ListBoxltem). Это подразумевает использование свойства Binding. RelativeSource для поиска в дереве элементов первого подходящего объекта ListBoxltem. Как только такой элемент будет найден, можно извлечь цвет его фона и использовать его соответствующим образом.
Ниже показан готовый шаблон, который применяет в области изогнутой границы выбранный фон. Элемент Border размещается внутри элемента управления Grid с белым фоном,
что исключает вероятность появления выбранного цвета в районе поля за пределами изогнутой
границы. В результате стиль выбранного элемента получается гораздо более привлекательным,
как показано на Рис. 31.
<DataTemplate>
<Grid Margin="0" Background="White">
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
Background="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=Background
}" CornerRadius="4">
<Grid Margin="3">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontWeight="Bold" Text="{Binding
Path=ModelNumber}"></TextBlock>
<TextBlock Grid.Row="1" Text="{Binding
Path=ModelName}"></TextBlock>
</Grid>
</Border>
</Grid>
</DataTemplate>
Рис. 31 Выделение элемента при помощи шаблона данных
Такой подход — использование для изменения шаблона выражения привязки — работает хорошо, если необходимое значение свойства можно извлечь из контейнера элемента.
Например, это прекрасный вариант, если требуется извлечь цвет фона и изображения выбранного элемента. Однако он не столь удобен, если требуется изменить шаблон более серьезным
образом.
Рис. 32 Разворачивание выбранного элемента списка
Например, возьмем список продуктов, показанный на Рис. 32. При выборе в этом списке
какого-то продукта, соответствующий ему элемент разворачивается, изменяясь из однострочного текста в поле с рисунком и полным описанием. Вдобавок, в этом примере еще также
применяются две рассмотренных ранее технологии. а именно — показ содержимого типа изображений в шаблоне и использование привязки данных для установки цвета фона элемента Border при выборе продукта в списке. Чтобы создать подобный список, необходимо воспользоваться подходом, несколько отличающимся от продемонстрированного в предыдущем примере. Свойство RelativeSource объекта Binding все равно должно применяться для поиска текущего элемента ListBoxItem. Однако извлекать цвет его фона больше не требуется. Вместо
этого необходимо выполнить проверку на предмет того, является он выбранным или нет. Если
нет, тогда лишнюю информацию можно скрыть путем установки его свойства Visibility.
Этот подход похож на тот, что демонстрировался в предыдущем примере, но кое в чем
отличается от него. В предыдущем примере можно было просто выполнить привязку непосредственно к необходимому значению так, чтобы фон элемента ListBoxItem становился таким же,
как и у объекта Border. А в данном случае требуется анализировать свойство ListBoxItem.IsSelected и устанавливать свойство Visibility другого элемента. Типы данных у этих
свойств не совпадают: IsSelected представляет собой булевское значение, в то время как Visibility берет значение из перечисления Visibility. Из-за этого привязать свойство Visibility к свойству IsSelected без помощи конвертера нельзя. Возможное решение предполагает применение
триггера данных так, чтобы при изменении свойства IsSelected элемента ListBoxItem изменялось и свойство Visibility контейнера.
Место в разметке, куда следует поместить этот триггер, тоже отличается. Размещать его
в ItemContainerStyle больше не имеет смысла, поскольку нужно, чтобы скрывался не весь элемент, а только одна его часть, а раз так, то триггер должен быть частью стиля, применяемого
только к одному контейнеру.
Ниже показана несколько упрощенная версия шаблона, в которой пока нет поведения
автоматического разворачивания, и которая вместо этого просто отображает всю информацию
(рисунок и описание) для каждого продукта в списке.
<DataTemplate x:Key="DataTemplate">
<Grid Background="White" >
<Grid.Style>
<Style>
<Setter Property="TextBlock.Foreground" Value="Black"></Setter>
</Style>
</Grid.Style>
<Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue"
Background="{Binding RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=Background
}" CornerRadius="4">
<StackPanel Margin="3">
<StackPanel>
<TextBlock Margin="3" Text="{Binding Path=Description}" TextWrapping="Wrap"
MaxWidth="250" HorizontalAlignment="Left"
FontWeight="Regular"></TextBlock>
<Image Source="{Binding Path=ProductImagePath, Converter={StaticResource ImagePathConverter}}"></Image>
<Button FontWeight="Regular"
HorizontalAlignment="Right" Padding="1"
Tag="{Binding}">View Details...</Button>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</DataTemplate>
Внутри элемента Border находится элемент StackPanel, удерживающий все содержимое.
Внутри этого элемента StackPanel, в свою очередь, находится второй элемент StackPanel, включающий содержимое, которое должно отображаться только для выбираемых элементов и состоит из описания, изображения и кнопки. Скрыть эту информацию можно, установив стиль
внутреннего (т.е. второго) элемента StackPanel с помощью триггера, как показано ниже.
<StackPanel>
<StackPanel.Style>
<Style>
<Style.Triggers>
<DataTrigger
Binding="{Binding
RelativeSource=
{
RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}
},
Path=IsSelected
}"
Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<TextBlock Margin="3" Text="{Binding Path=Description}"
TextWrapping="Wrap"
MaxWidth="250" HorizontalAlignment="Left"
FontWeight="Regular"></TextBlock>
<Image Source="{Binding Path=ProductImagePath, Converter={StaticResource ImagePathConverter}}"></Image>
<Button FontWeight="Regular"
HorizontalAlignment="Right" Padding="1"
Tag="{Binding}">View Details...</Button>
</StackPanel>
В этом примере нужно использовать не обычный триггер, a DataTrigger, потому что вычисляемое свойство находится в предшествующем элементе и получить к нему доступ можно
только с помощью выражения привязки данных.
Теперь при изменении значения свойства ListBoxItem.IsSelected на False значение свойства StackPanel.Visibility будет изменяться на Collapsed и приводить к сокрытию лишних (развернутых) деталей.
Селекторы стилей
Шаблоны данных являются наиболее мощными инструментами для изменения внешнего вида элементов в списке. Однако иногда они оказываются несколько излишней мерой.
Например, может быть потребоваться не радикальное изменение компоновки и содержимого
каждого элемента, а просто применение какого-то базового форматирования, например, изменения цветов переднего плана и фона или текста в списке. В таком случае гораздо логичнее использовать стиль.
Выше показывалось, как можно определить стиль так, чтобы он автоматически применялся к каждому контейнеру элементов. Для этого требуется всего лишь установить свойство
ListBox.ItemContainerStyle. Для установки свойств в ListBoxItem, как и в любом другом стиле,
можно использовать комбинацию элементов Setter. Задаваемое форматирование применяется к
каждому элементу в списке, хотя с помощью триггеров можно сделать и так, чтобы оно изменялось в зависимости от других деталей, например, в зависимости от того, является ли элемент
выбранным в текущий момент. Эта возможность также демонстрировалась в предыдущем
примере.
Однако существует еще одна дополнительная возможность, которой можно воспользоваться — так называемый селектор стилей, работающий аналогично селектору шаблонов.
Селектор стилей представляет собой специальный класс, в обязанности которого входит анализ
каждого элемента и применение подходящего стиля. Он позволяет варьировать стиль, используемый для каждого объекта данных, на основе конкретной информации об этом объекте.
Например, несложно создать селектор стилей, выделяющий текст дорогостоящих продуктов
каким-то другим цветом. Добиться подобного эффекта возможно и с помощью селектора шаб-
лонов, однако в данном случае больше подходит селектор стилей. Всё дело в том, что подход с
селектором шаблонов требует создания для дорогостоящих продуктов совершенно отдельного
шаблона, что вынуждает дублировать некоторые детали из стандартного шаблона, и делает более сложным их изменение в будущем. Селектор стилей позволяет использовать один единственный шаблон и просто соответствующим образом настроить в нем несколько свойств.
Селекторы стилей часто используются для применения стиля чередующихся строк набора характеристик форматирования, отличающих каждый второй элемент в списке. Обычно
для чередующихся строк устанавливается разный цвет фона, так чтобы они четко отличались
друг от друга, как показано на Рис. 33.
Рис. 33 Чередование строк при помощи селектора стилей
Чтобы создать селектор стиля, нужно создать класс, унаследованный от класса System.Windows.Controls.StyleSelector и переопределяющий метод SelectStyle(). Метод
SelectStyle() работает точно так же, как и метод SelectTempate() в селекторе шаблонов, но только возвращает не объект DataTemplate, а объект Style.
Ниже показан селектор стилей, применяющий к нечетным строкам один стиль, а к четным — другой. Для обеспечения возможности многократного использования оба стиля не
жестко кодируются, а предоставляются с помощью свойств.
public class AlternatingRowStyleSelector : StyleSelector
{
public Style DefaultStyle
{
get;
set;
}
public Style AlternateStyle
{
get;
set;
}
// Track the position.
private int i = 0;
public override Style SelectStyle(object item, DependencyObject
container)
{
// Reset the counter if this is the first item.
ItemsControl ctrl = ItemsControl.ItemsControlFromItemContainer(container);
if (item == ctrl.Items[0])
{
i = 0;
}
i++;
// Choose between the two styles based on the current position.
if (i % 2 == 1)
{
return DefaultStyle;
}
else
{
return AlternateStyle;
}
}
}
Чтобы получить результат, показанный на Рис. 33, осталось только определить стили,
которые должны использоваться. В этом примере для каждого элемента с нечетным номером
сохраняются параметры стандартного стиля. Следовательно, предоставить нужно только стиль,
который должен использоваться для четных элементов:
<Style x:Key="AlternateStyle">
<Setter Property="ListBoxItem.Background" Value="LightGray"
></Setter>
</Style>
Теперь этот стиль можно использовать для настройки селектора AlternatingRowStyleSelector, который применяется к данному списку:
<ListBox Grid.Row="1" Margin="7,3,7,10" Name="lstProducts" DisplayMemberPath="ModelName">
<ListBox.ItemContainerStyleSelector>
<local:AlternatingRowStyleSelector
AlternateStyle="{StaticResource AlternateStyle}"
></local:AlternatingRowStyleSelector>
</ListBox.ItemContainerStyleSelector>
</ListBox>
Как и селекторы шаблонов, селекторы стилей вычисляются только тогда, когда элемент
добавляется в список впервые. В случае добавления новых элементов в список между уже существующими элементами форматирование чередующихся строк будет утрачено. Решить эту
проблему можно, вручную очистив селектор стилей (путем установки для свойства ItemContainerStyleSelector значения null) и затем применив его снова.
Изменение компоновки элемента
Шаблоны данных и селекторы стилей предлагают удивительные возможности для
управления каждым аспектом представления элемента. Однако они не позволяют изменять порядок, в котором элементы располагаются по отношению друг к другу. Какие бы шаблоны и
стили не использовались, ListBox помещает каждый элемент в отдельную горизонтальную
строку и затем выстраивает эти строки друг за другом для создания списка.
Эту компоновку можно изменить, заменив контейнер, который элемент управления
ListBox использует для размещения своих дочерних элементов. Делается это путем установки
свойства ItemsPanelTemplate с помощью XAML-кода, определяющего панель, которая должна
применяться. В роли такой панели может выступать любой класс, который наследуется от System.Windows.Controls.Panel.
Ниже показан пример использования панели WrapPanel для компоновки элементов в соответствии с доступной шириной элемента управления ListBox (Рис. 34).
<ListBox Name="lstProducts" ItemTemplate="{StaticResource ItemTemplate}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SnapsToDevicePixels="True">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
Для того чтобы этот подход работал, необходимо также обязательно установить для
прикрепленного свойства ScrollViewer.HonzontalScrollBarVisibility значение Disabled. Это гарантирует, что элемент ScrollViewer (который элемент управления ListBox использует автоматически) никогда не будет использовать горизонтальную полосу прокрутки. Без этой детали
панель WrapPanel будет иметь бесконечную ширину для размещения своих элементов и. следовательно, станет эквивалентной горизонтальной панели StackPanel.
Рис. 34 Изменение компоновки элемента
Представления данных
При привязке коллекции (или объекта DataTable) к ItemsControl на заднем плане автоматически создается представление данных. Это представление данных размещается между источником данных и привязанным элементом управления. Представление данных — это своего
рода интерфейс к источнику данных. Оно отслеживает текущий элемент и поддерживает возможности сортировки, фильтрации и группирования. Эти возможности от самого объекта данных не зависят, что означает, что одни и те же данные можно привязывать разными способами
в разных частях окна или разных частях приложения. Например, одну и ту же коллекцию продуктов можно было бы привязать к двум разным спискам, но отфильтровать их так, чтобы в
них отображались разные записи.
То, какой объект представления используется, зависит от типа объекта данных. Все
представления унаследованы от класса CollectionView, но от него также наследуются и две
специализированных реализации: ListCollectionView и BindingListCollectionView. Ниже это
объясняется подробнее.
 Если источник данных реализует IBindingList, создается объект представления BindingListCollectionView. Такое происходит, когда привязывается ADO.NET- объект
DataTable.
 Если источник данных реализует не IBindingList, a IList, создается объект представления ListCollectionView. Такое случается, когда привязывается коллекция ObservableCollection, например, список продуктов.
 Если источник данных не реализует ни IBindingList, ни IList, но зато реализует IEnumerable, создается простейший объект представления CollectionView.
Извлечение объекта представления
Для получения используемого в текущий момент объекта представления служит статический метод GetDefaultView() класса System.Windows.Data.CollectionVievSource. При вызове
метода GetDefaultView() ему передается источник данных— используемая коллекция или объект DataTable. Ниже показан пример извлечения представления для привязанной к списку коллекции продуктов:
ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
Метод GetDefaultView() всегда возвращает ссылку ICollectionView. О приведении объекта представления к соответствующему классу, такому как ListCollectionView или BindingListCollectionView, в зависимости от источника данных, должен заботиться сам разработчик.
ListCollectionView view = (ListCollectionView)CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
Ниже будет показано, как объект представления можно применять для добавления
фильтрации, сортировки и группирования.
Фильтрация коллекций
Фильтрация позволяет отображать подмножество записей, отвечающих конкретным
условиям. Когда коллекция выступает в роли источника данных, фильтр задается с помощью
такого свойства объекта представления, как Filter.
Реализация свойства Filter является немного громоздкой. Filter принимает делегат Predicate, который указывает на специальный метод фильтрации (созданный самим разработчиком).
Ниже показан пример соединения представления с методом FilterProduct():
ListCollectionView view = (ListCollectionView)
CollectionViewSource.GetDefaultView(lstProducts.ItemsSource) ;
view.Filter = new Predicate<obiect>(FilterProduct) ;
Фильтрация предполагает проверку одного единственного элемента данных из коллекции и возврат значения true, если он должен присутствовать в списке, или false, если его там
быть не должно. Для этого при создании объекта Predicate просто указываться тип подлежащего проверке объекта. Не совсем удобным является то, что представление требует, чтобы использовался именно экземпляр Predicate<object>, т.е. применить что-нибудь более удобное
(например, Predicate<Product>), дабы избежать написания кода приведения типов, не получится.
Ниже показан простой метод, который отображает продукты только в том случае, если
их цена превышает $100.
public bool FilterProduct(Object item)
{
Product product = (Product)item; return (Droduct.UnitCost > 100);
}
Очевидно, что в жестком кодировании значений в условии фильтрации нет никакого
смысла. В реальном мире приложение, скорее всего, будет выполнять фильтрацию динамическим образом на основании какой-то другой информации, например, на базе критериев, предоставляемых пользователем, как показано на Рис. 35.
Рис. 35 Фильтрация списка продуктов
Для реализации такого сценария можно использовать две стратегии. В случае применения анонимного делегата, можно определить внутристрочный (inline) метод фильтрации,
предоставляющий доступ к любым локальным переменным, которые находятся в области действия текущего метода, например:
ListCollectionView view = (ListCollectionView)
CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.Filter = delegate(object item)
{
Product product = (Product)item;
return {product.UnitCost > 100);
}
Хотя данный подход является аккуратным и элегантным, в более сложных сценариях
фильтрации, скорее всего, предпочтение будет отдаваться использованию другой стратегии,
подразумевающей создание специального класса фильтрации. Дело в том, что в таких ситуациях зачастую требуется, чтобы фильтрация выполнялась на основании нескольких различных
критериев и существовала возможность их изменения в будущем.
Класс фильтра упаковывает критерии фильтрации и метод обратного вызова, выполняющий операцию фильтрации. Ниже показан простой класс фильтра, предназначенный для
фильтрации продуктов, цена которых является выше минимальной.
public class ProductByPriceFilterer
{
public decimal MinimumPrice
{
get;
set;
}
public ProductByPriceFilterer(decimal minimumPrice)
{
MinimumPrice = minimumPrice;
}
public bool FilterItem(Object item)
{
Product product = item as Product;
if (product != null)
{
if (product.UnitCost > MinimumPrice)
{
return true;
}
}
return false;
}
}
Далее приведен код, который создает фильтр ProductByPriceFilterer и использует его для
применения фильтрации по минимальной цене.
private void cmdFilter_Click(object sender, RoutedEventArgs e)
{
decimal minimumPrice;
if (Decimal.TryParse(txtMinPrice.Text, out minimumPrice))
{
ListCollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource) as ListCollectionView;
if (view != null)
{
filterer = new ProductByPriceFilterer(minimumPrice);
view.Filter = new Predicate<object>(filterer.FilterItem);
}
}
}
Может возникнуть идея создать (и многократно использовать) разные фильтры, например, MinMaxFilter, StringFilter и т.д., для фильтрации разных типов данных. Однако обычно
гораздо удобнее вместо этого создать просто один класс фильтра для каждого окна, к которому
требуется применить фильтрацию. Это ограничение обуславливается тем, что создавать цепочку из нескольких фильтров нельзя.
Если нужно иметь возможность изменить данный фильтр позже, не создавая заново
объект ProductByPriceFilter. потребуется сохранить ссылку на объект фильтра в виде переменной экземпляра в классе окна. После этого свойства фильтра можно будет изменять. Однако
также еще потребуется вызвать метод Refresh() объекта представления, чтобы принудительным
образом отфильтровать список заново. Ниже показан код, корректирующий параметры фильтра при каждом срабатывании события TextChanged в текстовом окне, содержащем минимальную цену.
private void txtMinPrice_TextChanged(object sender,
TextChangedEventArgs e)
{
ListCollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource) as ListCollectionView;
if (view != null)
{
decimal minimumPrice;
if (Decimal.TryParse(txtMinPrice.Text, out minimumPrice)
&& (filterer != null))
{
filterer.MinimumPrice = minimumPrice;
view.Refresh();
}
}
}
Удалить фильтр вообще можно путем установки для свойства Filter значения null:
private void cmdRemoveFilter_Click(object sender, RoutedEventArgs
e)
{
ListCollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource) as ListCollectionView;
if (view != null)
{
view.Filter = null;
}
}
Фильтрация объекта DataTable
В случае объекта DataTable фильтрация работает по-другому. Каждый объект DataTable
работает вместе с объектом DataView. Объект DataView в ADO.NET играет во многом ту же
самую роль, что и объект представления WPF. Подобно представлению WPF, он позволяет
фильтровать записи. Он также поддерживает и сортировку через свойство Sort. В отличие от
объекта представления WPF. Объект DataView позицию в наборе данных не отслеживает. Зато
он предоставляет дополнительные свойства, позволяющие блокировать возможности редактирования (такие как AllowDelete. AllowEdit и AllowNew).
Вполне возможно изменять способ фильтрации списка данных путем извлечения привязанного объекта DataView и непосредственного изменения его свойств. Однако было бы лучше,
если бы фильтрацию можно было настраивать через объект представления WPF, чтобы иметь
возможность продолжать использовать ту же модель.
WPF предоставляет такую возможность, однако с некоторыми ограничениями. В отличие от ListCollectionView, объект BindingListCollectionView, который применяется с DataTable,
не поддерживает свойство Filter. Точнее свойство BindingListCollectionView.CanFilter всегда
возвращает false, а попытка установить свойство Filter приводит к генерации исключения. Вместо этого он предоставляет свойство CustomFilter. Свойство CustomFilter никакой своей работы
не выполняет, оно просто берет указанную строку фильтра и использует ее для установки лежащего в основе свойства DataView. RowFilter.
Свойство DataView.RowFilter является довольно простым в применении. Оно принимает
строковое выражение фильтра, которое моделируется после выполнения фрагмента SQL-кода,
с помощью которого была написана конструкция WHERE в запросе SELECT. Из-за этого требуется следовать всем соглашениям SQL, например, разделять строковые значения и значений
дат одинарными кавычками (вот такими - ' ). А при желании использовать несколько условий,
их все нужно объединять в одну строку с помощью ключевых слов OR или AND.
Ниже показан пример, похожий на пример фильтрации коллекций, который приводился
ранее, но подразумевающий фильтрацию объекта DataTable с записями продуктов:
decimal minimumPrice;
if (Decimal.TryParse(txtMinPrice.Text, out minimumPrice))
{
BindingListCollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource) as BindingListCollectionView;
if (view != null)
{
view.CustomFilter = "UnitCost > " + minimumPrice.ToString();
}
}
Отметим, что в этом примере применяется обходной путь с преобразованием текста в
текстовом поле txtMinPrice в десятичное значение и затем обратно в строку, которая и должна
использоваться для фильтрации. Это требует приложения чуть большего количества усилий, но
исключает вероятность атак и ошибок с недействительными символами. В случае создания
строки фильтра путем просто конкатенации текста из поля txtMinPrice, она могла бы содержать
операщш фильтра (=. <. >) и ключевые слова (AND. OR), приводящие к выполнению фильтрации совершенно отличным от ожидаемого образом. Подобное могло бы произойти в результате
как намеренной атаки, так и из-за ошибки пользователя.
Сортировка
Представление также можно использовать и для реализации сортировки. Самым простым подходом является сортировка на основании значения одного или более свойств в каждом
элементе данных. Применяемые поля указываются с помощью объектов System.Compor.entModel.SortDescription. Каждый объект ScrtDescription указывает на поле, которое должно использоваться для сорт!гровки. и направление, в котором она должна выполняться (по возрастанию или по убыванию). Добавляются эти объекты SortDescription в том порядке,
в котором они должны применяться. Например, можно сделать так. чтобы сортировка сначааа
осуществлялась по категории, а затем — по названию модели.
Ниже приведен пример применения простой сортировки по названию модели в порядке
возрастания:
ICollectionView view = CollectionViewSource.GetDefaulcView(lstProducts.ItemsSource); view.SortDescriptions.Add(
new SortDescription("ModelName", ListSortDirection.Ascending)) ;
Поскольку в этом коде используется не специальный класс представления, а интерфейс
ICollectionView, он работает одинаково хорошо, каким бы ни был тип привязываемого источника данных. В случае BindingListCollectionView (при привязке объекта DataTable), объекты
SortDescription используются для создания сортировочной строки, которая применяется к лежащему в основе свойству DataView.Sort.
Как и следовало ожидать, при сортировке строк значения упорядочиваются по алфавиту,
а числа — в порядке нумерации. Чтобы применить другой порядок сортировки, сначала нужно
очистить существующую коллекцию SortDescription.
Также еще возможно выполнение специальной сортировки, но только в случае использования ListCollectionView (а не BindingListCollectionView). Класс ListCollectionView предоставляет свойство CustomSort, которое принимает объект IComparer, сравнивающий два элемента данных и указывающий, какой из них больше. Такой подход удобен, если требуется разработать процедуру сортировки, комбинирующую свойства для получения ключа сортировки.
В нем также есть смысл при наличии нестандартных правил сортировки. Например, может
быть нужно, чтобы несколько первых символов в коде продукта игнорировались, чтобы вычисление выполнялось по цене, чтобы поле перед сортировкой преобразовывалось в другой
тип данных или другое представление, и т.д. Ниже показан пример, в котором сначала подсчитывается количество букв в названии модели, а затем полученное значение используется для
определения порядка сортировки:
public class SortByModelNameLength : System.Collections.IComparer
{
public int Compare(object x, object y)
{
Product productX = (Product)x;
Product productY = (Product)y;
return
productX.ModelName.Length.CompareTo(productY.ModelName.Length);
}
}
Вот код, подключающий IComparer к представлению:
ListCollectionView view = (ListCollectionView)CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.CustomSort = new SortByModelNameLength();
В этом примере объект IComparer разработан так, чтобы он вписывался в конкретный
сценарий. Если необходимо иметь возможность многократного использования объекта IComparer с похожими данными, но в разных местах, его можно обобщить. Например, класс
SortByModelNameLength можно было бы заменить классом SortByTextLength. При создании
экземпляра SortByTextLength код тогда должен был бы предоставлять имя используемого
свойства (в виде строки), а метод Compare() мог бы с помощью рефлексии отыскивать его в
объекте данных (как было в примере с SingleCriteriaHighlightTemplateSelector. который приводился выше).
Группирование
Представления (во многом похожим на сортировку образом) позволяют применять и
группирование. Как и сортировка, группирование может выполняться простым путем (на основании значения какого-то одного свойства) и сложным (с помощью специального обратного
вызова).
Для выполнения группирования, прежде всего, нужно добавить объекты System.ComponentModel.PropertyGroupDescription в коллекцию CollectionView.GroupDescriptions.
Ниже показан пример группирования продуктов по названию категории:
ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.GroupDescriptions.Add(new PropertyGroupDescription("CategoryName"));
С этим примером связана одна проблема. Элементы будут упорядочены в отдельные
группы на основании их категории, но того, что к списку было применено какое-то группирование, будет не видно. На самом деле список будет выглядеть точно так же, как и если бы в нем
была просто выполнена сортировка по названию категории.
В действительности же происходит нечто большее, просто увидеть это при параметрах
по умолчанию невозможно. Когда используется группирование, для каждой группы создается
отдельный объект GroupItem. и все эти объекты GroupItem добавляются в список. GroupItem
представляет собой элемент управления содержимым, поэтому в каждом объекте GroupItem
находится соответствующий контейнер (наподобие ListBoxltem) с фактическими данными.
Сделать так, чтобы группы было видно, можно просто отформатировав элемент GroupItem так,
чтобы он выделялся (т.е. отличался от остальных элементов).
Для этого можно использовать стиль, применяющий форматирование ко всем объектам
GroupItem в списке. Однако наверняка просто форматирования будет недостаточно — например, может также возникнуть необходимость сделать так, чтобы для каждой группы отображался еще и заголовок, что требует помощи шаблона. Класс ItemsControl делает выполнение
обеих этих задач простым, благодаря свойству ItemsControl.GroupStyle, которое предоставляет
коллекцию объектов GroupStyle. Отметим, что несмотря на имя, класс GroupStyle стилем не
является. Он представляет собой просто удобный пакет, упаковывающий несколько полезных
параметров для конфигурирования объектов Groupltem. Свойства класса GroupStyle перечислены в Таблица 8.
Таблица 8 Свойства класса GroupStyle
Имя
ContainerStyle
Описание
Устанавливает стиль, который
должен применяться к генерируемому для каждой группы элементу GroupItem.
ContainerStyleSelector
Это свойство можно использовать
вместо свойства ContainerStyle для
предоставления класса, выбирающего подходящий для использования стиль на основании группы.
HeaderTemplate
Позволяет создавать шаблон для
отображения содержимого в начале
каждой группы.
HeaderTemplateSelector Это свойство можно использовать
вместо свойства HeaderTemplate
для предоставления класса, выбирающего подходящий для использования шаблон заголовка на основании группы.
Panel
Позволяет изменять шаблон, используемый для удержания групп.
Например, WrapPanel можно применять вместо стандартного StackPanel для создания списка, предусматривающего мозаичное размещение групп слева направо и вниз.
В этом примере нужно всего лишь, чтобы перед каждой группой отображался заголовок.
И тогда можно будет получить эффект, показанный на Рис. 36.
Рис. 36 Список продуктов с группировкой
Чтобы добавить заголовок для группы, нужно установить свойство GroupStyle.HeaderTemplate. Это свойство можно заполнить обычным шаблоном данных, подобным
тем, что показывались ранее в этой главе, и использовать внутри него любую комбинацию элементов и выражений привязки данных.
Однако присутствует одна тонкость. При написании выражения привязки привязку
нужно выполнить не в отношении объекта данных из списка (каковым в данном случае является объект Product), а в отношении предназначенного для этой группы объекта PropertyGroupDescription. Это означает, что при желании отобразить значения поля для этой группы
(как показано на Рис. 36), необходимо привязать свойство PropertyGroupDescription.Name, а не
свойство Product .CategoryName.
Ниже показан весь шаблон:
<ListBox Grid.Row="1" Margin="7,3,7,10" Name="lstProducts" DisplayMemberPath="ModelName">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold"
Foreground="White" Background="LightGreen" Margin="0,5,0,0" Padding="3"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>
Свойство ListBox.GroupStyle фактически представляет собой коллекцию объектов
GroupStyle. Это позволяет добавить множество уровней группирования. Чтобы сделать это,
необходимо добавить более одного объекта PropertyGroupDescription том порядке, в котором
должно применяться группирование и подгруппирование, а затем еще добавить соответствующий объект GroupStyle для форматирования каждого уровня.
Группирование часто используется вместе с сортировкой. Для сортировки групп нужно
всего лишь сделать так, чтобы первый используемый объект SortDescription осуществлял сортировку на основании поля группировки. Ниже показан код, в котором сначала в алфавитном
порядке по названию категории сортируются категории, а затем в алфавитном порядке по имени модели сортируется уже каждый продукт в этой категории:
view.SortDescriptions.Add(new SortDescription("CategoryName",
ListSortDirection.Ascending));
view.SortDescriptions.Add(new SortDescription("ModelName",
ListSortDirection.Ascending));
Одно из ограничений демонстрируемого здесь подхода с простым группированием заключается в том, что для выполнения группирования он требует наличия поля с дублированными значениями. Предыдущий пример будет работать потому, что многие продукты относятся к одной и той же категории и. соответственно, имеют дублированные значения в свойстве
CategoryName. Однако при попытке выполнить группирование по какому-то другому фрагменту информации, например, по полю UnitCost, этот подход уже не будет работать столь же хорошо. В таком случае он приведет к созданию отдельной группы для каждого продукта.
Для этой проблемы существует решение. Можно создать класс, анализирующий какойто фрагмент информации и помещающий его в концептуальную группу с целью отображения.
Такой прием очень часто используется для группирования объектов данных с числами или датами, разбиваемыми на определенные диапазоны. Например, в текущем примере можно было
бы одну группу создать для продуктов, цена которых составляет меньше $50, другую — для
продуктов, цена которых находится в диапазоне от $50 до $100, и т.д., как показано на Рис. 37.
Рис. 37 Группировка списка продуктов по диапазонам
Для реализации такого решения потребуется предоставить конвертер значений, анализирующий поле в источнике данных (или множество полей, если реализовать конвертер типа
IMultiValueConverter) и возвращающий заголовок группы. При условии использования одинакового заголовка группы для множества объектов данных, эти объекты будут помещаться в одну и ту же логическую группу.
Следующий код иллюстрирует конвертер, создающий диапазоны цен, которые были показаны на Рис. 37. Он спроектирован с определенной долей гибкости — в частности. в нем
можно задавать размер диапазонов группирования.
public class PriceRangeProductGrouper : IValueConverter
{
public int GroupInterval
{
get;
set;
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
decimal price = (decimal)value;
if (price < GroupInterval)
{
return String.Format("Less than {0:C}", GroupInterval);
}
else
{
int interval = (int)price / GroupInterval;
int lowerLimit = interval * GroupInterval;
int upperLimit = (interval + 1) * GroupInterval;
return String.Format("{0:C} to {1:C}", lowerLimit, upperLimit);
}
}
public object ConvertBack(object value, Type targetType, object
parameter, CultureInfo culture)
{
throw new NotSupportedException("This converter is for grouping only.");
}
}
Чтобы сделать этот класс даже еще более гибким, так чтобы он мог использоваться с
другими полями, в него можно было бы добавить другие свойства, которые бы позволяли устанавливать фиксированную часть текста заголовка и строку формата, который должен применяться при преобразовании числовых значений в текст заголовка.
Ниже показан код, в котором данный конвертер используется для применения группирования по диапазонам. Отметим, что продукты должны сначала сортироваться по цене,
иначе группироваться они будут на основании того, в каком месте списка они находятся.
ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.SortDescriptions.Add(new SortDescription("UnitCost",
ListSortDirection.Ascending));
PriceRangeProductGrouper grouper = new PriceRangeProductGrouper();
grouper.GroupInterval = 50;
view.GroupDescriptions.Add(new PropertyGroupDescription("UnitCost", grouper));
В этом примере вся работа выполняется в коде, но конвертер и представление также
можно создавать и декларативным образом, размещая их в коллекции Resources окна.
Создание представлений декларативным образом
Пока что все приведенные примеры работали одинаково. Они предполагали извлечение
нужного представления с помощью кода и последующее его изменение программным путем.
Однако существует и другой вариант: можно создавать класс CollectionViewSource декларативно в XAML-разметке и затем связывать этот класс CollectionViewSource со своими элементами управления (например, списком).
Двумя наиболее важными свойствами класса CollectionViewSource являются свойство
View, которое упаковывает объект представления, и свойство Source, которое упаковывает источник данных. У него также имеются и дополнительные свойства SortDescriptions и
GroupDescriptions, которые в точности повторяют имеющие такие же имена свойства представления, о которых уже рассказывалось. Когда класс CollectionViewSource создает представление, он просто передает значения этих свойств ему.
Класс CollectionViewSource также включает событие Filter, которое можно использовать
для выполнения фильтрации. Эта фильтрация работает точно таким же образом, как обратный
вызов Filter, предоставляемый объектом представления, с тем лишь исключением, что она
определяется в виде события, благодаря чему в XAML можно легко присоединять нужный обработчик событий.
Например, возьмем предыдущий пример, в котором продукты разбивались на группы с
помощью диапазонов цен. Ниже показано, как нужно было бы определить необходимые для
этого примера конвертер и класс CollectionViewSource декларативным образом:
<local:PriceRangeProductGrouper x:Key="Price50Grouper" GroupInterval="50"/>
<CollectionViewSource x:Key="GroupByRangeView">
<CollectionViewSource.SortDescriptions>
<component:SortDescription PropertyName="UnitCost" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="UnitCost" Converter="{StaticResource Price50Grouper}"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
Отметим, что класс SortDescription не относится ни к одному из пространств имен WPF.
Чтобы его использовать, необходимо добавить следующий псевдоним пространства имен:
xmlns:component="clr-namespace:System.ComponentModel;assembly=WindowsBase"
Создав объект CollectionViewSource можно сразу же привязаться к нему в списке:
<ListBox Name="lstProducts" ItemsSource="{Binding Source={StaticResource
GroupByRangeView}}">
На первый взгляд это выгладит несколько странно. Кажется, будто бы элемент управления ListBox привязывается непосредственно к объекту CollectionViewSource. а не к предлагаемому им представлению (которое хранится в свойстве CollectionViewSource. View). Однако в модели привязки данных WPF объект CollectionViewSource является особым исключением. Когда он используется в выражении привязки. WPF просит его создать свое представление
и затем привязывает это представление к соответствующему элементу.
Декларативный подход на самом деле не экономит никаких усилий. Все равно нужно
писать код для извлечения данных во время выполнения. Разница состоит лишь в том, что при
таком подходе данные этом коде нужно передавать в объект CollectionViewSource, а не прямо в
список:
ICollection<Product> products = App.StoreDB.GetProducts();
CollectionViewSource viewSource = (CollectionViewSource)
this.FindResource("GroupByRangeView") ;
viewSource.Source = products;
Альтернативный вариант решения — создать коллекцию продуктов как ресурс с помощью XAML-разметки, а затем привязать объект CollectionViewSource к этой коллекции декларативным образом. Однако при таком подходе все равно придется писать код для заполнения
коллекции продуктов.
Теперь, после рассмотрения подхода с использованием кода и подхода с разметкой для
конфигурирования представления, возникает логичный вопрос, какой из них лучше с точки
зрения проектирования. Оба являются вполне достойными. Выбор следует делать на основании
того, где должны быть сосредоточены детали представления данных.
Однако при желании использовать множество представлений, выбор подхода начинает
играть более серьезную роль. В таком случае рекомендуется определять все представления в
разметке, а для переключения на подходящее представление использовать код.
Навигация в представлении
Одной из самых простых вещей, которые можно делать с объектом представления, является определение количества элементов в списке (осуществляемое с помощью свойства Count)
и извлечение ссылки на текущий объект данных (Currentltem) или индекс текущей позиции
(CurrentPosition). Также еще доступен и ряд методов, позволяющих перемещаться от одной записи
к
другой,
среди
которых
MoveCurrentToFirst(),
MoveCurrentToLast(),
MoveCurrentToNext(), MoveCurrentToPrevious() и MoveCurrentToPosition(). Пока что эти детали
были не нужны, поскольку во всех приводившихся примерах для предоставления пользователю
возможности перехода от одной записи к следующей применялся список. Однако при желании
создать приложение для просмотра записей, может потребоваться предоставить свои собственные навигационные кнопки. На Рис. 38 показан пример такого приложения.
Рис. 38 Приложение с собственными навигационными кнопками
Привязанные текстовые поля, отображающие данные для привязанного продукта, остаются в том же виде. Нужно только, чтобы они указывали на подходящее свойство, как показано
ниже:
<TextBlock Margin="7">Model Number:</TextBlock>
<TextBox Margin="5" Grid.Column="1" Text="{Binding
Path=ModelNumber}"></TextBox>
В этом примере, однако, никакого элемента управления списком нет, поэтому о возможностях навигации разработчик должен позаботиться сам. Чтобы упростить себе жизнь, он
может сохранить ссылку на представление в виде переменной экземпляра в классе окна:
private ListCollectionView view;
В таком случае код будет приводить представление к соответствующему типу (ListCollectionView), а не использовать интерфейс ICollectionView. Интерфейс ICollectionView предлагает многие те же самые функциональные возможности, но не предоставляет свойство Count,
которое подсчитывает общее количество элементов в коллекции.
При первой загрузке окна можно извлечь данные, разместить их в элементе DataContext
окна и сохранить ссылку на представление:
private ICollection<Product> products;
public NavigateCollection()
{
InitializeComponent();
products = App.StoreDb.GetProducts();
this.DataContext = products;
view = (ListCollectionView)CollectionViewSource.GetDefaultView(this.DataContext);
view.CurrentChanged += new EventHandler(view_CurrentChanged);
lstProducts.ItemsSource = products;
}
Все необходимое для отображения коллекции элементов в окне делается во второй
строке. В частности, в ней вся коллекция объектов Product размещается в объекте DataContext.
Привязанные элементы управления в форме будут выполнять поиск вверх по дереву элементов
до тех пор, пока не найдут этот объект. Конечно, нужно, чтобы выражения привязки привязывались к текущему элементу в коллекции, а не к самой коллекции, но WPF способна догадаться
об этом самостоятельно и потому будет предоставлять их с текущим элементом автоматически,
так что добавлять лишний код не потребуется.
В предыдущем примере есть один дополнительный оператор. Он соединяет обработчик
событий с событием CurrentChanged представления. При срабатывании этого события могут
выполняться несколько полезных действий, например, включаться или отключаться кнопки
перехода назад и вперед в зависимости от текущей позиции и отображаться информация о текущей позиции в TextBlock в нижней части окна.
private void view_CurrentChanged(object sender, EventArgs e)
{
lblPosition.Text = "Record " + (view.CurrentPosition +
1).ToString() +
" of " + view.Count.ToString();
cmdPrev.IsEnabled = view.CurrentPosition > 0;
cmdNext.IsEnabled = view.CurrentPosition < view.Count - 1;
}
Этот код кажется кандидатом на привязку данных и использование триггеров. Однако
логика просто является слишком сложной (частично из-за необходимости добавлять в индекс 1
для получения номера позиции подлежащей отображению записи).
Последний шаг — написать логику для кнопок перехода к предыдущей и следующей записи. Поскольку эти кнопки автоматически отключаются, когда они не применимы, волноваться о перемещении перед первым элементом или после последнего элемента не нужно.
private void cmdNext_Click(object sender, RoutedEventArgs e)
{
view.MoveCurrentToNext();
}
private void cmdPrev_Click(object sender, RoutedEventArgs e)
{
view.MoveCurrentToPrevious();
}
Для увеличения привлекательности можно добавить в эту форму элемент управления
списком, так чтобы у пользователя была возможность как переходить по записям по очереди с
помощью кнопок, так и перепрыгивать сразу на конкретный элемент с помощью списка (Рис.
39).
В данном случае необходим элемент ComboBox, использующий свойство ItemsSource
(для извлечения полного списка продуктов) и применяющий привязку на свойстве Text (для
отображения правильного элемента):
<ComboBox Name="lstProducts" DisplayMemberPath="ModelName"
Text="{Binding Path=ModelName}"
></ComboBox>
При первом извлечении коллекции продуктов привязывается список:
lstProducts.ItemsSource = products;
Это может и не дать ожидаемого эффекта. По умолчанию элемент, выбираемый в ItemsControl, не синхронизируется с текущим элементом в представлении. Это означает, что при
выполнении нового выбора в списке пользователь будет не направляться к новой записи, а изменять свойство ModelName текущей записи. К счастью, существуют два простых способа решить эту проблему.
Рис. 39 Выпадающий список в приложении с навигацией
Первым способ является грубым и предполагает просто перемещение к новой записи
при каждом выборе какого-либо элемента в списке. Необходимый для этого код выглядит так:
private void lstProducts_SelectionChanged(object sender,
RoutedEventArgs e)
{
view.MoveCurrentTo(lstProducts.SelectedItem);
}
Более
простой
способ
—
установить
для
свойства
ItemsControl.IsSynchronizedWithCurrentItem значение true. В таком случае выбранный в текущий момент
элемент будет автоматически синхронизироваться в соответствии с текущей позицией представления, без всякого кода.
Поставщики данных
В большинстве рассмотренных до этого примерах высокоуровневый источник данных
предоставлялся за счет программной установки свойства DataContext элемента или свойства
ItemsSource спискового элемента управления. В принципе такой подход является наиболее
гибким, особенно если объект данных создается другим. Однако возможны и другие варианты.
Как уже показывалось выше, объект данных может объявляться ресурсом окна (или какого-нибудь другого контейнера). Такой подход работает хорошо, если можно, чтобы объект
создавался декларативным образом, но менее удобен, если требуется, чтобы во время выполнения устанавливалось соединение с каким-то внешним источником данных (например, базой
данных). Однако некоторые разработчики все равно прибегают к нему (зачастую ради того,
чтобы не писать код обработки событий). В целом он подразумевает просто создание объектаупаковщика, извлекающего необходимые данные в конструкторе. Например, раздел ресурсов
мог бы выглядеть так:
<Window.Resources>
<ProductListSource x:Key="products">
</ProductListSource>
</Window. Resources>
Здесь ProductListSource — это класс, который наследуется от Observable Collection<Products>. А это значит, что он может хранить список продуктов. Также у него в конструкторе имеется некоторая базовая логика, которая позволяет ему самостоятельно заполнять
себя данными.
Далее его можно было бы использовать в выражениях привязки других элементов:
<ListBox ItemsSource="{StaticResource products}"
Такой подход на первый взгляд кажется соблазнительным, но является немного рискованным. При добавлении кода обработки ошибок его придется размещать в классе ProductListSource и возможно даже отображать с его помощью соответствующее сообщение, объясняющее пользователю причину проблемы. Очевидно, такой подход подразумевает смешение модели данных, кода доступа данных и кода пользовательского интерфейса в одном месте и потому не особо полезен в рассматриваемом примере. Однако в случае создания данных без получения доступа к каким-либо внешним ресурсам (файлам, базам данных и т.д.) он может
иметь смысл.
Поставщики данных (data providers) в некотором роде расширяют эту ограниченную модель. Поставщик данных позволяет выполнять непосредственную привязку к объекту. определяемому в разметке в разделе ресурсов. Однако эта привязка осуществляется не с самим объектом, а с поставщиком данных, способным извлекать или создавать этот объект. Такой подход
удобен, если поставщик данных является полнофункциональным, например, обладает способностью инициировать при возникновении исключений события и предоставляет свойства, позволяющие конфигурировать другие касающиеся его работы детали. К сожалению, поставщики
данных, входящие в состав WPF. пока еще не соответствуют этому стандарту. Их возможности
являются слишком ограниченными для того, чтобы с ними стоило возиться в ситуации с внешними данными (например. при извлечении информации из базы данных или файла). Их лучше
использовать в более простых сценариях, например, для связывания вместе каких-нибудь элементов управления, предоставляющих входные данные отвечающему за вычисление результата
классу. Однако в данной ситуации они могут помочь только сократить объем кода обработки
событий в пользу разметки.
Все
поставщики
данных
наследуются
от
класса
System.Windows.Data.DataSourceProvider. На момент написания учебного пособия WPF предоставляет только два поставщика данных:
 ObjectDataProvider, который извлекает информацию путем вызова метода в другом
классе;
 XmlDataProvider, который извлекает информацию прямо из XML-файла.
Задачей обоих этих объектов является предоставление разработчику возможности создавать экземпляр объекта данных в XAML, не прибегая к помощи кода обработки событий.
Объект ObjectDataProvider
ObjectDataProvider позволяет извлекать информацию из другого класса в приложении.
Он предоставляет описанные ниже дополнительные возможности:
 Он может создавать необходимый объект и передавать параметры конструктору.
 Он может вызывать метод в этом объекте и передавать ему параметры этого метода.
 Он может создавать объект данных асинхронным образом. Т.е., он может дожидаться,
пока загрузится окно, и только затем начинать выполнять свою работу в фоновом режиме.
Например, ниже показан пример простого объекта Ob}ectDataProvider, который создает
экземпляр класса StoreDB, вызывает его метод GetProducts() и делает данные доступными для
остальной части окна:
<Window.Resources>
<ObjectDataProvider x:Key="ProductsProvider" ObjectType="{x:Type
local:StoreDb}" MethodName="GetProducts"></ObjectDataProvider>
</Window.Resources>
Далее можно создать привязку, извлекающую источник из этого объекта ObjectDataProvider:
<ListBox Name="lstProducts" DisplayMemberPath="ModelName" ItemsSource="{Binding Source={StaticResource productsProvider}}">
</ListBox>
Этот дескриптор выглядит так, будто бы он выполняет привязку к объекту ObjectDataProvider, но объект ObjectDataProvider понимает, что на самом деле привязаться необходимо к списку продуктов, который он возвращает из метода GetProducts().
Отметим, что ObjectDataProvider, как и другие поставщики данных, предназначен для
извлечения данных, но не их обновления. Другими словами, нет никакого способа заставить
ObjectDataProvider вызывать другой метод в классе StoreDB и инициировать обновление. И это
всего лишь один пример того, насколько менее мощными являются классы поставщиков данных в WPF по сравнению с другими аналогичными реализациями а других каркасах, например,
элементами управления DataSource в ASP.NET
Обработка ошибок
Как уже говорилось выше, у этого примера имеется одно огромное ограничение. При
создании этого окна анализатор XAML создает окно и вызывает метод GetProducts(), чтобы
установить привязку. Если метод GetProducts() возвращает правильные данные, всё работает
хорошо, однако в процессе получения данных может возникнуть необработанное исключение
(например, если база данных слишком занята или недоступна). Тогда это исключение передается вверх до вызова InitializeComponent() в конструкторе окна, в результате чего получается,
что оно должно быть перехвачено в коде, отображающем это окно, что приводит к путанице с
концептуальной точки зрения. И нет никакого способа продолжить выполнение и все равно
отобразить окно — даже если перехватить исключение в конструкторе, остальная часть окна
все равно не сможет быть инициализирована правильно.
Простого пути решения для этой проблемы не существует. Класс ObjectDataProvider
включает свойство IsInitialLoadEnabled, для которого можно установить значение false и тем
самым предотвратить вызов метода GetProducts() при первом создании окна, а затем инициировать вызов этого метода позже, с помощью метода Refresh(). К сожалению, в случае использования этого приема перестанет работать выражение привязки, поскольку список не сможет извлекать информацию о своем источнике данных.
Существует несколько вариантов действий в таком случае. Во-первых, можно сконструировать объект ObjectDataProvider программно, хотя это означает потерю преимуществ декларативной привязки, которые и послужили главной причиной для выбора ObjectDataProvider.
Другой вариант — сконфигурировать объект ObjectDataProvider так, чтобы он выполнял свою
работу асинхронно, как описывается в ниже. В таком случае исключения будут обрабатываться
незаметным образом, хотя сообщение трассировки с детальным описанием ошибки все равно
будет отображаться в окне Debug (Отладка).
Асинхронная поддержка
Большинство разработчиков не будут видеть особой необходимости в применении поставщика ObjectDataProvider. Обычно будет легче просто выполнить привязку прямо к объекту
данных и добавить небольшой фрагмент кода, который будет вызывать отвечающий за запрашивание данных класс (например, StoreDB). Однако все же существует одна причина, по которой может возникнуть желание использовать поставщик ObjectDataProvider, и заключается она
в возможности извлечения пользы из его поддержки для запрашивания данных асинхронным
образом.
<ObjectDataProvider IsAsynchronous="True" ... >
Все выглядит довольно просто. При установке для свойства ObjectDataProvider.IsAsynchronous значения true поставщик ObjectDataProvider выполняет свою работу в фоно-
вом потоке. Благодаря этому интерфейс, пока она выполняется, все равно остается активным.
После создания объекта данных и возвращения из метода, поставщик ObjectDataProvider делает
его доступным для всех привязанных элементов.
Поставщик XmlDataProvider
Поставщик XmlDataProvider предоставляет быстрый и простой способ извлечь XMLданные из отдельного файла, Web-сайта или ресурса приложения и сделать их доступными для
элементов в приложении. Он может работать в режиме "только для чтения", и не способен
иметь дело с XML-данными, поступающими из других источников (например, из записи в базе
данных, из сообщения Web-службы и т.д.), а потому является довольно специфическим инструментом.
Чтобы использовать поставщик XmlDataProvider, сначала нужно определить его, а затем
указать ему на соответствующий файл путем установки свойства Source:
<XmlDataProvider x:Key="products" Source="store.xml"
XPath="/Products"></XmlDataProvider>
Свойство Source можно также установить декларативно (как в данном примере) и программно (что важно, если точно не известно, как выглядит имя файла, который должен использоваться). По умолчанию XmlDataProvider загружает XML-содержимое асинхронным образом.
Ниже показана часть простого XML-файла, используемого в данном примере. Здесь весь
документ упаковывается в высокоуровневом элементе Products, каждый продукт размещается в
отдельном элементе Product, а свойства каждого продукта предоставляются в виде вложенных
элементов.
<?xml version="1.0" standalone="yes"?>
<Products>
<Product>
<ProductID>355</ProductID>
<CategoryID>16</CategoryID>
<ModelNumber>RU007</ModelNumber>
<ModelName>Rain Racer 2000</ModelName>
<ProductImage>image.gif</ProductImage>
<UnitCost>1499.99</UnitCost>
<Description>Looks like an ordinary bumbershoot, but don't be fooled!
Simply place Rain Racer's tip on the ground and press the release latch.
Within seconds, this ordinary rain umbrella converts into a two-wheeled
gas-powered mini-scooter. Goes from 0 to 60 in 7.5 seconds - even in a
driving rain! Comes in black, blue, and candy-apple red.</Description>
</Product>
</Products>
Для получения информации из XML-файла нужно применять XPath-выражения. XPath
— это мощный стандарт, который позволяет извлекать интересующие части документа. Подробное рассмотрение этого стандарта выходит за рамки данного учебного пособия.
Кратко об XPath можно сказать следующее: основной единицей обозначения в XPath является путь. Например, путь / обозначает корневой каталог XML-документа, путь /Products —
корневой элемент с именем <Products>. а путь /Products/Product— что должен быть выбран
каждый элемент <Product> внутри элемента <Products>.
В случае использования XPath с XmalDataProvider первое, что нужно сделать — это
идентифицировать корневой узел. В данном случае это означает, что необходимо выбрать элемент <Products>, который содержит все данные. (При желании сфокусироваться на каком-то
определенном разделе XML-документа, нужно было бы использовать другой высокоуровневый
элемент.)
<XmlDataProvider x:Key="products" Source="store.xml"
XPath="/Products"></XmlDataProvider>
Следующий шаг — привязать список. При работе с XmlDataProvider для этого следует
использовать не свойство Binding.XPath. а свойство Binding.Path. Это дает гибкость погружаться в XML-документ настолько глубоко, насколько это необходимо.
Ниже показан код разметки, в котором извлекаются все элементы <Product>:
<ListBox Name="lstProducts" Margin="5" DisplayMemberPath="ModelName"
ItemsSource="{Binding Source={StaticResource products},XPath=Product}" ></ListBox>
Устанавливая свойство XPath в выражении привязки, следует помнить о том, что это
выражение относится к текущей позиции в XML-документе. Поэтому указывать полный путь
/Products/Product в выражении привязки списка не нужно. Вместо этого можно просто использовать относительный путь Product, начинающийся с узла <Products>, который был выбран поставщиком XmlDataProvider.
И. наконец, последнее, что необходимо сделать — это подсоединить каждый элемент,
отображающий детали продукта. Предоставляемое выражение XPath опять-таки вычисляется
относительно текущего узла (каковым для текущего продукта будет элемент <Product>). Ниже
приведен пример, в котором выполняется привязка к элементу <ModelNumber>:
<TextBox Margin="5" Grid.Column="1" Text="{Binding
XPath=ModelNumber}"></TextBox>
После внесения всех этих изменений получится пример привязки XML, практически
идентичный примерам привязки объектов, которые приводились до этого, с той лишь разницей, что данные в нем будут интерпретироваться как обычный текст. Чтобы преобразовать их в
данные другого типа или в другое представление, потребуется использовать конвертер значений.
Вид окна приложения, использующего XmlDataProvider, показан на Рис. 40.
Рис. 40 Приложение, использующее XmlDataProvider
Библиографический список
WPF: Windows Presentation Foundation в .NET 4.0 с примерами на
C# 2010 для профессионалов. Москва, Вильямс, 2011 г.
2. Charles Petzold. 3D Programming for Windows®: Three-Dimensional Graphics Programming for the Windows Presentation Foundation. Microsoft Press, 2007
1.
Мэтью Мак-Дональд.
3. Adam Nathan; Daniel Lehenbauer. Windows Presentation Foundation Unleashed. Sams
Publishing, 2007 г.
Download