идеи для приложений wpf
Дизайн интерфейса приложения на WPF, и немного о юзабилити
Дизайн в WPF строится по принципу матрешек. Главное окно — это большой контейнер, внутри которого располагаются элементы управления (контролы) или другие контейнеры. Координат нет, а чтобы содержимое не болталось — используются привязки. Допустим, нам нужно главное меню — создаем для него контейнер, делаем привязку к верхней стороне рабочего окна. Внутрь контейнера главного меню уже помещаем кнопочки с привязкой влево. Расположение объектов внутри контейнера скользящее, поэтому, если левая кнопочка увеличится в размере, то остальные просто без проблем пододвинутся и все.
Всю эту компоновку можно делать в Visual Studio, но зачем нужен Expression Blend, спросите вы? А потому что там есть стили! По-хорошему, чтобы дизайнеру научиться работать в программе Blend — ему надо посидеть рядом с программистом, который бы объяснил что такое контролы, какой из них TextBox, а какой Button. Мне изначально казалось, что такие вещи должны знать все, кто занимается веб-дизайном. Наверное это верстальщики должны быть в курсе. А вот дизайнеры все как один живут под девизом «Достаточно знать фотошоп, а там хоть трава не расти». Для меня дизайн — это в первую очередь пропорции и гаммы, ну и потом уже закорючки. А фотошоп — это один из инструментов, которых много. И если мне нужно приложение, то я хочу чтобы дизайнеры мне расставляли кнопочки сразу прямо там где они и будут работать.
Дизайнера WPF было найти сложно всего, если не считать проектировщика архитектуры — такового я вообще не нашел. Итак, мне крупно, повезло и я нашел самоотверженного человека, готового постигать Blend и WPF. После трёх объяснений на пальцах дизайнер без всяких взбрыков был уже морально готов творить. Я сам из породы не рисующих дизайнеров, как и Татьяныч, но все же сделал над собой усилие и открыл паинт. Получилось вот так:
Моему дизайнеру было совершенно не жалко свои силы и страшно было браться за Blend, поэтому он начал все же в фотошопе. После 4-5 итераций подкручивания и переделывания получилось вот такое:
Да-да, согласен, желтые кнопочки боковых панелей слишком оттягивают внимание, но это уже мелочи.
Кстати, о птичках. Недавно видел обсуждение вопроса, о том, какое должно быть образование у специалиста по пользовательским интерфейсам. Говорят, что и гуманитарное и техническое, рассуждали которого больше. Мне как специалисту по профориентации это было смешно. Говорят, ведь, что образование ума не прибавляет. Точно образование и не влияет на способности человека к разным видам деятельности. А еще, художественная гармоничность и логика функциональности в одном человеке не могут сочетаться. Поэтому, дизайн пользовательского интерфейса не может быть продуктом труда одного человека, как минимум двоих тут надо.
Какие приложения WPF реального мира существуют?
В настоящее время я работаю с WPF, и я видел несколько примеров на dnrtv и некоторых других местах. Я бы хотел увидеть больше реальных вещей. Кто-нибудь знает хорошие примеры?
Этот вопрос/поток нуждается в обновлении.
Ребята в thirteen23 создали красивый клиент Twitter под названием blu, который в последнее время получает много внимания.
Git и клиент Mercurial для Windows или Mac. http://sourcetreeapp.com
www.clearoffice.com
Excel, полностью написанный в WPF.
Вот отличный пример приложения WPF, который действительно имеет смысл в деловом мире:
Билли Холлис о том, как начать работать с WPF
Billy Hollis разрабатывает это приложение для клиента, который хочет войти в пространство SaaS с приложением, которое не только отлично смотрится, но также хорошо работает в различных средах, где удобство использования является ключевым.
Сначала я могу думать о BabySmash
Yahoo Messenger – это еще один
Итак, на самом деле это не так.
из WPF 4 Unleashed 2010 by Adam Nathan
И за последние несколько лет многие приложения на основе WPF были выпущены от компаний, таких как
как Autodesk, SAP, Disney, Blockbuster, Roxio, AMD, Hewlett Packard, Lenovo и многие другие.
Разумеется, у самой Microsoft есть длинный список программного обеспечения, созданного с помощью WPF
Чтобы проверить элементы WPF, используемые в любом приложении на основе WPF, вы можете использовать инструмент Snoop, доступный из http://snoopwpf.codeplex.com.
ManicTime – это подробное приложение отслеживания времени для хранения вкладок, на которые идет ваш рабочий день. Посмотрите, как вы проводите время с тегами, графиками и отчетами.
Он хорошо использует возможности рисования WPF.
Я смотрю на использование dotPeek, он хорошо написан.
Мне всегда нравилось Family show от Vertigo
Спасибо, что указали на Witty и family.show. Я работал над обоими приложениями.
В настоящее время в WPF не работает много реальных приложений. Я думаю, что больше приложений начнет появляться в следующем году, так как все больше разработчиков и дизайнеров приступят к ускорению разработки и дизайна WPF.
Версия Windows Bria из Counterpath построена с использованием WPF.
Zune Desktop Player, а также множество приложений для Windows 7, создаются с помощью WPF.
Пока это не основное приложение, наше новое приложение управления движением написано с помощью WPF. Приложение используется для управления моторизованными пейзажами по сетевым соединениям, оно используется в живом театре для перемещения вертушек, штор и т.д.
Приложение называется SpikeMark.
MSN 9.0 Beta находится в WPF
Версия Windows Nomadesk (www.nomadesk.com) использует WPF
Это онлайн-сервис для резервного копирования, совместного использования и синхронизации данных.
Ознакомьтесь с MailWasher Pro 2010 на http://www.firetrust.com/en/products/mailwasher-pro, который использует WPF для интерфейса и С++ для бэкэнд
Отличный NHibernate Profiler (в котором также есть версии для EF и Linq-to-SQL).
Посмотрите также на список приложений wpf –
NovaMind для Windows – коммерческое приложение Mind Mapping, полностью написанное в WPF и использующее множество пользовательских чертежей и анимация (я работаю для NovaMind).
Пока нет, но я слышал, что следующая версия Live Messenger скорее всего будет WPF. Это может стоить взглянуть на то, что было сделано. Microsoft определенно нуждается в том, чтобы увеличить свои собственные приложения, обновленные до WPF, чтобы действительно “продать”.
Начиная с версии 3.5, клиент Evernote Windows – это WPF… к большому огорчению многих своих клиентов.
CookDiary написано (мной) в WPF. Это программа для управления рецептами, планирования и приготовления пищи.
Существует также Nemo Documents. Интуитивно понятный файловый менеджер с календарным представлением.
Также… “Samsung Kies” для управления “телефонами Samsung” производится в WPF
Также.. Панель управления графикой и мультимедиа Intel (вы найдете это почти на каждом ПК с графическим чипом Intel… [новые] у меня есть чипсет Intel Q45)
Быстрый старт с WPF. Часть 1. Привязка, INotifyPropertyChanged и MVVM
По разным причинам большинство из нас использует десктопные приложения, как минимум, браузер 🙂 А у некоторых из нас возникает необходимость в написании своих. В этой статье я хочу пробежаться по процессу разработки несложного десктопного приложения с использованием технологии Windows Presentation Foundation (WPF) и применением паттерна MVVM. Желающих продолжить чтение прошу под кат.
В чём особенность WPF?
Два основных отличия WPF от других средств построения десктопных приложений:
Я не буду углубляться в подробности, т.к. это не совсем тема статьи. Если интересно, то гуглить XAML, WPF rendering, milcore.dll и DirectX 🙂
О чём эта статья?
Эта статья содержит пример приложения, построенного на технологии WPF:
Я постараюсь ориентировать материал статьи в практическую сторону в стиле «повторяй за мной» с пояснениями.
Что нам понадобится для повторения статьи?
Так же в этом разделе я опишу создание проекта.
Запускаем VS, создаём новый проект, тип приложения выбираем WPF App (.NET Framework) (можно ввести в строке поиска справа вверху), называем как угодно.
После создания нового проекта откроется окно редактора интерфейса, у меня оно выглядит так
Внизу находится редактор разметки, вверху — предпросмотр интерфейса окна, но можно поменять относительное расположение редактора кода и предпросмотра интерфейса так, что они будут располагаться в горизонтальном порядке, с помощью вот этих кнопок (справа на границе двух областей):
Перед тем, как начать
Элементы окна (их ещё называют контрОлами от слова Control) должны размещаться внутри контейнера или внутри другого элемента типа ContentControl. Контейнер — это специальный контрол, позволяющий разместить внутри себя несколько дочерних контролов и организовать их взаимное расположение. Примеры контейнеров:
Есть и другие контейнеры. Поскольку контейнер тоже является контролом, то внутри контейнера могут быть вложенные контейнеры, содержащие вложенные контейнеры и так далее. Это позволяет гибко располагать контролы относительно друг друга. Так же с помощью контейнеров мы можем не менее гибко управлять поведением вложенных контролов при изменении размеров окна.
MVVM и интерфейс INotifyPropertyChanged. Копия текста.
Итогом этого примера станет приложение с двумя контролами, в одном из которых можно редактировать текст, а в другом только просматривать. Изменения из одного в другой будут переходить синхронно без явного копирования текста с помощью привязки (binding).
Теперь сосредоточимся на цели этого примера. Мы хотим, чтобы при наборе текста в текстбоксе этот же текст синхронно отображался в текстблоке, избежав при этом явной операции копирования текста. Нам понадобится некая связующая сущность, и вот тут-то мы и подошли к такой штуке, как привязка (binding), о которой было сказано выше. Привязка в терминологии WPF — это механизм, позволяющий связывать некоторые свойства контролов с некоторыми свойствами объекта C#-класса и выполнять взаимное обновление этих свойств при изменении одной из частей связки (это может работать в одну, в другую или в обе стороны сразу). Для тех, кто знаком с Qt, можно провести аналогию слотов и сигналов. Чтобы не растягивать время, перейдём к коду.
Итак, для организации привязки нужны свойства контролов и некое свойство некоего C#-класса. Для начала разберёмся с XAML-кодом. Текст обоих контролов хранится в свойстве Text, поэтому добавим привязку для этих свойств. Делается это так:
Мы сделали привязку, но пока непонятно к чему 🙂 Нам нужен объект какого-то класса и какое-то свойство в этом объекте, к которому будет выполнена привязка (как ещё говорят, на которое нужно забиндиться).
Так что это за класс? Этот класс называется вьюмоделью (view model) и служит как раз связующим звеном между view (интерфейсом или его частями) и model (моделью, т.е. теми частями кода, которые отвечают за логику приложения. Это позволяет отделить (в какой-то степени) логику приложения от интерфейса (представления, view) и называется паттерном Model-View-ViewModel (MVVM). В рамках WPF этот класс также называется DataContext.
Однако, просто написать класс вьюмодели недостаточно. Нужно ещё как-то оповещать механизм привязки о том, что свойство вьюмодели или свойство вью изменилось. Для этого существует специальный интерфейс INotifyPropertyChanged, который содержит событие PropertyChanged. Реализуем этот интерфейс в рамках базового класса BaseViewModel. В дальнейшем все наши вьюмодели мы будем наследовать от этого базового класса, чтобы не дублировать реализацию интерфейса. Итак, добавим в проект каталог ViewModels, а в этот каталог добавим файл BaseViewModel.cs. Получим такую структуру проекта:
Код реализации базовой вьюмодели:
Создадим для нашего класса MainWindow свою вьюмодель, унаследовавшись от базовой. Для этого в том же каталоге ViewModels создадим файл MainWindowViewModel.cs, внутри которого будет такой код:
Шикарно! Теперь нужно добавить в эту вьюмодель свойство, на которое будем биндить текст наших контролов. Поскольку это текст, тип этого свойства должен быть string:
В итоге получим такой код
Так, кажется, справились. Осталось забиндиться на это свойство из вьюхи и готово. Давайте сделаем это прямо сейчас:
Ништяк, запускаем проект, набираем текст в текстбокс иииии… ничего не происходит))) Ну, ничего страшного, на самом деле мы идём правильной дорогой, просто пока ещё не дошли до нужной точки.
Предлагаю на минутку остановиться и подумать, чего же нам не хватает. Вьюха у нас есть. Вьюмодель тоже. Свойства вроде забиндили. Нужный интерфейс реализовали. Проделали кучу работы ради копирования жалкой строчки текста, за что нам это. 111
Ладно, шутки в сторону. Мы забыли создать объект вьюмодели и кое-что ещё (об этом позже). Сам класс мы описали, но это ничего не значит, ведь у нас нет объектов этого класса. Ок, где нужно хранить ссылку на этот объект? Ближе к началу примера я упомянул некий DataContext, используемый в WPF. Так вот, у любой вью есть свойство DataContext, которому мы можем присвоить ссылку на нашу вьюмодель. Сделаем это. Для этого откроем файл MainWindow.xaml и нажмём F7, чтобы открыть код этой вьюхи. Он практически пустой, в нём есть только конструктор класса окна. Добавим в него создание нашей вьюмодели и поместим её в DataContext окна (не забываем добавить using с нужным неймспейсом):
Это было просто, но этого всё равно не хватает. По-прежнему при запуске приложения никакой синхронизации текста не происходит. Что ещё нужно сделать?
Нужно вызвать событие PropertyChanged при изменении свойства SynchronizedText и сообщить вьюхе о том, что она должна следить за этим событием. Итак, чтобы вызвать событие, модифицируем код вьюмодели:
Что мы тут сделали? Добавили скрытое поле для хранения текста, обернули его в уже существующее свойство, а при изменении этого свойства не только меняем скрытое поле, но и вызываем метод OnPropertyChanged, определённый в базовой вьюмодели и вызывающий событие PropertyChanged, объявленное в интерфейсе INotifyPropertyChanged, так же реализованное в базовой вьюмодели. Получается, что при каждом изменении текста возникает событие PropertyChanged, которому передаётся имя свойства вьюмодели, которое было изменено.
Ну, почти всё, финишная прямая! Осталось указать вьюхе, что оно должно слушать событие PropertyChanged:
Помимо того, что мы указали, по какому триггеру должно происходить обновление, мы так же указали, в какую сторону это обновление отслеживается: от вью к вьюмодели или наоборот. Поскольку в текстбоксе мы вводим текст, то нам интересны изменения только во вью, поэтому выбираем режим OneWayToSource. В случае с текстблоком всё ровно наоборот: нам интересны изменения во вьюмодели, чтобы отобразить их во вью, поэтому выбираем режим OneWay. Если бы нам нужно было, чтобы изменения отслеживались в обе стороны, можно было не указывать Mode вообще, либо указать TwoWay явно.
Итак, запускаем программу, набираем текст и voi-la! Текст синхронно меняется, и мы нигде ничего не копировали!
Спасибо за внимание, продолжение следует. Будем разбираться с DataTemplate и паттерном Command.
Идеи для приложений wpf
Приложения WPF с шаблоном проектирования модель-представление-модель представления
Джош Смит (Josh Smith)
Cодержание
Порядок против хаоса
Эволюция шаблона модель-представление-модель представления
Почему разработчики WPF так любят MVVM
Пример приложения
Пересылка логики команд
Иерархия классов модели представления
Класс ViewModelBase
Класс CommandViewModel
Класс MainWindowViewModel
Применение представления к модели представления
Модель данных и репозиторий
Новая форма ввода данных о клиенте
Представление всех клиентов
Заключение
Разработка интерфейса пользователя для профессионального приложения — это нелегко. Она может быть густой смесью данных, проектирования взаимодействий, визуального проектирования, возможностей подключения, многопоточности, безопасности, интернационализации, проверки допустимости, модульного тестирования и впридачу капелька «живой воды». Учитывая, что пользовательский интерфейс представляет нижележащую систему и должен удовлетворять непредсказуемым стилистическим требованиям пользователей, он может быть наиболее непостоянной частью многих приложений.
Есть популярные шаблоны проектирования, которые помогают приручить этого монстра, но задача надлежащего разделения множества вопросов и их разрешения может быть трудной. Чем сложнее шаблоны, тем вероятнее, что потом будут использоваться упрощения, обесценивающие все предыдущие попытки сделать все как лучше.
Не всегда виноваты шаблоны проектирования. Иногда мы используем сложные шаблоны проектирования, для которых нужно писать много кода, потому что используемая платформа пользовательского интерфейса не совместима с более простым шаблоном. Нужна платформа, позволяющая создавать пользовательские интерфейсы с помощью простых, проверенных временем и одобренных программистами шаблонов проектирования. К счатью, Windows Presentation Foundation (WPF) обеспечивает именно это.
По мере того, как WPF получает всё большее признание в мире программистов и пользователей, сообщество WPF разрабатывает собственную «экосистему» шаблонов и практических рекомендаций. В этой статье я рассмотрю некоторые из этих практических рекомендаций по проектированию и реализации клиентских приложений в WPF. Используя основные возможности WPF вместе с шаблоном проектирования «модель-представление-модель представления» (MVVM), я разберу пример программы, показывающий, что «правильно» сделать приложение WPF совсем нетрудно.
Изучив эту статью, вы узнаете, как подогнать друг к другу шаблоны данных, команды, привязку данных, систему ресурсов и шаблон MVVM для создания простой, пригодной для тестирования и надежной инфраструктуры, в которой может процветать любое приложение WPF. Пример программы, сопровождающий эту статью, может служить шаблоном для настоящего приложения WPF, использующего MVVM в качестве основной архитектуры. Модульные тесты в примере решения показывают, насколько легко тестировать функции пользовательского интерфейса приложения, если эти функции представлены набором классов ViewModel. Прежде чем погрузиться в подробности, рассмотрим сперва, зачем использовать такой шаблон, как MVVM.
Порядок против Хаоса
Использовать шаблоны проектирования в простой программе типа «Здравствуй, мир!» не нужно и даже вредно. Любой квалифицированный разработчик сразу поймет пару строк кода. Но по мере того, как увеличивается число возможностей программы, увеличивается и число строк кода и движущихся частей. В какой-то момент сложности системы и присущие ей проблемы вынуждают разработчиков организовывать код так, чтобы его было проще понимать, обсуждать, расширять и устранять неполадки. Мы уменьшаем когнитивный хаос сложной системы, называя известными именами определенные элементы исходного кода. Имя, присваиваемое фрагменту кода, мы определяем по его функциональной роли в системе.
Разработчики часто намеренно структурируют код в соответствии с шаблоном проектирования, не позволяя шаблонам возникать естесственным путем. Оба подхода хороши, но в этой статье я расскажу о преимуществах непосредственного использования MVVM в качестве архитектуры приложения WPF. Названия некоторых классов включают известные термины из шаблона MVVM, например окончание «ViewModel», если класс — абстракция представления. Этот подход помогает избегать упомянутого выше когнитивного хаоса. Вместо этого вы можете долго и счастливо пребывать в состоянии контролируемого хаоса, этого естественного состояния дел в большинстве проектов по разработке профессионального программного обеспечения!
Эволюция шаблона «модель-представление-модель представления»
Популярные шаблоны проектирования упрощали людям жизнь с первых шагов создания пользовательских интерфейсов программ. Например, шаблон модель-представление-презентатор (MVP) была популярна на различных платформах программирования пользовательских интерфейсов. MVP — это разновидность шаблона модель-представление-контроллер, которому уже несколько десятков лет. Если вам никогда не приходилось использовать шаблон MVP, ниже приведено его краткое описание. То, что видно на экране, — это представление; данные, которые там отображены — это модель, а презентатор объединяет их вместе. Представление нуждается в презентаторе для заполнения данными модели, реакции на ввод пользователя, предоставления проверки ввода (в том числе за счет передачи этой функции модели) и других подобных задач. Если нужны дополнительные сведения о шаблоне «модель-представление-презентатор», я советую прочесть статью Жана Поля Буду (Jean-Paul Boodhoo) «Шаблоны проектирования» за август 2006 года.
В 2004 Мартин Фаулер (Martin Fowler) опубликовал статью о шаблоне под названием Модель презентации (PM). Шаблон «модель презентации» похож на MVP в том плане. что он отделяет представление от его поведения и состояния. Любопытная часть шаблона PM в том, что создается абстракция представления, которая называется моделью презентации. Представление, таким образом, становится просто результатом обработки модели презентации. Согласно Фаулеру, модель презентации постоянно обновляет свое представление, поэтому они остаются синхронизированными друг с другом. Эта логика синхронизации существует в виде кода в классах модели презентации.
В 2005 году Джон Госсман (John Gossman), сейчас один из архитекторов WPF и Silverlight в корпорации Microsoft, рассказал в своем блоге о шаблоне модель-представление-модель представления (MVVM). MVVM совпадает с моделью презентации Фаулера в том плане. что оба шаблона содержат абстракцию представления, содержащую состояние и поведение представления. Фаулер ввел модель презентации как способ создания независимого от платформы пользовательского интерфейса абстракции представления, а Госсман предложил MVVM как стандартизированный способ использовать основные функции WPF для упрощения создания пользовательских интерфейсов. В этом смысле я считаю MVVM частным вариантом более общего шаблона PM, приспособленным для платформ WPF и Silverlight.
В прекрасной статье Гленна Блока (Glenn Block) «Prism: шаблоны для создания составных приложений с помощью WPF» в выпуске за сентябрь 2008 года описано руководство Microsoft по составным приложениям для WPF. Термин ViewModel (модель представления) не используется. Для описания абстракции представления используется термин «модель презентации». Однако в этой статье я буду называть шаблон аббревиатурой MVVM, а абстракцию представления — моделью представления. В сообществах WPF и Silverlight такая терминология значительно более распространена.
В отличие от презентатора в MVP, модель представления не нуждается в ссылке на представление. Представление привязывается к свойствам модели представления, которая, в свою очередь, представляет данные в объектах модели и других состояниях, нужных для этого представления. Привязки между представлением и моделью представления создавать легко, потому что объект модели представления устанавливается как контекст DataContext представления. Если изменяются значения в модели представления, эти новые значения автоматически переходят в представление через привязку данных. Когда пользователь нажимает кнопку в представлении, для произведения нужного действия выполняется команда в модели представления. Все изменения данных модели всегда производит модель представления, а не представление.
Классы представления не знают о существовании классов модели, а модель представления и модель не знают о представлении. Модель на самом деле вообще не имеет представления о том, что существуют модель представления и представление. Это очень слабо связанная конструкция, и это дает ряд преимуществ, о которых я вскоре расскажу.
Почему разработчики WPF так любят MVVM
Если разработчик привык к WPF и MVVM, эти последние становится трудно различить. MVVM — своеобразный общепринятый язык разработчиков WPF, потому что он хорошо приспособлен для платформы WPF, а WPF создавался для упрощения сборки приложений с помощью шаблона MVVM (и других). В корпорации Майкрософт MVVM использовался для внутренних целей при разработке приложений WPF, например Microsoft Expression Blend, пока основная платформа WPF еще только создавалась. Многие части WPF, например модель контроля без просмотра и шаблоны данных, используют значительное разделение показа от состояния и поведения, применяемое в MVVM.
Самый важный момент WPF, делающий MVVM очень удобным шаблоном, – это инфраструктура привязки данных. За счет привязки свойств представления к модели представления получается слабое связывание этих компонентов, что полностью освобождает разработчика от необходимости писать в модели представления код, непосредственно обновляющий представление. Система привязки данных поддерживает также проверку допустимости ввода, обеспечивающую стандартный путь передачи ошибок проверки допустимости представлению.
Еще две функции WPF, делающие этот шаблон таким полезным, – это шаблоны данных и система ресурсов. Шаблоны данных применяют представления к объектам модели представления, показанным в интерфейсе пользователя. Можно объявить шаблоны в коде XAML и позволить системе ресурсов за вас автоматически находить и применять эти шаблоны во время выполнения. Узнать о привязке и шаблонах данных подробнее можно в моей статье за июль 2008 года «Данные и WPF. Настройка отображения данных при помощи привязки данных и WPF».
Если бы в WPF не было поддержки команд, шаблон MVVM не был бы таким универсальным. В этой статье я покаже вам, как модель представления может предоставлять команды представлению, таким образом позволяя ему пользоваться своими функциями. Если вам не знакома система команд, советую прочесть обширную статью Брайана Нойса (Brian Noyes) «WPF для знатоков. Знакомство с маршрутизированными событиями и командами в WPF», в выпуске за сентябрь 2008.
Помимо функций WPF (и Silverlight 2), за счет которых MVVM становится естественным способом структурирования приложения, этот шаблон популярен еще и потому, что классы модели представления легко поддаются модульному тестированию. Если логика взаимодействия приложения находится в наборе классов модели представления, легко написать тестирующий ее код. В каком-то смысле представления и модульные тесты — разные типы потребителей модели представления. Набор тестов для модели представленияs приложения обеспечивает свободное и быстрое регрессионное тестирование, уменьшающее стоимость поддержки приложения в будущем.
Помимо удобства созания автоматических регрессионных тестов, тестируемость классов модели представления может помочь правильно проектировать пользовательские интерфейсы, для которых легко делать темы оформления. Проектируя приложение, часто можно решить, поместить ли что-то в представление или в модель представления, представив себе, нужно ли будет писать модульный тест, потребляющий модель представления. Если вы можете написать модульные тесты для модели представления, не создавая объекты пользовательского интерфейса, можно также полностью заключить модель представления в тему оформления, так как у нее нет зависимостей от определенных визуальных элементов.
Наконец, для разработчиков, сотрудничающих с проектировщиками визуальных форм, использование MVVM упрощает создание непрерывного потока работ проетировщика и разработчика. Так как представление — не более чем необязательный потребитель модели представления, нетрудно убрать одно представление и заменить его для отображения модели представления новым. Это простое действие позволяет быстро создавать прототипы и оценивать пользовательские интерфейсы, сделанные проектировщиками.
Команда разработчиков может уделить основное внимание созданию устойчивых классов модели представления, а команда проектировщиков — на удобных для пользователя представлениях. СЧтобы свести воедино результаты работы обоих команд, нужно немногим больше, чем проверка наличия правильных привязок в файле XAML представления.
Приложение может содержать любое количество «рабочих областей», каждую из которых пользователь может открыть, перейдя по командной ссылке в области переходов слева. Все рабочие области находятся на вкладке TabControl основной зоны содержания. Пользователь может закрыть рабочую область, нажав кнопку «Close» на элементе вкладки этой рабочей области. У приложения есть две рабочих области: «All Customers» и «New Customer». После запуска приложения, если открыто несколько рабочих областей, пользовательский интерфейс выглядит примерно так, как на рис. 1.
Рис. 1 Рабочие области
Одновременно можно открыть только один экземпляр рабочей области «All Customers», а рабочих областей «New Customer» можно открывать сколько угодно. Если пользователь хочет создать нового клиента, ему нужно заполнить форму ввода данных на рис. 2.
Рис. 2 Форма ввода данных о новом клиенте
После заполнения формы ввода данных допустимыми значениями и нажатия кнопки «Save» в элементе вкладки появляется имя нового клиента, и этот клиент добавляется в список всех клиентов. Приложение не поддерживает удаление и изменение существующих клиентов, но эти функции, как и многие другие, легко добавить поверх существующей архитектуры приложения. Теперь вы знаете, что делает пример приложения; посмотрим, как оно было спроектировано и реализовано.
Пересылка логики команд
Каждое представление в приложении содержит пустой файл кода поддержки, содержащий только стандартный шаблон кода, вызывающий InitializeComponent в конструкторе класса. Можно просто удалить файлы кода поддержки представлений из проекта, а приложение все равно будет компилироваться и выполняться правильно. Несмотря на отстутствие методов обработки событий в представлениях, при нажатии пользователем кнопки приложение реагирует и выполняет требования пользователя. Это происходит потому, что созданы привязки свойства Command с элементами управления Hyperlink, Button и MenuItem, отображаемыми в пользовательском интерфейсе. Эти привязки обеспечивают предоставление объектов ICommand исполнением модели представления при воздействии пользователем на элементы управления. Можно смотреть на объект команды как на адаптер, позволяющий легко употребить функции модели представления из представления, объявленного в коде XAML.
В то время как модель представления предоставляет свойства экземпляра типа ICommand, в объекте команды для выполнения этой задачи обычно используется объект модели представления. Один из возможных шаблонов реализации — создать закрытый вложенный класс в классе модели представления, чтобы команда имела доступ к закрытым членам содержащей его модели представления и не засоряла пространство имен. Этот вложенный класс реализует интерфейс ICommand, а в его конструктор включена ссылка на содержащую его модель представления. Однако создание вложенного класса, реализующего ICommand для каждой команды, представленной моделью представления, может сделать класс модели представления слишком большим. Чем больше кода — тем больше может возникнуть ошибок.
В примере приложения класс RelayCommand разрешает эту проблему. RelayCommand позволяет вводить логику команды через делеаты, передаваемые в его конструктор. Этот подход позволяет сжато и четко реализовывать команды в классах модели представления. RelayCommand — это упрощенный вариант DelegateCommand, используемого в Библиотеке составных приложений Microsoft. Класс RelayCommand показан на рис. 3.
Рис. 3. Класс RelayCommand
Событие CanExecuteChanged, часть реализации интерфейса ICommand, имеет ряд интересных функций. Оно передает подписку на событие событию CommandManager.RequerySuggested. Это гарантирует, что командная инфраструктура WPF опрашивает все объекты RelayCommand, могут ли они совершать выполнение при любом обращении к встроенным командам. В следующем коде класса CustomerViewModel, о котором я подробнее расскажу позже, показано, как настроить RelayCommand при помощи лямбда-выражений:
Иерархия класса ViewModel
Большинству классов модели представления нужны одинаковые возможности. Им часто нужно реализовывать интерфейс InotifyPropertyChanged, обычно им нужно отображаемое название, удобное для чтения пользователем, и им нужна возможность закрываться (то есть удаляться из пользовательского интерфейса). Эта проблема естественным образом приспосабливается к созданию одного или двух базовых классов модели представления, чтобы новые классы модели представления могли наследовать все общие функции базового класса. Классы модели представления формируют иерархию наследования, показанную на рис. 4.
Рис. 4 Иерархия наследования
Иметь базовый класс для всех моделей представления совершенно не обязательно. Если вы предпочитаете получать функции классов, собирая вместе много меньших классов, а не используя наследование, нет проблем. Как и любой другой шаблон проектировнаия, MVVM — просто набор рекомендаций, а не правила.
ViewModelBase — корневой класс иерархии, поэтому он реализаует общий интерфейс INotifyPropertyChanged и имеет свойство DisplayName. Интерфейс INotifyPropertyChanged содержит событие под названием PropertyChanged. Если у свойства объекта модели представления есть новое значение, она может создать событие PropertyChanged для уведомления системы привязки WPF о новом значении. После получения этого уведомления система привязки опрашивает свойство, и привязанное свойство какого-то элемента пользовательского интерфейса получит новое значение.
Для того, чтобы WPF знал, какое свойство объекта модели представления изменилось, класс PropertyChangedEventArgs предоставляет свойство PropertyName типа String. Следует внимательно следить за тем, чтобы передавать правильное название свойства в аргумент этого события; в противном случае WPF станет опрашивать на предмет нового значения неправильное свойство.
Один интересный аспект ViewModelBase состоит в том, что он предоставляет возможность проверить, что свойство с данным именем действительно существует в объекте модели представления. Это очень полезно при переработке кода, потому что при изменении имени свойства через функцию перерабткт кода Visual Studio 2008 не будут обновлены строки в исходном коде, которые содержат имя этого свойства (собсьтвенно, этого и не должно происходить). Возбуждение события PropertyChanged с неверным именем свойства в аргументе события может привести к тонким ошибкам, которые трудно найти, поэтому эта небольшая функция очень экономит время. Код ViewModelBase, добавляющий эту полезную поддержку, показан на рис. 5.
Рис. 5. Проверка свойства
Простейший единый подкласс ViewModelBase — это CommandViewModel. Он предоставляет свойство Command типа ICommand. MainWindowViewModel предоставляет коллекцию этих объектов через свое свойство Commands. В области переходов на левой стороне главного окна отображается ссылка для каждого объекта CommandViewModel, предоставленного классом MainWindowViewModel, например «View all customers» и «Create new customer». Когда пользователь переходит по ссылке, выполняя одну из этих команд, на вкладке TabControl главного окна открывается рабочая область. Определение класса CommandViewModel показано здесь:
В файле MainWindowResources.xaml содержится шаблон DataTemplate, ключ которого — «CommandsTemplate». MainWindow использует этот шаблон для сборки коллекции объектов CommandViewModel, упомянутой ранее. Шаблон просто вычисляет каждый объект CommandViewModel как ссылку в ItemsControl. Свойство Command каждой гиперссылки Hyperlink привязано к свойству Command CommandViewModel. Этот код XAML показан на рис. 6.
Рис. 6 Вычисление списка команд
Как видно из приведенной выше схемы классов, класс WorkspaceViewModel является производным от ViewModelBase, и в нем добавлена возможность закрываться. Под словом «закрываться» я имею в виду, что что-то удаляет рабочую область из пользовательского интерфейса во время выполнения. От WorkspaceViewModel являются производными три класса: MainWindowViewModel, AllCustomersViewModel и CustomerViewModel. Запрос MainWindowViewModel на закрытие обрабатывается классом App, создающим MainWindow и его модель представления, как показано на рис. 7.
Рис. 7 Создание модели представления
MainWindow содержит элемент меню, свойство Command которого привязано к свойству CloseCommand MainWindowViewModel. Когда пользователь щелкает этот элемент меню, класс App отвечает вызовом метода Close окна, вот так:
MainWindowViewModel содержит налюбдаемую коллекицю объектов WorkspaceViewModel, которая называется Workspace. Главное окно содержит вкладку TabControl, свойство ItemsSource которой привязано к этой коллекции. Каждый элемент вкладки имеет кнопку «Close», свойство Command которой привязано к CloseCommand соответствующего экземпляра WorkspaceViewModel. Сокращенная версия шаблона, настраивающего все элементы вкладки, показана в приведенном ниже коде. Код находится в MainWindowResources.xaml, а шаблон разъясняет, как собрать элемент вкладки с кнопкой «Close».
Когда пользователь нажимает кнопку «Close» в элементе вкладки, выполняется CloseCommand для этого WorkspaceViewModel, из-за этого создается его событие RequestClose. MainWindowViewModel наблюдает за событием RequestClose своих рабочих областей и по запросу удаляет рабочую область из коллекции Workspaces. Так как свойство ItemsSource вкладки TabControl объекта MainWindow привязано к наблюдаемойу коллекции объектов WorkspaceViewModel, удаление элемента набора приводит к удалению соответствующей рабочей области со вкладки TabControl. Эта логика из MainWindowViewModel показана на рис. 8.
Рис. 8 Удаление рабочей области из пользовательского интерфейса
В проекте UnitTests файл MainWindowViewModelTests.cs содержит метод тестирования, проверяющий правильную работу этой функции. Простота, с которой можно создавать модульные тесты для классов модели представления, — большое преимущество шаблона MVVM, потому что он позволяет очень легко тестировать функции приложения без нужды писать код, затраивающий пользовательский интерфейс. Этот метод тестирования показан на рис. 9.
Рис. 9. Метод тестирования
Применение представления к модели представления
MainWindowViewModel косвенно добавляет и удаляет объекты WorkspaceViewModel из TabControl главного окна. Опираясь на привязку данных, свойство Content TabItem получает для отображения объект, произошедший от ViewModelBase. ViewModelBase — не элемент пользовательского интерфейса, поэтому изначально не поддерживает самовычисление. По умолчанию в WPF невизуальный объект визуализуется с помощью отображения результатов вызова его метода ToString в блоке TextBlock. Вам нужно явно не это, если, конечно, ваши пользователи не мечтают увидеть имена типов классов модели представления!
Сообщить WPF, как собирать объект модели представления, можно с легкостью при помощи типизированных шаблонов DataTemplate. К типизированному шаблону DataTemplate не присоединено значение x:Key, но его свойство DataType установлено на экземлпяр класса Type. Если WPF пытается вычислить один из объектов модели представления, он проверит, есть ли у системы ресурсы в зоне досягаемости типизированный шаблон DataTemplate, тип DataType которого совпадает с типом (или является базовым классом для типа) вашего объекта модели представления. Если он таковой находит, он использует этот шаблон для сборки объекта модели представления, на который ссылается свойство Content элемента вкладки.
Файл MainWindowResources.xaml содержит ResourceDictionary. Этот словарь добавляется к иерархии ресурсов главного окна, то есть ресурсы, которые он содержит, находятся в пределах ресурсов окна. Когда содержимое элемента вкладки устанавливается на объект модели представления, типизированный шаблон DataTemplate из этого словаря предоставляет представление (то есть пользовательский элемент управления) для его визуализации, как на рис. 10.
Рис. 10 Предоставление представления
Не нужно писать код, определяющий, какое представление показывать для объекта модели представления. Система ресурсов WPF выполняет за вас всю тяжелую работу, позволяя вам уделить основное внимание более важным вещам. В более сложных вариантах можно программно выбирать представление, но чаще всего это не нужно.
Модель данных и репозиторий
Вы знаете, как оболочка приложения загружает, отображает и закрывает объекты модели представления. Теперь, разобравшись с основными понятиями, можно рассмотреть подробности реализации, более специфические для этого типа приложений. Прежде чем погружаться глубже в две рабочих области приложения, «All Customers» и «New Customer», рассмотрим сперва модель данных и классы доступа к данным. Проектирование этих классов не имеет практически никакого отношения к шаблону MVVM, потому что можно создать класс модели представления, адаптирующий почти любой объект данных во что-то подходящее для WPF.
Единственный класс модели в примере программы — это Customer. Этот класс содержит несколько свойств, представляющих информацию о клиенте компании, например имя, фамилию и адрес электронной почты. Он предоставляет сообщения проверки допустимости, реализуя стандартный интерфейс IDataErrorInfo, который куда старше WPF. В классе Customer нет ничего, что выдает его применение в архитектуре MVVM и даже в приложении WPF. Этот класс мог запросто прийти из устаревшей деловой библиотеки.
Класс CustomerRepository представляет несколько методов, позволяющих получать все доступные объекты Customer, добавлять новый Customer в репозиторий и проверять, если ли уже Customer в репозитории. Так как приложение не позволяет пользователю удалять клиентов, репозиторий не дает вам возможности удалять клиента. При попадании нового Customer в CustomerRepository через метод AddCustomer запускается событие CustomerAdded.
Очевидно, что модель данных этого приложения очень мала по сравнению с тем, чего требуют настоязие бизнес-приложений, но это неважно. Важно понять, как классы модели представления используются Customer и CustomerRepository. Заметьте, что CustomerViewModel — это обертка для объекта Customer. Она представляет состояние Customer и другое состояние, используемое элементом управления CustomerView, через набор свойств. CustomerViewModel не копирует состояние Customer; он просто представляет его через делегирование, вот так:
Когда пользователь создает нового клиента и нажимает кнопку «Save» в элементе управления CustomerView, CustomerViewModel, связанный с этим представлением, добавит новый объект Customer к CustomerRepository. Из-за этого порождается событие CustomerAdded, дающее AllCustomersViewModel знать, что в набор AllCustomers следует добавить новый CustomerViewModel. С некой точки зрения, CustomerRepository действует как механизм синхронизации между разными моделями представлений, работающими с объектами Customer. Можно представить это себе как использование шаблона проектирования «посредник». В следующих разделах я подробнее расскажу о том, как это работает, а пока что можно посмотреть на схему на рис. 11, чтобы в целом представить, как соединяются все части.
Рис. 11 Связи Customer
Форма ввода данных о новом клиенте
Когда пользователь переходит по ссылке «Create new customer», MainWindowViewModel добавляет новый объект CustomerViewModel к своему списку рабочих областей, а элемент CustomerView его отображает. После того, как пользователь вводит допустимые значения в поля ввода, кнопка «Save» входит в состояние «включено», чтобы пользователь мог сохранить сведения о новом клиенте. Ничего необычного, просто обычная форма ввода данных с проверкой ввода и кнопкой «Сохранить».
Класс Customer имеет встроенную поддержку проверки допустимости, доступную через реализацию интерфейса IDataErrorInfo. Эта проверка следит за тем, чтобы у клиента было имя, правильно оформленный адрес электронной почты и (если это человек) фамилия. Если свойство IsCompany Customer возвращает истину, свойство LastName не может иметь значения (смысл в том, что у компании не может быть фамилии). Эта логика проверки имеет смысл с точки зрения объекта Customer, но не соответствует нуждам пользовательского интерфейса. Пользовательский интерфейс требует, чтобы пользователь указал, является ли новый клиент человеком или компанией. Селектор типа клиента изначально имеет значение «(Not Specified)». Как пользовательскому интерфейсу сообщить пользователю, что тип клиента не указан, если свойство Customer IsCompany позволяет только два значения — истина и ложь?
Метод тестирования на рис. 12 показывает, как эта функция работает в CustomerViewModel. CustomerViewModel предоставляет свойство CustomerTypeOptions так, что у селектора типа клиента есть для отображения три строки. Он также представляет свойство CustomerType, сохраняющее выбранную строку в селекторе. Когда установлен CustomerType, он сопоставляет строковое значение с логическим значением для свойства IsCompany нижележащего объекта Customer. На рис. 13 показаны эти два свойства.
Рис. 12. Метод тестирования
Рис. 13 Свойства CustomerType
Элемент управления CustomerView содержит ComboBox, привязанный к этим свойствам, как показано ниже.
Когда изменяется выбранный элемент в этом ComboBox, интерфейс IDataErrorInfo источника данных опрашивается для проверки, действительно ли новое значение. Это происходит потому, что у привязки свойства SelectedItem ValidatesOnDataErrors установлено на значение «истина». Так как источник данных — объект CustomerViewModel, система привязки спрашивает у этого объекта CustomerViewModel об ошибке проверки допустимости для свойства CustomerType. Большинство времени CustomerViewModel делегирует все запросы об ошибках проверки допустимости входящему в него объекту Customer. Но так как Customer не знает о состоянии «не выбрано» для свойства IsCompany, класс CustomerViewModel должен обработать проверку заново выбранного элемента в элементе управления ComboBox. Этот код показан на рис. 14.
Рис. 14 Проверка объекта CustomerViewModel
Ключевой момент этого кода — то, что реализация IDataErrorInfo в объекте CustomerViewModel может обрабатывать запросы проверки допустимости свойства, характерные для модели представления, и делегировать другие запросы объекту Customer. Это позволяет использовать логику проверки в классах модели и иметь дополнительную проверку для свойств, которая имеет смысл только для классов модель представления.
Способность сохранять CustomerViewModel доступна для представления через свойство SaveCommand. Эта команда использует класс RelayCommand, описанный выше, что позволяет CustomerViewModel решать, можно ли ему сохранить себя и что делать, если ему сказано сохранить свое состояние. В этом приложении сохранение нового клиента значит просто добавление его в CustomerRepository. Для решения вопроса о том, можно ли сохранить нового клиента, нужно получить согласие от двух сторон. Следует спросить объект Customer, допустим ли он, а CustomerViewModel должен решить, допустим ли он. Это двойное решение нужно из-за свойств и проверки, хараеткрных для модели представления, о которых мы говорили выше. Логика сохранения CustomerViewModel показана на рис. 15.
Рис. 15 Логика сохранения CustomerViewModel
Использование здесь модели представления значительно упрощает создание представление, которое может отображать объект Customer и позволяет такие вещи, как состояние «не выбрано» логического свойства. Оно также позволяет с легкостью сообщить клиенту, что нужно сохранить его состояние. Если бы представление было привязано прямо к объекту Customer, представоение потребовало бы гораздо больше кода, чтобы все заработало правильно. В хорошо спроектированной архитектуре MVVM код поддержки для большинства представлений должен быть пуст или в крайнем случае содержать код, управляющий элементами управления и ресурсами, содержащимися в этом представлении. Иногда также необходимо в коде поддержки представления написать код, взаимодействующий с объектом модели представления, например, фиксацию события или вызов метода, вызвать который иначе (из самой модели представления) было бы очень трудно.
Представление «All Customers»
Пример приложения содержит также рабочую область, отображающую всех клиентов в элементе ListView. Клиенты в списке сгруппированы по принципу, компании они или люди. Пользователь может выбрать одного или несколько клиентов и посмотреть сумму общих продаж в правом нижнем углу.
Пользовательский интерфейс — элемент управления AllCustomersView, собирающий объект AllCustomersViewModel. Каждый ListViewItem представляет объект CustomerViewModel в наборе AllCustomers, представленный объектом AllCustomerViewModel. В предыдущем разделе вы видели, как CustomerViewModel может собирать форму ввода данных, а теперь такой же объект CustomerViewModel собирается как элемент ListView. Класс CustomerViewModel не знает, какие визуальные элементы его отображают, поэтому его можно использовать повторно подобным образом.
AllCustomersView создает группы, которые видны в ListView. Этого он добивается, привязывая ItemsSource ListView к CollectionViewSource, настроенной, как на рис. 16.
Рис. 16 CollectionViewSource
Связь между ListViewItem и объектом CustomerViewModel устанавливается за счет свойства ItemContainerStyle элемента ListView. Стиль, присвоенный этому свойству, применяется к каждому ListViewItem, что позволяет привязывать свойства ListViewItem к свойствам CustomerViewModel. Одна важная привязка в этом стиле создает связь между свойством IsSelected ListViewItem и свойством IsSelected CustomerViewModel, как показано ниже.
Когда выбирается или снимается выбор CustomerViewModel, изменяется общая сумма продаж для всех выбранных клиентов. Класс AllCustomersViewModel ответственен за поддержание этого значения, чтобы ContentPresenter ниже элемента ListView мог показывать правильное число. На рис. 17 показано, как AllCustomersViewModel наблюдает за каждым клиентом, выбран ли он, и уведомленяет представление, если нужно обновить значение на экране.
Рис. 17 Наблюдение за состоянием «выбран» или «не выбран»
WPF может предложить разработчикам приложений очень многое; нужно начать мыслить немного иначе, чтобы научиться пользоваться этими возможностями. Шаблон модель-представление-модель представления — простой и эффективный набор рекомендаций для проектирования и реализации приложения WPF. Он позволяет разделять данные, поведение и представление, а значит, контролировать тот хаос, который называется разработкой программного обеспечения.
Я хотел бы поблагодарить Джона Госсмана (John Gossman) за помощь с этой статьей.