Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Осн
.pdf
Ограниченный объем и обзорный характер настоящего пособия не позволяет описывать синтаксические особенности XAML. Их требуется изучать, пользуясь соответствующей документацией. Приведем только основные принципы эффективного использования возможностей XAML для разработки приложений WPF.
Описание элементов GUI на XAML – это часть подготовки исходных текстов приложения. Текст на XAML компилируется совместно с императивными модулями. При разметке на XAML можно использовать пространства имен CLR (Common Language Runtime), то есть на XAML
доступны программные элементы (классы и их свойства), реализованные на языках CLR, например на C#. Пространства имен CLR и, в частности, WPF доступны на XAML. В вышеприведенном примере используются классы StackPanel и Button из пространства имен
System.Windows.Controls.
XAML позволяет создавать экземпляры классов и обращаться к их свойствам (но не к методам). На XAML удобно заполнять свойства классов, являющиеся коллекциями, причем компилятор обеспечивает упрощенную запись некоторых наиболее часто используемых элементов. В вышеприведенном примере создается экземпляр класса StackPanel. Этот класс предоставляет свойство-коллекцию Children, в которую добавлены два элемента – экземпляры класса Button. Как видно из примера, заголовок блока Children (дочерние элементы контейнера) для удобства опущен, так что сами дочерние элементы (кнопки Button) располагаются непосредственно внутри блока-контейнера
StackPanel.
21
Декларативная парадигма подразумевает описание некоторого набора программных элементов, соединенных между собой статическими отношениями. Наиболее часто используется отношение «частьцелое». WPF-приложение обычно имеет корневой контейнер (например, главное окно), содержащий внутри себя иерархию дочерних элементов. Поскольку XAML основан на XML, формирование иерархических структур организовано естественным образом. Но одним только построением деревьев (композицией) при разработке приложений не обойтись. Часто требуется задавать и другие отношения, например: ассоциации, зависимости одних элементов от других, связи по данным.
Для этого используются расширения XAML (markup extensions).
Рассмотрим пример, в котором три текстовых поля (тип TextBlock), сгруппированных под одним общим заголовком, отображают шестнадцатеричные представления своих собственных фоновых цветов (свой-
ство Background):
<GroupBox Header="Some colors"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="10"
>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
>
<TextBlock
Width="100"
TextAlignment="Center"
Background="LemonChiffon" Text="{Binding Background, Rela-
tiveSource={RelativeSource Self}}" />
<TextBlock
Width="100"
TextAlignment="Center"
Background="SteelBlue" Text="{Binding Background, Rela-
tiveSource={RelativeSource Self}}"
22
/>
<TextBlock
Width="100"
TextAlignment="Center"
Background="Tomato" Text="{Binding Background, Rela-
tiveSource={RelativeSource Self}}" />
</StackPanel>
</GroupBox>
Этот фрагмент создает следующее визуальное представление:
Очевидно, что поместить свойство Background «внутрь» свойства Text нельзя, тут нет отношения часть-целое. XAML-атрибут Text, соответствующий одноименному свойству класса TextBlock, получает значение “{Binding ...}”. Такая запись является расширением Binding Markup Extension, использующимся для связывания (синхронизации) данных. Выше рассматривались вопросы «подключения» данных к элементам GUI и были приведены принципы организации данных, участвующих в связывании, но до сих пор не был затронут сам способ связывания. В силу определенных причин иллюстрировать этот аспект удобнее на XAML, но следует понимать, что WPF определяет CLR-класс Binding (пространство имен System.Windows.Data) и свойства экземпляров этого класса задаются посредством Binding Extension.
В приведенном примере свойство Text объекта TextBlock является приемником данных (binding target), а источником (binding source) вы-
ступает свойство Background того же самого объекта, поэтому применяется относительный путь к источнику (свойство RelativeSource объ-
23
екта Binding, установленное в Self). Таким образом, свойства Text и Background связываются и синхронизируются. Фактический механизм сложнее, потому что тип свойства Background – Brush, а не string, как у свойства Text. Такое несовпадение разрешается применением конвертера, выполняющего преобразование. В данном случае конвертер неявный. Он и преобразует Brush к string, формируя запись цвета кисти в шестнадцатеричном виде.
Приведем далее еще один несложный фрагмент на C# и XAML, который позволит уже непосредственно перейти к реализации GUI в соответствии с принципами MVVM.
// CLR class Example
class Example : DependencyObject
{
//--------------------------------------------
static string[] digits = new string[]
{
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"
}; //--------------------------------------------
public static readonly
DependencyProperty OutputValueProperty = DependencyProperty.Register
(
"OutputValue",
typeof(string),
typeof(Example),
new PropertyMetadata("unset") );
//--------------------------------------------
public string OutputValue
{
get
{
return (string)GetValue(OutputValueProperty);
}
set
{
SetValue(OutputValueProperty, value);
}
24
}
//--------------------------------------------
public static readonly DependencyProperty InputValueProperty = DependencyProperty.Register
(
"InputValue",
typeof(int),
typeof(Example),
new PropertyMetadata(0, OnInputValueChanged) );
//--------------------------------------------
public int InputValue
{
get
{
return (int)GetValue(InputValueProperty);
}
set
{
SetValue(InputValueProperty, value);
}
}
//--------------------------------------------
static void OnInputValueChanged
(
DependencyObject d, DependencyPropertyChangedEventArgs e
)
{
Example me = d as Example; int n = (int)e.NewValue; if((n < 0) || (n > 9))
{
me.OutputValue = n.ToString();
}
else
{
me.OutputValue = digits[n];
}
}
//--------------------------------------------
}
// end of CLR class Example
25
И фрагмент на XAML (неполный, некоторые атрибуты окна опущены):
<Window ...
xmlns:custom="clr-namespace:Example"
...
>
<Window.Resources>
<custom:Example x:Key="ex" InputValue="3"/> </Window.Resources>
<GroupBox
Header="numbers"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Padding="5"
>
<StackPanel>
<TextBlock
HorizontalAlignment="Center"
Text="{Binding OutputValue, Source={StaticResource ex}}"
/>
<Slider
Width="100"
Minimum="-3"
Maximum="12"
SmallChange="1"
TickPlacement="TopLeft" Margin="0, 5, 0, 0" Value="{Binding InputValue,
Source={StaticResource ex}}"
/>
</StackPanel>
</GroupBox>
</Window>
Этот фрагмент создает следующее визуальное представление:
26
В отличие от предыдущих статичных примеров этот демонстрирует «поведение»: при перемещении слайдера автоматически меняется надпись. Из-за простоты выполняемых действий здесь нет необходимости в блоке Model. В качестве ViewModel выступает CLR-класс Example. Он определяет два связываемых свойства: int InputValue и string OutputValue. Callback-метод OnInputValueChanged, вызываемый при из-
менении значения InputValue, преобразует число в строку по некоторому алгоритму и присваивает эту строку свойству OutputValue. Блок View представлен на XAML. В нем пространство имен Example отображается на префикс custom, поэтому на XAML появляется возможность использовать CLR-класс Example. Экземпляр этого класса создается в качестве
статического ресурса окна (в словаре Window.Resources), так что к нему допустимы обращения по заданному ключу “ex”.
Элементы TextBlock и Slider связаны с объектом Example посредством Binding; свойство Value объекта Slider связано со свойством InputValue объекта Example, свойство Text объекта TextBlock отображает OutputValue. Как видно из примера, реализация класса Example не осведомлена ни о каких элементах GUI, но предоставляет синхронизируемый контекст данных, состоящий в данном случае из двух свойств (InputValue и OutputValue), смысл которых определен моделью и известен дизайнеру GUI, но без каких-либо сведений об алгоритмах реализации перевода входного значения в выходное.
27
Команды и события
Рассмотрим пример, в котором пользователь может щелчком мыши или нажатием комбинации клавиш на клавиатуре добавлять на рабочую область окна именованные абстрактные блоки и затем перетаскивать их. Это позволит проиллюстрировать использование команд
(commands), событий (events), жестов (gestures).
При добавлении блока требуется создать новый экземпляр визуального представления этого блока. Дизайнер GUI может определить декларативный шаблон визуального представления (ControlTemplate), а императивный обработчик команды выполнит создание нового объекта в соответствии с этим шаблоном.
Пример ControlTemplate на XAML (в секции Window.Resources):
<ControlTemplate x:Key="BlockTemplate"> <Border
CornerRadius="5"
Background="White"
BorderThickness="1"
BorderBrush="Black"
Padding="20"
>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="Black"
TextAlignment="Center"
Text="{Binding Name}" />
</Border>
</ControlTemplate>
Текст внутри текстового поля (элемент TextBlock) синхронизирован со свойством Name из контекста данных, который будет установлен для вновь созданного блока. В качестве такого контекста будет выступать объект, описываемый простым CLR-классом ModelBlock:
class ModelBlock
28
{
public string Name
{
get { return name; } set { name = value; }
}
string name;
}
Добавление нового блока на рабочую область инициирует пользователь посредством устройств ввода (клавиатуры и мыши). Пользователь может выполнить щелчок левой клавишей мыши в определенной позиции на рабочей области, требуя разместить новый блок именно здесь. Либо он может нажать на клавиатуре комбинацию Ctrl+N, что должно приводить к добавлению нового блока без какого-либо позиционирования (будем для определенности использовать размещение в центре рабочей области). Пользователю доступно также перетаскивание блоков по рабочей области, выполняемое перемещением мыши с удерживанием левой клавиши нажатой. Перетаскивание начинается с выбора блока, что осуществляется щелчком левой клавиши мыши, когда курсор находится над требуемым блоком, без отпускания клавиши. Выбор и перемещение блоков с клавиатуры не поддерживается (в целях сокращения данного обзорного материала).
Таким образом, модель представления определяет три команды: добавить блок с позиционированием – CommandAddBlockLocated, добавить блок без позиционирования – CommandAddBlockUnlocated, выбрать блок – CommandSelectBlock. Перетаскивание блока не является командой и обслуживается специализированным образом.
Команды являются вторичными по отношению к событиям ввода, т.е. команды генерируются на основе обработки событий. Как для событий, так и для команд платформа WPF использует принцип маршрутизации (routing). Маршрутизация позволяет «проводить» сигнальную информацию по дереву визуальных элементов, чтобы в обработке мог-
29
ли участвовать не только узлы-активаторы, но и вышестоящие узлыконтейнеры, вплоть до окна верхнего уровня. На платформе WPF есть класс RoutedCommand, являющийся специальной маршрутизируемой реализацией интерфейса ICommand. Фактическое исполнение команды осуществляется методом-делегатом, соответствующим интерфейсному методу Execute (выполнить). Команда, вообще говоря, допускает задание делегата также для предварительной проверки допустимости выполнения в текущем контексте (интерфейсный метод CanExecute), но в данном простом примере все команды выполняются безусловно.
Произвольное (покоординатное, абсолютное) позиционирование элементов GUI в WPF допускается только для одной разновидности контейнеров – Canvas. Поэтому в рассматриваемом примере для дизайнера нет свободы выбора панели. Модель представления определяет свойство PlotArea (тип Canvas), которое должно быть связано на XAML с Canvas-объектом разметки. Модель представления также предполагает связывание на XAML свойства BlockTemplate с приведенным выше шаблоном (ресурс с ключом “BlockTemplate”).
Создание объектов PlotArea и ViewModel на XAML (в секции
Window.Resources):
<Canvas x:Key="PlotArea" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
</Canvas>
<custom:ViewModel
x:Key="View" PlotArea="{StaticResource PlotArea}"
BlockTemplate="{StaticResource BlockTemplate}"/>
Компонент ViewModel представлен следующим CLR-классом:
class ViewModel : DependencyObject
{
//--------------------------------------------
30
