264 61 781KB
Russian Pages 69
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ "ЛИПЕЦКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ" Кафедра автоматизированных систем управления
О. А. НАЗАРКИН
Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Основные средства WPF
УЧЕБНОЕ ПОСОБИЕ по дисциплине «Проектирование человеко-машинного интерфейса»
Заведующий кафедрой АСУ ______________ Сараев П. В. Объем 3 п. л. Тираж 100 экз.
Липецк Липецкий государственный технический университет 2014
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ "ЛИПЕЦКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ"
О. А. НАЗАРКИН
Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Основные средства WPF
УЧЕБНОЕ ПОСОБИЕ по дисциплине «Проектирование человеко-машинного интерфейса»
Утверждаю к печати
Проректор по учебной работе ЛГТУ
Объем 3,75 п. л.
______________ Ю. П. Качановский
Тираж 100 экз.
______ ______________ ________ число месяц год
Липецк Липецкий государственный технический университет 2014
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ "ЛИПЕЦКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ"
О. А. НАЗАРКИН
РАЗРАБОТКА ГРАФИЧЕСКОГО ПОЛЬЗОВАТЕЛЬСКОГО ИНТЕРФЕЙСА В СООТВЕТСТВИИ С ПАТТЕРНОМ MODEL-VIEW-VIEWMODEL НА ПЛАТФОРМЕ WINDOWS PRESENTATION FOUNDATION. ОСНОВНЫЕ СРЕДСТВА WPF
УЧЕБНОЕ ПОСОБИЕ по дисциплине «Проектирование человеко-машинного интерфейса»
Липецк Липецкий государственный технический университет 2014
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ "ЛИПЕЦКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ"
О. А. НАЗАРКИН
Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Основные средства WPF
УЧЕБНОЕ ПОСОБИЕ по дисциплине «Проектирование человеко-машинного интерфейса»
Липецк Липецкий государственный технический университет 2014
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
УДК 004.514(07) H191 Рецензенты: Г.А. Воробьев, канд. техн. наук; кафедра электроники, телекоммуникаций и компьютерных технологий Липецкого государственного педагогического университета.
H191
Назаркин, О. А. Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Основные средства WPF [Текст]: учеб. пособие по дисциплине «Проектирование человекомашинного интерфейса» / О. А. Назаркин. - Липецк : Изд-во Липецкого государственного технического университета, 2014. - 59 с. ISBN 978-5-88247-679-2 Пособие предназначено для студентов направлений «Информатика и вычислительная техника», «Программная инженерия», «Математическое обеспечение и администрирование информационных систем», а также родственных направлений и специальностей. Для изучения пособия требуется знание C#, XML и общее представление о программной платформе .NET. В пособии анализируются принципы, положенные в основу WPF, а также MVVM – современный паттерн реализации систем с GUI. Даны простые примеры и рекомендации по проектированию и реализации программных модулей. Пособие ориентировано на построение у студентов прочного фундамента из элементарных концепций. Ил. 3. Библиогр.: 3 назв. УДК 004.514(07) H191
ISBN 978-5-88247-679-2
© ФГБОУ ВПО «Липецкий государственный технический университет», 2014
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Содержание Введение. О двух аспектах сложности GUI .................................................. 7 Паттерн MVC как основа разработки GUI .................................................. 9 MVVM как результат эволюционного развития паттерна MVC ..............13 Связывание данных ......................................................................................17 Событийно-ориентированный ввод.............................................................21 XAML в WPF..................................................................................................25 Команды и события .......................................................................................33 Классовая декомпозиция MVVM .................................................................45 Библиографический список..........................................................................68
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Введение. О двух аспектах сложности GUI Разработка графического пользовательского интерфейса (Graphical User Interface, GUI) не без оснований считается трудоемким процессом. Сложность GUI объективно определяется большим количеством информации, которую развитый пользовательский интерфейс должен пропускать через свои многочисленные и разнородные элементы (окна, панели, кнопки, надписи, графические фрагменты и многие другие) в рамках человеко-машинного взаимодействия. Нередко прикладные задачи
требуют
сотен
диалоговых
элементов. Окно текстового
процессора, в котором набираются эти строки, служит наглядным подтверждением. (Причем возможность скрыть редко используемые инструменты редактирования и оставить на виду только самые необходимые
не упрощает, а, напротив, еще более усложняет
организацию интерфейса!) Но существует и другой аспект сложности GUI, не связанный непосредственно с предметной областью и решаемыми задачами. Иными словами, эта сложность будет присутствовать и в относительно простых по форме интерфейсах. Дело в том, что для программной реализации GUI требуется особый язык. Универсальные языки программирования не позволяют лаконично описывать структуру типичного GUI и алгоритмы его функционирования. Кроме того, доступ приложений
к
средствам
графического
вывода
и
получение
информации от устройств ввода контролируются операционной системой. Следовательно, она должна предоставлять соответствующие системные
библиотеки, пользуясь которыми
приложения могут
безопасно и согласованно осуществлять ввод и вывод информации в диалоговом
режиме.
Таким
образом,
требуется
определенная
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
совокупность
системного
программного
и
лингвистического
обеспечения, которую для краткости часто называют платформой GUI. Современные
интегрированные
системы
программирования
предоставляют разработчикам мощные и гибкие платформы GUI. Как следует ожидать, они достаточно сложны, их освоение может потребовать длительного времени, а для правильного и эффективного применения этих средств необходимо понимать принципы и модели, положенные в их основу. В настоящем пособии рассматривается платформа WPF – Windows Presentation Foundation, точнее, лишь очень узкий участок спектра ее возможностей. Задача пособия — показать, как с использованием базовых элементов WPF можно разрабатывать лаконичные, надежные, согласованные графические интерфейсы. Предлагается описание идей и концепций, составляющих основу программных паттернов MVC и MVVM,
а
также
инфраструктурных
иллюстрируется составляющих
WPF:
применение зависимых
связываемых данных, маршрутизируемых событий, команд.
основных свойств,
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Паттерн MVC как основа разработки GUI Одним из исторически первых паттернов разработки GUI является паттерн MVC (Model-View-Controller). Его задача – изолировать модель предметной
области
от
ее
представления
в
пользовательском
интерфейсе. Здесь под изоляцией понимается «неосведомленность» модели о том, что она отображается пользователю. При этом модель должна принимать команды на выполнение каких-либо действий, предусмотренных
предметной
областью, возвращать по запросу
значения своих внутренних атрибутов, а также сообщать об изменениях внутреннего состояния, чтобы можно было синхронно обновлять отображение (рис. 1).
Рис. 1. Принцип изоляции модели от представления Такого рода концепция является основой всех паттернов GUI. Начинающие разработчики ПО обязаны научиться при решении любых диалоговых задач отделять данные и бизнес-логику от представления. Критерий правильности декомпозиции: программный интерфейс модели, т.е. набор операций, которые можно с ней производить, не должен
никаким образом затрагивать аспект пользовательского
интерфейса. Модель не должна содержать методов вроде «Отобразить себя на участке экрана» или «Прочитать значения из полей ввода». Модель является «черным ящиком» для GUI, а он, в свою очередь, для модели просто не существует.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
При анализе этой схемы возникает вопрос: кому и каким образом модель сообщает об изменениях своего состояния? Ответ на первую часть вопроса («Кому?») может быть неопределенным: никому в отдельности, а просто во внешнюю среду; кто слушает, тот услышит. А вот для ответа на вторую часть вопроса («Каким образом?») требуется поддержка со стороны платформы. Представим себе, что в модели в ходе обработки данных изменилось значение некоторой внутренней переменной состояния. Модель
должна
сообщить
об
этом,
не
имея
информации
о
потенциальных приемниках сообщения. Распространенный способ решения такой задачи – callback-механизм («обратный вызов»), т.е. вызов функции, адрес которой заносится в модель из внешней среды. Callback-функция даже может не получать аргументов, а ее вызов просто сигнализирует, что внутреннее состояние модели изменилось и желающие узнать подробности этих изменений могут воспользоваться стандартным опросом состояния модели через ее программный интерфейс. Алгоритм callback-функции по возможности должен быть быстрым, чтобы не влиять на ход работы модели. Кроме того, следует избегать рекурсии, при которой внутри callback-функции вызываются какиелибо действия над моделью, вновь приводящие к изменению ее состояния и необходимости нового callback-вызова, и так далее. (Рекурсия в данном случае не является принципиальным запретом, но ее последствия должны контролироваться.) Модель не интересуют детали callback-реализации, она предполагает только то, что эта функция по смыслу соответствует возбуждению события «Состояние модели изменено». Пользовательский интерфейс включает две составляющие: ввод и вывод. Решаемые в их рамках задачи существенно связаны друг с другом. Например, при манипуляциях, осуществляемых пользователем
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
с помощью мыши над экранным представлением какого-либо объекта, требуется непрерывно отслеживать координаты курсора мыши (ввод) и синхронно с этим отображать объект в новом положении (вывод). В этом контексте неразрывная связь между вводом и выводом относится
к
«поведенческому»
принципиальной
монолитности
аспекту,
она
программных
не
означает
компонентов,
обслуживающих подобные схемы манипулирования. Да и алгоритмы графического
рендеринга
почти
не
имеют
ничего
общего
с
алгоритмами реакции на пользовательский ввод. Для получения гибких структурных
решений
желательно
разнести
функции
ввода
и
графического рендеринга по разным блокам (рис. 2).
Рис. 2. Разделение блоков ввода и вывода Схема на рис. 2 представляет классический паттерн MVC. Блок графического рендеринга
обычно обозначается View, обработка
пользовательского ввода – Controller. Смысловая нагрузка термина controller (блок управления) указывает на то, что этот компонент является ответственным за принятие решений по управлению моделью (Model) на основании данных и команд, получаемых от пользователя через устройства ввода. Компонент View (представление) обычно не имеет ни возможности, ни необходимости воздействовать на модель. Его задача – «вытягивать» из модели и проецировать на экран текущие значения отображаемых атрибутов, своевременно реагируя на сигналы об обновлении состояния.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
MVVM как результат эволюционного развития паттерна MVC Паттерн MVC является очень мощной и полезной абстракцией. Он позволяет создавать гибкие, удобные для модификации приложения. Недостатки этого паттерна не следуют непосредственно из его структуры. Можно считать, что в тех программных проектах, в которых за реализацию графического представления интерфейса отвечают программисты (точнее, специалисты, имеющие возможность вносить в приложение императивные конструкции, алгоритмы, сценарии), MVC обеспечивает близкую к оптимальной схему разработки, в том числе с учетом производительности и эффективности доступа к ресурсам. Однако программисты редко обладают талантами графических дизайнеров. Визуальное, стилистическое оформление GUI – это задача для художника, а не для специалиста в области информатики. В то же время дизайнер не обязан владеть каким-либо алгоритмическим языком для описания действий по обработке данных. Чтобы понять, что паттерн MVC не обеспечивает естественное разделение труда между дизайнерами и программистами, достаточно представить типичные действия по реализации, например, экранной формы. Визуальный
редактор
форм,
обычно
входящий
в
любую
современную систему программирования, позволяет дизайнеру в графическом режиме расположить визуальные компоненты, назначить им цвета, шрифты, эффекты, стили. Но как указать способы извлечения конкретных
данных
для отображения в каждом из экранных
элементов? Ведь для этого нужно следить за оповещениями об обновлении состояния модели, обращаться к ее программному интерфейсу, вызывать методы ее объектов, передавать им аргументы,
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
получать возвращаемые значения, при необходимости конвертировать данные модели в удобный для отображения формат. Все это требует описания на алгоритмическом языке. С другой стороны, чтобы написать процедуры реакции на события пользовательского ввода, программист должен знать, какие конкретно элементы GUI использовал дизайнер на форме, какова общая схема визуальной компоновки, определяющая действия при изменении размеров и других свойств формы, каковы условия и ограничения видимости и доступности элементов и многое другое. Следовательно, дизайнер в лучшем случае сможет выполнить статичные эскизы экранной формы в разных режимах ее работы при разных условиях, а программист должен создать все алгоритмические процедуры обслуживания. Если дизайнер решит изменить набор графических элементов, применить к ним иную компоновку, то программист будет вынужден переписать алгоритмы. Таким образом, появляется
вынужденная
программистов
от
и
дизайнеров,
необоснованная что
в
проекте
зависимость по
разработке
программного обеспечения может расцениваться как организационный дисбаланс. Для восстановления баланса
усилий
и инициатив разных
участников проекта желательно, во-первых, устранить необходимость применения императивных алгоритмических сценариев в блоке графического отображения, во-вторых, сделать реакцию на события пользовательского
ввода
независимой
от
конкретных
типов
и
расположения элементов GUI. Эти задачи решает паттерн MVVM (Model-View-ViewModel), в котором компонент ViewModel – модель представления – является
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
адаптером модели для взаимодействия с графическим интерфейсом (рис. 3).
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 3. Модель представления как адаптер (посредник) Основная идея паттерна заключается в том, что создается так называемый контекст данных, т.е. динамически обновляемая и автоматически синхронизируемая область данных. Любой модуль, который подключен к контексту данных, всегда «видит» актуальные значения его элементов. Контекст также позволяет изменять данные, доступные для записи соответствующим подключенным блокам, при этом автоматически будут обновлены все компоненты, зависимые от этих изменений. Это позволяет «подключать» визуальные элементы GUI
к
контексту
данных
так
же
просто,
как
подключаются
электрические кабельные соединения, достаточно только задать соответствие «контактов».
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Связывание данных Платформа должна предоставлять программную реализацию контекста данных,
точнее
механизм
«подключения», связывания разных
компонентов посредством синхронизируемых полей данных. На платформе WPF для этого используется технология Data Binding (связывание данных). Подробности содержатся в соответствующей документации, а в настоящем пособии приведем основные ее концепции и возможности. Data
Binding
рассматривает
для
каждого
поля
данных,
участвующего в связывании, две стороны: источник (source) и приемник (target). Связывание характеризуется направлением, в котором при прозрачной поддержке платформы осуществляется автоматическая синхронизация данных. При однонаправленном связывании данные автоматически переносятся только из источника в приемник. При двунаправленном связывании инициатором обновления может служить любая сторона. Существуют и другие схемы обновления, но они находят применение существенно реже. Тип данных, участвующих в связывании, может быть любым. Ограничения накладываются не на типы, а на способ хранения самих значений. Обычная переменная не может участвовать в Data Binding. В самом деле, обычная переменная – это, по сути, участок памяти, доступ к которому почти не контролируется, так что невозможно выполнить автоматическое
обновление
всех
связанных
с
этой
ячейкой
компонентов. Более реалистичным способом является использование функций вида GetValue/SetValue, которые следует вызывать всякий раз при обращении к соответствующей переменной на чтение или запись.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
В .NET принято оформлять такие данные в виде свойств объектов (properties),
поскольку для свойств предусмотрены стандартные
операции get и set, позволяющие синтаксически обращаться к полю данных как к обычной переменной (например, Width = 100; Height = Width/2). При этом будут вызываться алгоритмические действия, заданные в описании операций get (при чтении значения) и set (при записи значения). Таким образом, для участия данных в Data Binding их обязательно оформляют в виде свойств каких-либо объектов. Однако было бы крайне неудобно в каждой операции get или set описывать процедуру
извлечения
распространения
данных
волны
из
связанных
обновлений.
Логично
ячеек
или
поручить
это
инфраструктуре платформы и нагрузить операции get и set только вызовами
методов
GetValue/SetValue,
определенными
для
соответствующего системного компонента, причем их желательно унаследовать. В WPF такой вспомогательный класс называется DependencyObject. Кроме переменные,
того,
нужно
чтобы
GetValue/SetValue,
каким-то
передавать
эти
образом
идентифицировать
идентификаторы
методам
унаследованным от DependencyObject. Анализ
показывает, что для полноценной поддержки Data Binding требуются не только идентификаторы ячеек, выделенных для хранения данных, но и дополнительные программные элементы, например callback-функции, вызываемые при изменении значений. Следовательно, свойство, участвующее в Data Binding, характеризуется не просто именем и типом, но и совокупностью ассоциированных друг с другом полей, образующих объект. При создании этого объекта (в WPF его класс называется DependencyProperty) могут быть указаны callback-функция оповещения, исходное значение и некоторые другие метаданные, т.е. данные о самом
свойстве
(property
metadata).
Ссылка
на
объект
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
DependencyProperty передается в методы GetValue/SetValue. А поскольку метаданные
любых
свойств
классанаследника
DependencyObject
относятся не к экземпляру, а к самому типу, то описатель свойства должен быть статическим членом класса. Приведем пример описания булева (Boolean) свойства с именем IsEnabled, являющегося членом класса Example: public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.Register ( “IsEnabled”, // имя свойства typeof(Boolean), // тип свойства typeof(Example), // тип класса-владельца new PropertyMetadata ( true, // исходное значение OnEnabledChanged // callback-функция оповещения ) ); public Boolean IsEnabled { get { return (Boolean)GetValue(IsEnabledProperty); } set { SetValue(IsEnabledProperty, value); } } Как видно, синтаксис довольно громоздкий, причем в примере описан далеко не полный вариант. Вероятно, должны быть веские причины и существенные «выгоды» использования подобных свойств. И положительные стороны этой технологии, действительно, весьма полезны. Любое DependencyProperty способно участвовать в Data Binding. Можно считать, что оно и является тем «слотом», через который данные модели предметной области «подключаются» к отображающим их элементам GUI. Визуальный дизайнер получает возможность без написания какого-либо программного кода указывать каналы обмена данными GUI с моделью, просто выбирая их из списка доступных
Dependency-свойств.
Каждый
такой
канал
будет
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
автоматически поставлять данные модели элементам GUI и при необходимости передавать обратно их изменения, осуществленные пользователем
в
процессе
ввода.
Какому
классу
принадлежат
«связываемые» свойства, как их описание попадает в визуальный редактор системы программирования и что представляют из себя «соединительные кабели» – это отдельные вопросы, важные для понимания
паттерна
MVVM.
Они
будут
рассмотрены
далее.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Событийно-ориентированный ввод В событийно-ориентированном GUI присутствуют сигналы от устройств ввода – события (events), происходящие во время манипуляций пользователя над визуальными элементами или инициируемые посредством
клавиатуры.
Возникающие
события,
как
правило,
ассоциированы с участками экрана. Так, например, при щелчке мышью событие «Щелчок» (Click) логично соотносить с той областью, над которой в данный момент находится курсор мыши. Программист, описывающий алгоритм обработки события, должен знать смысл, заложенный
в
соответствующее
действие
пользователя,
иначе
программе невозможно адекватно отреагировать на него. Различных типов событий (мышиные, клавиатурные и другие, относящиеся
к
манипуляциям
посредством
устройств
ввода)
относительно немного. Так, мышь можно перемещать, на ней можно нажимать клавиши, можно перемещать мышь с нажатыми клавишами, можно выполнять кратные щелчки (двойной), можно крутить на мыши колесо, если оно имеется, – едва ли список действий с этим привычным манипулятором
можно
существенно
расширить.
Но
смысл,
вкладываемый пользователем в любые манипуляции, зависит как от состояния программы, так и от состояния устройств ввода (координаты указателя, состояние клавиш, и др.), поэтому множество «смыслов манипуляций» почти бесконечно. Сам
по
проектируются
себе
этот
самые
факт
не
является
разнообразные
проблемным.
программы
с
Ведь
самыми
разнообразными функциями и поведением. Проблемы начинаются в том случае, когда программист, создающий алгоритмы обработки событий, не имеет информации об элементах GUI, с которыми события
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
соотнесены. А не имеет он этой информации по той причине, что дизайнер мог еще даже не решить, из каких именно визуальных элементов интерфейс.
будет
состоять
Трудно
или
разрабатываемый невозможно
пользовательский
«вслепую»
обеспечивать
адекватное поведение программы. Позволить дизайнеру и программисту работать над проектом параллельно – такой сценарий в указанных условиях кажется почти недостижимым. Тем не менее большинство проблем с изоляцией алгоритмов обработки событий пользовательского ввода от конкретных источников возникновения этих событий можно решить при поддержке со стороны платформы. Для этого WPF предлагает следующие средства: команды (commands), маршрутизируемые события (routed events), систему автоматического размещения визуальных элементов (layout system). Понятие команда является смысловым обобщением события. Например, событие «Щелчок на кнопке открытия файла» может обобщаться командой
«Открыть файл». Тогда
соответствующие
действия по открытию файла могут быть вызваны и любым другим способом – из меню, нажатием клавиатурной комбинации, выбором файла из списка и т.д. При необходимости команды могут получать параметры. Программист выполняет реализацию смысловой логики команд, не имея информации о том, как они будут вызваны из GUI, а дизайнер должен иметь возможность «подключать» те или иные элементы GUI к командам. Таким образом, команды являются своего рода «слотами» управляющих сигналов, аналогично тому, как связываемые свойства являются «слотами» данных. Пользовательские команды обычно генерируются элементами типа кнопок, пунктов меню или гиперссылок. Но события от устройств ввода этим не исчерпываются. Важную роль играют действия с помощью
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
устройств позиционирования и клавиатуры. Поэтому WPF использует концепцию жеста (gesture), который может связывать команду с некоторым состоянием устройств ввода (например, команда будет вызвана при нажатии правой кнопки мыши с одновременным удерживанием клавиши Ctrl). Жесты ввода могут быть соотнесены с любым элементом GUI. Одним из распространенных пользовательских действий является изменение
размеров
окон
или
каких-либо
других
областей,
заполненных визуальным содержимым. Реакция на это действие должна заключаться в том, что дочерние элементы окна тоже изменяют свои размеры и/или перепозиционируются в соответствии с некоторой логикой. Очевидно, что программная реализация этой логики требует знания конкретных элементов GUI, порядка их расположения, их визуальных и смысловых свойств. Эта проблема гораздо серьезнее, чем рассмотренная выше поддержка действий пользователя, сводимых к обобщенным командам и жестам. Логику размещения элементов уже нельзя изолировать от конкретного состава GUI, определяемого дизайнером. Но WPF и здесь предлагает решение, причем подходит к нему с неожиданной стороны: платформа вообще освобождает программиста от необходимости писать алгоритмы управления размерами и позициями элементов. Платформа берет на себя выполнение всех этих действий, а их декларативная настройка возлагается на дизайнера. Такая технология называется системой размещения (layout system). Она
оперирует
не
абсолютными
координатами
элементов,
а
относительными – в пределах объемлющего контейнера. Причем разметка задается не числовыми значениями, а характеристиками с более общим смыслом, например: «по центру контейнера», «во всю ширину контейнера». Поэтому настройка разметки приобретает
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
качественный, а не количественный характер, что освобождает дизайнера от необходимости описания каких-либо вычислений на алгоритмическом языке. Подробности WPF Layout System приведены в соответствующей документации, а пока можно сделать вывод о том, что система размещения позволяет дизайнеру свободно конструировать сложный и динамичный облик GUI, не пересекаясь с работой программиста. Обобщенные команды и сигналы к изменению разметки являются вторичными
по
отношению
к
низкоуровневым
сообщениям
пользовательского ввода, которые в Windows адресуются окнам – контролируемым
операционной
системой
объектам.
Системные
сообщения всегда передаются в очередь (message queue) какого-либо окна, идентифицируемого уникальным дескриптором HWND (а окном в абстрактном смысле могут являться и кнопки, и строки ввода текста, и элементы прокрутки, и вообще самые разнообразные виджеты). Без информации о дескрипторе окна невозможно даже получить сообщения (так как неизвестно, из какой очереди их следует извлекать). WPF, так или иначе, работает поверх системного механизма ввода, поэтому сказанное справедливо и для всех создаваемых в WPF-приложении окон. WPF предоставляет дизайнеру GUI набор управляющих элементов пользовательского
интерфейса
(controls).
Все
инфраструктурой
платформы,
обеспечивающей
они
пользуются
преобразование
низкоуровневых сообщений Windows в команды, жесты, действия перепозиционирования. преобразование
Фактически
сначала
Windows-сообщений
в
происходит специальные
маршрутизируемые события (routed events), а уже на их основе генерируются обобщенные сигналы типа команд.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Термин «маршрутизация событий» означает, что информация о событиях перемещается по дереву визуальных элементов GUI. Если Windows-события диспетчеризуются, т.е. поступают только в очередь одного окна – целевого, то в отличие от них маршрутизируемые события последовательно проходят путь между целевым визуальным элементом и контейнером самого высшего уровня (окном приложения). Такая
стратегия
обеспечивает
возможность
иерархической
компоновки элементов GUI из нескольких составляющих. Независимо от того, из каких частей состоит элемент, все они объединяются в дерево, по которому проходят маршрутизируемые события, так что контейнер верхнего уровня имеет возможность отслеживать события, возникающие ниже. Таким образом, дизайнер практически не ограничен в способах объединения базовых элементов в сложные конструкции
для
достижения
требуемых
декоративных
или
эргономических целей. Однако команды и жесты могут связываться как с отдельными частями, так и со всей иерархической конструкцией в целом – WPF автоматически «поднимает» события, если они не обработаны на нижних уровнях. Для программиста это означает дополнительную независимость от фактического состава GUI и возможность
получения
управляющих
сигналов
от
некоторых
обобщенных «контейнеров», «блоков», «страниц», «регионов».
XAML в WPF Утверждение о том, что при работе над GUI дизайнер должен быть освобожден от необходимости пользоваться языком программирования, следует уточнить. Во-первых, дизайнер не должен описывать какиелибо алгоритмы в императивном стиле (в виде последовательности инструкций структурная
для
исполнения).
компоновка
и
Во-вторых, задача визуальное
дизайнера
оформление
GUI
– с
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
использованием предопределенных объектов и инструментов, а не создание каких-либо новых моделей взаимодействия программных компонентов. Дизайнер фактически конструирует метаданные, а не программные
модули.
Такая
работа
является
описательной,
декларативной. Следовательно, дизайнер в своей работе может пользоваться декларативным языком, описывающим структурные взаимоотношения между различными сущностями в программе. Язык XAML (Extensible Application Markup Language, расширяемый язык разметки приложений) является основным языком разметки GUI на платформе WPF. Это декларативный предметно-ориентированный расширяемый язык, основанный на XML. Во многих случаях при хорошей поддержке со стороны специализированного текстового редактора верстка на XAML даже удобнее визуального редактирования. Вот пример описания на XAML некоторого фрагмента GUI:
Yes
No
Этот фрагмент создает следующее визуальное представление:
Ограниченный объем и обзорный характер настоящего пособия не позволяет описывать синтаксические особенности XAML. Их требуется
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
изучать, пользуясь соответствующей документацией. Приведем только основные принципы эффективного использования возможностей 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. Декларативная парадигма подразумевает описание некоторого набора
программных
элементов,
соединенных
между
собой
статическими отношениями. Наиболее часто используется отношение «часть-целое». WPF-приложение обычно имеет корневой контейнер (например, главное окно), содержащий внутри себя иерархию дочерних элементов.
Поскольку
XAML
основан
на
XML, формирование
иерархических структур организовано естественным образом. Но одним
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
только
построением
деревьев
(композицией)
при
разработке
приложений не обойтись. Часто требуется задавать и другие отношения, например: ассоциации, зависимости одних элементов от других, связи по данным. Для этого используются расширения XAML (markup extensions). Рассмотрим пример, в котором три текстовых поля (тип TextBlock), сгруппированных
под
одним
общим
заголовком,
отображают
шестнадцатеричные представления своих собственных фоновых цветов (свойство Background):
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Этот фрагмент создает следующее визуальное представление:
Очевидно, что поместить свойство 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 объекта Binding, установленное в Self). Таким образом, свойства Text и Background связываются и синхронизируются. Фактический механизм сложнее, потому что тип свойства Background – Brush, а не string, как у свойства Text. Такое несовпадение разрешается применением конвертера, выполняющего преобразование. В данном
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
случае конвертер неявный. Он и преобразует 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); } } //-------------------------------------------public static readonly DependencyProperty InputValueProperty = DependencyProperty.Register ( "InputValue",
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
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 И фрагмент на XAML (неполный, некоторые атрибуты окна опущены):
Этот фрагмент создает следующее визуальное представление:
В отличие от предыдущих статичных примеров этот демонстрирует «поведение»: при перемещении слайдера автоматически меняется надпись.
Из-за
простоты
выполняемых
действий
здесь
нет
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
необходимости в блоке 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, но без каких-либо сведений об алгоритмах реализации перевода входного значения в выходное.
Команды и события Рассмотрим пример, в котором пользователь может щелчком мыши или нажатием комбинации клавиш на клавиатуре добавлять на рабочую область окна именованные абстрактные блоки и затем перетаскивать их. Это позволит проиллюстрировать использование команд (commands), событий (events), жестов (gestures). При добавлении блока требуется создать новый экземпляр визуального
представления
этого блока. Дизайнер
GUI может
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
определить
декларативный
шаблон
визуального
представления
(ControlTemplate), а императивный обработчик команды выполнит создание нового объекта в соответствии с этим шаблоном. Пример ControlTemplate на XAML (в секции Window.Resources):
Текст внутри текстового поля (элемент TextBlock) синхронизирован со свойством Name из контекста данных, который будет установлен для вновь созданного блока. В качестве такого контекста будет выступать объект, описываемый простым CLR-классом ModelBlock: class ModelBlock { public string Name { get { return name; } set { name = value; } } string name; } Добавление
нового блока
на
рабочую область инициирует
пользователь посредством устройств ввода (клавиатуры и мыши). Пользователь может выполнить щелчок левой клавишей мыши в
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
определенной позиции на рабочей области, требуя разместить новый блок именно здесь. Либо он может нажать на клавиатуре комбинацию Ctrl+N, что должно приводить к добавлению нового блока без какоголибо позиционирования (будем для определенности использовать размещение в центре рабочей области). Пользователю доступно также перетаскивание
блоков
по
рабочей
области,
выполняемое
перемещением мыши с удерживанием левой клавиши нажатой. Перетаскивание начинается с выбора блока, что осуществляется щелчком левой клавиши мыши, когда курсор находится над требуемым блоком, без отпускания клавиши. Выбор и перемещение блоков с клавиатуры не поддерживается (в целях сокращения данного обзорного материала). Таким образом, модель представления определяет три команды: добавить блок с позиционированием – CommandAddBlockLocated, добавить блок без позиционирования – CommandAddBlockUnlocated, выбрать блок – CommandSelectBlock. Перетаскивание блока не является командой и обслуживается специализированным образом. Команды являются вторичными по отношению к событиям ввода, т.е. команды генерируются на основе обработки событий. Как для событий, так и для команд платформа WPF использует принцип маршрутизации (routing). Маршрутизация позволяет «проводить» сигнальную информацию по дереву визуальных элементов, чтобы в обработке могли участвовать не только узлы-активаторы, но и вышестоящие узлы-контейнеры, вплоть до окна верхнего уровня. На платформе WPF есть класс RoutedCommand, являющийся специальной маршрутизируемой реализацией интерфейса ICommand. Фактическое исполнение
команды
соответствующим
осуществляется
интерфейсному
методу
методом-делегатом, Execute (выполнить).
Команда, вообще говоря, допускает задание делегата также для предварительной проверки допустимости выполнения в текущем
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
контексте (интерфейсный метод CanExecute), но в данном простом примере все команды выполняются безусловно. Произвольное (покоординатное, абсолютное) позиционирование элементов GUI в WPF допускается только для одной разновидности контейнеров – Canvas. Поэтому в рассматриваемом примере для дизайнера нет свободы выбора панели. Модель представления определяет свойство PlotArea (тип Canvas), которое должно быть связано на XAML с Canvas-объектом разметки. Модель представления также предполагает связывание на XAML свойства BlockTemplate с приведенным выше шаблоном (ресурс с ключом “BlockTemplate”). Создание объектов PlotArea и ViewModel на XAML (в секции Window.Resources):
Компонент ViewModel представлен следующим CLR-классом: class ViewModel : DependencyObject { //-------------------------------------------public static RoutedCommand CommandAddBlockUnlocated = new RoutedCommand(); //-------------------------------------------public static RoutedCommand CommandAddBlockLocated = new RoutedCommand(); //-------------------------------------------public static RoutedCommand CommandSelectBlock = new RoutedCommand(); //-------------------------------------------public static ExecutedRoutedEventHandler ExecuteCommandAddBlockUnlocated = AddBlockUnlocated; //--------------------------------------------
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
public static ExecutedRoutedEventHandler ExecuteCommandAddBlockLocated = AddBlockLocated; //-------------------------------------------public static ExecutedRoutedEventHandler ExecuteCommandSelectBlock = SelectBlock; //-------------------------------------------static void AddBlockUnlocated ( object parameter, ExecutedRoutedEventArgs e ) { ViewModel view = e.Parameter as ViewModel; Point p = new Point ( view.PlotArea.ActualWidth / 2, view.PlotArea.ActualHeight / 2 ); AddBlock(view, p); e.Handled = true; } //-------------------------------------------static void AddBlockLocated ( object parameter, ExecutedRoutedEventArgs e ) { ViewModel view = e.Parameter as ViewModel; Point p = Mouse.GetPosition(view.PlotArea); AddBlock(view, p); e.Handled = true; } //-------------------------------------------static void AddBlock(ViewModel view, Point p) { ContentControl blockWrapper = new ContentControl(); blockWrapper.Template = view.BlockTemplate; ModelBlock b = new ModelBlock(); b.Name = "block #" + view.PlotArea.Children.Count.ToString(); blockWrapper.DataContext = b; MouseGesture mg = new MouseGesture(); mg.MouseAction = MouseAction.LeftClick;
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
MouseBinding mb = new MouseBinding(); mb.Gesture = mg; mb.Command = ViewModel.CommandSelectBlock; mb.CommandParameter = view; blockWrapper.InputBindings.Add(mb); Canvas.SetLeft(blockWrapper, p.X); Canvas.SetTop(blockWrapper, p.Y); view.PlotArea.Children.Add(blockWrapper); } //-------------------------------------------static void SelectBlock ( object parameter, ExecutedRoutedEventArgs e ) { ViewModel view = e.Parameter as ViewModel; new ViewMovingElement().StartMoving ( e.OriginalSource as FrameworkElement ); e.Handled = true; } //-------------------------------------------public static readonly DependencyProperty PlotAreaProperty = DependencyProperty.Register ( "PlotArea", typeof(Canvas), typeof(ViewModel) ); //-------------------------------------------public Canvas PlotArea { get { return GetValue(PlotAreaProperty) as Canvas; } set { SetValue(PlotAreaProperty, value); } }
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//-------------------------------------------public static readonly DependencyProperty BlockTemplateProperty = DependencyProperty.Register ( "BlockTemplate", typeof(ControlTemplate), typeof(ViewModel) ); //-------------------------------------------public ControlTemplate BlockTemplate { get { return GetValue(BlockTemplateProperty) as ControlTemplate; } set { SetValue(BlockTemplateProperty, value); } } //-------------------------------------------} Действия по фактическому добавлению объектов в контейнер PlotArea заключаются в том, что сначала создается новый объект ContentControl, затем для него устанавливаются шаблон визуального представления (свойство Template) и контекст данных (свойство DataContext). Шаблон представления определен на XAML и доступен CLR-коду через свойство BlockTemplate класса ViewModel, а в качестве контекста данных устанавливается новый объект ModelBlock. Блоки различаются по именам (свойство Name класса ModelBlock), и для генерации новых имен используется текущее количество дочерних элементов контейнера (свойство PlotArea.Children.Count).
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Для доступа ко всем этим объектам требуется ссылка на экземпляр класса ViewModel – такая ссылка обеспечивается параметром команды. (Далее будет показано, как задается этот параметр на XAML.) Элемент GUI, отображающий созданный блок, должен быть подключен к обработке событий пользовательского ввода. Для этого создается связывание щелчка левой кнопкой мыши (жест LeftClick) с командой
CommandSelectBlock.
Этот
MouseBinding-объект
добавляется в коллекцию InputBindings объекта ContentControl. Таким образом, при щелчке левой кнопкой мыши на каком-либо элементе,
определенном
визуальным
представлением
блока,
автоматически будет инициироваться команда CommandSelectBlock. Для позиционирования блока в пределах рабочей области PlotArea вызываются статические методы класса Canvas (SetLeft и SetTop). Координаты курсора мыши можно получить из статического метода Mouse.GetPosition, а поскольку координаты элемента GUI в WPF имеют смысл только относительно какого-либо контейнера, то метод GetPosition получает в качестве параметра ссылку на PlotArea. Все действия по перетаскиванию блока инкапсулирует CLR-класс ViewMovingElement: class ViewMovingElement { //-------------------------------------------bool dragging; //-------------------------------------------Point eventPosition; //-------------------------------------------public void StartMoving ( FrameworkElement elementToControl ) { if (!dragging) {
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
dragging = true; elementToControl.MouseLeftButtonUp += OnMouseLeftButtonUp; elementToControl.MouseMove += OnMouseMove; FrameworkElement canvasContainer = elementToControl.Parent as FrameworkElement; eventPosition = Mouse.GetPosition(canvasContainer); Mouse.Capture(elementToControl); } } //-------------------------------------------void OnMouseLeftButtonUp ( Object sender, MouseButtonEventArgs e ) { dragging = false; FrameworkElement feThis = sender as FrameworkElement; Mouse.Capture(null); feThis.MouseLeftButtonUp -= OnMouseLeftButtonUp; feThis.MouseMove -= OnMouseMove; e.Handled = true; } //-------------------------------------------void OnMouseMove ( Object sender, MouseEventArgs e ) { if (dragging && e.LeftButton == MouseButtonState.Pressed) { FrameworkElement feThis = sender as FrameworkElement; FrameworkElement canvasContainer = feThis.Parent as FrameworkElement; Point thisPosition = e.GetPosition(canvasContainer); Vector shift = thisPosition - eventPosition; double left = Canvas.GetLeft(feThis);
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
double top = Canvas.GetTop(feThis); Canvas.SetLeft(feThis, left + shift.X); Canvas.SetTop(feThis, top + shift.Y); eventPosition = thisPosition; } e.Handled = true; } //-------------------------------------------} В этом классе реализован следующий алгоритм. Во время вызова метода StartMoving выполняется проверка, находится ли объект в фазе
(перетаскивание),
dragging
для
чего
используется
соответствующий флажок. Если нет, то происходит переключение в эту фазу. Текущая позиция курсора мыши (тип данных Point) заносится в переменную eventPosition. Подключается прослушивание событий MouseLeftButtonUp
и
на
MouseMove
визуальном
элементе,
инициировавшем выполнение команды CommandSelectBlock и вызов метода StartMoving. Устанавливается «перехват» событий вводавывода от мыши (mouse capture), чтобы этот элемент мог реагировать на перемещение курсора мыши даже за пределами рабочей области. При
обработке
события
OnMouseLeftButtonUp) противоположные
выполняются
действия:
(метод
MouseLeftButtonUp в
осуществляется
некотором выход
смысле из
фазы
перетаскивания, снимается перехват (и вообще прослушивание) событий от мыши. После этого объект ViewMovingElement может быть утилизирован сборщиком мусора. Пока
блок
находится
в
фазе
перетаскивания,
вызывается
обработчик события MouseMove (метод OnMouseMove). Действия этого метода сводятся к вычислению смещения мыши относительно предыдущей позиции и модификации положения перетаскиваемого визуального элемента относительно родительского контейнера Canvas.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Выше было показано связывание жеста LeftClick с командой CommandSelectBlock в CLR-коде в императивном стиле, поскольку нужно было ассоциировать жест с новым элементом GUI, не присутствующим в разметке на XAML. С другой стороны, команды добавления нового блока ассоциируются с самим окном, выступающим в роли «источника» блоков, эта связь статична и является в некотором смысле атрибутом окна, поэтому связывание удобно описать на XAML:
Свойство
CommandParameter
объектов
MouseBinding
и
KeyBinding получает ссылку на объект ViewModel (созданный непосредственно в разметке на XAML как статический ресурс с ключом “View”), поэтому обработчики команд могут пользоваться этим параметром для доступа к модели представления. Помимо связывания команд с событиями пользовательского ввода, требуется также связывание команд с конкретными делегатамиобработчиками, что является окончательным звеном цепочки передачи информации от сигналов ввода к алгоритмам их обработки. Эта связь создается на XAML в виде коллекции объектов CommandBinding (свойство Window.CommandBindings):
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Здесь
используются
только
события
Executed,
являющиеся
сигналами к исполнению команд. Делегаты для их обработки определены как статические поля класса ViewModel.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Классовая декомпозиция MVVM При реализации GUI в соответствии с паттерном MVVM на WPF можно использовать различные наборы CLR-классов. Рассмотрим некоторые типичные конфигурации. Класс Model во всех вариантах представляет модель предметной области. Компонент View ни в одном из вариантов не выделяется в отдельный класс и ему соответствует вся совокупность XAML-фрагментов. Вариант A. Классы Model, ViewModel Это
минималистичная
схема
для несложных
приложений,
управляемых несколькими командами. Класс ViewModel предоставляет для View контекст данных – некоторый набор связываемых свойств (dependency properties). Обработчики команд и событий определяются также в
классе ViewModel, а связывание команд и событий
(CommandBindings и InputBindings) описывается дизайнером на XAML. Вариант B. Классы Model, ViewModel, ViewController Основной недостаток варианта A состоит в смешивании внутри класса ViewModel контекста данных и программной логики поведения. Вариант B устраняет этот недостаток, вынося алгоритмы реакции на события и команды за пределы ViewModel. Таким образом, ViewModel по-прежнему
предоставляет
все
связываемые
свойства
для
использования на XAML посредством {Binding ...}, т.е. определяет программный интерфейс для взаимодействия с View, но не осведомлен о
реализации
поведения
GUI
обработчиков.
Аспект
инкапсулируется
в
реализации классе
фактического
ViewController.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Одновременно
это
позволяет
сделать
класс
ViewModel
неосведомленным даже о существовании класса Model, что также можно считать серьезным преимуществом варианта B с точки зрения структурной гибкости.
Вариант C. Классы Model, ViewModel, ViewController, ViewEvents, ViewCommands В этом варианте происходит дальнейшее разделение аспектов поведения программы. В программных проектах с развитым GUI монолитный класс ViewController может оказаться неудобным по причине неестественного смешивания в нем разноуровневых каналов пользовательского управления. Выполнение одних задач инициируется обобщенными командами (более высокий, абстрактный уровень), других – конкретными событиями от физических устройств ввода (относительно низкий уровень). Схемы подключения делегатовобработчиков и алгоритмы обработки в одном и другом случаях разные, поэтому логично разнести их по разным классам. Следует отметить, что, в принципе, возможно, но далеко не всегда эффективно искусственное формирование абстрактных команд на основе низкоуровневых событий. Например, можно устроить приложение таким образом, что при каждом
перемещении
мыши
генерировалась
бы
специально
определенная команда, тогда применение варианта C не имеет смысла. Однако производительность такого решения низкая, а накладные расходы на связывание – завышенные. Рассмотрим пример построения приложения на основе варианта C как наиболее гибкого. В процессе разработки этого примера станут более
отчетливыми
взаимодействия.
роли
каждого
из
объектов
— участников
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ViewController
является
адаптером
модели,
позволяющим
применять, возможно даже параллельно, разные типы моделей с разными программными интерфейсами. Такая структура полностью изолирует класс Model от остальных составляющих блока ViewModel, т.е. ViewCommands и ViewEvents. Класс ViewCommands предназначен для передачи сигналов и сообщений из GUI в модель через контроллер. Роль класса ViewEvents заключается не столько во взаимодействии с моделью, сколько в обслуживании сложного поведения элементов GUI, недоступного для реализации чисто декларативными средствам. Как правило,
инициатором
обращения
к
ViewEvents
является
ViewCommands, при этом к элементам GUI подключаются обработчики событий
(event
handlers),
«автоматически»,
по
мере
которые
в
дальнейшем
возникновения событий
вызываются в
системе.
Отключается реакция на события по мере завершения соответствующих сценариев действий пользователя, например по окончании прямого манипулирования. Функции программы в рассматриваемом примере — загрузка изображений с диска, плавное попиксельное панорамирование с помощью мыши, а также простейшая модификация (в данном случае, получение негатива). В этом примере значительную смысловую нагрузку имеет класс Model, реализующий растровые операции с высокой производительностью. Кроме того, показана эффективность управления приложением на основе системы высокоуровневых команд, инициируемых разными способами (как с помощью устройств ввода, так и элементами GUI). Для лаконичной иллюстрации роли ViewEvents выбран несколько искусственный прием: реализована дополнительная функциональность панорамирования, отсутствующая в стандартном компоненте Image. Напомним, что в эргономичных просмотрщиках нередко применяется «перетаскивание» видимого в окне участка большого изображения
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
мышью.
Несложно
реализовать
такое
поведение,
обрабатывая
низкоуровневые события, генерируемые мышью, и вызывая методы стандартного компонента WPF ScrollViewer, в контейнер которого помещен компонент Image. Основные
составляющие
программного
проекта
—
файлы
MainWindow.xaml (декларативная часть на языке XAML), Model.cs, ViewModel.cs, ViewController.cs, ViewCommands.cs и ViewEvents.cs (императивная часть на языке C#). // начало файла Model.cs using System; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; namespace ImageViewer { class Model { //------------------------------------------public ImageSource Picture { get { if(exposeInverted) { return invertedPicture; } else { return picture; } } } //------------------------------------------public int PixelWidth { get { return picture.PixelWidth;
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
} } //------------------------------------------public int PixelHeight { get
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
{ return picture.PixelHeight; } } //------------------------------------------public void LoadImage(string path) { BitmapImage bm = new BitmapImage ( new Uri ( path, UriKind.Absolute ) ); picture = bm; invertedPicture = null; exposeInverted = false; } //------------------------------------------public void InvertImage() { if(picture == null) { return; } exposeInverted = !exposeInverted; if (exposeInverted && (invertedPicture == null)) { MakeInverted(); } } //------------------------------------------private void MakeInverted() { invertedPicture = new WriteableBitmap(picture); invertedPicture.Lock();
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
int width = invertedPicture.PixelWidth; int height = invertedPicture.PixelHeight; unsafe { UInt32* buffer = (UInt32*) invertedPicture.BackBuffer; int stride = invertedPicture. BackBufferStride / 4; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { buffer[x] = GetInverted(buffer[x]); } buffer += stride; } } invertedPicture.AddDirtyRect ( new Int32Rect(0, 0, width, height) ); invertedPicture.Unlock(); } //------------------------------------------private UInt32 GetInverted(UInt32 pixel) { UInt32 r = (pixel & 0x00FF0000) >> 16; UInt32 g = (pixel & 0x0000FF00) >> 8; UInt32 b = (pixel & 0x000000FF); r = 255 - r; g = 255 - g; b = 255 - b; return 0xFF000000 |
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
(r Allow unsafe code). Модель определяет три интерфейсных свойства: Picture, PixelWidth и PixelHeight, и два интерфейсных метода: LoadImage и InvertImage. Тип свойства Picture — абстрактный ImageSource, который является удобным для визуального отображения стандартными средствами WPF, например компонент Image, но в то же время в нужной степени закрывает внутреннюю реализацию (на самом деле изображения хранятся в переменных типов BitmapImage и WritableBitmap). Однако ImageSource не определяет интерфейсных свойств для точных размеров изображения в пикселах (вместо этого используются приведенные экранные размеры, с учетом разрешения дисплея). Поэтому в модель дополнительно введены пиксельные размеры. Метод LoadImage принимает в качестве аргумента абсолютный путь к изображению.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Обычно такого рода пути формируются с помощью стандартного системного диалога открытия файлов (но модель, естественно, не осведомлена об этих способах). После вызова метода LoadImage свойство Picture должно содержать ссылку на загруженные растровые данные. После первого обращения к InvertImage свойство Picture будет содержать инвертированную версию изображения, а дальнейшие вызовы InvertImage действуют подобно переключателю, но уже без каких-либо операций над пикселями. // начало файла ViewController.cs using System; using System.Windows; using System.Windows.Media; namespace ImageViewer { class ViewController { //------------------------------------------private Model model = new Model(); //------------------------------------------public ImageSource LoadImage(string path) { model.LoadImage(path); return model.Picture; } //------------------------------------------public ImageSource InvertImage() { model.InvertImage(); return model.Picture; } //------------------------------------------public double PixelWidth { get { return model.PixelWidth;
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
} } //------------------------------------------public double PixelHeight { get { return model.PixelHeight; } } //------------------------------------------} } // конец файла ViewController.cs Особых пояснений код контроллера в этом простом примере не требует. Очевидно, что он является интерфейсным адаптером. Объект класса Model агрегирован в объект класса ViewController, поэтому контроллер управляет и временем жизни модели. // начало файла ViewModel.cs using System; using System.Windows; using System.Windows.Media; using System.Windows.Input; namespace ImageViewer { class ViewModel : DependencyObject { //------------------------------------------public ViewController controller = new ViewController(); //------------------------------------------public static readonly DependencyProperty PictureProperty = DependencyProperty.Register ( "Picture", typeof(ImageSource), typeof(ViewModel) );
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
public ImageSource Picture { get { return (ImageSource) GetValue(PictureProperty); } set { SetValue(PictureProperty, value); } } //------------------------------------------public static readonly DependencyProperty PicturePathProperty = DependencyProperty.Register ( "PicturePath", typeof(string), typeof(ViewModel) ); public string PicturePath { get { return (string) GetValue(PicturePathProperty); } set { SetValue(PicturePathProperty, value); } } //------------------------------------------public static readonly DependencyProperty PictureWidthProperty = DependencyProperty.Register ( "PictureWidth", typeof(double), typeof(ViewModel)
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
); public double PictureWidth { get { return (double) GetValue(PictureWidthProperty); } set { SetValue ( PictureWidthProperty, value ); } } //------------------------------------------public static readonly DependencyProperty PictureHeightProperty = DependencyProperty.Register ( "PictureHeight", typeof(double), typeof(ViewModel) ); public double PictureHeight { get { return (double) GetValue(PictureHeightProperty); } set { SetValue ( PictureHeightProperty, value ); }
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
} //------------------------------------------public static RoutedCommand LoadImage = new RoutedCommand(); public static ExecutedRoutedEventHandler ExecutedLoadImageHandler = ViewCommands.ExecuteLoadImage; //------------------------------------------public static RoutedCommand InvertImage = new RoutedCommand(); public static ExecutedRoutedEventHandler ExecutedInvertImageHandler = ViewCommands.ExecuteInvertImage; //------------------------------------------public static RoutedCommand ClickImage = new RoutedCommand(); public static ExecutedRoutedEventHandler ExecutedClickImageHandler = ViewCommands.ExecuteClickImage; //------------------------------------------} } // конец файла ViewModel.cs Структура класса ViewModel определяется тем, что именно он взаимодействует с декларативными средствами на XAML. Несколько громоздкий синтаксис скрывает «слоты» (связываемые свойства) Picture,
PicturePath,
PictureWidth
и
PictureHeight.
Механизмы
связывания были подробно описаны ранее, здесь они не имеют какихлибо особенностей. Кроме связываемых свойств, определены также статические поля трех
команд:
LoadImage,
InvertImage
и
ClickImage.
Команды
связываются с обработчиками в декларативном коде, а в классе ViewModel для этого определены статические поля (соответственно, ExecutedLoadImageHandler,
ExecutedInvertImageHandler
и
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ExecutedClickImageHandler). Реализация этих делегатов содержится в классе ViewCommands. Объект
класса
ViewController
агрегирован
в
объект
класса
ViewModel. Эта переменная для сокращения программного текста объявлена как общедоступная (public) и не оформлена в виде свойства (property). // начало файла ViewCommands.cs using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using Microsoft.Win32; namespace ImageViewer { class ViewCommands { //------------------------------------------public static void ExecuteLoadImage ( object target, ExecutedRoutedEventArgs e ) { OpenFileDialog ofd = new OpenFileDialog(); ofd.Filter = "Image files|*.jpg;*.jpeg;*.png;*.gif;*.bmp"; Nullable result = ofd.ShowDialog(); if(result.HasValue) { if(result.Value == true) { ViewModel vm = e.Parameter as ViewModel; vm.PicturePath = ofd.FileName;
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
vm.Picture = vm.controller.LoadImage ( vm.PicturePath ); vm.PictureWidth = vm.controller.PixelWidth; vm.PictureHeight = vm.controller.PixelHeight; } } } //------------------------------------------public static void ExecuteInvertImage ( object target, ExecutedRoutedEventArgs e ) { ViewModel vm = e.Parameter as ViewModel; vm.Picture = vm.controller.InvertImage(); } //------------------------------------------public static void ExecuteClickImage ( object target, ExecutedRoutedEventArgs e ) { FrameworkElement fe = target as FrameworkElement; while(true) { FrameworkElement node = fe.Parent as FrameworkElement; if (node == null) { break; }
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (node.GetType() == typeof(ScrollViewer)) { ViewModel vm = e.Parameter as ViewModel; new ViewEvents(). HandlePictureDragging ( fe, node as ScrollViewer ); break; } } } //------------------------------------------} } // конец файла ViewCommands.cs В рассматриваемом примере класс ViewCommands — статический. С одной стороны, у него нет необходимости в хранении данных экземпляра (у него вообще нет своих данных). С другой стороны, этот класс не вполне самостоятельный и фактически активно пользуется данными объекта ViewModel, ссылка на который присутствует в аргументах всех команд. Алгоритмические действия по обработке команд здесь сводятся к перенаправлению контроллеру сообщений из GUI. Модификация значений таких связываемых свойств объекта ViewModel, как Picture, автоматически отражается на визуальных элементах GUI. В методе ExecuteClickImage обрабатывается начало перетаскивания мышью видимой области изображения. Этот метод вызывается как реакция на команду ClickImage, которая может быть инициирована только одним способом — нажатием левой клавиши мыши на
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
изображении
внутри
компонента
Image.
Связывание
команды
ClickImage приведено в декларативном коде на XAML. Если в дереве визуальных элементов выше от узла Image обнаружится компонент ScrollViewer, происходит создание экземпляра ViewEvents и вызов его метода HandlePictureDragging. Если компонента ScrollViewer в дереве нет, команда игнорируется.
// начало файла ViewEvents.cs using System; using System.Windows; using System.Windows.Input; using System.Windows.Controls; namespace ImageViewer { class ViewEvents { //------------------------------------------private ScrollViewer scroller; private bool dragging = false; private Point previousPoint; //------------------------------------------public void HandlePictureDragging ( FrameworkElement targetElement, ScrollViewer argScroller ) { if(Mouse.LeftButton == MouseButtonState.Pressed) { scroller = argScroller; targetElement.MouseLeftButtonUp += OnMouseLeftButtonUp; targetElement.MouseMove += OnMouseMove;
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
previousPoint = Mouse.GetPosition(scroller); Mouse.Capture(targetElement); dragging = true; } } //------------------------------------------private void OnMouseMove ( object sender, MouseEventArgs e ) { if(dragging && (e.LeftButton == MouseButtonState.Pressed)) { Point nowPoint = e.GetPosition(scroller); Vector shift = nowPoint - previousPoint; previousPoint = nowPoint; Point currentOffset = new Point ( scroller.ContentHorizontalOffset, scroller.ContentVerticalOffset ); scroller.ScrollToHorizontalOffset ( currentOffset.X - shift.X ); scroller.ScrollToVerticalOffset ( currentOffset.Y - shift.Y ); } e.Handled = true;
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
} //------------------------------------------private void OnMouseLeftButtonUp
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
( object sender, MouseButtonEventArgs e ) { dragging = false; FrameworkElement fe = sender as FrameworkElement; fe.MouseLeftButtonUp -= OnMouseLeftButtonUp; fe.MouseMove -= OnMouseMove; Mouse.Capture(null); e.Handled = true; } //------------------------------------------} } // конец файла ViewEvents.cs Экземпляр
класса
ViewEvents
существует
только
во время
панорамирования изображения мышью. В момент отжатия левой клавиши мыши обработчики событий снимаются с визуального элемента, ссылок на объект ViewEvents не остается и он утилизируется сборщиком мусора. Центральную роль в обслуживании панорамирования мышью играют вызовы методов ScrollToHorizontalOffset и ScrollToVerticalOffset класса ScrollViewer. Это происходит при реакции на движение мыши (OnMouseMove). Вычисляется относительное смещение курсора с момента предыдущего события MouseMove и на соответствующую величину параллельно переносится видимая область в контейнере
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ScrollViewer. Таким образом, изображению невозможно сместиться от курсора мыши — оно будто «приклеивается» к движущемуся курсору.
В этом декларативном описании формируется дерево визуальных элементов рабочей области главного окна приложения. Экземпляр CLRкласса ViewModel помещен в секции ресурсов окна. Определено связывание команд (CommandBinding) с обработчиками. Определено также связывание жестов устройств ввода с командами (KeyBinding, MouseBinding).
Команда
LoadImage
вызывается
как
нажатием
комбинации клавиш Ctrl+O, так и кнопкой с надписью «Open image».
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Команда InvertImage вызывается комбинацией клавиш Ctrl+I или кнопкой «Invert image». Команда ClickImage инициируется нажатием левой клавиши мыши на визуальном элементе Image.
Библиографический список 1. 2.
3.
Мандел, Т. Разработка пользовательского интерфейса : пер. с англ. / Т.Мандел - Москва: ДМК Пресс, 2001. - 416 с. Макдональд, М. WPF4: Windows Presentation Foundation в .NET 4.0 с примерами на C# 2010 для профессионалов. : пер. с англ. / М. Макдональд - Москва: Вильямс, 2011. - 1024 с. Макдональд, М. WPF: Windows Presentation Foundation в .NET 4.5 с примерами на C# 5.0 для профессионалов. : пер. с англ. / М. Макдональд Москва: Вильямс, 2013. - 1024 с.
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Учебное издание
Назаркин Олег Александрович
Разработка графического пользовательского интерфейса в соответствии с паттерном Model-View-Viewmodel на платформе Windows Presentation Foundation. Основные средства WPF Учебное пособие по дисциплине «Проектирование человеко-машинного интерфейса»
Редактор M.Ю. Болгова. Подписано в печать 14.11.2014. Формат 60x84 1/16. Бумага офсетная. Ризография. Объем 3,75 пл. Тираж 100 экз. Заказ №
Издательство Липецкого государственного технического университета. Полиграфическое подразделение Издательства ЛГТУ. 398600, Липецк, ул. Московская, 30.