Когда была выпущена версия 1.0 платформы .NET, программисты, нуждающиеся в построении графических настольных приложений, использовали два API-интерфейса под названиями Windows Forms и GDI+, упакованные преимущественно в сборках
System.Windows.Forms.dll
и System.Drawing.dll
. Наряду с тем, что Windows Forms и GDI+ все еще являются жизнеспособными API-интерфейсами для построения традиционных настольных графических пользовательских интерфейсов, начиная с версии .NET 3.0, поставляется альтернативный API-интерфейс с таким же предназначением — Windows Presentation Foundation (WPF). В выпуске .NET Core 3.0 интерфейсы WPF и Windows Forms объединены с семейством .NET Core.
В начале этой вводной главы, посвященной WPF, вы ознакомитесь с мотивацией, лежащей в основе новой инфраструктуры для построения графических пользовательских интерфейсов, что поможет увидеть отличия между моделями программирования Windows Forms/GDI+ и WPF. Затем анализируется роль ряда важных классов, включая
Application
, Window
, ContentControl
, Control
, UIElement
и FrameworkElement
.
В настоящей главе будет представлена грамматика на основе XML, которая называется расширяемым языком разметки приложений (Extensible Application Markup Language — XAML). Вы изучите синтаксис и семантику XAML (в том числе синтаксис присоединяемых свойств, роль преобразователей типов и расширений разметки).
Глава завершается исследованием визуальных конструкторов WPF, встроенных в Visual Studio, за счет построения вашего первого приложения WPF. Вы научитесь перехватывать действия клавиатуры и мыши, определять данные уровня приложения и выполнять другие распространенные задачи WPF.
На протяжении многих лет в Microsoft создавали инструменты для построения графических пользовательских интерфейсов (для низкоуровневой разработки на C/C++/Windows API, VB6, MFC и т.д.) настольных приложений. Каждый инструмент предлагает кодовую базу для представления основных аспектов приложения с графическим пользовательским интерфейсом, включая главные окна, диалоговые окна, элементы управления, системы меню и другие базовые аспекты. После начального выпуска платформы .NET инфраструктура Windows Forms быстро стала предпочтительным подходом к разработке пользовательских интерфейсов благодаря своей простой, но очень мощной объектной модели.
Хотя с помощью Windows Forms было успешно создано множество полноценных настольных приложений, дело в том, что данная программная модель несколько ассиметрична. Попросту говоря, сборки
System.Windows.Forms.dll
и System.Drawing.dll
не предоставляют прямую поддержку для многих дополнительных технологий, требуемых при построении полнофункционального настольного приложения. Чтобы проиллюстрировать сказанное, рассмотрим узкоспециализированную разработку графических пользовательских интерфейсов до выпуска WPF (табл. 24.1).
Как видите, разработчик, использующий Windows Forms, вынужден заимствовать типы из нескольких несвязанных API-интерфейсов и объектных моделей. Несмотря на то что применение всех разрозненных API-интерфейсов синтаксически похоже (в конце концов, это просто код С#), каждая технология требует радикально иного мышления. Например, навыки, необходимые для создания трехмерной анимации с использованием DirectX, совершенно отличаются от тех, что нужны для привязки данных к экранной сетке. Конечно, программисту Windows Forms чрезвычайно трудно в равной степени хорошо овладеть природой каждого API-интерфейса.
Инфраструктура WPF специально создавалась для объединения ранее несвязанных задач программирования в одну унифицированную объектную модель. Таким образом, при разработке трехмерной анимации больше не возникает необходимости в ручном кодировании с применением API-интерфейса DirectX (хотя это можно делать), поскольку нужная функциональность уже встроена в WPF. Чтобы продемонстрировать, насколько все стало яснее, в табл. 24.2 представлена модель разработки настольных приложений, введенная в .NET 3.0.
Очевидное преимущество здесь в том, что программисты приложений .NET теперь имеют единственный симметричный API-интерфейс для всех распространенных потребностей, появляющихся во время разработки графических пользовательских интерфейсов настольных приложений. Освоившись с функциональностью основных сборок WPF и грамматикой XAML, вы будете приятно удивлены, насколько быстро с их помощью можно создавать сложные пользовательские интерфейсы.
Возможно, одно из наиболее значительных преимуществ заключается в том, что инфраструктура WPF предлагает способ аккуратного отделения внешнего вида и поведения приложения с графическим пользовательским интерфейсом от программной логики, которая им управляет. Используя язык XAML, пользовательский интерфейс приложения можно определять через разметку XML. Такая разметка (в идеале генерируемая с помощью инструментов вроде Microsoft Visual Studio или Blend для Visual Studio) затем может быть подключена к связанному файлу кода для обеспечения внутренней части функциональности программы.
На заметку! Применение языка XAML не ограничено приложениями WPF. Любое приложение может использовать XAML для описания дерева объектов .NET, даже если они не имеют никакого отношения к видимому пользовательскому интерфейсу.
По мере погружения в WPF вас может удивить, насколько высокую гибкость обеспечивает эта "настольная разметка". Язык XAML позволяет определять в разметке не только простые элементы пользовательского интерфейса (кнопки, таблицы, окна со списками и т.д.), но также интерактивную двумерную и трехмерную графику, анимацию, логику привязки данных и функциональность мультимедиа (наподобие воспроизведения видео).
Кроме того, XAML облегчает настройку визуализации элемента управления. Например, определение круглой кнопки, на которой выполняется анимация логотипа компании, требует всего нескольких строк разметки. Как показано в главе 27, элементы управления WPF могут быть модифицированы посредством стилей и шаблонов, которые позволяют изменять весь внешний вид приложения с минимальными усилиями. В отличие от разработки с помощью Windows Forms единственной веской причиной для построения специального элемента управления WPF с нуля является необходимость в изменении поведения элемента управления (например, добавление специальных методов, свойств или событий либо создание подкласса существующего элемента управления с целью переопределения виртуальных членов). Если нужно просто изменить внешний вид элемента управления (как в случае с круглой анимированной кнопкой), то это можно делать полностью через разметку.
Наборы инструментов для построения графических пользовательских интерфейсов, такие как Windows Forms, MFC или VB6, выполняют все запросы графической визуализации (включая визуализацию элементов управления вроде кнопок и окон со списком) с применением низкоуровневого API-интерфейса на основе С (GDI), который в течение многих лет был частью Windows. Интерфейс GDI обеспечивает адекватную производительность для типовых бизнес-приложений или простых графических программ; однако если приложению с пользовательским интерфейсом нужна была высокопроизводительная графика, то приходилось обращаться к услугам DirectX.
Программная модель WPF полностью отличается тем, что при визуализации графических данных GDI не используется. Все операции визуализации (двумерная и трехмерная графика, анимация, визуализация элементов управления и т.д.) теперь работают с API-интерфейсом DirectX. Очевидная выгода такого подхода в том, что приложения WPF будут автоматически получать преимущества аппаратной и программной оптимизации. Вдобавок приложения WPF могут задействовать развитые графические службы (эффекты размытия, сглаживания, прозрачности и т.п.) без сложностей, присущих программированию напрямую с применением API-интерфейса DirectX.
На заметку! Хотя WPF переносит все запросы визуализации на уровень DirectX, нельзя утверждать, что приложение WPF будет работать настолько же быстро, как приложение, построенное с использованием неуправляемого языка C++ и DirectX. Несмотря на значительные усовершенствования, вносимые в WPF с каждым новым выпуском, если вы намереваетесь строить настольное приложение, которое требует максимально возможной скорости выполнения (вроде трехмерной игры), то неуправляемый C++ и DirectX по-прежнему будут наилучшим выбором.
Чтобы подвести итоги сказанному до сих пор: WPF — это API-интерфейс, предназначенный для построения настольных приложений, который интегрирует разнообразные настольные API-интерфейсы в единую объектную модель и обеспечивает четкое разделение обязанностей через XAML. В дополнение к указанным важнейшим моментам приложения WPF также выигрывают от простого способа интеграции со службами, что исторически было довольно сложным. Ниже кратко перечислены основные функциональные возможности WPF.
• Множество диспетчеров компоновки (гораздо больше, чем в Windows Forms) для обеспечения исключительно гибкого контроля над размещением и изменением позиций содержимого.
• Применение расширенного механизма привязки данных для связывания содержимого с элементами пользовательского интерфейса разнообразными способами.
• Встроенный механизм стилей, который позволяет определять "темы" для приложения WPF.
• Использование векторной графики, поддерживающей автоматическое изменение размеров содержимого с целью соответствия размерам и разрешающей способности экрана, который отображает пользовательский интерфейс приложения.
• Поддержка двумерной и трехмерной графики, анимации, а также воспроизведения видео- и аудио-роликов.
• Развитый типографский API-интерфейс, который поддерживает документы XML Paper Specification (XPS), фиксированные документы (WYSIWYG), документы нефиксированного формата и аннотации в документах (например, API-интерфейс Sticky Notes).
• Поддержка взаимодействия с унаследованными моделями графических пользовательских интерфейсов (такими как Windows Forms, ActiveX и HWND-дескрипторы Win32). Например, в приложение WPF можно встраивать специальные элементы управления Windows Forms и наоборот.
Теперь, получив определенное представление о том, что инфраструктура WPF привносит в платформу, давайте рассмотрим разнообразные типы приложений, которые могут быть созданы с применением данного API-интерфейса. Многие из перечисленных выше возможностей будут подробно исследованы в последующих главах.
В конечном итоге инфраструктура WPF — не многим более чем коллекция типов, встроенных в сборки .NET Core. В табл. 24.3 описаны основные сборки, используемые при разработке приложений WPF, на каждую из которых должна быть добавлена ссылка, когда создается новый проект. Как и следовало ожидать, проекты WPF в Visual Studio ссылаются на эти обязательные сборки автоматически.
В этих четырех сборках определены новые пространства имен, а также классы, интерфейсы, структуры, перечисления и делегаты .NET Core. В табл. 24.4 описана роль некоторых (но далеко не всех) важных пространств имен.
В начале путешествия по программной модели WPF вы исследуете два члена пространства имен
System.Windows
, которые являются общими при традиционной разработке любого настольного приложения: Application
и Window
.
На заметку! Если вы создавали пользовательские интерфейсы для настольных приложений с использованием API-интерфейса Windows Forms, то имейте в виду, что сборки
System.Windows.Forms.*
и System.Drawing.*
никак не связаны с WPF. Они относятся к первоначальному инструментальному набору .NET для построения графических пользовательских интерфейсов, т.е. Windows Forms/GDI+.
Класс
System.Windows.Application
представляет глобальный экземпляр выполняющегося приложения WPF. В нем имеется метод Run()
(для запуска приложения) и комплект событий, которые можно обрабатывать для взаимодействия с приложением на протяжении его времени жизни (наподобие Startup
и Exit
). В табл. 24.5 описаны основные свойства класса Application
.
В любом приложении WPF нужно будет определить класс, расширяющий
Application
. Внутри такого класса определяется точка входа программы (метод Main()
), которая создает экземпляр данного подкласса и обычно обрабатывает события Startup
и Exit
(при необходимости). Вот пример:
// Определить глобальный объект приложения для этой программы WPF.
class MyApp : Application
{
[STAThread]
static void Main(string[] args)
{
// Создать объект приложения.
MyApp app = new MyApp();
// Зарегистрировать события Startup/Exit.
app.Startup += (s, e) => { /* Запуск приложения */ };
app.Exit += (s, e) => { /* Завершение приложения */ };
}
}
В обработчике события
Startup
чаще всего обрабатываются входные аргументы командной строки и запускается главное окно программы. Как и следовало ожидать, обработчик события Exit
представляет собой место, куда можно поместить любую необходимую логику завершения программы(например, сохранение пользовательских предпочтений).
На заметку! Метод
Main()
приложения WPF должен быть снабжен атрибутом [STAThread]
, который гарантирует, что любые унаследованные объекты СОМ, используемые приложением, являются безопасными в отношении потоков. Если не аннотировать метод Main()
подобным образом, тогда во время выполнения возникнет исключение. Даже после появления в версии C# 9.0 операторов верхнего уровня вы все равно будете стремиться использовать в приложениях WPF традиционный метод Main()
. В действительности метод Main()
генерируется автоматически.
Еще одним интересным свойством класса
Application
является Windows
, обеспечивающее доступ к коллекции, которая представляет все окна, загруженные в память для текущего приложения WPF. Вспомните, что создаваемые новые объекты Window
автоматически добавляются в коллекцию Application.Windows
. Ниже приведен пример метода, который сворачивает все окна приложения(возможно в ответ на нажатие определенного сочетания клавиш или выбор пункта меню конечным пользователем):
static void MinimizeAllWindows()
{
foreach (Window wnd in Application.Current.Windows)
{
wnd.WindowState = WindowState.Minimized;
}
}
Вскоре будет построено несколько приложений WPF, а пока давайте выясним основную функциональность типа Window и изучим несколько важных базовых классов WPF.
Класс
System.Windows.Window
(из сборки PresentationFramework.dll
) представляет одиночное окно, которым владеет производный от Application
класс, включая все отображаемые главным окном диалоговые окна. Тип Window
вполне ожидаемо имеет несколько родительских классов, каждый из которых привносит дополнительную функциональность.
На рис. 24.1 показана цепочка наследования (и реализуемые интерфейсы) для класса
System.Windows.Window
, как она выглядит в браузере объектов Visual Studio.
По мере чтения этой и последующих глав вы начнете понимать функциональность, предлагаемую многими базовыми классами WPF. Далее представлен краткий обзор функциональности каждого базового класса (полные сведения ищите в документации по .NET 5).
Непосредственным родительским классом
Window
является класс ContentControl
, который вполне можно считать самым впечатляющим из всех классов WPF. Базовый класс ContentControl
снабжает производные типы способностью размещать в себе одиночный фрагмент содержимого, который, выражаясь упрощенно, относится к визуальным данным, помещенным внутрь области элемента управления через свойство Content
. Модель содержимого WPF позволяет довольно легко настраивать базовый вид и поведение элемента управления ContentControl
.
Например, когда речь идет о типичном "кнопочном" элементе управления, то обычно предполагается, что его содержимым будет простой строковый литерал (ОК, Cancel, Abort и т.д.). Если для описания элемента управления WPF применяется XAML, а значение, которое необходимо присвоить свойству
Content
, может быть выражено в виде простой строки, тогда вот как установить свойство Content
внутри открывающего определения элемента:
На заметку! Свойство
Content
можно также устанавливать в коде С#, что позволяет изменять внутренности элемента управления во время выполнения.
Однако содержимое может быть практически любым. Например, пусть нужна "кнопка", которая содержит в себе что-то более интересное, нежели простую строку — возможно специальную графику или текстовый фрагмент. В других инфраструктурах для построения пользовательских интерфейсов, таких как Windows Forms, потребовалось бы создать специальный элемент управления, что могло повлечь за собой написание значительного объема кода и сопровождение полностью нового класса. Благодаря модели содержимого WPF необходимость в этом отпадает.
Когда свойству
Content
должно быть присвоено значение, которое невозможно выразить в виде простого массива символов, его нельзя присвоить с использованием атрибута в открывающем определении элемента управления. Взамен понадобится определить данные содержимого неявно внутри области действия элемента. Например, следующий элемент
включает в качестве содержимого элемент
, который сам имеет уникальные данные (а именно —
и
):
Для установки сложного содержимого можно также применять синтаксис "свойство-элемент" языка XAML. Взгляните на показанное далее функционально эквивалентное определение
, которое явно устанавливает свойство Content
с помощью синтаксиса "свойство-элемент" (дополнительная информация о XAML будет дана позже в главе, так что пока не обращайте внимания на детали):
Имейте в виду, что не каждый элемент WPF является производным от класса
ConentConrtol
, поэтому не все элементы поддерживают такую уникальную модель содержимого (хотя большинство поддерживает). Кроме того, некоторые элементы управления WPF вносят несколько усовершенствований в только что рассмотренную базовую модель содержимого. В главе 25 роль содержимого WPF раскрывается более подробно.
В отличие от
ContentControl
все элементы управления WPF разделяют в качестве общего родительского класса базовый класс Control
. Он предоставляет многочисленные члены, которые необходимы для обеспечения основной функциональности пользовательского интерфейса. Например, в классе Control
определены свойства для установки размеров элемента управления, прозрачности, порядка обхода по нажатию клавиши <ТаЬ>
, отображаемого курсора, цвета фона и т.д. Более того, данный родительский класс предлагает поддержку шаблонных служб. Как объясняется в главе 27, элементы управления WPF могут полностью изменять способ визуализации своего внешнего вида, используя шаблоны и стили. В табл. 24.6 кратко описаны основные члены типа Control
, сгруппированные по связанной функциональности.
Базовый класс
FrameworkElement
предоставляет несколько членов, которые применяются повсюду в инфраструктуре WPF, в том числе для поддержки раскадровки(в целях анимации)и привязки данных, а также возможности именования членов (через свойство Name
), получения любых ресурсов, определенных производным типом, и установки общих измерений производного типа. Основные члены класса FrameworkElement
кратко описаны в табл. 24.7.
Из всех типов в цепочке наследования класса
Window
наибольший объем функциональности обеспечивает базовый класс UIElement
. Его основная задача — предоставить производному типу многочисленные события, чтобы он мог получать фокус и обрабатывать входные запросы. Например, в классе UIElement
предусмотрены многочисленные события для обслуживания операций перетаскивания, перемещения курсора мыши, клавиатурного ввода, ввода посредством пера и сенсорного ввода.
Модель событий WPF будет подробно описана в главе 25; тем не менее, многие основные события будут выглядеть вполне знакомо (
MouseMove
, MouseDown
, MouseEnter
, MouseLeave
, KeyUp
и т.д.). В дополнение к десяткам событий родительский класс UIElement
предлагает свойства, предназначенные для управления фокусом, состоянием доступности, видимостью и логикой проверки попадания (табл. 24.8).
Класс
Visual
предлагает основную поддержку визуализации в WPF, которая включает проверку попадания для графических данных, координатную трансформацию и вычисление ограничивающих прямоугольников. В действительности при рисовании данных на экране класс Visual
взаимодействует с подсистемой DirectX. Как будет показано в главе 26, инфраструктура WPF поддерживает три возможных способа визуализации графических данных, каждый из которых отличается в плане функциональности и производительности. Применение типа Visual
(и его потомков вроде DrawingVisual
) является наиболее легковесным путем визуализации графических данных, но также подразумевает написание вручную большого объема кода для учета всех требуемых служб. Более подробно об этом пойдет речь в главе 26.
Инфраструктура WPF поддерживает отдельную разновидность свойств .NET под названием свойства зависимости. Выражаясь упрощенно, данный стиль свойств предоставляет дополнительный код, чтобы позволить свойству реагировать на определенные технологии WPF, такие как стили, привязка данных, анимация и т.д. Чтобы тип поддерживал подобную схему свойств, он должен быть производным от базового класса
DependencyObject
. Несмотря на то что свойства зависимости являются ключевым аспектом разработки WPF, большую часть времени их детали скрыты от глаз. В главе 25 мы рассмотрим свойства зависимости более подробно.
Последним базовым классом для типа
Window
(помимо System.Object
, который здесь не требует дополнительных пояснений) является DispatherObject
. В нем определено одно интересное свойство Dispatcher
, которое возвращает ассоциированный объект System.Windows.Threading.Dispatcher
. Класс Dispatcher
— это точка входа в очередь событий приложения WPF, и он предоставляет базовые конструкции для организации параллелизма и многопоточности. Объект Dispatcher
обсуждался в главе 15.
Приложения WPF производственного уровня обычно будут использовать отдельные инструменты для генерации необходимой разметки XAML. Как бы ни были удобны такие инструменты, важно понимать общую структуру языка XAML. Для содействия процессу изучения доступен популярный (и бесплатный) инструмент, который позволяет легко экспериментировать с XAML.
Когда вы только приступаете к изучению грамматики XAML, может оказаться удобным в применении бесплатный инструмент под названием Kaxaml. Этот популярный редактор/анализатор XAML доступен по ссылке
https://github.com/punker76/kaxaml
.
На заметку! Во многих предшествующих изданиях книги мы направляли читателей на веб-сайт
www.kaxaml.com
, но, к сожалению, он прекратил свою работу. Ян Каргер (https://github.com/punker76
) сделал ответвление от старого кода и потрудился над его улучшением. Его версия инструмента доступна в GitHub по ссылке https://github.com/punker76/kaxaml/releases
. Стоит выразить благодарность создателям за великолепный инструмент Kaxaml и Яну за то, что он сохранил его; Kaxaml помог многочисленным разработчикам изучить XAML.
Редактор Kaxaml полезен тем, что не имеет никакого понятия об исходном коде С#, обработчиках ошибок или логике реализации. Он предлагает намного более прямолинейный способ тестирования фрагментов XAML, нежели использование полноценного шаблона проекта WPF в Visual Studio. К тому же Kaxaml обладает набором интегрированных инструментов, в том числе средством выбора цвета, диспетчером фрагментов XAML и даже средством "очистки XAML", которое форматирует разметку XAML на основе заданных настроек. Открыв Kaxaml в первый раз, вы найдете в нем простую разметку для элемента управления
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Подобно объекту
Window
объект Page
содержит разнообразные диспетчеры компоновки и элементы управления. Тем не менее, в отличие от Window
объекты Page
не могут запускаться как отдельные сущности. Взамен они должны помещаться внутрь подходящего хоста, такого как NavigationWindow
или Frame
. Хорошая новость в том, что в элементах
и
можно вводить идентичную разметку.
На заметку! Если в окне разметки Kaxaml заменить элементы
и
элементами
и
, тогда можно нажать клавишу <F5> и отобразить на экране новое окно.
В качестве начального теста введите следующую разметку в панели XAML, находящейся в нижней части окна Kaxaml:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
В верхней части окна Kaxaml появится визуализированная страница (рис. 24.2).
Во время работы с Kaxaml помните, что данный инструмент не позволяет писать разметку, которая влечет за собой любую компиляцию кода (но разрешено использовать
х:Name
). Сюда входит определение атрибута х:Class
(для указания файла кода), ввод имен обработчиков событий в разметке или применение любых ключевых слов XAML, которые также предусматривают компиляцию кода (вроде FieldModifier
или ClassModifier
). Попытка поступить так приводит к ошибке разметки.
Корневой элемент XAML-документа WPF (такой как
,
,
или
) почти всегда будет ссылаться на два заранее определенные пространства имен XML:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Первое пространство имен XML,
http://schemas.microsoft.com/winfx/2006/xaml/presentation
, отображает множество связанных c WPF пространств имен .NET для использования текущим файлом *.xaml
(System.Windows
, System.Windows.Controls
, System.Windows.Data
, System.Windows.Ink
, System.Windows.Media
, System.Windows.Navigation
и т.д.).
Это отображение "один ко многим" в действительности жестко закодировано внутри сборок WPF (
WindowsBase.dll
, PresentationCore.dll
и PresentationFramework.dll
) с применением атрибута [XmlnsDefinition]
уровня сборки. Например, если открыть браузер объектов Visual Studio и выбрать сборку PresentationCore.dll
, то можно увидеть списки, подобные показанному ниже, в котором импортируется пространство имен System.Windows
:
[assembly: XmlnsDefinition(
"http://schemas.microsoft.com/winfx/2006/xaml/presentation",
"System.Windows")]
Второе пространство имен XML,
http://schemas.microsoft.com/winfx/2006/xaml
, используется для добавления специфичных для XAML "ключевых слов" (термин выбран за неимением лучшего), а также пространства имен System.Windows.Markup
:
[assembly: XmlnsDefinition(
"http://schemas.microsoft.com/winfx/2006/xaml",
"System.Windows.Markup")]
Одно из правил любого корректно сформированного документа XML (не забывайте, что грамматика XAML основана на XML) состоит в том, что открывающий корневой элемент назначает одно пространство имен XML в качестве первичного пространства имен, которое обычно представляет собой пространство имен, содержащее самые часто применяемые элементы. Если корневой элемент требует включения дополнительных вторичных пространств имен (как видно здесь), то они должны быть определены с использованием уникального префикса (чтобы устранить возможные конфликты имен). По соглашению для префикса применяется просто
х
, однако он может быть любым уникальным маркером, таким как XamlSpecificStuff
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:XamlSpecificStuff="http://schemas.microsoft.com/winfx/2006/xaml">
Очевидный недостаток определения длинных префиксов для пространств имен XML связан с тем, что
XamlSpecificStuff
придется набирать всякий раз, когда в файле XAML нужно сослаться на один из элементов, определенных в этом пространстве имен XML. Из-за того, что префикс XamlSpecificStuff
намного длиннее, давайте ограничимся х
.
Помимо ключевых слов
x:Name
, х:Class
и x:Code
пространство имен http://schemas.microsoft.com/winfх/2006/xaml
также предоставляет доступ к дополнительным ключевым словам XAML, наиболее распространенные из которых кратко описаны в табл. 24.9.
В дополнение к двум указанным объявлениям пространств имен XML можно (а иногда и нужно) определить дополнительные префиксы дескрипторов в открывающем элементе документа XAML. Обычно так поступают, когда необходимо описать в XAML класс .NET Core, определенный во внешней сборке.
Например, предположим, что было построено несколько специальных элементов управления WPF, которые упакованы в библиотеку по имени
MyControls.dll
. Если теперь требуется создать новый объект Window, в котором применяются созданные элементы, то можно установить специальное пространство имен XML, отображаемое на библиотеку MyControls.dll
, с использованием маркеров clr-namespace
и assembly
. Ниже приведен пример разметки, создающей префикс дескриптора по имени myCtrls
, который может применяться для доступа к элементам управления в этой библиотеке:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:myCtrls="clr-namespace:MyControls;assembly=MyControls"
Title="MainWindow" Height="350" Width="525">
Маркеру
clr-namespace
назначается название пространства имен .NET Core в сборке, в то время как маркер assembly
устанавливается в дружественное имя внешней сборки *.dll
. Такой синтаксис можно использовать для любой внешней библиотеки .NET Core, которой желательно манипулировать внутри разметки. В настоящее время в этом нет необходимости, но в последующих главах понадобится определять специальные объявления пространств имен XML для описания типов в разметке.
На заметку! Если нужно определить в разметке класс, который является частью текущей сборки, но находится в другом пространстве имен .NET Core, то префикс дескриптора
xmlns
определяется без атрибута assembly=:xmlns:myCtrls="clr-namespace:SomeNamespacelnMyApp"
Многие ключевые слова вы увидите в действии в последующих главах там, где они потребуются; тем не менее, в качестве простого примера взгляните на следующее XAML-определение
, в котором применяются ключевые слова ClassModifier
и FieldModifier
, а также x:Name
и х:Class
(вспомните, что редактор Kaxaml не позволяет использовать ключевые слова XAML, вовлекающие компиляцию, такие как x:Code
, х:FieldModifier
или х:ClassModifier
):
По умолчанию все определения типов C#/XAML являются открытыми (
public
), а члены — внутренними (internal
). Однако для показанного выше определения XAML результирующий автоматически сгенерированный файл содержит внутренний тип класса с открытой переменной-членом Button
:
internal partial class MainWindow : System.Windows.Window,
System.Windows.Markup.IComponentConnector
{
public System.Windows.Controls.Button myButton;
...
}
После установки корневого элемента и необходимых пространств имен XML следующая задача заключается в наполнении корня дочерним элементом. В реальном приложении WPF дочерним элементом будет диспетчер компоновки (такой как
Grid
или StackPanel
), который в свою очередь содержит любое количество дополнительных элементов, описывающих пользовательский интерфейс. Такие диспетчеры компоновки рассматриваются в главе 25, а пока предположим, что элемент
будет содержать единственный элемент Button
.
Как было показано ранее в главе, элементы XAML отображаются на типы классов или структур внутри заданного пространства имен .NET Core, тогда как атрибуты в открывающем дескрипторе элемента отображаются на свойства или события конкретного типа. В целях иллюстрации введите в редакторе Kaxaml следующее определение
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
FontSize="20" Background="Green" Foreground="Yellow"/>
Обратите внимание, что присвоенные свойствам значения представлены с помощью простого текста. Это может выглядеть как полное несоответствие типам данных, поскольку после создания такого элемента
Button
в коде C# данным свойствам будут присваиваться не строковые объекты, а значения специфических типов данных. Например, ниже показано, как та же самая кнопка описана в коде:
public void MakeAButton()
{
Button myBtn = new Button();
myBtn.Height = 50;
myBtn.Width = 100;
myBtn.FontSize = 20;
myBtn.Content = "OK!";
myBtn.Background = new SolidColorBrush(Colors.Green);
myBtn.Foreground = new SolidColorBrush(Colors.Yellow);
}
Оказывается, что инфраструктура WPF поставляется с несколькими классами преобразователей типов, которые будут применяться для трансформации простых текстовых значений в корректные типы данных. Такой процесс происходит прозрачно (и автоматически).
Тем не менее, нередко возникает потребность в присваивании атрибуту XAML намного более сложного значения, которое невозможно выразить посредством простой строки. Например, пусть необходимо построить специальную кисть для установки свойства
Background
элемента Button
. Создать кисть подобного рода в коде довольно просто:
public void MakeAButton()
{
...
// Необычная кисть для фона.
LinearGradientBrush fancyBruch =
new LinearGradientBrush(Colors.DarkGreen, Colors.LightGreen, 45);
myBtn.Background = fancyBruch;
myBtn.Foreground = new SolidColorBrush(Colors.Yellow);
}
Но можно ли представить эту сложную кисть в виде строки? Нет, нельзя! К счастью, в XAML предусмотрен специальный синтаксис, который можно использовать всякий раз, когда нужно присвоить сложный объект в качестве значения свойства; он называется синтаксисом "свойство-элемент".
Синтаксис "свойство-элемент" позволяет присваивать свойству сложные объекты. Ниже показано описание XAML элемента Button, в котором для установки свойства
Background
применяется объект LinearGradientBrush
:
FontSize="20" Foreground="Yellow">
Обратите внимание, что внутри дескрипторов
и
определена вложенная область по имени
, а в ней — специальный элемент
. (Пока не беспокойтесь о коде кисти; вы освоите графику WPF в главе 26.)
Любое свойство может быть установлено с использованием синтаксиса "свойство-элемент", который всегда сводится к следующему шаблону:
<ОпределяющийКласс>
<ОпределяющийКласс.СвойствоОпределяющегоКласса>
ОпределяющийКласс.СвойствоОпределяющегоКласса>
ОпределяющийКласс>
Хотя любое свойство может быть установлено с применением такого синтаксиса, указание значения в виде простой строки, когда подобное возможно, будет экономить время ввода. Например, вот гораздо более многословный способ установки свойства
Width
элемента Button
:
FontSize="20" Foreground="Yellow">
...
100
В дополнение к синтаксису "свойство-элемент" в XAML поддерживается специальный синтаксис, используемый для установки значения присоединяемого свойства. По существу присоединяемое свойство позволяет дочернему элементу устанавливать значение свойства, которое определено в родительском элементе. Общий шаблон, которому нужно следовать, выглядит так:
<РодительскийЭлемент>
<ДочернийЭлемент РодительскийЭлемент.СвойствоРодительскогоЭлемента
= "Значение">
РодительскийЭлемент>
Самое распространенное применение синтаксиса присоединяемых свойств связано с позиционированием элементов пользовательского интерфейса внутри одного из классов диспетчеров компоновки (
Grid
, DockPanel
и т.д.). Диспетчеры компоновки более подробно рассматриваются в главе 25, а пока введите в редакторе Kaxaml следующую разметку:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Width="20" Fill="DarkBlue"/>
Здесь определен диспетчер компоновки
Canvas
, который содержит элемент Ellipse
. Обратите внимание, что с помощью синтаксиса присоединяемых свойств элемент Ellipse
способен информировать свой родительский элемент (Canvas
) о том, где располагать позицию его левого верхнего угла.
В отношении присоединяемых свойств следует иметь в виду несколько моментов. Прежде всего, это не универсальный синтаксис, который может применяться к любому свойству любого родительского элемента. Скажем, приведенная далее разметка XAML содержит ошибку:
Canvas.Top="40" Canvas.Left="90"
Height="20" Width="20" Fill="DarkBlue"/>
Присоединяемые свойства являются специализированной формой специфичной для WPF концепции, которая называется свойством зависимости. Если только свойство не было реализовано в весьма специальной манере, то его значение не может быть установлено с использованием синтаксиса присоединяемых свойств. Свойства зависимости подробно исследуются в главе 25.
На заметку! В Visual Studio имеется средство
IntelliSense
, которое отображает допустимые присоединяемые свойства, доступные для установки заданным элементом.
Как уже объяснялось, значения свойств чаще всего представляются в виде простой строки или через синтаксис "свойство-элемент". Однако существует еще один способ указать значение атрибута XAML — применение расширений разметки. Расширения разметки позволяют анализатору XAML получать значение для свойства из выделенного внешнего класса. Это может обеспечить большие преимущества, поскольку для получения значений некоторых свойств требуется выполнение множества операторов кода.
Расширения разметки предлагают способ аккуратного расширения грамматики XAML новой функциональностью. Расширение разметки внутренне представлено как класс, производный от
MarkupExtension
. Следует отметить, что необходимость в построении специального расширения разметки возникает крайне редко. Тем не менее, некоторые ключевые слова XAML (вроде х:Array
, x:Null
, х:Static
и х:Туре
) являются замаскированными расширениями разметки!
Расширение разметки помещается между фигурными скобками:
<Элемент УстанавливаемоеСвойство = "{ РасширениеРазметки }" />
Чтобы увидеть расширение разметки в действии, введите в редакторе Kaxaml следующий код:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CorLib="clr-namespace:System;assembly=mscorlib">
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height="285" Width="325">
Также вспомните, что попытка помещения внутрь области
Window
сразу нескольких элементов вызовет ошибки разметки и компиляции. Причина в том, что свойству Content
окна (или по существу любого потомка ContentControl
) может быть присвоен только один объект. Следовательно, приведенная далее разметка XAML приведет к ошибкам разметки и компиляции:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height="285" Width="325">
FontSize="15" Content="Enter
Information"/>
Понятно, что от окна, допускающего наличие только одного элемента управления, мало толку. Когда окно должно содержать несколько элементов, их потребуется расположить внутри любого числа панелей. В панель будут помещены все элементы пользовательского интерфейса, которые представляют окно, после чего сама панель выступает в качестве единственного объекта, присваиваемого свойству
Content
окна. Пространство имен System.Windows.Controls
предлагает многочисленные панели, каждая из которых по-своему обслуживает внутренние элементы. С помощью панелей можно устанавливать поведение элементов управления при изменении размеров окна пользователем — будут они оставаться в тех же местах, где были размещены на этапе проектирования, располагаться свободным потоком слева направо или сверху вниз и т.д.
Элементы управления типа панелей также разрешено помещать внутрь других панелей (например, элемент управления
DockPanel
может содержать StackPanel
со своими элементами), чтобы обеспечить высокую гибкость и степень управления. В табл. 25.2 кратко описаны некоторые распространенные элементы управления типа панелей WPF.
В последующих нескольких разделах вы узнаете, как применять распространенные типы панелей, копируя заранее определенную разметку XAML в редактор Kaxaml, который был установлен в главе 24. Все необходимые файлы XAML находятся в подкаталоге
PanelMarkup
внутри Chapter_25
. Во время работы с Kaxaml для эмуляции изменения размеров окна нужно изменить высоту или ширину элемента Page
в разметке.
При наличии опыта работы с Windows Forms панель
Canvas
вероятно покажется наиболее привычной, т.к. она делает возможным абсолютное позиционирование содержимого пользовательского интерфейса. Если конечный пользователь изменяет размер окна, делая его меньше, чем размер компоновки, обслуживаемой панелью Canvas
, то внутреннее содержимое будет невидимым до тех пор, пока контейнер не увеличится до размера, равного или превышающего размер области Canvas
.
Чтобы добавить содержимое к
Canvas
, сначала понадобится определить требуемые элементы управления внутри области между открывающим и закрывающим дескрипторами Canvas
. Затем для каждого элемента управления необходимо указать левый верхний угол с использованием свойств Canvas.Тор
и Canvas.Left
; именно здесь должна начинаться визуализация. Правый нижний угол каждого элемента управления можно задать неявно, устанавливая свойства Canvas.Height
и Canvas.Width
, либо явно с применением свойств Canvas.Right
и Canvas.Bottom
.
Для демонстрации
Canvas
в действии откройте готовый файл SimpleCanvas.xaml
в редакторе Kaxaml. Определение Canvas
должно иметь следующий вид (в случае загрузки примеров в приложение WPF дескриптор Page
нужно будет заменить дескриптором Window
):
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height="285" Width="325">
Width="80" Content="OK"/>
Width="328" Height="27"
FontSize="15"
Content="Enter Car Information"/>
Width="193" Height="25"/>
Width="193" Height="25"/>
Content="Pet Name"/>
Width="193" Height="25"/>
В верхней половине экрана отобразится окно, показанное на рис. 25.1.
Обратите внимание, что порядок объявления элементов содержимого внутри
Canvas
не влияет на расчет местоположения; на самом деле местоположение основано на размере элемента управления и значениях его свойств Canvas.Top
, Canvas.Bottom
, Canvas.Left
и Canvas.Right
.
На заметку! Если подэлементы внутри
Canvas
не определяют специфическое местоположение с использованием синтаксиса присоединяемых свойств (например, Canvas.Left
и Canvas.Тор
), тогда они автоматически прикрепляются к левому верхнему углу Canvas
.
Применение типа
Canvas
может показаться предпочтительным способом организации содержимого (т.к. он выглядит настолько знакомым), но данному подходу присущи некоторые ограничения. Во-первых, элементы внутри Canvas
не изменяют свои размеры динамически при использовании стилей или шаблонов (скажем, их шрифты остаются незатронутыми). Во-вторых, панель Canvas
не пытается сохранять элементы видимыми, когда конечный пользователь уменьшает размер окна.
Пожалуй, наилучшим применением типа
Canvas
является позиционирование графического содержимого. Например, при построении изображения с использованием XAML определенно понадобится сделать так, чтобы все линии, фигуры и текст оставались на своих местах, а не динамически перемещались в случае изменения пользователем размера окна. Мы еще вернемся к Canvas
в главе 26 при обсуждении служб визуализации графики WPF.
Панель
WrapPanel
позволяет определять содержимое, которое будет протекать сквозь панель, когда размер окна изменяется. При позиционировании элементов внутри WrapPanel
их координаты верхнего левого и правого нижнего углов не указываются, как обычно делается в Canvas
. Однако для каждого подэлемента допускается определение значений свойств Height
и Width
(наряду с другими свойствами), чтобы управлять их общим размером в контейнере.
Поскольку содержимое внутри
WrapPanel
не пристыковывается к заданной стороне панели, порядок объявления элементов играет важную роль (содержимое визуализируется от первого элемента до последнего). В файле SimpleWrapPanel.xaml
находится следующая разметка (заключенная внутрь определения Page
):
FontSize="15" Content="Enter Car
Information"/>
Когда эта разметка загружена, при изменении ширины окна содержимое выглядит не особо привлекательно, т.к. оно перетекает слева направо внутри окна (рис. 25.2).
По умолчанию содержимое
WrapPanel
перетекает слева направо. Тем не менее, если изменить значение свойства Orientation
на Vertical
, то можно заставить содержимое перетекать сверху вниз:
Orientation ="Vertical">
Панель
WrapPanel
(как и ряд других типов панелей) может быть объявлена с указанием значений ItemWidth
и ItemHeight
, которые управляют стандартным размером каждого элемента. Если подэлемент предоставляет собственные значения Height
и/или Width
, то он будет позиционироваться относительно размера, установленного для него панелью. Взгляните на следующую разметку:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height="100" Width="650">
ItemWidth ="200"
ItemHeight ="30">
В результате визуализации получается окно, показанное на рис. 25.3 (обратите внимание на размер и позицию элемента управления
Button
, для которого было задано уникальное значение Width
).
После просмотра рис. 25.3 вы наверняка согласитесь с тем, что панель
WrapPanel
— обычно не лучший выбор для организации содержимого непосредственно в окне, поскольку ее элементы могут беспорядочно смешиваться, когда пользователь изменяет размер окна. В большинстве случаев WrapPanel
будет подэлементом панели другого типа, позволяя небольшой области окна переносить свое содержимое при изменении размера (как, например, элемент управления ToolBar
).
Подобно
WrapPanel
элемент управления StackPanel
организует содержимое внутри одиночной строки, которая может быть ориентирована горизонтально или вертикально (по умолчанию) в зависимости от значения, присвоенного свойству Orientation
. Однако отличие между ними заключается в том, что StackPanel
не пытается переносить содержимое при изменении размера окна пользователем. Взамен элементы в StackPanel
просто растягиваются (согласно выбранной ориентации), приспосабливаясь к размеру самой панели StackPanel
. Например, в файле SimpleStackPanel.xaml
содержится разметка, которая в результате дает вывод, показанный на рис. 25.4:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height="200" Width="400">
FontSize="15" Content="Enter Car Information"/>
Если присвоить свойству
Orientation
значение Horizontal
, тогда визуализированный вывод станет таким, как на рис. 25.5:
Подобно
WrapPanel
панель StackPanel
тоже редко применяется для организации содержимого прямо внутри окна. Панель StackPanel
должна использоваться как вложенная панель в какой-нибудь главной панели.
Из всех панелей, предоставляемых API-интерфейсами WPF, панель
Grid
является, несомненно, самой гибкой. Аналогично таблице HTML панель Grid
может состоять из набора ячеек, каждая из которых имеет свое содержимое. При определении Grid
выполняются перечисленные ниже шаги.
1. Определение и конфигурирование каждой колонки.
2. Определение и конфигурирование каждой строки.
3. Назначение содержимого каждой ячейке сетки с применением синтаксиса присоединяемых свойств.
На заметку! Если не определить какие-либо строки и колонки, то по умолчанию элемент
Grid
будет состоять из единственной ячейки, которая заполняет всю поверхность окна. Кроме того, если не установить ячейку (колонку и строку) для подэлемента внутри Grid
, тогда он автоматически разместится в колонке 0 и строке 0.
Первые два шага (определение колонок и строк) выполняются с использованием элементов
Grid.ColumnDefinitions
и Grid.RowDefinitions
, которые содержат коллекции элементов ColumnDefinition
и RowDefinition
соответственно. Каждая ячейка внутри сетки на самом деле является подлинным объектом .NET, так что можно желаемым образом настраивать внешний вид и поведение каждого элемента.
Ниже представлено простое определение
Grid
(из файла SimpleGrid.xaml
), которое организует содержимое пользовательского интерфейса, как показано на рис. 25.6:
Grid.Column="0" Content ="Left!"/>
Системы меню в WPF представлены классом Menu, который поддерживает коллекцию объектов
MenuItem
. При построении системы меню в XAML каждый объект MenuItem
можно заставить обрабатывать разнообразные события, наиболее примечательным из которых является Click
, возникающее при выборе подэлемента конечным пользователем. В рассматриваемом примере создаются два пункта меню верхнего уровня (File (Файл) и Tools (Сервис); позже будет построено меню Edit (Правка)), которые содержат в себе подэлементы Exit (Выход) и Spelling Hints (Подсказки по правописанию) соответственно.
В дополнение к обработке события
Click
для каждого подэлемента необходимо также обработать события MouseEnter
и MouseExit
, которые применяются для установки текста в строке состояния. Добавьте в контекст элемента DockPanel
следующую разметку:
MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/>
MouseLeave ="MouseLeaveArea" Click ="ToolsSpellingHints_Click"
Cursor="Help" />
Ваш элемент управления
ToolBar
образован из двух элементов управления Button
, которые предназначены для обработки тех же самых событий теми же методами из файла кода. С помощью такого приема можно дублировать обработчики для обслуживания и пунктов меню, и кнопок панели инструментов. Хотя в данной панели применяются типичные нажимаемые кнопки, вы должны принимать во внимание, что тип ToolBar
"является" ContentControl
, а потому на его поверхность можно помещать любые типы (скажем, раскрывающиеся списки, изображения и графику). Еще один интересный аспект связан с тем, что кнопка Check (Проверить) поддерживает специальный курсор мыши через свойство Cursor
.
На заметку! Элемент
Toolbar
может быть дополнительно помещен внутрь элемента ToolBarTray
, который управляет компоновкой, стыковкой и перетаскиванием для набора объектов ToolBar
.
Элемент управления строкой состояния (
StatusBar
) стыкуется с нижней частью DockPanel
и содержит единственный элемент управления TextBlock
, который ранее в главе не использовался. Элемент TextBlock
можно применять для хранения текста с форматированием вроде выделения полужирным и подчеркивания, добавления разрывов строк и т.д. Поместите приведенную ниже разметку сразу после предыдущего определения элемента управления ToolBar
:
Финальный аспект проектирования нашего пользовательского интерфейса связан с определением поддерживающего разделители элемента
Grid
, в котором определены две колонки. Слева находится элемент управления Expander
, помещенный внутрь StackPanel
, который будет отображать список предполагаемых вариантов правописания, а справа — элемент TextBox
с поддержкой многострочного текста, линеек прокрутки и включенной проверкой орфографии. Элемент Grid
может быть целиком размещен в левой части родительской панели DockPanel
. Чтобы завершить определение пользовательского интерфейса окна, добавьте следующую разметку XAML, расположив ее непосредственно под разметкой, которая описывает StatusBar
:
Spelling Hints
Margin="10,10,10,10">
SpellCheck.IsEnabled ="True"
AcceptsReturn ="True"
Name ="txtData" FontSize ="14"
BorderBrush ="Blue"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
К настоящему моменту пользовательский интерфейс окна готов. Понадобится лишь предоставить реализации оставшихся обработчиков событий. Начните с обновления файла кода C# так, чтобы каждый из обработчиков событий
MouseEnter
и MouseLeave
устанавливал в текстовой панели строки состояния подходящее сообщение, которое окажет помощь конечному пользователю:
public partial class MainWindow : System.Windows.Window
{
...
protected void MouseEnterExitArea(object sender, RoutedEventArgs args)
{
statBarText.Text = "Exit the Application";
}
protected void MouseEnterToolsHintsArea(object sender, RoutedEventArgs args)
{
statBarText.Text = "Show Spelling Suggestions";
}
protected void MouseLeaveArea(object sender, RoutedEventArgs args)
{
statBarText.Text = "Ready";
}
}
Теперь приложение можно запустить. Текст в строке состояния должен изменяться в зависимости от того, над каким пунктом меню или кнопкой панели инструментов находится курсор.
Инфраструктура WPF имеет встроенную поддержку проверки правописания, независимую от продуктов Microsoft Office. Это значит, что использовать уровень взаимодействия с СОМ для обращения к функции проверки правописания Microsoft Word не понадобится: та же самая функциональность добавляется с помощью всего нескольких строк кода.
Вспомните, что при определении элемента управления
TextBox
свойство Spellcheck.IsEnabled
устанавливается в true
. В результате неправильно написанные слова подчеркиваются красной волнистой линией, как происходит в Microsoft Office. Более того, лежащая в основе программная модель предоставляет доступ к механизму проверки правописания, который позволяет получить список предполагаемых вариантов для слов, написанных с ошибкой.
Добавьте в метод
ToolsSpellingHints_Click()
следующий код:
protected void ToolsSpellingHints_Click(object sender, RoutedEventArgs args)
{
string spellingHints = string.Empty;
// Попробовать получить ошибку правописания
// в текущем положении курсора ввода.
SpellingError error = txtData.GetSpellingError(txtData.CaretIndex);
if (error != null)
{
// Построить строку с предполагаемыми вариантами правописания.
foreach (string s in error.Suggestions)
{
spellingHints += $"{s}\n";
}
// Отобразить предполагаемые варианты и раскрыть элемент Expander.
lblSpellingHints.Content = spellingHints;
expanderSpelling.IsExpanded = true;
}
}
Приведенный выше код довольно прост. С применением свойства
CaretIndex
извлекается объект SpellingError
и вычисляется текущее положение курсора ввода в текстовом поле. Если в указанном месте присутствует ошибка (т.е. значение error не равно null
), тогда осуществляется проход в цикле по списку предполагаемых вариантов с использованием свойства Suggestions
. После того, как все предполагаемые варианты для неправильно написанного слова получены, они помещаются в элемент Label
внутри элемента Expander
.
Вот и все! С помощью нескольких строк процедурного кода (и приличной порции разметки XAML) заложены основы для функционирования текстового процессора. После изучения управляющих команд будут добавлены дополнительные возможности.
Инфраструктура WPF предлагает поддержку того, что может считаться независимыми от элементов управления событиями, через архитектуру команд. Обычное событие .NET Core определяется внутри некоторого базового класса и может использоваться только этим классом или его потомками. Следовательно, нормальные события .NET Core тесно привязаны к классу, в котором они определены.
По контрасту команды WPF представляют собой похожие на события сущности, которые не зависят от специфического элемента управления и во многих случаях могут успешно применяться к многочисленным (и на вид несвязанным) типам элементов управления. Вот лишь несколько примеров: WPF поддерживает команды копирования, вырезания и вставки, которые могут использоваться в разнообразных элементах пользовательского интерфейса (вроде пунктов меню, кнопок панели инструментов и специальных кнопок), а также клавиатурные комбинации (скажем, <Ctrl+C> и <Ctrl+V>).
В то время как другие инструментальные наборы, ориентированные на построение пользовательских интерфейсов (вроде Windows Forms), предлагают для таких целей стандартные события, их применение обычно дает в результате избыточный и трудный в сопровождении код. Внутри модели WPF в качестве альтернативы можно использовать команды. Итогом обычно оказывается более компактная и гибкая кодовая база.
Инфраструктура WPF поставляется с множеством встроенных команд, каждую из которых можно ассоциировать с соответствующей клавиатурной комбинацией (или другим входным жестом). С точки зрения программирования команда WPF — это любой объект, поддерживающий свойство (часто называемое
Command
), которое возвращает объект, реализующий показанный ниже интерфейс ICommand
:
public interface ICommand
{
// Возникает, когда происходят изменения, влияющие
// на то, должна выполняться команда или нет.
event EventHandler CanExecuteChanged;
// Определяет метод, который выясняет, может ли
// команда выполняться в ее текущем состоянии.
bool CanExecute(object parameter);
// Определяет метод для вызова при обращении к команде.
void Execute(object parameter);
}
В WPF предлагаются разнообразные классы команд, которые открывают доступ к примерно сотне готовых объектов команд. В таких классах определены многочисленные свойства, представляющие специфические объекты команд, каждый из которых реализует интерфейс
ICommand
. В табл. 25.3 кратко описаны избранные стандартные объекты команд.
Для подключения любого свойства команд WPF к элементу пользовательского интерфейса, который поддерживает свойство
Command
(такому как Button
или MenuItem
), потребуется проделать совсем небольшую работу. В качестве примера модифицируйте текущую систему меню, добавив новый пункт верхнего уровня по имени Edit (Правка) с тремя подэлементами, которые позволяют копировать, вставлять и вырезать текстовые данные:
BorderBrush ="Black">
MouseLeave ="MouseLeaveArea"
Click ="FileExit_Click"/>
MouseEnter ="MouseEnterToolsHintsArea"
MouseLeave ="MouseLeaveArea"
Click ="ToolsSpellingHints_Click"/>
Обратите внимание, что свойству
Command
каждого подэлемента в меню Edit присвоено некоторое значение. В результате пункты меню автоматически получают корректные имена и горячие клавиши (например, <Ctrl+C> для операции вырезания) в пользовательском интерфейсе меню, и приложение теперь способно копировать, вырезать и вставлять текст без необходимости в написании процедурного кода.
Если вы запустите приложение и выделите какую-то часть текста, то сразу же сможете пользоваться новыми пунктами меню. Вдобавок приложение также оснащено возможностью реагирования на стандартную операцию щелчка правой кнопкой мыши, предлагая пользователю те же самые пункты в контекстном меню.
Если объект команды нужно подключить к произвольному событию (специфичному для приложения), то придется прибегнуть к написанию процедурного кода. Задача несложная, но требует чуть больше логики, чем можно видеть в XAML. Например, пусть необходимо, чтобы все окно реагировало на нажатие клавиши <F1>, активизируя ассоциированную с ним справочную систему. Также предположим, что в файле кода для главного окна определен новый метод по имени
SetFICommandBinding()
, который вызывается внутри конструктора после вызова InitializeComponent()
:
public MainWindow()
{
InitializeComponent();
SetF1CommandBinding();
}
Метод
SetFICommandBinding()
будет программно создавать новый объект CommandBinding
, который можно применять всякий раз, когда требуется привязать объект команды к заданному обработчику событий в приложении. Сконфигурируйте объект CommandBinding
для работы с командой ApplicationCommands.Help
, которая автоматически выдается по нажатию клавиши <F1>:
private void SetF1CommandBinding()
{
CommandBinding helpBinding = new CommandBinding(ApplicationCommands.Help);
helpBinding.CanExecute += CanHelpExecute;
helpBinding.Executed += HelpExecuted;
CommandBindings.Add(helpBinding);
}
Большинство объектов
CommandBinding
будет обрабатывать событие CanExecute
(которое позволяет указать, инициируется ли команда для конкретной операции программы) и событие Executed
(где можно определить код, подлежащий выполнению после того, как команда произошла). Добавьте к типу, производному от Window
, следующие обработчики событий (форматы методов регламентируются ассоциированными делегатами):
private void CanHelpExecute(object sender, CanExecuteRoutedEventArgs e)
{
// Если нужно предотвратить выполнение команды,
// то можно установить CanExecute в false.
e.CanExecute = true;
}
private void HelpExecuted(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show("Look, it is not that difficult. Just type something!",
"Help!");
}
В предыдущем фрагменте кода метод
CanHelpExecute()
реализован так, что справка по нажатию <F1
> всегда разрешена; это делается путем возвращения true
. Однако если в определенных ситуациях справочная система отображаться не должна, то необходимо предпринять соответствующую проверку и возвращать false
. Созданная "справочная система", отображаемая внутри HelpExecute()
, представляет собой всего лишь обычное окно сообщения. Теперь можете запустить приложение. После нажатия <F1> появится ваше окно сообщения.
Чтобы завершить текущий пример, вы добавите функциональность сохранения текстовых данных во внешнем файле и открытия файлов
*.txt
для редактирования. Можно пойти длинным путем, вручную добавив программную логику, которая включает и отключает пункты меню в зависимости от того, имеются ли данные внутри TextBox
. Тем не менее, для сокращения усилий можно прибегнуть к услугам команд.
Начните с обновления элемента
MenuItem
, который представляет меню File верхнего уровня, путем добавления двух новых подменю, использующих объекты Save
и Open
класса ApplicationCommands
:
MouseEnter ="MouseEnterExitArea"
MouseLeave ="MouseLeaveArea" Click ="FileExit_Click"/>
Вспомните, что все объекты команд реализуют интерфейс
ICommand
, в котором определены два события (CanExecute
и Executed
). Теперь необходимо разрешить окну выполнять указанные команды, предварительно проверив возможность делать это в текущих обстоятельствах; раз так, можете определить обработчик события для запуска специального кода.
Понадобится наполнить коллекцию
CommandBindings
, поддерживаемую окном. В разметке XAML потребуется применить синтаксис "свойство-элемент" для определения области Window.CommandBindings
, в которую помещаются два определения CommandBinding
. Модифицируйте определение Window
, как показано ниже:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MySpellChecker" Height="331" Width="508"
WindowStartupLocation ="CenterScreen" >
Executed="OpenCmdExecuted"
CanExecute="OpenCmdCanExecute"/>
Executed="SaveCmdExecuted"
CanExecute="SaveCmdCanExecute"/>
...
Щелкните правой кнопкой мыши на каждом из атрибутов
Executed
и CanExecute
в редакторе XAML и выберите в контекстном меню пункт Navigate to Event Handler (Перейти к обработчику события). Как объяснялось в главе 24, в результате автоматически сгенерируется заготовка кода для обработчика события. Теперь в файле кода C# для окна должны присутствовать четыре пустых обработчика событий.
Реализация обработчиков события
CanExecute
будет сообщать окну, что можно инициировать соответствующие события Executed
в любой момент, для чего свойство CanExecute
входного объекта CanExecuteRoutedEventArgs
устанавливается в true
:
private void OpenCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void SaveCmdCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
Обработчики соответствующего события
Executed
выполняют действительную работу по отображению диалоговых окон открытия и сохранения файла; они также отправляют данные из TextBox
в файл. Начните с импортирования пространств имен System.IO
и Microsoft.Win32
в файл кода. Окончательный код прямолинеен:
private void OpenCmdExecuted(object sender, ExecutedRoutedEventArgs e)
{
// Создать диалоговое окно открытия файла и показать
// в нем только текстовые файлы.
var openDlg = new OpenFileDialog { Filter = "Text Files |*.txt"};
// Был ли совершен щелчок на кнопке ОК?
if (true == openDlg.ShowDialog())
{
// Загрузить содержимое выбранного файла.
string dataFromFile = File.ReadAllText(openDlg.FileName);
// Отобразить строку в TextBox.
txtData.Text = dataFromFile;
}
}
private void SaveCmdExecuted(object sender, ExecutedRoutedEventArgs e)
{
var saveDlg = new SaveFileDialog { Filter = "Text Files |*.txt"};
// Был ли совершен щелчок на кнопке ОК?
if (true == saveDlg.ShowDialog())
{
// Сохранить данные из TextBox в указанном файле.
File.WriteAllText(saveDlg.FileName, txtData.Text);
}
}
На заметку! Система команд WPF более подробно рассматривается в главе 28, где будут создаваться специальные команды на основе
ICommand
и RelayCommands
.
Итак, пример и начальное знакомство с элементами управления WPF завершены. Вы узнали, как работать с базовыми командами, системами меню, строками состояния, панелями инструментов, вложенными панелями и несколькими основными элементами пользовательского интерфейса (вроде
TextBox
и Expander
). В следующем примере вы будете иметь дело с более экзотическими элементами управления, а также с рядом важных служб WPF.
Вы могли заметить, что в предыдущем примере кода передавался параметр
RoutedEventArgs
, а не EventArgs
. Модель маршрутизируемых событий является усовершенствованием стандартной модели событий CLR и спроектирована для того, чтобы обеспечить возможность обработки событий в манере, подходящей описанию XAML дерева объектов. Предположим, что имеется новый проект приложения WPF по имени WpfRoutedEvents
. Модифицируйте описание XAML начального окна, добавив следующий элемент управления Button
, который определяет сложное содержимое:
Click ="btnClickMe_Clicked">
Fancy Button!
Height ="25" Width ="50" Cursor="Hand"
Canvas.Left="25" Canvas.Top="12"/>
Height = "15" Width ="36"
Canvas.Top="17" Canvas.Left="32"/>
Обратите внимание, что в открывающем определении элемента
Button
было обработано событие Click
за счет указания имени метода, который должен вызываться при возникновении события. Событие Click
работает с делегатом RoutedEventHandler
, который ожидает обработчик события, принимающий object
в первом параметре и System.Winodws.RoutedEventArgs
во втором. Реализуйте такой обработчик:
public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
{
// Делать что-нибудь, когда на кнопке произведен щелчок.
MessageBox.Show("Clicked the button");
}
После запуска приложения окно сообщения будет отображаться независимо от того, на какой части содержимого кнопки был выполнен щелчок (зеленый элемент
Ellipse
, желтый элемент Ellipse
, элемент Label
или поверхность элемента Button
). В принципе это хорошо. Только представьте, насколько громоздким оказалась бы обработка событий WPF, если бы пришлось обрабатывать событие Click
для каждого из упомянутых подэлементов. Дело не только в том, что создание отдельных обработчиков событий для каждого аспекта Button
— трудоемкая задача, а еще и в том, что в результате получился бы сложный в сопровождении код.
К счастью, маршрутизируемые события WPF позаботятся об автоматическом вызове единственного обработчика события
Click
вне зависимости от того, на какой части кнопки был совершен щелчок. Выражаясь просто, модель маршрутизируемых событий автоматически распространяет событие вверх (или вниз) по дереву объектов в поисках подходящего обработчика.
Точнее говоря, маршрутизируемое событие может использовать три стратегии маршрутизации. Если событие перемещается от точки возникновения вверх к другим областям определений внутри дерева объектов, то его называют пузырьковым событием. И наоборот, если событие перемещается от самого внешнего элемента (например,
Window
) вниз к точке возникновения, то его называют туннельным событием. Наконец, если событие инициируется и обрабатывается только элементом, внутри которого оно возникло (что можно было бы описать как нормальное событие CLR), то его называют прямым событием.
В текущем примере, когда пользователь щелкает на внутреннем овале желтого цвета, событие
Click
поднимается на следующий уровень области определения (Canvas
), затем на StackPanel
и в итоге на уровень Button
, где обрабатывается. Подобным же образом, если пользователь щелкает на Label
, то событие всплывает на уровень StackPanel
и, в конце концов, попадает в элемент Button
.
Благодаря такому шаблону пузырьковых маршрутизируемых событий не придется беспокоиться о регистрации специфичных обработчиков события
Click
для всех членов составного элемента управления. Однако если необходимо выполнить специальную логику обработки щелчков для нескольких элементов внутри того же самого дерева объектов, то это вполне можно делать.
В целях иллюстрации предположим, что щелчок на элементе управления
outerEllipse
должен быть обработан в уникальной манере. Сначала обработайте событие MouseDown
для этого подэлемента (графически визуализируемые типы вроде Ellipse
не поддерживают событие Click
, но могут отслеживать действия кнопки мыши через события MouseDown
, MouseUp
и т.д.):
Click ="btnClickMe_Clicked">
Height ="25" MouseDown ="outerEllipse_MouseDown"
Width ="50" Cursor="Hand" Canvas.Left="25" Canvas.Top="12"/>
Canvas.Top="17" Canvas.Left="32"/>
Затем реализуйте подходящий обработчик событий, который в демонстрационных целях будет просто изменять свойство
Title
главного окна:
public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
// Изменить заголовок окна.
this.Title = "You clicked the outer ellipse!";
}
Далее можно выполнять разные действия в зависимости от того, на чем конкретно щелкнул конечный пользователь (на внешнем эллипсе или в любом другом месте внутри области кнопки).
На заметку! Пузырьковые маршрутизируемые события всегда перемещаются из точки возникновения до следующей определяющей области. Таким образом, в рассмотренном примере щелчок на элементе
innerEllipse
привел бы к попаданию события в контейнер Canvas
, а не в элемент outerEllipse
, потому что оба элемента являются типами Ellipse
внутри области определения Canvas
.
В текущий момент, когда пользователь щелкает на объекте
outerEllipse
, запускается зарегистрированный обработчик события MouseDown
для данного объекта Ellipse
, после чего событие всплывет до события Click
кнопки. Чтобы информировать WPF о необходимости останова пузырькового распространения по дереву объектов, свойство Handled
параметра MouseButtonEventArgs
понадобится установить в true
:
public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
// Изменить заголовок окна.
this.Title = "You clicked the outer ellipse!";
// Остановить пузырьковое распространение.
e.Handled = true;
}
В таком случае обнаружится, что заголовок окна изменился, но окно
MessageBox
, отображаемое обработчиком события Click
элемента Button
, не появляется. По существу пузырьковые маршрутизируемые события позволяют сложной группе содержимого действовать либо как единый логический элемент (например, Button
), либо как отдельные элементы (скажем, Ellipse
внутри Button
).
Строго говоря, маршрутизируемые события по своей природе могут быть пузырьковыми (как было описано только что) или туннельными. Туннельные события (имена которых начинаются с префикса
Preview
— наподобие PreviewMouseDown
) спускаются от самого верхнего элемента до внутренних областей определения дерева объектов. В общем и целом для каждого пузырькового события в библиотеках базовых классов WPF предусмотрено связанное туннельное событие, которое возникает перед его пузырьковым аналогом. Например, перед возникновением пузырькового события MouseDown
сначала инициируется туннельное событие PreviewMouseDown
.
Обработка туннельных событий выглядит очень похожей на обработку любых других событий: нужно просто указать имя обработчика события в разметке XAML (или при необходимости применить соответствующий синтаксис обработки событий C# в файле кода) и реализовать такой обработчик в коде. Для демонстрации взаимодействия туннельных и пузырьковых событий начните с организации обработки события
PreviewMouseDown
для объекта outerEllipse
:
MouseDown ="outerEllipse_MouseDown"
PreviewMouseDown ="outerEllipse_PreviewMouseDown"
Width ="50" Cursor="Hand" Canvas.Left="25" Canvas.Top="12"/>
Затем модифицируйте текущее определение класса С#, обновив обработчики событий (для всех объектов) за счет добавления данных о событии в переменную-член
_mouseActivity
типа string
с использованием входного объекта аргументов события. В результате появится возможность наблюдать за потоком событий, появляющихся в фоновом режиме.
public partial class MainWindow : Window
{
string _mouseActivity = string.Empty;
public MainWindow()
{
InitializeComponent();
}
public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
{
AddEventInfo(sender, e);
MessageBox.Show(_mouseActivity, "Your Event Info");
// Очистить строку для следующего цикла.
_mouseActivity = "";
}
private void AddEventInfo(object sender, RoutedEventArgs e)
{
_mouseActivity += string.Format(
"{0} sent a {1} event named {2}.\n", sender,
e.RoutedEvent.RoutingStrategy,
e.RoutedEvent.Name);
}
private void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
AddEventInfo(sender, e);
}
private void outerEllipse_PreviewMouseDown(object sender,
MouseButtonEventArgs e)
{
AddEventInfo(sender, e);
}
}
Обратите внимание, что ни в одном обработчике событий пузырьковое распространение не останавливается. После запуска приложения отобразится окно с уникальным сообщением, которое зависит от места на кнопке, где был произведен щелчок. На рис. 25.15 показан результат щелчка на внешнем объекте
Ellipse
.
Итак, почему события WPF обычно встречаются парами (одно туннельное и одно пузырьковое)? Ответ можно сформулировать так: благодаря предварительному просмотру событий появляется возможность выполнения любой специальной логики (проверки достоверности данных, отключения пузырькового распространения и т.п.) перед запуском пузырькового аналога событий. В качестве примера предположим, что создается элемент
TextBox
, который должен содержать только числовые данные. В нем можно было бы обработать событие PreviewKeyDown
; если выясняется, что пользователь ввел нечисловые данные, то пузырьковое событие легко отменить, установив свойство Handled
в true
.
Как несложно было предположить, при построении специального элемента управления, который поддерживает специальные события, событие допускается реализовать так, чтобы оно могло распространяться пузырьковым (или туннельным) образом по дереву разметки XAML. В настоящей главе мы не рассматриваем процесс создания специальных маршрутизируемых событий (хотя он не особо отличается от построения специального свойства зависимости). Если интересно, загляните в раздел "Routed Events Overview" ("Обзор маршрутизируемых событий") документации по .NET Core, где предлагается несколько обучающих руководств, которые помогут в освоении этой темы.
В оставшемся материале главы будет построено новое приложение WPF с применением Visual Studio. Целью является создание пользовательского интерфейса, который состоит из виджета
TabControl
, содержащего набор вкладок. Каждая вкладка будет иллюстрировать несколько новых элементов управления WPF и интересные API-интерфейсы, которые могут быть задействованы в разрабатываемых проектах. Попутно вы также узнаете о дополнительных возможностях визуальных конструкторов WPF из Visual Studio.
Первым делом создайте новый проект приложения WPF по имени
WpfControlsAndAPIs
. Как упоминалось ранее, начальное окно будет содержать элемент управления TabControl
с четырьмя вкладками, каждая из которых отображает набор связанных элементов управления и/или API-интерфейсов WPF. Установите свойство Width
окна в 800
, а свойство Height
окна в 350
.
Перетащите элемент управления
TabControl
из панели инструментов Visual Studio на поверхность визуального конструктора и модифицируйте его разметку следующим образом:
VerticalAlignment="Stretch">
Вы заметите, что два элемента типа вкладок предоставляются автоматически. Чтобы добавить дополнительные вкладки, нужно щелкнуть правой кнопкой мыши на узле
TabControl
в окне Document Outline и выбрать в контекстном меню пункт Add TabItem (Добавить TabItem). Можно также щелкнуть правой кнопкой мыши на элементе TabControl
в визуальном конструкторе и выбрать тот же самый пункт меню или просто ввести разметку в редакторе XAML. Добавьте одну дополнительную вкладку, используя любой из подходов.
Обновите разметку каждого элемента управления
TabItem
в редакторе XAML и измените их свойство Header
, указывая Ink API
, Data Binding
и DataGrid
. Окно визуального конструктора должно выглядеть примерно так, как на рис. 25.16.
Имейте в виду, что выбранная для редактирования вкладка становится активной, и можно формировать ее содержимое, перетаскивая элементы управления из панели инструментов. Располагая определением основного элемента управления
TabControl
, можно проработать детали каждой вкладки, одновременно изучая дополнительные средства API-интерфейса WPF.
Первая вкладка предназначена для раскрытия общей роли интерфейса Ink API, который позволяет легко встраивать в программу функциональность рисования. Конечно, его применение не ограничивается приложениями для рисования; Ink API можно использовать для разнообразных целей, включая фиксацию рукописного ввода.
На заметку! В оставшейся части главы (и в последующих главах, посвященных WPF) вместо применения разнообразных окон визуального конструктора будет главным образом напрямую редактироваться разметка XAML. Хотя процедура перетаскивания элементов управления работает нормально, чаще всего компоновка оказывается нежелательной (Visual Studio добавляет границы и заполнение на основе того, где размещен элемент), а потому приходится тратить значительное время на очистку разметки XAML.
Начните с замены дескриптора
Grid
в элементе управления TabItem
, помеченном как Ink API, дескриптором StackPanel
и добавления закрывающего дескриптора. Разметка должна иметь такой вид:
Добавьте (используя редактор XAML) в
StackPanel
новый элемент управления ToolBar
по имени InkToolbar
со свойством Height
, установленным в 60
:
Добавьте в
Toolbar
три элемента управления RadioButton
внутри панели WrapPanel
и элемента управления Border
:
Content="Ink Mode!" IsChecked="True" />
Когда элемент управления
RadioButton
помещается не внутрь родительской панели, он получает пользовательский интерфейс, идентичный пользовательскому интерфейсу элемента управления Button
! Именно потому элементы управления RadioButton
были упакованы в панель WrapPanel
.
Далее добавьте элемент
Separator
и элемент ComboBox
, свойство Width
которого установлено в 175
, а свойство Margin
— в 10,0,0,0
. Добавьте три дескриптора ComboBoxItem
с содержимым Red
, Green
и Blue
и сопроводите весь элемент управления ComboBox
еще одним элементом Separator
:
В данном примере необходимо, чтобы три добавленных элемента управления
RadioButton
были взаимно исключающими. В других инфраструктурах для построения графических пользовательских интерфейсов такие связанные элементы требуют помещения в одну групповую рамку. Поступать подобным образом в WPF нет нужды. Взамен элементам управления просто назначается то же самое групповое имя, что очень удобно, поскольку связанные элементы не обязаны физически находиться внутри одной области, а могут располагаться где угодно в окне.
Класс
RadioButton
имеет свойство IsChecked
, значения которого переключаются между true
и false
, когда конечный пользователь щелкает на элементе пользовательского интерфейса. К тому же элемент управления RadioButton
предоставляет два события (Checked
и Unchecked
), которые можно применять для перехвата такого изменения состояния.
Финальным элементом управления внутри
ToolBar
будет Grid
, содержащий три элемента управления Button
. Поместите после последнего элемента управления Separator
следующую разметку:
Width="70" Content="Save Data"/>
Width="70" Content="Load Data"/>
Width="70" Content="Clear"/>
Финальным элементом управления для
TabControl
является InkCanvas
. Поместите показанную ниже разметку после закрывающего дескриптора ToolBar
, но перед закрывающим дескриптором StackPanel
:
Теперь все готово к тестированию программы, для чего понадобится нажать клавишу <F5>. Должны отобразиться три взаимно исключающих переключателя, раскрывающийся список с тремя элементами и три кнопки (рис. 25.17).
Следующая задача для вкладки
Ink API
связана с организацией обработки события Click
для каждого элемента управления RadioButton
. Как вы поступали в других проектах WPF, просто щелкните на значке с изображением молнии в окне Properties среды Visual Studio и введите имена обработчиков событий. С помощью упомянутого приема свяжите событие Click
каждого элемента управления RadioButton
с тем же самым обработчиком по имени RadioButtonClicked
. После обработки всех трех событий Click
обработайте событие SelectionChanged
элемента управления ComboBox
, используя обработчик по имени ColorChanged
. В результате должен получиться следующий код С#:
public partial class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
// Вставить сюда код, требуемый при создании объекта.
}
private void RadioButtonClicked(object sender,RoutedEventArgs e)
{
// TODO: добавить сюда реализацию обработчика событий.
}
private void ColorChanged(object sender,SelectionChangedEventArgs e)
{
// TODO: добавить сюда реализацию обработчика событий.
}
}
Обработчики событий будут реализованы позже, так что оставьте их пока пустыми.
Вы добавите элемент управления
InkCanvas
путем прямого редактирования разметки XAML. Имейте в виду, что панель инструментов Visual Studio по умолчанию не отображает все возможные компоненты WPF, но содержимое панели инструментов можно обновлять.
Щелкните правой кнопкой мыши где-нибудь в области панели инструментов и выберите в контекстном меню пункт Choose Items (Выбрать элементы). Вскоре появится список возможных компонентов для добавления в панель инструментов. Вас интересует элемент управления
InkCanvas
(рис. 25.18).
На заметку! Элементы управления Ink API не совместимы с визуальным конструктором XAML в версии Visual Studio 16.8.3 (текущая версия на момент написания главы) или Visual Studio 16.9 Preview 2. Использовать элементы управления можно, но только не через визуальный конструктор.
Простое добавление
InkCanvas
делает возможным рисование в окне. Рисовать можно с помощью мыши либо, если есть устройство, воспринимающее касания, то пальца или цифрового пера. Запустите приложение и нарисуйте что-нибудь (рис. 25.19).
Элемент управления
InkCanvas
обеспечивает нечто большее, чем просто рисование штрихов с помощью мыши (или пера); он также поддерживает несколько уникальных режимов редактирования, управляемых свойством EditingMode
, которому можно присвоить любое значение из связанного перечисления InkCanvasEditingMode
. В данном примере вас интересует режим Ink
, принятый по умолчанию, который только что демонстрировался, режим Select
, позволяющий пользователю выбирать с помощью мыши область для перемещения или изменения размера, и режим EraseByStroke
, который удаляет предыдущий штрих мыши.
На заметку! Штрих — это визуализация, которая происходит во время одиночной операции нажатия и отпускания кнопки мыши. Элемент управления
InkCanvas
сохраняет все штрихи в объекте StrokeCollection
, который доступен с применением свойства Strokes
.
Обновите обработчик
RadioButtonClicked()
следующей логикой, которая помещает InkCanvas
в нужный режим в зависимости от выбранного переключателя RadioButton
:
private void RadioButtonClicked(object sender,RoutedEventArgs e)
{
// В зависимости от того, какая кнопка отправила событие,
// поместить InkCanvas в нужный режим оперирования.
this.MyInkCanvas.EditingMode =
(sender as RadioButton)?.Content.ToString() switch
{
// Эти строки должны совпадать со значениями свойства Content
// каждого элемента RadioButton.
"Ink Mode!" => InkCanvasEditingMode.Ink,
"Erase Mode!" => InkCanvasEditingMode.EraseByStroke,
"Select Mode!" => InkCanvasEditingMode.Select,
_ => this.MyInkCanvas.EditingMode
};
}
Вдобавок установите
Ink
как стандартный режим в конструкторе окна. Там же установите стандартный выбор для ComboBox
(элемент управления ComboBox
более подробно рассматривается в следующем разделе):
public MainWindow()
{
this.InitializeComponent();
// Установить режим Ink в качестве стандартного.
this.MyInkCanvas.EditingMode = InkCanvasEditingMode.Ink;
this.inkRadio.IsChecked = true;
this.comboColors.SelectedIndex = 0;
}
Теперь запустите программу еще раз, нажав <F5>. Войдите в режим
Ink
и нарисуйте что-нибудь. Затем перейдите в режим Erase
и сотрите ранее нарисованное (курсор мыши автоматически примет вид стирающей резинки). Наконец, переключитесь в режим Select
и выберите несколько линий, используя мышь в качестве лассо.
Охватив элемент, его можно перемещать по поверхности холста, а также изменять размеры. На рис. 25.20 демонстрируются разные режимы в действии.
После заполнения элемента управления
ComboBox
(или ListBox
) есть три способа определения выбранного в них элемента. Во-первых, когда необходимо найти числовой индекс выбранного элемента, должно применяться свойство SelectedIndex
(отсчет начинается с нуля; значение -1
представляет отсутствие выбора). Во-вторых, если требуется получить объект, выбранный внутри списка, то подойдет свойство SelectedItem
. В-третьих, свойство SelectedValue
позволяет получить значение выбранного объекта (обычно с помощью вызова ToString()
).
Последний фрагмент кода, который понадобится добавить для данной вкладки, отвечает за изменение цвета штрихов, нарисованных в
InkCanvas
. Свойство DefaultDrawingAttributes
элемента InkCanvas
возвращает объект DrawingAttributes
, который позволяет конфигурировать многочисленные аспекты пера, включая его размер и цвет (помимо других настроек). Модифицируйте код C# следующей реализацией метода ColorChanged()
:
private void ColorChanged(object sender, SelectionChangedEventArgs e)
{
// Получить выбранный элемент в раскрывающемся списке.
string colorToUse =
(this.comboColors.SelectedItem as ComboBoxItem)?.Content.ToString();
// Изменить цвет, используемый для визуализации штрихов.
this.MyInkCanvas.DefaultDrawingAttributes.Color =
(Color)ColorConverter.ConvertFromString(colorToUse);
}
Вспомните, что
ComboBox
содержит коллекцию ComboBoxIterns
. В сгенерированной разметке XAML присутствует такое определение:
В результате обращения к свойству
SelectedItem
получается выбранный элемент ComboBoxItem
, который хранится как экземпляр общего типа Object
. После приведения Object
к ComboBoxItem
извлекается значение Content
, которое будет строкой Red
, Green
или Blue
. Эта строка затем преобразуется в объект Color
с применением удобного служебного класса ColorConverter
. Снова запустите программу. Теперь должна появиться возможность переключения между цветами при визуализации изображения.
Обратите внимание, что элементы управления
ComboBox
и ListBox
также могут иметь сложное содержимое, а не только список текстовых данных. Чтобы получить представление о некоторых возможностях, откройте редактор XAML для окна и измените определение элемента управления ComboBox
, поместив в него набор элементов StackPanel
, каждый из которых содержит Ellipse
и Label
(свойство Width
элемента ComboBox
установлено в 175
):
SelectionChanged="ColorChanged">
VerticalAlignment="Center" Content="Red"/>
VerticalAlignment="Center" Content="Green"/>
VerticalAlignment="Center" Content="Blue"/>
В определении каждого элемента
StackPanel
выполняется присваивание значения свойству Tag
, что является быстрым и удобным способом выявления, какой стек элементов был выбран пользователем (для этого существуют и лучшие способы, но пока достаточно такого). С указанной поправкой необходимо изменить реализацию метода ColorChanged()
:
private void ColorChanged(object sender, SelectionChangedEventArgs e)
{
// Получить свойство Tag выбранного элемента StackPanel.
string colorToUse = (this.comboColors.SelectedItem
as StackPanel).Tag.ToString();
...
}
После запуска программы элемент управления
ComboBox
будет выглядеть так, как показано на рис. 25.21.
Последняя часть вкладки
Ink API
позволит сохранять и загружать данные контейнера InkCanvas
, а также очищать его содержимое, добавляя обработчики событий для кнопок в панели инструментов. Модифицируйте разметку XAML для кнопок за счет добавления разметки, отвечающей за события щелчков:
Width="70" Content="Save Data"
Click="SaveData"/>
Width="70" Content="Load Data"
Click="LoadData"/>
Width="70" Content="Clear"
Click="Clear"/>
Импортируйте пространства имен
System.IO
и System.Windows.Ink
в файл кода. Реализуйте обработчики событий следующим образом:
private void SaveData(object sender, RoutedEventArgs e)
{
// Сохранить все данные InkCanvas в локальном файле.
using (FileStream fs = new FileStream("StrokeData.bin", FileMode.Create))
this.MyInkCanvas.Strokes.Save(fs);
fs.Close();
MessageBox.Show("Image Saved","Saved");
}
private void LoadData(object sender, RoutedEventArgs e)
{
// Наполнить StrokeCollection из файла.
using(FileStream fs = new FileStream("StrokeData.bin",
FileMode.Open, FileAccess.Read))
StrokeCollection strokes = new StrokeCollection(fs);
this.MyInkCanvas.Strokes = strokes;
}
private void Clear(object sender, RoutedEventArgs e)
{
// Очистить все штрихи.
this.MyInkCanvas.Strokes.Clear();
}
Теперь должна появиться возможность сохранения данных в файле, их загрузки из файла и очистки
InkCanvas
от всех данных. Таким образом, работа с первой вкладкой элемента управления TabControl
завершена, равно как и исследование интерфейса Ink API. Конечно, о технологии Ink API можно рассказать еще много чего, но теперь вы должны обладать достаточными знаниями, чтобы продолжить изучение темы самостоятельно. Далее вы узнаете, как применять привязку данных WPF.
Элементы управления часто служат целью для разнообразных операций привязки данных. Выражаясь просто, привязка данных представляет собой действие по подключению свойств элемента управления к значениям данных, которые могут изменяться на протяжении жизненного цикла приложения. Это позволяет элементу пользовательского интерфейса отображать состояние переменной в коде. Например, привязку данных можно использовать для решения следующих задач:
• отмечать флажок элемента управления
Checkbox
на основе булевского свойства заданного объекта:
• отображать в элементах
TextBox
информацию, извлеченную из реляционной базы данных:
• подключать элемент
Label
к целому числу, представляющему количество файлов в папке.
При работе со встроенным механизмом привязки данных WPF важно помнить о разнице между источником и местом назначения операции привязки. Как и можно было ожидать, источником операции привязки данных являются сами данные (булевское свойство, реляционные данные и т.д.), а местом назначения (или целью) — свойство элемента управления пользовательского интерфейса, в котором задействуется содержимое данных (вроде свойства элемента управления
CheckBox
или TextBox
).
В дополнение к привязке традиционных данных инфраструктура WPF делает возможной привязку элементов, как было продемонстрировано в предшествующих примерах. Это значит, что можно привязать (скажем) видимость свойства к свойству состояния отметки флажка. Такое действие было определенно возможным в Windows Forms, но требовало реализации через код. Инфраструктура WPF предлагает развитую экосистему привязки данных, которая способна почти целиком поддерживаться в разметке. Она также позволяет обеспечивать синхронизацию источника и цели в случае изменения значений данных.
В окне Document Outline замените элемент управления
Grid
во второй вкладке панелью StackPanel
. Создайте следующую начальную компоновку с применением панели инструментов и окна Properties среды Visual Studio:
Minimum = "1" Maximum = "100" LargeChange="1" SmallChange="1"/>
BorderThickness="2" Content = "0"/>
Обратите внимание, что объект
ScrollBar
(названный здесь mySB
) сконфигурирован с диапазоном от 1
до 100
. Цель заключается в том, чтобы при изменении положения ползунка линейки прокрутки (либо по щелчку на символе стрелки влево или вправо) элемент Label
автоматически обновлялся текущим значением. В настоящий момент значение свойства Content
элемента управления Label
установлено в "0"
; тем не менее, оно будет изменено посредством операции привязки данных.
Механизмом, обеспечивающим определение привязки в разметке XAML, является расширение разметки
{Binding}
. Хотя привязки можно определять посредством Visual Studio, это столь же легко делать прямо в разметке. Отредактируйте разметку XAML свойства Content
элемента Label
по имени labelSBThumb
следующим образом:
Content = "{Binding Path=Value, ElementName=mySB}"/>
Обратите внимание на значение, присвоенное свойству
Content
элемента Label
. Конструкция {Binding}
обозначает операцию привязки данных. Значение ElementName
представляет источник операции привязки данных (объект ScrollBar
), a Path
указывает свойство, к которому осуществляется привязка (свойство Value
линейки прокрутки).
Если вы запустите программу снова, то обнаружите, что содержимое метки обновляется на основе значения линейки прокрутки по мере перемещения ползунка.
Для определения операции привязки данных в XAML может использоваться альтернативный формат, при котором допускается разбивать значения, указанные расширением разметки
{Binding}
, за счет явной установки свойства DataContext
в источник операции привязки:
BorderThickness="2"
DataContext = "{Binding ElementName=mySB}"
Content = "{Binding Path=Value}" />
В текущем примере вывод будет идентичным. С учетом этого вполне вероятно вас интересует, в каких случаях необходимо устанавливать свойство
DataContext
явно. Поступать так может быть удобно из-за того, что подэлементы способны наследовать свои значения в дереве разметки.
Подобным образом можно легко устанавливать один и тот же источник данных для семейства элементов управления, не повторяя избыточные фрагменты XAML-разметки
"{Binding ElementName=X, Path=Y}"
во множестве элементов управления. Например, пусть в панель StackPanel
вкладки добавлен новый элемент Button
(вскоре вы увидите, почему он имеет настолько большой размер):
Чтобы сгенерировать привязки данных для множества элементов управления, вы могли бы применить Visual Studio, но взамен введите модифицированную разметку в редакторе XAML:
DataContext = "{Binding ElementName=mySB}">
...
BorderThickness="2"
Content = "{Binding Path=Value}"/>
Здесь свойство
DataContext
панели StackPanel
устанавливается напрямую. Таким образом, при перемещении ползунка не только отображается текущее значение в элементе Label
, но и в соответствии с этим текущим значением увеличивается размер шрифта элемента Button
(на рис. 25.22 показан возможный вывод).
Вместо ожидаемого целого числа для представления положения ползунка тип
ScrollBar
использует значение double
. Следовательно, по мере перемещения ползунка внутри элемента Label
будут отображаться разнообразные значения с плавающей точкой (вроде 61.0576923076923
), которые выглядят не слишком интуитивно понятными для конечного пользователя, почти наверняка ожидающего увидеть целые числа (такие как 61
, 62
, 63
и т.д.).
При желании форматировать данные можно добавить свойство
ContentStringFormat
с передачей ему специальной строки и спецификатора формата .NET Core:
BorderThickness="2" Content = "{Binding Path=Value}"
ContentStringFormat="The value is:
{0:F0}"/>
Если в спецификаторе формата отсутствует какой-либо текст, тогда его понадобится предварить пустым набором фигурных скобок, который является управляющей последовательностью для XAML. Такой прием уведомляет процессор о том, что следующие за
{}
символы представляют собой литералы, а не, скажем, конструкцию привязки. Вот обновленная разметка XAML:
BorderThickness="2" Content = "{Binding Path=Value}"
ContentStringFormat="{}{0:F0}"/>
На заметку! При привязке свойства
Text
элемента управления пару "имя-значение" объекта StringFormat
можно добавлять прямо в конструкции привязки. Она должна быть отдельной только для свойств Content
.
Если требуется нечто большее, чем просто форматирование данных, тогда можно создать специальный класс, реализующий интерфейс
IValueCVonverter
из пространства имен System.Windows.Data
. В интерфейсе IValueCVonverter
определены два члена, позволяющие выполнять преобразование между источником и целью (в случае двунаправленной привязки). После определения такой класс можно применять для дальнейшего уточнения процесса привязки данных.
Вместо использования свойства форматирования можно применять преобразователь значений для отображения целых чисел внутри элемента управления
Label
. Добавьте в проект новый класс (по имени MyDoubleConverter
) со следующим кодом:
using System;
using System.Windows.Data;
namespace WpfControlsAndAPIs
{
public class MyDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// Преобразовать значение double в int.
double v = (double)value;
return (int)v;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// Поскольку заботиться здесь о "двунаправленной" привязке
// не нужно, просто возвратить значение value.
return value;
}
}
}
Метод
Convert()
вызывается при передаче значения от источника (ScrollBar
) к цели (свойство Content
элемента Label
). Хотя он принимает много входных аргументов, для такого преобразования понадобится манипулировать только входным аргументом типа object
, который представляет текущее значение double
. Данный тип можно использовать для приведения к целому и возврата нового числа.
Метод
ConvertBack()
будет вызываться, когда значение передается от цели к источнику (если включен двунаправленный режим привязки). Здесь мы просто возвращаем значение value
. Это позволяет вводить в TextBox
значение с плавающей точкой (например, 99.9
) и автоматически преобразовывать его в целочисленное значение (99
), когда пользователь перемещает фокус из элемента управления. Такое "бесплатное" преобразование происходит из-за того, что метод Convert()
будет вызываться еще раз после вызова ConvertBack()
. Если просто возвратить null
из ConvertBack()
, то синхронизация привязки будет выглядеть нарушенной, т.к. элемент TextBox
по-прежнему будет отображать число с плавающей точкой.
Чтобы применить построенный преобразователь в разметке, сначала нужно создать локальный ресурс, представляющий только что законченный класс. Не переживайте по поводу механики добавления ресурсов; тема будет детально раскрыта в нескольких последующих главах. Поместите показанную ниже разметку сразу после открывающего дескриптора
Window
:
Далее обновите конструкцию привязки для элемента управления
Label
:
BorderThickness="2"
Content = "{Binding Path=Value,
Converter={StaticResource
DoubleConverter}}" />
Теперь после запуска приложения вы будете видеть только целые числа.
Специальный преобразователь данных можно также регистрировать в коде. Начните с очистки текущего определения элемента управления
Label
внутри вкладки Data Binding
, чтобы расширение разметки {Binding}
больше не использовалось:
BorderThickness="2" />
Добавьте оператор
using
для System.Windows.Data
и в конструкторе окна вызовите новый закрытый вспомогательный метод по имени SetBindings()
, код которого показан ниже:
using System.Windows.Data;
...
namespace WpfControlsAndAPIs
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
...
SetBindings();
}
...
private void SetBindings()
{
// Создать объект Binding.
Binding b = new Binding
{
// Зарегистрировать преобразователь, источник и путь.
Converter = new MyDoubleConverter(),
Source = this.mySB,
Path = new PropertyPath("Value")
// Вызвать метод SetBindingO объекта Label.
this.labelSBThumb.SetBinding(Label.ContentProperty, b);
}
}
}
}
Единственная часть метода
SetBindings()
, которая может выглядеть несколько необычной — вызов SetBinding()
. Обратите внимание, что первый параметр обращается к статическому, доступному только для чтения полю ContentProperty
класса Label
. Как вы узнаете далее в главе, такая конструкция называется свойством зависимости. Пока просто имейте в виду, что при установке привязки в коде первый аргумент почти всегда требует указания имени класса, нуждающегося в привязке (Label
в рассматриваемом случае), за которым следует обращение к внутреннему свойству с добавлением к его имени суффикса Property
. Запустив приложение, можно удостовериться в том, что элемент Label
отображает только целые числа.
В предыдущем примере привязки данных иллюстрировался способ конфигурирования двух (или большего количества) элементов управления для участия в операции привязки данных. Наряду с тем, что это удобно, возможно также привязывать данные из файлов XML, базы данных и объектов в памяти. Чтобы завершить текущий пример, вы должны спроектировать финальную вкладку элемента управления
DataGrid
, которая будет отображать информацию, извлеченную из таблицы Inventory
базы данных AutoLot
.
Как и с другими вкладками, начните с замены текущего элемента
Grid
панелью StackPanel
, напрямую обновив разметку XAML в Visual Studio. Внутри нового элемента StackPanel
определите элемент управления DataGrid
по имени gridInventory
:
С помощью диспетчера пакетов NuGet добавьте в проект следующие пакеты:
•
Microsoft.EntityFrameworkCore
•
Microsoft.EntityFrameworkCore.SqlServer
•
Microsoft.Extensions.Configuration
•
Microsoft.Extensions.Configuration.Json
Если вы предпочитаете добавлять пакеты в интерфейсе командной строки .NET Core, тогда введите приведенные далее команды (в каталоге решения):
dotnet add WpfControlsAndAPIs package Microsoft.EntityFrameworkCore
dotnet add WpfControlsAndAPIs package Microsoft.EntityFrameworkCore.SqlServer
dotnet add WpfControlsAndAPIs package Microsoft.Extensions.Configuration
dotnet add WpfControlsAndAPIs package Microsoft.Extensions.Configuration.Json
Затем щелкните правой кнопкой мыши на имени решения, выберите в контекстном меню пункт Add►Existing Project (Добавить►Существующий проект) и добавьте проекты
AutoLot.Dal
и AutoLot.Dal.Models
из главы 23, а также ссылки на эти проекты. Сделать это можно также с помощью интерфейса командной строки, выполнив показанные ниже команды (вам придется скорректировать пути к проектам согласно требованиям имеющейся операционной системы):
dotnet sln .\Chapter25_AllProjects.sln add ..\Chapter_23\AutoLot.Models
dotnet sln .\Chapter25_AllProjects.sln add ..\Chapter_23\AutoLot.Dal
dotnet add WpfControlsAndAPIs reference ..\Chapter_23\AutoLot.Models
dotnet add WpfControlsAndAPIs reference ..\Chapter_23\AutoLot.Dal
Убедитесь, что в проекте
AutoLot.Dal
все еще присутствует ссылка на проект AutoLot.Dal.Models
. Добавьте в файл MainWindow.xaml.cs
следующие пространства имен:
using System.Linq;
using AutoLot.Dal.EfStructures;
using AutoLot.Dal.Repos;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
Добавьте в
MainWindow.cs
два свойства уровня модуля для хранения экземпляров реализации IConfiguration
и класса ApplicationDbContext
:
private IConfiguration _configuration;
private ApplicationDbContext _context;
Добавьте новый метод по имени
GetConfigurationAndContext()
для хранения экземпляров реализации IConfiguration
и класса ApplicationDbContext
и вызовите его в конструкторе. Вот полный код метода:
private void GetConfigurationAndDbContext()
{
_configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true)
.Build();
var optionsBuilder =
new DbContextOptionsBuilder();
var connectionString =
_configuration.GetConnectionString("AutoLot");
optionsBuilder.UseSqlServer(connectionString,
sqlOptions => sqlOptions.EnableRetryOnFailure());
_context = new ApplicationDbContext(optionsBuilder.Options);
}
Добавьте в проект новый файл JSON по имени
appsettings.json
. Щелкните правой кнопкой мыши на имени этого файла в окне Solution Explorer, выберите в контекстном меню пункт Properties (Свойства) и установите свойство Copy То Output Directory (Копировать в выходной каталог) в Copy always (Всегда копировать). Вы можете добиться того же самого результата с помощью файла проекта:
Always
Модифицируйте файл JSON, как показано ниже (приведя строку подключения в соответствие со своей средой):
{
"ConnectionStrings": {
"AutoLotFinal": "server=.,5433;Database=AutoLot;
User Id=sa;Password=P@ssw0rd;"
}
}
Откройте файл
MainWindow.xaml.cs
, добавьте последнюю вспомогательную функцию по имени ConfigureGrid()
и вызовите ее в конструкторе после конфигурирования ApplicationDbContext
. Понадобится добавить лишь несколько строк кода:
private void ConfigureGrid()
{
using var repo = new CarRepo(_context);
gridInventory.ItemsSource = repo
.GetAllIgnoreQueryFilters()
.ToList()
.Select(x=> new {
x.Id,
Make=x.MakeName,
x.Color,
x.PetName
});
}
Запустив проект, вы увидите данные, заполняющие сетку. При желании сделать сетку более привлекательной можно применить окно Properties в Visual Studio для редактирования свойств сетки, чтобы улучшить ее внешний вид.
На этом текущий пример завершен. В последующих главах вы увидите в действии другие элементы управления, но к настоящему моменту вы должны чувствовать себя увереннее с процессом построения пользовательских интерфейсов в Visual Studio, а также при работе с разметкой XAML и кодом С#.
Подобно любому API-интерфейсу .NET Core внутри WPF используется каждый член системы типов .NET Core (классы, структуры, интерфейсы, делегаты, перечисления) и каждый член типа (свойства, методы, события, константные данные, поля только для чтения и т.д.). Однако в WPF также поддерживается уникальная программная концепция под названием свойство зависимости.
Как и "нормальное" свойство .NET Core (которое в литературе, посвященной WPF, часто называют свойством CLR), свойство зависимости можно устанавливать декларативно с помощью разметки XAML или программно в файле кода. Кроме того, свойства зависимости (подобно свойствам CLR) в конечном итоге предназначены для инкапсуляции полей данных класса и могут быть сконфигурированы как доступные только для чтения, только для записи или для чтения и записи.
Вы будете практически всегда пребывать в блаженном неведении относительно того, что фактически устанавливаете (или читаете) свойство зависимости, а не свойство CLR! Например, свойства
Height
и Width
, которые элементы управления WPF наследуют от класса FrameworkElement
, а также член Content
, унаследованный от класса ControlContent
, на самом деле являются свойствами зависимости:
С учетом всех указанных сходств возникает вопрос: зачем нужно было определять в WPF новый термин для такой знакомой концепции? Ответ кроется в способе реализации свойства зависимости внутри класса. Пример кода будет показан позже, а на высоком уровне все свойства зависимости создаются описанным ниже способом.
• Класс, который определяет свойство зависимости, должен иметь в своей цепочке наследования класс
DependencyObject
.
• Одиночное свойство зависимости представляется как открытое, статическое, допускающее только чтение поле типа
DependencyProperty
. По соглашению это поле именуется путем снабжения имени оболочки CLR (см. последний пункт списка) суффиксом Property.
• Переменная типа
DependencyProperty
регистрируется посредством вызова статического метода DependencyProperty.Register()
, который обычно происходит в статическом конструкторе или встраивается в объявление переменной.
• В классе будет определено дружественное к XAML свойство CLR, которое вызывает методы, предоставляемые классом
DependencyObject
, для получения и установки значения.
После реализации свойства зависимости предлагают несколько мощных инструментов, которые применяются разнообразными технологиями WPF, в том числе привязкой данных, службами анимации, стилями, шаблонами и т.д. Мотивацией создания свойств зависимости было желание предоставить способ вычисления значений свойств на основе значений из других источников. Далее приведен список основных преимуществ, которые выходят далеко за рамки простой инкапсуляции данных, обеспечиваемой свойствами CLR.
• Свойства зависимости могут наследовать свои значения от определения XAML родительского элемента. Например, если в открывающем дескрипторе Window определено значение для атрибута
FontSize
, то все элементы управления внутри Window
по умолчанию будут иметь тот же самый размер шрифта.
• Свойства зависимости поддерживают возможность получать значения, которые установлены элементами внутри их области определения XAML, например, в случае установки элементом
Button
свойства Dock
родительского контейнера DockPanel
. (Вспомните, что присоединяемые свойства делают именно это, поскольку являются разновидностью свойств зависимости.)
• Свойства зависимости позволяют инфраструктуре WPF вычислять значение на основе множества внешних значений, что может быть важно для служб анимации и привязки данных.
• Свойства зависимости предоставляют поддержку инфраструктуры для триггеров WPF (также довольно часто используемых при работе с анимацией и привязкой данных).
Имейте в виду, что во многих случаях вы будете взаимодействовать с существующим свойством зависимости способом, идентичным работе с обычным свойством CLR (благодаря оболочке CLR). В предыдущем разделе, посвященном привязке данных, вы узнали, что если необходимо установить привязку данных в коде, то должен быть вызван метод
SetBinding()
на целевом объекте операции и указано свойство зависимости, с которым будет работать привязка:
private void SetBindings()
{
Binding b = new Binding
{
// Зарегистрировать преобразователь, источник и путь.
Converter = new MyDoubleConverter(),
Source = this.mySB,
Path = new PropertyPath("Value")
};
// Указать свойство зависимости.
this.labelSBThumb.SetBinding(Label.ContentProperty, b);
}
Вы увидите похожий код в главе 27 во время исследования запуска анимации в коде:
// Указать свойство зависимости.
rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim);
Потребность в построении специального свойства зависимости возникает только во время разработки собственного элемента управления WPF. Например, когда создается класс
UserControl
с четырьмя специальными свойствами, которые должны тесно интегрироваться с API-интерфейсом WPF, они должны быть реализованы с применением логики свойств зависимости.
В частности, если нужно, чтобы свойство было целью операции привязки данных или анимации, если оно обязано уведомлять о своем изменении, если свойство должно быть в состоянии работать в качестве установщика в стиле WPF или получать свои значения от родительского элемента, то возможностей обычного свойства CLR окажется не достаточно. В случае использования обычного свойства другие программисты действительно могут получать и устанавливать его значение, но если они попытаются применить такое свойство внутри контекста службы WPF, то оно не будет работать ожидаемым образом. Поскольку заранее нельзя узнать, как другие пожелают взаимодействовать со свойствами специальных классов
UserControl
, нужно выработать в себе привычку при построении специальных элементов управления всегда определять свойства зависимости.
Прежде чем вы научитесь создавать специальные свойства зависимости, давайте рассмотрим внутреннюю реализацию свойства
Height
класса FrameworkElement
. Ниже приведен соответствующий код (с комментариями):
// FrameworkElement "является" DependencyObject.
public class FrameworkElement : UIElement, IFrameworkInputElement,
IInputElement, ISupportInitialize, IHaveResources, IQueryAmbient
{
...
// Статическое поле только для чтения типа DependencyProperty.
public static readonly DependencyProperty HeightProperty;
// Поле DependencyProperty часто регистрируется
// в статическом конструкторе класса.
static FrameworkElement()
{
...
HeightProperty = DependencyProperty.Register(
"Height",
typeof(double),
typeof(FrameworkElement),
new FrameworkPropertyMetadata((double) 1.0 / (double) 0.0,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(FrameworkElement.OnTransformDirty)),
new ValidateValueCallback(FrameworkElement.IsWidthHeightValid));
}
// Оболочка CLR, реализованная с использованием
// унаследованных методов GetValue()/SetValue().
public double Height
{
get { return (double) base.GetValue(HeightProperty); }
set { base.SetValue(HeightProperty, value); }
}
}
Как видите, по сравнению с обычными свойствами CLR свойства зависимости требуют немалого объема дополнительного кода. В реальности зависимость может оказаться даже еще более сложной, чем показано здесь (к счастью, многие реализации проще свойства
Height
).
В первую очередь вспомните, что если в классе необходимо определить свойство зависимости, то он должен иметь в своей цепочке наследования
DependencyObject
, т.к. именно этот класс определяет методы GetValue()
и SetValue()
, применяемые в оболочке CLR. Из-за того, что класс FrameworkElement
"является" DependencyObject
, указанное требование удовлетворено.
Далее вспомните, что сущность, где действительно хранится значение свойства (значение
double
в случае Height
), представляется как открытое, статическое, допускающее только чтение поле типа DependencyProperty
. По соглашению имя этого свойства должно всегда формироваться из имени связанной оболочки CLR с добавлением суффикса Property
:
public static readonly DependencyProperty HeightProperty;
Учитывая, что свойства зависимости объявляются как статические поля, они обычно создаются (и регистрируются) внутри статического конструктора класса. Объект
DependencyProperty
создается посредством вызова статического метода DependencyProperty.Register()
. Данный метод имеет множество перегруженных версий, но в случае свойства Height
он вызывается следующим образом:
HeightProperty = DependencyProperty.Register(
"Height",
typeof(double),
typeof(FrameworkElement),
new FrameworkPropertyMetadata((double)0.0,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(FrameworkElement.OnTransformDirty)),
new ValidateValueCallback(FrameworkElement.IsWidthHeightValid));
Первым аргументом, передаваемым методу
DependencyProperty.Register()
, является имя обычного свойства CLR класса (Height
), а второй аргумент содержит информацию о типе данных, который его инкапсулирует (double
). Третий аргумент указывает информацию о типе класса, которому принадлежит свойство (FrameworkElement
). Хотя такие сведения могут показаться избыточными (в конце концов, поле HeightProperty
уже определено внутри класса FrameworkElement
), это довольно продуманный аспект WPF, поскольку он позволяет одному классу регистрировать свойства в другом классе (даже если его определение было запечатано).
Четвертый аргумент, передаваемый методу
DependencyProperty.Register()
в рассмотренном примере, представляет собой то, что действительно делает свойства зависимости уникальными. Здесь передается объект FrameworkPropertyMetadata
, который описывает разнообразные детали относительно того, как инфраструктура WPF должна обрабатывать данное свойство в плане уведомлений с помощью обратных вызовов (если свойству необходимо извещать других, когда его значение изменяется). Кроме того, объект FrameworkPropertyMetadata
указывает различные параметры (представленные перечислением FrameworkPropertyMetadataOptions
), которые управляют тем, на что свойство воздействует (работает ли оно с привязкой данных, может ли наследоваться и т.д.). В данном случае аргументы конструктора FrameworkPropertyMetadata
можно описать так:
new FrameworkPropertyMetadata(
// Стандартное значение свойства.
(double)0.0,
// Параметры метаданных.
FrameworkPropertyMetadataOptions.AffectsMeasure,
// Делегат, который указывает на метод,
// вызываемый при изменении свойства.
new PropertyChangedCallback(FrameworkElement.OnTransformDirty)
)
Поскольку последний аргумент конструктора
FrameworkPropertyMetadata
является делегатом, обратите внимание, что он указывает на статический метод OnTransformDirty()
класса FrameworkElement
. Код метода OnTransformDirty()
здесь не приводится, но имейте в виду, что при создании специального свойства зависимости всегда можно указывать делегат PropertyChangeCallback
, нацеленный на метод, который будет вызываться в случае изменения значения свойства.
Это подводит к финальному параметру метода
DependencyProperty.Register()
— второму делегату типа ValidateValueCallback
, указывающему на метод класса FrameworkElement
, который вызывается для проверки достоверности значения, присваиваемого свойству:
new ValidateValueCallback(FrameworkElement.IsWidthHeightValid)
Метод
IsWidthHeightValid()
содержит логику, которую обычно ожидают найти в блоке установки значения свойства (как более подробно объясняется в следующем разделе):
private static bool IsWidthHeightValid(object value)
{
double num = (double) value;
return ((!DoubleUtil.IsNaN(num) && (num >= 0.0))
&& !double.IsPositiveInfinity(num));
}
После того, как объект
DependencyProperty
зарегистрирован, остается упаковать поле в обычное свойство CLR (Height
в рассматриваемом случае). Тем не менее, обратите внимание, что блоки get
и set
не просто возвращают или устанавливают значение double
переменной-члена уровня класса, а делают это косвенно с использованием методов GetValue()
и SetValue()
базового класса System.Windows.DependencyObject
:
public double Height
{
get { return (double) base.GetValue(HeightProperty); }
set { base.SetValue(HeightProperty, value); }
}
Подводя итог, следует отметить, что свойства зависимости выглядят как обычные свойства, когда вы извлекаете или устанавливаете их значения в разметке XAML либо в коде, но "за кулисами" они реализованы с помощью гораздо более замысловатых программных приемов. Вспомните, что основным назначением этого процесса является построение специального элемента управления, имеющего специальные свойства, которые должны быть интегрированы со службами WPF, требующими взаимодействия через свойства зависимости (например, с анимацией, привязкой данных и стилями).
Несмотря на то что часть реализации свойства зависимости предусматривает определение оболочки CLR, вы никогда не должны помещать логику проверки достоверности в блок set. К тому же оболочка CLR свойства зависимости не должна делать ничего кроме вызовов
GetValue()
или SetValue()
.
Исполняющая среда WPF сконструирована таким образом, что если написать разметку XAML, которая выглядит как установка свойства, например:
то исполняющая среда вообще обойдет блок установки свойства
Height
и напрямую вызовет метод SetValue()
! Причина такого необычного поведения связана с простым приемом оптимизации. Если бы исполняющая среда WPF обращалась к блоку установки свойства Height
, то ей пришлось бы во время выполнения выяснять посредством рефлексии, где находится поле DependencyProperty
(указанное в первом аргументе SetValue()
), ссылаться на него в памяти и т.д. То же самое остается справедливым и при написании разметки XAML, которая извлекает значение свойства Height
— метод GetValue()
будет вызываться напрямую. Но раз так, тогда зачем вообще строить оболочку CLR? Дело в том, что XAML в WPF не позволяет вызывать функции в разметке, поэтому следующий фрагмент приведет к ошибке:
На самом деле установку или получение значения в разметке с применением оболочки CLR следует считать способом сообщения исполняющей среде WPF о необходимости вызова методов
GetValue()/SetValue()
, т.к. напрямую вызывать их в разметке невозможно. А что, если обратиться к оболочке CLR в коде, как показано ниже?
Button b = new Button();
b.Height = 10;
В таком случае, если блок
set
свойства Height
содержит какой-то код помимо вызова SetValue()
, то он должен выполниться, потому что оптимизация синтаксического анализатора XAML в WPF не задействуется.
Запомните основное правило: при регистрации свойства зависимости используйте делегат
ValidateValueCallback
для указания на метод, который выполняет проверку достоверности данных. Такой подход гарантирует корректное поведение независимо от того, что именно применяется для получения/установки свойства зависимости — разметка XAML или код.
Если к настоящему моменту вы слегка запутались, то такая реакция совершенно нормальна. Создание свойств зависимости может требовать некоторого времени на привыкание. Как бы то ни было, но это часть процесса построения многих специальных элементов управления WPF, так что давайте рассмотрим, каким образом создается свойство зависимости.
Начните с создания нового проекта приложения WPF по имени
CustomDependencyProperty
. Выберите в меню Project (Проект) пункт Add User Control (WPF) (Добавить пользовательский элемент управления (WPF)) и создайте элемент управления с именем ShowNumberControl.xaml
.
На заметку! Более подробные сведения о классе
UserControl
в WPF ищите в главе 27, а пока просто следуйте указаниям по мере проработки примера.
Подобно окну типы
UserControl
в WPF имеют файл XAML и связанный файл кода. Модифицируйте разметку XAML пользовательского элемента управления, чтобы определить простой элемент Label
внутри Grid
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace: CustomDependencyProperty"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
Background="LightBlue"/>
В файле кода для данного элемента создайте обычное свойство .NET Core, которое упаковывает поле типа
int
и устанавливает новое значение для свойства Content
элемента Label
:
public partial class ShowNumberControl : UserControl
{
public ShowNumberControl()
{
InitializeComponent();
}
// Обычное свойство .NET Core.
private int _currNumber = 0;
public int CurrentNumber
{
get => _currNumber;
set
{
_currNumber = value;
numberDisplay.Content = CurrentNumber.ToString();
}
}
}
Обновите определение XAML в
MainWindow.xml
, объявив экземпляр специального элемента управления внутри диспетчера компоновки StackPanel
. Поскольку специальный элемент управления не входит в состав основных сборок WPF, понадобится определить специальное пространство имен XML, которое отображается на него. Вот требуемая разметка:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:myCtrls="clr-namespace: CustomDependencyProperty"
xmlns:local="clr-namespace: CustomDependencyProperty"
mc:Ignorable="d"
Title="Simple Dependency Property App" Height="450" Width="450"
WindowStartupLocation="CenterScreen">
HorizontalAlignment="Left" x:Name="myShowNumberCtrl"
CurrentNumber="100"/>
Похоже, что визуальный конструктор Visual Studio корректно отображает значение, установленное в свойстве
CurrentNumber
(рис. 25.23).
Однако что, если к свойству
CurrentNumber
необходимо применить объект анимации, который обеспечит изменение значения свойства от 100 до 200 в течение 10 секунд? Если это желательно сделать в разметке, тогда область myCtrls:ShowNumberControl
можно изменить следующим образом:
После запуска приложения объект анимации не сможет найти подходящую цель и сгенерируется исключение. Причина в том, что свойство
CurrentNumber
не было зарегистрировано как свойство зависимости! Чтобы устранить проблему, возвратитесь в файл кода для специального элемента управления и полностью закомментируйте текущую логику свойства (включая закрытое поддерживающее поле).
Теперь добавьте показанный ниже код, чтобы свойство
CurrentNumber
создавалось как свойство зависимости:
public int CurrentNumber
{
get => (int)GetValue(CurrentNumberProperty);
set => SetValue(CurrentNumberProperty, value);
}
public static readonly DependencyProperty CurrentNumberProperty =
DependencyProperty.Register("CurrentNumber",
typeof(int),
typeof(ShowNumberControl),
new UIPropertyMetadata(0));
Работа похожа на ту, что делалась в реализации свойства
Height
: тем не менее, предыдущий фрагмент кода регистрирует свойство непосредственно в теле, а не в статическом конструкторе (что хорошо). Также обратите внимание, что объект UIPropertyMetadata
используется для определения стандартного целочисленного значения (0
) вместо более сложного объекта FrameworkPropertyMetadata
. В итоге получается простейшая версия CurrentNumber
как свойства зависимости.
Хотя у вас есть свойство зависимости по имени
CurrentNumber
(и исключение больше не генерируется), анимация пока еще не наблюдается. Следующей корректировкой будет указание функции, вызываемой для выполнения проверки достоверности данных. В данном примере предполагается, что нужно обеспечить нахождение значения свойства CurrentNumber
в диапазоне между 0 и 500.
Добавьте в метод
DependencyProperty.Register()
последний аргумент типа ValidateValueCallback
, указывающий на метод по имени ValidateCurrentNumber
.
Здесь
ValidateValueCallback
является делегатом, который может указывать только на методы, возвращающие тип bool
и принимающие единственный аргумент типа object
. Экземпляр object
представляет присваиваемое новое значение. Реализация ValidateCurrentNumber
должна возвращать true
, если входное значение находится в ожидаемом диапазоне, и false
в противном случае:
public static readonly DependencyProperty CurrentNumberProperty =
DependencyProperty.Register("CurrentNumber",
typeof(int),
typeof(ShowNumberControl),
new UIPropertyMetadata(100),
new ValidateValueCallback(ValidateCurrentNumber));
// Простое бизнес-правило: значение должно находиться
// в диапазоне между 0 и 500.
public static bool ValidateCurrentNumber(object value) =>
Convert.ToInt32(value) >= 0 && Convert.ToInt32(value) <= 500;
Итак, допустимое число уже есть, но анимация по-прежнему отсутствует. Последнее изменение, которое потребуется внести — передать во втором аргументе конструктора
UIPropertyMrtadata
объект PropertyChangedCallback
. Данный делегат может указывать на любой метод, принимающий DependencyObject
в первом параметре и DependencyPropertyChangeEventArgs
во втором. Модифицируйте код следующим образом:
// Обратите внимание на второй параметр конструктора UIPropertyMetadata.
public static readonly DependencyProperty CurrentNumberProperty =
DependencyProperty.Register("CurrentNumber", typeof(int),
typeof(ShowNumberControl),
new UIPropertyMetadata(100,
new PropertyChangedCallback(CurrentNumberChanged)),
new ValidateValueCallback(ValidateCurrentNumber));
Конечной целью внутри метода
CurrentNumberChamged()
будет изменение свойства Content
объекта Label
на новое значение, присвоенное свойству CurrentNumber
. Однако возникает серьезная проблема:метод CurrentNumberChanged()
является статическим, т.к. он должен работать со статическим объектом DependencyProperty
. Как тогда получить доступ к объекту Label
для текущего экземпляра ShowNumberControl
? Нужная ссылка содержится в первом параметре DependencyObject
. Новое значение можно найти с применением входных аргументов события. Ниже показан необходимый код, который будет изменять свойство Content
объекта Label
:
private static void CurrentNumberChanged(DependencyObject depObj,
DependencyPropertyChangedEventArgs args)
{
// Привести DependencyObject к ShowNumberControl.
ShowNumberControl c = (ShowNumberControl)depObj;
// Получить элемент управления Label в ShowNumberControl.
Label theLabel = c.numberDisplay;
// Установить для Label новое значение.
theLabel.Content = args.NewValue.ToString();
}
Видите, насколько долгий путь пришлось пройти, чтобы всего лишь изменить содержимое метки! Преимущество заключается в том, что теперь свойство зависимости
CurrentNumber
может быть целью для стиля WPF, объекта анимации, операции привязки данных и т.д. Снова запустив приложение, вы легко заметите, что значение изменяется во время выполнения.
На этом обзор свойств зависимости WPF завершен. Хотя теперь вы должны гораздо лучше понимать, что они позволяют делать, и как создавать собственные свойства подобного рода, имейте в виду, что многие детали здесь не были раскрыты.
Если вам однажды понадобится создавать множество собственных элементов управления, поддерживающих специальные свойства, тогда загляните в подраздел "Properties" ("Свойства") раздела "Systems" ("Системы") документации по WPF
(https://docs.microsoft.com/ru-ru/dotnet/desktop/wpf/
).Там вы найдете намного больше примеров построения свойств зависимости, присоединяемых свойств, разнообразных способов конфигурирования метаданных и массу других подробных сведений.
В главе рассматривались некоторые аспекты элементов управления WPF, начиная с обзора набора инструментов для элементов управления и роли диспетчеров компоновки (панелей). Первый пример был посвящен построению простого приложения текстового процессора. В нем демонстрировалось использование интегрированной в WPF функциональности проверки правописания, а также создание главного окна с системой меню, строкой состояния и панелью инструментов.
Более важно то, что вы научились строить команды WPF. Эти независимые от элементов управления события можно присоединять к элементу пользовательского интерфейса или входному жесту для автоматического наследования готовой функциональности (например, операций с буфером обмена).
Кроме того, вы узнали немало сведений о построении пользовательских интерфейсов в XAML и попутно ознакомились с интерфейсом Ink API, предлагаемым WPF. Вы также получили представление об операциях привязки данных WPF, включая использование класса
DataGrid
из WPF для отображения информации из специальной базы данных AutoLot
.
Наконец, вы выяснили, что инфраструктура WPF добавляет уникальный аспект к традиционным программным примитивам .NET Core, в частности к свойствам и событиям. Как было показано, механизм свойств зависимости позволяет строить свойство, которое может интегрироваться с набором служб WPF (анимации, привязки данных, стили и т.д.). В качестве связанного замечания: механизм маршрутизируемых событий предоставляет событию способ распространяться вверх или вниз по дереву разметки.
В настоящей главе рассматриваются возможности графической визуализации WPF. Вы увидите, что инфраструктура WPF предоставляет три отдельных способа визуализации графических данных: фигуры, рисунки и визуальные объекты. Разобравшись в преимуществах и недостатках каждого подхода, вы приступите к исследованию мира интерактивной двумерной графики с использованием классов из пространства имен
System.Windows.Shapes
. Затем будет показано, как с помощью рисунков и геометрических объектов визуализировать двумерные данные в легковесной манере. И, наконец, вы узнаете, каким образом добиться от визуального уровня максимальной функциональности и производительности.
Попутно затрагиваются многие связанные темы, такие как создание специальных кистей и перьев, применение графических трансформаций к визуализации и выполнение операций проверки попадания. В частности вы увидите, как можно упростить решение задач кодирования графики с помощью интегрированных инструментов Visual Studio и дополнительного средства под названием Inkscape.
На заметку! Графика является ключевым аспектом разработки WPF. Даже если вы не строите приложение с интенсивной графикой (вроде видеоигры или мультимедийного приложения), то рассматриваемые в главе темы критически важны при работе с такими службами, как шаблоны элементов управления, анимация и настройка привязки данных.
В WPF используется особая разновидность графической визуализации, которая известна под названием графика режима сохранения (retained mode). Выражаясь просто, это означает, что после применения разметки XAML или процедурного кода для генерирования графической визуализации инфраструктура WPF несет ответственность за сохранение визуальных элементов и обеспечение их корректной перерисовки и обновления оптимальным способом. Таким образом, визуализируемые графические данные присутствуют постоянно, даже когда конечный пользователь скрывает изображение, изменяя размер окна или сворачивая его, перекрывая одно окно другим и т.д.
По разительному контрасту предшествующие версии API-интерфейсов графической визуализации от Microsoft (включая GDI+ в Windows Forms) были графическими системами прямого режима (immediate mode). В такой модели ответственность за корректное "запоминание" и обновление визуализируемых элементов на протяжении времени жизни приложения возлагалась на программиста. Например, в приложении Windows Forms визуализация фигуры вроде прямоугольника предусматривала обработку события
Paint
(или переопределение виртуального метода OnPaint()
), получение объекта Graphics
для рисования прямоугольника и, что важнее всего, добавление инфраструктуры, обеспечивающей сохранение изображения в ситуации, когда пользователь изменил размеры окна (например, за счет создания переменных-членов для представления позиции прямоугольника и вызова метода Invalidate()
во многих местах кода).
Переход от графики прямого режима к графике режима сохранения — действительно удачное решение, т.к. программистам приходится писать и сопровождать гораздо меньший объем рутинного кода для поддержки графики. Однако речь не идет о том, что API-интерфейс графики WPF полностью отличается от более ранних инструментальных наборов визуализации. Например, как и GDI+, инфраструктура WPF поддерживает разнообразные типы объектов кистей и перьев, приемы проверки попадания, области отсечения, графические трансформации и т.д. Поэтому если у вас есть опыт работы с GDI+ (или GDI на языке C/C++), то вы уже имеете неплохое представление о способе выполнения базовой визуализации в WPF.
Как и с другими аспектами разработки приложений WPF, существует выбор из нескольких способов выполнения графической визуализации после принятия решения делать это посредством разметки XAML или процедурного кода C# (либо их комбинации). В частности, инфраструктура WPF предлагает следующие три индивидуальных подхода к визуализации графических данных.
• Фигуры. Инфраструктура WPF предоставляет пространство имен
System.Windows.Shapes
, в котором определено небольшое количество классов для визуализации двумерных геометрических объектов (прямоугольников, эллипсов, многоугольников и т.п.). Хотя такие типы просты в использовании и очень мощные, в случае непродуманного применения они могут привести к значительным накладным расходам памяти.
• Рисунки и геометрические объекты. Второй способ визуализации графических данных в WPF предполагает работу с классами, производными от абстрактного класса
System.Windows.Media.Drawing
. Используя классы, подобные GeometryDrawing
или ImageDrawing
(в дополнение к различным геометрическим объектам), можно визуализировать графические данные в более легковесной (но менее функциональной) манере.
• Визуальные объекты. Самый быстрый и легковесный способ визуализации графических данных в WPF предусматривает работу с визуальным уровнем, который доступен только через код С#. С применением классов, производных от
System.Windows.Media.Visual
, можно взаимодействовать непосредственно с графической подсистемой WPF.
Причина предоставления разных способов решения той же самой задачи (т.е. визуализации графических данных) связана с расходом памяти и в конечном итоге с производительностью приложения. Поскольку WPF является системой, интенсивно использующей графику, нет ничего необычного в том, что приложению требуется визуализировать сотни или даже тысячи различных изображений на поверхности окна, и выбор реализации (фигуры, рисунки или визуальные объекты) может оказать огромное влияние.
Важно понимать, что при построении приложения WPF высока вероятность использования всех трех подходов. В качестве эмпирического правила запомните: если нужен умеренный объем интерактивных графических данных, которыми может манипулировать пользователь (принимающих ввод от мыши, отображающих всплывающие подсказки и т.д.), то следует применять члены из пространства имен
System.Windows.Shapes
.
Напротив, рисунки и геометрические объекты лучше подходят, когда необходимо моделировать сложные и по большей части не интерактивные векторные графические данные с использованием разметки XAML или кода С#. Хотя рисунки и геометрические объекты способны реагировать на события мыши, а также поддерживают проверку попадания и операции перетаскивания, для выполнения таких действий обычно приходится писать больше кода.
Наконец, если требуется самый быстрый способ визуализации значительных объемов графических данных, то должен быть выбран визуальный уровень. Например, предположим, что инфраструктура WPF применяется для построения научного приложения, которое должно отображать тысячи точек на графике данных. За счет использования визуального уровня точки на графике можно визуализировать оптимальным образом. Как будет показано далее в главе, визуальный уровень доступен только из кода С#, но не из разметки XAML.
Независимо от выбранного подхода (фигуры, рисунки и геометрические объекты или визуальные объекты) всегда будут применяться распространенные графические примитивы, такие как кисти (для заполнения ограниченных областей), перья (для рисования контуров) и объекты трансформаций (которые видоизменяют данные). Исследование начинается с классов из пространства имен
System.Windows.Shapes
.
На заметку! Инфраструктура WPF поставляется также с полнофункциональным API-интерфейсом, который можно использовать для визуализации и манипулирования трехмерной графикой, но в книге он не рассматривается.
Члены пространства имен
System.Windows.Shapes
предлагают наиболее прямолинейный, интерактивный и самый затратный в плане расхода памяти способ визуализации двумерного изображения. Это небольшое пространство имен (расположенное в сборке PresentationFramework.dll
) состоит всего из шести запечатанных классов, которые расширяют абстрактный базовый класс Shape
: Ellipse
, Rectangle
, Line
, Polygon
, Polyline
и Path
.
Абстрактный класс
Shape
унаследован от класса FrameworkElement
, который сам унаследован от UIElement
. В указанных классах определены члены для работы с изменением размеров, всплывающими подсказками, курсорами мыши и т.п. Благодаря такой цепочке наследования при визуализации графических данных с применением классов, производных от Shape
, объекты получаются почти такими же функциональными (с точки зрения взаимодействия с пользователем), как элементы управления WPF.
Скажем, для выяснения, щелкнул ли пользователь на визуализированном изображении, достаточно обработать событие
MouseDown
. Например, если написать следующую разметку XAML для объекта Rectangle
внутри элемента управления Grid
начального окна Window
:
MouseDown="myRect_MouseDown"/>
то можно реализовать обработчик события
MouseDown
, который изменяет цвет фона прямоугольника в результате щелчка на нем:
private void myRect_MouseDown(object sender, MouseButtonEventArgs e)
{
// Изменить цвет прямоугольника в результате щелчка на нем.
myRect.Fill = Brushes.Pink;
}
В отличие от других инструментальных наборов, предназначенных для работы с графикой, вам не придется писать громоздкий код инфраструктуры, в котором вручную сопоставляются координаты мыши с геометрическим объектом, выясняется попадание курсора внутрь границ, выполняется визуализация в неотображаемый буфер и т.д. Члены пространства имен
System.Windows.Shapes
просто реагируют на зарегистрированные вами события подобно типичному элементу управления WPF (Button
и т.д.).
Недостаток всей готовой функциональности связан с тем, что фигуры потребляют довольно много памяти. Если строится научное приложение, которое рисует тысячи точек на экране, то использование фигур будет неудачным выбором (по существу таким же расточительным в плане памяти, как визуализация тысяч объектов
Button
). Тем не менее, когда нужно сгенерировать интерактивное двумерное векторное изображение, фигуры оказываются прекрасным вариантом.
Помимо функциональности, унаследованной от родительских классов
UIElement
и FrameworkElement
, в классе Shape
определено множество собственных членов, наиболее полезные из которых кратко описаны в табл. 26.1.
На заметку! Если вы забудете установить свойства
Fill
и Stroke
, то WPF предоставит "невидимые" кисти, вследствие чего фигура не будет видна на экране!
Вы построите приложение WPF, которое способно визуализировать фигуры, с применением XAML и С#, и попутно исследуете процесс проверки попадания. Создайте новый проект приложения WPF по имени
RenderingWithShapes
и измените заголовок главного окна в MainWindow.xaml
на Fun with Shapes!
. Модифицируйте первоначальную разметку XAML для элемента Window
, заменив Grid
панелью DockPanel
, которая содержит (пока пустые) элементы Toolbar
и Canvas
. Обратите внимание, что каждому содержащемуся элементу посредством свойства Name
назначается подходящее имя.
picture
Заполните элемент
ToolBar
набором объектов RadioButton
, каждый из которых содержит объект специфического класса, производного от Shape
. Легко заметить, что каждому элементу RadioButton
назначается то же самое групповое имя GroupName
(чтобы обеспечить взаимное исключение) и также подходящее индивидуальное имя.
Click="CircleOption_Click">
Click="RectOption_Click">
Click="LineOption_Click">
X1="10" Y1="10" Y2="25" X2="25"
StrokeStartLineCap="Triangle" StrokeEndLineCap="Round" />
Как видите, объявление объектов
Rectangle
, Ellipse
и Line
в разметке XAML довольно прямолинейно и требует лишь минимальных комментариев. Вспомните, что свойство Fill
позволяет указать кисть для рисования внутренностей фигуры. Когда нужна кисть сплошного цвета, можно просто задать жестко закодированную строку известных значений, а соответствующий преобразователь типа сгенерирует корректный объект. Интересная характеристика типа Rectangle
связана с тем, что в нем определены свойства RadiusX
и RadiusY
, позволяющие визуализировать скругленные углы.
Объект
Line
представлен своими начальной и конечной точками с использованием свойств X1
, Х2
, Y1
и Y2
(учитывая, что высота и ширина при описании линии имеют мало смысла). Здесь устанавливается несколько дополнительных свойств, которые управляют тем, как визуализируются начальная и конечная точки объекта Line
, а также настраивают параметры штриха. На рис. 26.1 показана визуализированная панель инструментов в визуальном конструкторе WPF среды Visual Studio.
С помощью окна Properties (Свойства) среды Visual Studio создайте обработчик события
MouseLeftButtonDown
для Canvas
и обработчик события Click
для каждого элемента RadioButton
. Цель заключается в том, чтобы в коде C# визуализировать выбранную фигуру (круг, квадрат или линию), когда пользователь щелкает внутри Canvas
. Первым делом определите следующее вложенное перечисление (и соответствующую переменную-член) внутри класса, производного от Window
:
public partial class MainWindow : Window
{
private enum SelectedShape
{ Circle, Rectangle, Line }
private SelectedShape _currentShape;
}
В каждом обработчике
Click
установите переменную-член currentShape
в корректное значение SelectedShape
:
private void CircleOption_Click(object sender, RoutedEventArgs e)
{
_currentShape = SelectedShape.Circle;
}
private void RectOption_Click(object sender, RoutedEventArgs e)
{
_currentShape = SelectedShape.Rectangle;
}
private void LineOption_Click(object sender, RoutedEventArgs e)
{
_currentShape = SelectedShape.Line;
}
Посредством обработчика события
MouseLeftButtonDown
элемента Canvas
будет визуализироваться подходящая фигура (предопределенного размера) в начальной точке, которая соответствует позиции (х
, у
) курсора мыши. Ниже приведена полная реализация с последующим анализом:
private void CanvasDrawingArea_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
Shape shapeToRender = null;
// Сконфигурировать корректную фигуру для рисования.
switch (_currentShape)
{
case SelectedShape.Circle:
shapeToRender = new Ellipse() { Fill = Brushes.Green,
Height = 35, Width = 35 };
break;
case SelectedShape.Rectangle:
shapeToRender = new Rectangle()
{ Fill = Brushes.Red, Height = 35, Width = 35,
RadiusX = 10, RadiusY = 10 };
break;
case SelectedShape.Line:
shapeToRender = new Line()
{
Stroke = Brushes.Blue,
StrokeThickness = 10,
X1 = 0, X2 = 50, Y1 = 0, Y2 = 50,
StrokeStartLineCap= PenLineCap.Triangle,
StrokeEndLineCap = PenLineCap.Round
};
break;
default:
return;
}
// Установить левый верхний угол для рисования на холсте.
Canvas.SetLeft(shapeToRender, e.GetPosition(canvasDrawingArea).X);
Canvas.SetTop(shapeToRender, e.GetPosition(canvasDrawingArea).Y);
// Нарисовать фигуру.
canvasDrawingArea.Children.Add(shapeToRender);
}
На заметку! Возможно, вы заметили, что объекты
Ellipse
, Rectangle
и Line
, создаваемые в методе canvasDrawingArea_MouseLeftButtonDown()
, имеют те же настройки свойств, что и соответствующие определения XAML. Вполне ожидаемо, код можно упростить, но это требует понимания объектных ресурсов WPF, которые будут рассматриваться в главе 27.
В коде проверяется переменная-член
_currentShape
с целью создания корректного объекта, производного от Shape
. Затем устанавливаются координаты левого верхнего угла внутри Canvas
с использованием входного объекта MouseButtonEventArgs
. Наконец, в коллекцию объектов UIElement
, поддерживаемую Canvas
, добавляется новый производный от Shape
объект. Если запустить программу прямо сейчас, то она должна позволить щелкать левой кнопкой мыши где угодно на холсте и визуализировать в позиции щелчка выбранную фигуру.
Имея в распоряжении элемент
Canvas
с коллекцией объектов, может возникнуть вопрос: как динамически удалить элемент, скажем, в ответ на щелчок пользователя правой кнопкой мыши на фигуре? Это делается с помощью класса VisualTreeHelper
из пространства имен System.Windows.Media
. Роль "визуальных деревьев" и "логических деревьев" более подробно объясняется в главе 27, а пока организуйте обработку события MouseRightButtonDown
объекта Canvas
и реализуйте соответствующий обработчик:
private void CanvasDrawingArea_MouseRightButtonDown(object sender,
MouseButtonEventArgs e)
{
// Сначала получить координаты x,y позиции,
// где пользователь выполнил щелчок.
Point pt = e.GetPosition((Canvas)sender);
// Использовать метод HitTestO класса VisualTreeHelper, чтобы
// выяснить, щелкнул ли пользователь на элементе внутри Canvas.
HitTestResult result = VisualTreeHelper.HitTest(canvasDrawingArea, pt);
// Если переменная result не равна null, то щелчок произведен на фигуре.
if (result != null)
{
// Получить фигуру, на которой совершен щелчок, и удалить ее из Canvas.
canvasDrawingArea.Children.Remove(result.VisualHit as Shape);
}
}
Метод начинается с получения точных координат (
х
, у
) позиции, где пользователь щелкнул внутри Canvas
, и проверки попадания посредством статического метода VisualTreeHelper.HitTest()
. Возвращаемое значение — объект HitTestResult
— будет установлено в null
, если пользователь выполнил щелчок не на UIElement
внутри Canvas
. Если значение HitTestResult
не равно null
, тогда с помощью свойства VisualHit
можно получить объект UIElement
, на котором был совершен щелчок, и привести его к типу, производному от Shape
(вспомните, что Canvas
может содержать любой UIElement
, а не только фигуры). Детали, связанные с "визуальным деревом", будут изложены в главе 27.
На заметку! По умолчанию метод
VisualTreeHelper.HitTest()
возвращает объект UIElement
самого верхнего уровня, на котором совершен щелчок, и не предоставляет информацию о других объектах, расположенных под ним (т.е. перекрытых в Z-порядке).
В результате внесенных модификаций должна появиться возможность добавления фигуры на
Canvas
щелчком левой кнопкой мыши и ее удаления щелчком правой кнопкой мыши.
До настоящего момента вы применяли объекты типов, производных от
Shape
, для визуализации содержимого элементов RadioButton
с использованием разметки XAML и заполняли Canvas
в коде С#. Во время исследования роли кистей и графических трансформаций в данный пример будет добавлена дополнительная функциональность. К слову, в другом примере главы будут иллюстрироваться приемы перетаскивания на объектах UIElement
. А пока давайте рассмотрим оставшиеся члены пространства имен System.Windows.Shapes
.
В текущем примере используются только три класса, производных от
Shape
. Остальные дочерние классы (Polyline
, Polygon
и Path
) чрезвычайно трудно корректно визуализировать без инструментальной поддержки (такой как инструмент Blend для Visual Studio или другие инструменты, которые могут создавать векторную графику) — просто потому, что они требуют определения большого количества точек для своего выходного представления. Ниже представлен краткий обзор остальных типов Shapes
.
Тип
Polyline
позволяет определить коллекцию координат (х
, у
) (через свойство Points
) для рисования последовательности линейных сегментов, не требующих замыкания. Тип Polygon
похож, но запрограммирован так, что всегда замыкает контур, соединяя начальную точку с конечной, и заполняет внутреннюю область с помощью указанной кисти. Предположим, что в редакторе Kaxaml создан следующий элемент StackPanel
:
Points ="10,10 40,40
10,90 300,50"/>
Points ="40,10 70,80 10,50" />
На рис. 26.2 показан визуализированный вывод в Kaxaml.
Применяя только типы
Rectangle
, Ellipse
, Polygon
, Polyline
и Line
, нарисовать детализированное двумерное векторное изображение было бы исключительно трудно, т.к. упомянутые примитивы не позволяют легко фиксировать графические данные, подобные кривым, объединениям перекрывающихся данных и т.д. Последний производный от Shape
класс, Path
, предоставляет возможность определения сложных двумерных графических данных в виде коллекции независимых геометрических объектов. После того, как коллекция таких геометрических объектов определена, ее можно присвоить свойству Data
класса Path
, где она будет использоваться для визуализации сложного двумерного изображения.
Свойство
Data
получает объект класса, производного от System.Windows.Media.Geometry
, который содержит ключевые члены, кратко описанные в табл. 26.2.
Классы, которые расширяют класс
Geometry
(табл. 26.3), выглядят очень похожими на свои аналоги, производные от Shape
. Например, класс EllipseGeometry
имеет члены, подобные членам класса Ellipse
. Крупное отличие связано с тем, что производные от Geometry
классы не знают, каким образом визуализировать себя напрямую, поскольку они не являются UIElement
. Взамен классы, производные от Geometry
, представляют всего лишь коллекцию данных о точках, которая указывает объекту Path
, как их визуализировать.
На заметку! Класс
Path
не является единственным классом в инфраструктуре WPF, который способен работать с коллекцией геометрических объектов. Например, классы DoubleAnimationUsingPath
, DrawingGroup
, GeometryDrawing
и даже UIElement
могут использовать геометрические объекты для визуализации с применением свойств PathGeometry
, ClipGeometry
, Geometry
и Clip
соответственно.
В показанной далее разметке для элемента
Path
используется несколько типов, производных от Geometry
. Обратите внимание, что свойство Data
объекта Path
устанавливается в объект GeometryGroup
, который содержит объекты других производных от Geometry
классов, таких как EllipseGeometry
, RectangleGeometry
и LineGeometry
. Результат представлен на рис.26.3.
Изображение на рис. 26.3 может быть визуализировано с применением показанных ранее классов
Line
, Ellipse
и Rectangle
. Однако это потребовало бы помещения различных объектов UIElement
в память. Когда для моделирования точек рисуемого изображения используются геометрические объекты, а затем коллекция геометрических объектов помещается в контейнер, который способен визуализировать данные (Path
в рассматриваемом случае), то тем самым сокращается расход памяти.
Теперь вспомните, что класс
Path
имеет ту же цепочку наследования, что и любой член пространства имен System.Windows.Shapes
, а потому обладает возможностью отправлять такие же уведомления о событиях, как другие объекты UIElement
. Следовательно, если определить тот же самый элемент
в проекте Visual Studio, тогда выяснить, что пользователь щелкнул в любом месте линии, можно будет за счет обработки события мыши (не забывайте, что редактор Kaxaml не разрешает обрабатывать события для написанной разметки).
Из всех классов, перечисленных в табл. 26.3, класс
PathGeometry
наиболее сложен для конфигурирования в терминах XAML и кода. Причина объясняется тем фактом, что каждый сегмент PathGeometry
состоит из объектов, содержащих разнообразные сегменты и фигуры (скажем, ArcSegment
, BezierSegment
, LineSegment
, PolyBezierSegment
, PolyLineSegment
, PolyQuadraticBezierSegment
и т.д.). Вот пример объекта Path
, свойство Data
которого было установлено в элемент PathGeometry
, состоящий из различных фигур и сегментов:
Point1="100,0"
Point2="200,200"
Point3="300,100"/>
Size="50,50" RotationAngle="45"
IsLargeArc="True" SweepDirection="Clockwise"
Point="200,100"/>
По правде говоря, лишь немногим программистам придется когда-либо вручную строить сложные двумерные изображения, напрямую описывая объекты производных от
Geometry
или PathSegment
классов. Позже в главе вы узнаете, как преобразовывать векторную графику в операторы "мини-языка" моделирования путей, которые можно применять в разметке XAML.
Даже с учетом содействия со стороны упомянутых ранее инструментов объем разметки XAML, требуемой для определения сложных объектов
Path
, может быть устрашающе большим, т.к. данные состоят из полных описаний различных объектов классов, производных от Geometry
или PathSegment
. Для того чтобы создавать более лаконичную разметку, в классе Path
поддерживается специализированный "мини-язык".
Например, вместо установки свойства
Data
объекта Path
в коллекцию объектов классов, производных от Geometry
и PathSegment
, его можно установить в одиночный строковый литерал, содержащий набор известных символов и различных значений, которые определяют фигуру, подлежащую визуализации. Ниже приведен простой пример, а его результирующий вывод показан на рис. 26.4:
Data="M 10,75 C 70,15 250,270 300,175 H 240" />
Команда
М
(от move — переместить) принимает координаты (х
, у
) позиции, которая представляет начальную точку рисования. Команда С
(от curve — кривая) принимает последовательность точек для визуализации кривой (точнее кубической кривой Безье), а команда Н
(от horizontal — горизонталь) рисует горизонтальную линию.
И снова следует отметить, что вам очень редко придется вручную строить или анализировать строковый литерал, содержащий инструкции мини-языка моделирования путей. Тем не менее, цель в том, чтобы разметка XAML, генерируемая специализированными инструментами, не казалась вам совершенно непонятной.
Каждый способ графической визуализации (фигуры, рисование и геометрические объекты, а также визуальные объекты) интенсивно использует кисти, которые позволяют управлять заполнением внутренней области двумерной фигуры. Инфраструктура WPF предлагает шесть разных типов кистей, и все они расширяют класс
System.Windows.Media.Brush
. Несмотря на то что Brush
является абстрактным классом, его потомки, описанные в табл. 26.4, могут применяться для заполнения области содержимым почти любого мыслимого вида.
Классы
DrawingBrush
и VisualBrush
позволяют строить кисть на основе существующего класса, производного от Drawing
или Visual
. Такие классы кистей используются при работе с двумя другими способами визуализации графики WPF (рисунками или визуальными объектами) и будут объясняться далее в главе.
Класс
ImageBrush
позволяет строить кисть, отображающую данные изображения из внешнего файла или встроенного ресурса приложения, который указан в его свойстве ImageSource
. Оставшиеся типы кистей (LinearGradientBrush
и RadialGradientBrush
) довольно просты в применении, хотя требуемая разметка XAML может оказаться многословной. К счастью, в среде Visual Studio поддерживаются интегрированные редакторы кистей, которые облегчают задачу генерации стилизованных кистей.
Давайте обновим приложение WPF для рисования
RenderingShapes
, чтобы использовать в нем более интересные кисти. В трех фигурах, которые были задействованы до сих пор при визуализации данных в панели инструментов, применяются обычные сплошные цвета, так что их значения можно зафиксировать с помощью простых строковых литералов. Чтобы сделать задачу чуть более интересной, теперь вы будете использовать интегрированный редактор кистей. Удостоверьтесь в том, что в IDE-среде открыт редактор XAML для начального окна и выберите элемент Ellipse
. В окне Properties отыщите категорию Brush (Кисть) и щелкните на свойстве Fill
(рис. 26.5).
В верхней части редактора кистей находится набор свойств, которые являются "совместимыми с кистью" для выбранного элемента (т.е.
Fill
, Stroke
и OpacityMask
). Под ними расположен набор вкладок, которые позволяют конфигурировать разные типы кистей, включая текущую кисть со сплошным цветом. Для управления цветом текущей кисти можно применять инструмент выбора цвета, а также ползунки ARGB (alpha, red, green, blue — прозрачность, красный, зеленый, синий). С помощью этих ползунков и связанной с ними области выбора цвета можно создавать сплошной цвет любого вида. Используйте указанные инструменты для изменения цвета в свойстве Fill
элемента Ellipse
и просмотрите результирующую разметку XAML. Как видите, цвет сохраняется в виде шестнадцатеричного значения:
Что более интересно, тот же самый редактор позволяет конфигурировать и градиентные кисти, которые применяются для определения последовательностей цветов и точек перехода цветов. Вспомните, что редактор кистей предлагает набор вкладок, первая из которых позволяет установить пустую кисть для отсутствующего визуализированного вывода. Остальные четыре дают возможность установить кисть сплошного цвета (как только что было показано), градиентную кисть, мозаичную кисть и кисть с изображением.
Щелкните на вкладке градиентной кисти; редактор отобразит несколько новых настроек (рис. 26.6).
Три кнопки в левом нижнем углу позволяют выбрать линейный градиент, радиальный градиент или обратить градиентные переходы. Полоса внизу покажет текущий цвет каждого градиентного перехода, который будет представлен специальным ползунком. Перетаскивая ползунок по полосе градиента, можно управлять смещением градиента. Кроме того, щелкая на конкретном ползунке, можно изменять цвет определенного градиентного перехода с помощью селектора цвета. Наконец, щелчок прямо на полосе градиента позволяет добавлять градиентные переходы.
Потратьте некоторое время на освоение этого редактора, чтобы построить радиальную градиентную кисть, содержащую три градиентных перехода, и установить их цвета. На рис. 26.6 показан пример кисти, использующей три оттенка зеленого цвета.
В результате IDE-среда обновит разметку XAML, добавив набор специальных кистей и присвоив их совместимым с кистями свойствам (свойство
Fill
элемента Ellipse
в рассматриваемом примере) с применением синтаксиса "свойство-элемент":
Теперь, когда вы построили специальную кисть для определения XAML элемента Ellipse, соответствующий код C# устарел в том плане, что он по-прежнему будет визуализировать круг со сплошным зеленым цветом. Для восстановления синхронизации модифицируйте нужный оператор
case
, чтобы использовать только что созданную кисть. Ниже показано необходимое обновление, которое выглядит более сложным, чем можно было ожидать, т.к. шестнадцатеричное значение преобразуется в подходящий объект Color
посредством класса System.Windows.Media.ColorConverter
(результат изменения представлен на рис. 26.7):
case SelectedShape.Circle:
shapeToRender = new Ellipse() { Height = 35, Width = 35 };
// Создать кисть RadialGradientBrush в коде.
RadialGradientBrush brush = new RadialGradientBrush();
brush.GradientStops.Add(new GradientStop(
(Color)ColorConverter.ConvertFromString("#FF77F177"), 0));
brush.GradientStops.Add(new GradientStop(
(Color)ColorConverter.ConvertFromString("#FF11E611"), 1));
brush.GradientStops.Add(new GradientStop(
(Color)ColorConverter.ConvertFromString("#FF5A8E5A"), 0.545));
shapeToRender.Fill = brush;
break;
Кстати, объекты
GradientStop
можно строить, указывая простой цвет в качестве первого параметра конструктора с применением перечисления Colors
, которое дает сконфигурированный объект Color
:
GradientStop g = new GradientStop(Colors.Aquamarine, 1);
Если требуется более тонкий контроль, то можно передавать объект
Color
, сконфигурированный в коде, например:
Color myColor = new Color() { R = 200, G = 100, B = 20, A = 40 };
GradientStop g = new GradientStop(myColor, 34);
Разумеется, использование перечисления
Colors
и класса Color
не ограничивается градиентными кистями. Их можно применять всякий раз, когда необходимо представить значение цвета в коде.
В сравнении с кистями перо представляет собой объект для рисования границ геометрических объектов или в случае класса
Line
либо PolyLine
— самого линейного геометрического объекта. В частности, класс Pen
позволяет рисовать линию указанной толщины, представленной значением типа double
. Вдобавок объект Pen
может быть сконфигурирован с помощью того же самого вида свойств, что и в классе Shape
, таких как начальный и конечный концы пера, шаблоны точек-тире и т.д. Например, для определения атрибутов пера к определению фигуры можно добавить следующую разметку:
StartLineCap="Round" />
Во многих случаях создавать объект
Pen
непосредственно не придется, потому что это делается косвенно, когда присваиваются значения свойствам вроде StrokeThickness
производного от Shape
типа (а также других типов UIElement
). Однако строить специальный объект Pen
удобно при работе с типами, производными от Drawing
(которые рассматриваются позже в главе). Среда Visual Studio не располагает редактором перьев как таковым, но позволяет для выбранного элемента конфигурировать все свойства, связанные со штрихами, с использованием окна Properties.
В завершение обсуждения фигур будет рассмотрена тема трансформаций. Инфраструктура WPF поставляется с многочисленными классами, которые расширяют абстрактный базовый класс
System.Winodws.Media.Transform
. В табл. 26.5 кратко описаны основные классы, производные от Transform
.
Трансформации могут применяться к любым объектам
UIElement
(например, к объектам производных от Shape
классов, а также к элементам управления Button
, TextBox
и т.п.). Используя классы трансформаций, можно визуализировать графические данные под заданным углом, скашивать изображение на поверхности и растягивать, сжимать либо поворачивать целевой элемент разными способами.
На заметку! Хотя объекты трансформаций могут применяться повсеместно, вы сочтете их наиболее удобными при работе с анимацией WPF и специальными шаблонами элементов управления. Как будет показано далее в главе, анимацию WPF можно использовать для включения в специальный элемент управления визуальных подсказок, предназначенных конечному пользователю.
Назначать целевому объекту (
Button
, Path
и т.д.) трансформацию (либо целый набор трансформаций) можно с помощью двух общих свойств, LayoutTransform
и RenderTransform
.
Свойство
LayoutTransform
удобно тем, что трансформация происходит перед визуализацией элементов в диспетчере компоновки и потому не влияет на операции Z-упорядочивания (т.е. трансформируемые данные изображений не перекрываются).
С другой стороны, трансформация из свойства
RenderTransform
инициируется после того, как элементы попали в свои контейнеры, поэтому вполне возможно, что элементы будут трансформированы с перекрытием друг друга в зависимости от того, как они организованы в контейнере.
Вскоре вы добавите к проекту
RenderingWithShapes
некоторую трансформирующую логику. Чтобы увидеть объект трансформации в действии, откройте редактор Kaxaml, определите внутри корневого элемента Page
или Window
простой элемент StackPanel
и установите свойство Orientation
в Horizontal
. Далее добавьте следующий элемент Rectangle
, который будет нарисован под углом в 45 градусов с применением объекта RotateTransform
:
Здесь элемент
Button
скашивается на поверхности на 20 градусов посредством трансформации SkewTransform
:
Для полноты картины ниже приведен элемент
Ellipse
, масштабированный на 20% с помощью трансформации ScaleTransform
(обратите внимание на значения, установленные в свойствах Height
и Width
), а также элемент TextBox
, к которому применена группа объектов трансформации:
Следует отметить, что в случае применения трансформации выполнять какие-либо ручные вычисления для реагирования на проверку попадания, перемещение фокуса ввода и аналогичные действия не придется. Графический механизм WPF самостоятельно решает такие задачи. Например, на рис. 26.8 можно видеть, что элемент
TextBox
по-прежнему реагирует на клавиатурный ввод.
Теперь нужно внедрить в пример
RenderingWithShapes
логику трансформации. Помимо применения объектов трансформации к одиночному элементу (Rectangle
, TextBox
и т.д.) их можно также применять к диспетчеру компоновки, чтобы трансформировать все внутренние данные. Например, всю панель DockPanel
главного окна можно было бы визуализировать под углом:
...
В рассматриваемом примере это несколько чрезмерно, так что добавьте последнюю (менее радикальную) возможность, которая позволит пользователю зеркально отобразить целый контейнер
Canvas
и всю содержащуюся в нем графику. Начните с добавления в ToolBar
финального элемента ToggleButton
со следующим определением:
Content="Flip Canvas!"/>
Внутри обработчика события
Click
для нового элемента ToggleButton
создайте объект RotateTransform
и подключите его к объекту Canvas
через свойство LayoutTransform
, если элемент ToggleButton
отмечен. Если же элемент ToggleButton
не отмечен, тогда удалите трансформацию, установив свойство LayoutTransform
в null
.
private void FlipCanvas_Click(object sender, RoutedEventArgs e)
{
if (flipCanvas.IsChecked == true)
{
RotateTransform rotate = new RotateTransform(-180);
canvasDrawingArea.LayoutTransform = rotate;
}
else
{
canvasDrawingArea.LayoutTransform = null;
}
}
Запустите приложение и добавьте несколько графических фигур в область
Canvas
, следя за тем, чтобы они находились впритык к ее краям. После щелчка на новой кнопке обнаружится, что фигуры выходят за границы Canvas
(рис. 26.9). Причина в том, что не был определен прямоугольник отсечения.
Исправить проблему легко. Вместо того чтобы вручную писать сложную логику отсечения, просто установите свойство
ClipToBounds
элемента Canvas
в true
, предотвратив визуализацию дочерних элементов вне границ родительского элемента. После запуска приложения можно заметить, что графические данные больше не покидают границы отведенной области.
Последняя крошечная модификация, которую понадобится внести, связана с тем фактом, что когда пользователь зеркально отображает холст, щелкая на кнопке переключения, а затем щелкает на нем для рисования новой фигуры, то точка, где был произведен щелчок, не является той позицией, куда попадут графические данные. Взамен они появятся в месте нахождения курсора мыши.
Чтобы устранить проблему, примените тот же самый объект трансформации к рисуемой фигуре перед выполнением визуализации (через
RenderTransform
). Ниже показан основной фрагмент кода:
private void CanvasDrawingArea_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
// Для краткости код не показан.
if (flipCanvas.IsChecked == true)
{
RotateTransform rotate = new RotateTransform(-180);
shapeToRender.RenderTransform = rotate;
}
// Установить левую верхнюю точку для рисования на холсте.
Canvas.SetLeft(shapeToRender, e.GetPosition(canvasDrawingArea).X);
Canvas.SetTop(shapeToRender, e.GetPosition(canvasDrawingArea).Y);
// Нарисовать фигуру.
canvasDrawingArea.Children.Add(shapeToRender);
}
На этом исследование пространства имен
System.Windows.Shapes
, кистей и трансформаций завершено. Прежде чем перейти к анализу роли визуализации графики с использованием рисунков и геометрических объектов, имеет смысл выяснить, каким образом IDE-среда Visual Studio способна упростить работу с примитивными графическими элементами.
В предыдущем примере разнообразные трансформации применялись за счет ручного ввода разметки и написания кода С#. Наряду с тем, что поступать так вполне удобно, последняя версия Visual Studio поставляется со встроенным редактором трансформаций. Вспомните, что получателем служб трансформаций может быть любой элемент пользовательского интерфейса, в том числе диспетчер компоновки, содержащий различные элементы управления. Для демонстрации работы с редактором трансформаций Visual Studio будет создан новый проект приложения WPF по имени
FunWithTransforms
.
Первым делом разделите первоначальный элемент
Grid
на две колонки с применением встроенного редактора сетки (точные размеры колонок роли не играют). Далее отыщите в панели инструментов элемент управления StackPanel
и добавьте его так, чтобы он занял все пространство первой колонки Grid
; затем добавьте в панель StackPanel
три элемента управления Button
:
Добавьте обработчики событий для кнопок:
private void Skew(object sender, RoutedEventArgs e)
{
}
private void Rotate(object sender, RoutedEventArgs e)
{
}
private void Flip(object sender, RoutedEventArgs e)
{
}
Чтобы завершить пользовательский интерфейс, создайте во второй колонке элемента
Grid
произвольную графику (используя любой прием, представленный ранее в главе). Вот разметка, применяемая в данном примере:
Height="186" Width="92" Stroke="Black"
Canvas.Left="20" Canvas.Top="31">
Height="101" Width="110" Stroke="Black"
Canvas.Left="122" Canvas.Top="126">
Окончательная компоновка показана на рис. 26.10.
Как упоминалось ранее, IDE-среда Visual Studio предоставляет встроенный редактор трансформаций, который можно найти в окне Properties. Раскройте раздел Transform (Трансформация), чтобы отобразить области RenderTransform и LayoutTransform редактора (рис. 26.11).
Подобно разделу Brush раздел Transform предлагает несколько вкладок, предназначенных для конфигурирования разнообразных типов графической трансформации текущего выбранного элемента. В табл. 26.6 описаны варианты трансформации, доступные на этих вкладках (в порядке слева направо).
Испытайте каждую из описанных трансформаций,используя в качестве цели специальную фигуру (для отмены выполненной операции просто нажимайте <Ctrl+Z>). Как и многие другие аспекты раздела Transform окна Properties, каждая трансформация имеет уникальный набор параметров конфигурации, которые должны стать вполне понятными, как только вы просмотрите их. Например, редактор трансформации Skew позволяет устанавливать значения скоса х и у, а редактор трансформации Flip дает возможность зеркально отображать относительно оси х или у и т.д.
Реализации обработчиков для всех кнопок будут более или менее похожими. Мы сконфигурируем объект трансформации и присвоим его объекту
myCanvas
. Затем после запуска приложения можно будет щелкать на кнопке, чтобы просматривать результат применения трансформации. Ниже приведен полный код обработчиков (обратите внимание на установку свойства LayoutTransform
, что позволяет фигурам позиционироваться относительно родительского контейнера):
private void Flip(object sender, System.Windows.RoutedEventArgs e)
{
myCanvas.LayoutTransform = new ScaleTransform(-1, 1);
}
private void Rotate(object sender, System.Windows.RoutedEventArgs e)
{
myCanvas.LayoutTransform = new RotateTransform(180);
}
private void Skew(object sender, System.Windows.RoutedEventArgs e)
{
myCanvas.LayoutTransform = new SkewTransform(40, -20);
}
Несмотря на то что типы
Shape
позволяют генерировать интерактивную двумерную поверхность любого вида, из-за насыщенной цепочки наследования они потребляют довольно много памяти. И хотя класс Path
может помочь снизить накладные расходы за счет применения включенных геометрических объектов (вместо крупной коллекции других фигур), инфраструктура WPF предоставляет развитый API-интерфейс рисования и геометрии, который визуализирует еще более легковесные двумерные векторные изображения.
Входной точкой в этот API-интерфейс является абстрактный класс
System.Windows.Media.Drawing
(из сборки PresentationCore.dll
), который сам по себе всего лишь определяет ограничивающий прямоугольник для хранения результатов визуализации.
Инфраструктура WPF предлагает разнообразные классы, расширяющие
Drawing
, каждый из которых представляет отдельный способ рисования содержимого (табл. 26.7).
Будучи более легковесными, производные от
Drawing
типы не обладают встроенной возможностью обработки событий, т.к. они не являются UIElement
или FrameworkElement
(хотя допускают программную реализацию логики проверки попадания).
Другое ключевое отличие между типами, производными от
Drawing
, и типами, производными от Shape
, состоит в том, что производные от Drawing
типы не умеют визуализировать себя, поскольку не унаследованы от UIElement
! Для отображения содержимого производные типы должны помещаться в какой-то контейнерный объект (в частности DrawingImage
, DrawingBrush
или DrawingVisual
).
Класс
DrawingImage
позволяет помещать рисунки и геометрические объекты внутрь элемента управления Image из WPF, который обычно применяется для отображения данных из внешнего файла. Класс DrawingBrush
дает возможность строить кисть на основе рисунков и геометрических объектов, которая предназначена для установки свойства, требующего кисть. Наконец, класс DrawingVisual
используется только на "визуальном" уровне графической визуализации, полностью управляемом из кода С#.
Хотя работать с рисунками немного сложнее, чем с простыми фигурами, отделение графической композиции от графической визуализации делает типы, производные от
Drawing
, гораздо более легковесными, чем производные от Shape
типы, одновременно сохраняя их ключевые службы.
Ранее в главе элемент
Path
заполнялся группой геометрических объектов примерно так:
Поступая подобным образом, вы достигаете интерактивности
Path
при чрезвычайной легковесности, присущей геометрическим объектам. Однако если необходимо визуализировать аналогичный вывод и отсутствует потребность в любой (готовой) интерактивности, тогда тот же самый элемент
можно поместить внутрь DrawingBrush
:
При помещении группы геометрических объектов внутрь
DrawingBrush
также понадобится установить объект Pen
, применяемый для рисования границ, потому что свойство Stroke
больше не наследуется от базового класса Shape
. Здесь был создан элемент Pen
с теми же настройками, которые использовались в значениях Stroke
и StrokeThickness
из предыдущего примера Path
.
Кроме того, поскольку свойство
Fill
больше не наследуется от класса Shape
, нужно также применять синтаксис "элемент-свойство" для определения объекта кисти, предназначенного элементу DrawingGeometry
, со сплошным оранжевым цветом, как в предыдущих настройках Path
.
Теперь объект
DrawingBrush
можно использовать для установки значения любого свойства, требующего объекта кисти. Например, после подготовки следующей разметки в редакторе Kaxaml с помощью синтаксиса "элемент-свойство" можно рисовать изображение по всей поверхности Page
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Или же элемент
DrawingBrush
можно применять для установки другого совместимого с кистью свойства, такого как свойство Background
элемента Button
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Независимо от того, какое совместимое с кистью свойство устанавливается с использованием специального объекта
DrawingBrush
, визуализация двумерного графического изображения в итоге получается с намного меньшими накладными расходами, чем в случае визуализации того же изображения посредством фигур.
Тип
DrawingImage
позволяет подключать рисованный геометрический объект к элементу управления Image
из WPF. Взгляните на следующую разметку:
В данном случае элемент
GeometryDrawing
был помещен внутрь элемента DrawingImage
, а не DrawingBrush
. С применением элемента DrawingImage
можно установить свойство Source
элемента управления Image
.
По всей видимости, вы согласитесь с тем, что художнику будет довольно трудно создавать сложное векторное изображение с использованием инструментов и приемов, предоставляемых средой Visual Studio. В распоряжении художников есть собственные наборы инструментов, которые позволяют производить замечательную векторную графику. Изобразительными возможностями подобного рода не обладает ни IDE-среда Visual Studio, ни сопровождающий ее инструмент Microsoft Blend. Перед тем, как векторные изображения можно будет импортировать в приложение WPF, они должны быть преобразованы в выражения путей. После этого можно программировать с применением сгенерированной объектной модели, используя Visual Studio.
На заметку! Используемое изображение (
LaserSign.svg
) и экспортированные данные путей (LaserSign.xaml
) можно найти в подкаталоге Chapter_26 загружаемого кода примеров. Изображение взято из статьи Википедии по ссылке https://ru.wikipedia.org/wiki/Символы_опасности
.
Прежде чем можно будет импортировать сложные графические данные (такие как векторная графика) в приложение WPF, графику понадобится преобразовать в данные путей. Чтобы проиллюстрировать, как это делается, возьмите пример файла изображения
.svg
с упомянутым выше знаком опасности лазерного излучения. Затем загрузите и установите инструмент с открытым кодом под названием Inkscape
(из веб-сайта www.inkscape.org
). С помощью Inkscape
откройте файл LaserSign.svg
из подкаталога Chapter_26
. Вы можете получить запрос о модернизации формата. Установите настройки, как показано на рис. 26.12.
Следующие шаги поначалу покажутся несколько странными, но на самом деле они представляют собой простой способ преобразования векторных изображений в разметку XAML. Когда изображение приобрело желаемый вид, необходимо выбрать пункт меню File► Print (Файл►Печать). В открывшемся окне нужно ввести имя файла и выбрать место, где он должен быть сохранен, после чего щелкнуть на кнопке Save (Сохранить). В результате получается файл
*.xps
(или *.oxps
).
На заметку! В зависимости от нескольких переменных среды в конфигурации системы сгенерированный файл будет иметь либо расширение
.xps
, либо расширение .oxps
. В любом случае дальнейший процесс идентичен.
Форматы
*.xps
и *.oxps
в действительности представляют собой архивы ZIP. Переименовав расширение в .zip
, файл можно открыть в проводнике файлов (либо в 7-Zip или в предпочитаемой утилите архивации). Файл содержит иерархию папок, приведенную на рис. 26.13.
Необходимый файл находится в папке
Pages
(Documents/1/Pages
) и называется 1.fpage
. Откройте его в текстовом редакторе и скопируйте в буфер все данные кроме открывающего и закрывающего дескрипторов FixedPage
. Данные путей затем можно поместить внутрь элемента Canvas
главного окна в Kaxaml. В итоге изображение будет показано в окне XAML.
На заметку! В последней версии
Inkscape
есть возможность сохранить файл в формате Microsoft XAML. К сожалению, на момент написания главы он не был совместим с WPF.
Создайте новый проект приложения WPF по имени
InteractiveLaserSign
. Измените значения свойств Height
и Width
элемента Window
соответственно на 600
и 650
и замените элемент Grid
элементом Canvas
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:InteractiveLaserSign"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="650">
Скопируйте полную разметку XAML из файла
1.fpage
(исключая внешний дескриптор FixedPage
) и вставьте ее в элемент управления Canvas
внутри MainWindow
. Просмотрев окно в режиме проектирования, легко удостовериться в том, что знак опасности лазерного излучения успешно воспроизводится в приложении.
Заглянув в окно Document Outline, вы заметите, что каждая часть изображения представлена как XAML-элемент
Path
. Если изменить размеры элемента Window
, то качество изображения останется тем же самым безотносительно к тому, насколько большим сделано окно. Причина в том, что изображения, представленные с помощью элементов Path
, визуализируются с применением механизма рисования и математики, а не за счет манипулирования пикселями.
Вспомните, что маршрутизируемое событие распространяется туннельным и пузырьковым образом, поэтому щелчок на любом элементе
Path
внутри Canvas
может быть обработан обработчиком событий щелчка на Canvas
. Модифицируйте разметку Canvas
следующим образом:
Добавьте обработчик событий с таким кодом:
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is Path p)
{
p.Fill = new SolidColorBrush(Colors.Red);
}
}
Запустите приложение и щелкните на линиях, чтобы увидеть эффекты.
Теперь вы понимаете процесс генерации данных путей для сложной графики и знаете, как взаимодействовать с графическими данными в коде. Вы наверняка согласитесь, что наличие у профессиональных художников возможности генерировать сложные графические данные и экспортировать их в виде разметки XAML исключительно важна. После того как графические данные сохранены в файле XAML, разработчики могут импортировать разметку иписать код для взаимодействия с объектной моделью.
Последний вариант визуализации графических данных с помощью WPF называется визуальным уровнем. Ранее уже упоминалось, что доступ к нему возможен только из кода (он не дружественен по отношению к разметке XAML). Несмотря на то что подавляющее большинство приложений WPF будут хорошо работать с применением фигур, рисунков и геометрических объектов, визуальный уровень обеспечивает самый быстрый способ визуализации крупных объемов графических данных. Визуальный уровень также может быть полезен, когда необходимо визуализировать единственное изображение в крупной области. Например, если требуется заполнить фон окна простым статическим изображением, тогда визуальный уровень будет наиболее быстрым способом решения такой задачи. Кроме того, он удобен, когда нужно очень быстро менять фон окна в зависимости от ввода пользователя или чего-нибудь еще.
Далее будет построена небольшая программа, иллюстрирующая основы использования визуального уровня.
Абстрактный класс
System.Windows.Media.Visual
предлагает минимальный набор служб (визуализацию, проверку попадания, трансформации) для визуализации графики, но не предоставляет поддержку дополнительных невизуальных служб, которые могут приводить к разбуханию кода (события ввода, службы компоновки, стили и привязка данных). Класс Visual
является абстрактным базовым классом. Для выполнения действительных операций визуализации должен применяться один из его производных классов. В WPF определено несколько подклассов Visual, в том числе DrawingVisual
, Viewport3DVisual
и ContainerVisual
.
Рассматриваемый ниже пример сосредоточен только на
DrawingVisual
— легковесном классе рисования, который используется для визуализации фигур, изображений или текста.
Чтобы визуализировать данные на поверхности с применением класса
DrawingVisual
, понадобится выполнить следующие основные шаги:
• получить объект
DrawingContext
из DrawingVisual
;
• использовать объект
DrawingContext
для визуализации графических данных.
Эти два шага представляют абсолютный минимум, необходимый для визуализации каких-то данных на поверхности. Тем не менее, когда нужно, чтобы визуализируемые графические данные реагировали на вычисления при проверке попадания (что важно для добавления взаимодействия с пользователем), потребуется также выполнить дополнительные шаги:
• обновить логическое и визуальное деревья, поддерживаемые контейнером, на котором производится визуализация;
• переопределить два виртуальных метода из класса
FrameworkElement
, позволив контейнеру получать созданные визуальные данные.
Давайте исследуем последние два шага более подробно. Чтобы продемонстрировать применение класса
DrawingVisual
для визуализации двумерных данных, создайте в Visual Studio новый проект приложения WPF по имени RenderingWithVisuals
. Первой целью будет использование класса DrawingVisual
для динамического присваивания данных элементу управления Image
из WPF. Начните со следующего обновления разметки XAML окна для обработки события Loaded
:
Title="Fun With Visual Layer" Height="450" Width="800"
Loaded="MainWindow_Loaded">
Замените элемент
Grid
панелью StackPanel
и добавьте в нее элемент Image
:
Элемент управления
Image
пока не имеет значения в свойстве Source
, т.к. оно будет устанавливаться во время выполнения. С событием Loaded
связана работа по построению графических данных в памяти с применением объекта DrawingBrush
. Удостоверьтесь в том, что файл MainWindow.cs
содержит операторы using для следующих пространств имен:
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
Вот реализация обработчика события
Loaded
:
private void MainWindow_Loaded(
object sender, RoutedEventArgs e)
{
const int TextFontSize = 30;
// Создать объект System.Windows.Media.FormattedText.
FormattedText text = new FormattedText(
"Hello Visual Layer!",
new System.Globalization.CultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface(this.FontFamily, FontStyles.Italic,
FontWeights.DemiBold, FontStretches.UltraExpanded),
TextFontSize,
Brushes.Green,
null,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
// Создать объект DrawingVisual и получить объект DrawingContext.
DrawingVisual drawingVisual = new DrawingVisual();
using(DrawingContext drawingContext =
drawingVisual.RenderOpen())
{
// Вызвать любой из методов DrawingContext для визуализации данных.
drawingContext.DrawRoundedRectangle(
Brushes.Yellow, new Pen(Brushes.Black, 5),
new Rect(5, 5, 450, 100), 20, 20);
drawingContext.DrawText(text, new Point(20, 20));
}
// Динамически создать битовое изображение,
// используя данные в объекте DrawingVisual.
RenderTargetBitmap bmp = new RenderTargetBitmap(
500, 100, 100, 90, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
// Установить источник для элемента управления Image.
myImage.Source = bmp;
}
В коде задействовано несколько новых классов WPF, которые будут кратко описаны ниже. Метод начинается с создания нового объекта
FormattedText
, который представляет текстовую часть конструируемого изображения в памяти. Как видите, конструктор позволяет указывать многочисленные атрибуты, в том числе размер шрифта, семейство шрифтов, цвет переднего плана и сам текст.
Затем через вызов метода
RenderOpen()
на экземпляре DrawingVisual
получается необходимый объект DrawingContext
. Здесь в DrawingVisual
визуализируется цветной прямоугольник со скругленными углами, за которым следует форматированный текст. В обоих случаях графические данные помещаются в DrawingVisual
с применением жестко закодированных значений, что не слишком хорошо в производственном приложении, но вполне подходит для такого простого теста.
Несколько последних операторов отображают
DrawingVisual
на объект RenderTagetBitmap
, который является членом пространства имен System.Windows.Media.Imaging
. Этот класс принимает визуальный объект и трансформирует его в растровое изображение, находящееся в памяти. Затем устанавливается свойство Source
элемента управления Image
и получается вывод, показанный на рис. 26.14.
На заметку! Пространство имен
System.Windows.Media.Imaging
содержит дополнительные классы кодирования, которые позволяют сохранять находящийся в памяти объект RenderTargetBitmap
в физический файл в разнообразных форматах. Детали ищите в описании JpegBitmapEncoder
и связанных с ним классов.
Хотя применение
DrawingVisual
для рисования на фоне элемента управления WPF представляет интерес, возможно чаще придется строить специальный диспетчер компоновки (Grid
, StackPanel
, Canvas
и т.д.), который внутренне использует визуальный уровень для визуализации своего содержимого. После создания такого специального диспетчера компоновки его можно подключить к обычному элементу Window
(а также Page
или UserControl
) и позволить части пользовательского интерфейса использовать высоко оптимизированный агент визуализации, в то время как для визуализации некритичных графических данных будут применяться фигуры и рисунки.
Если дополнительная функциональность, предлагаемая специализированным диспетчером компоновки, не требуется, то можно просто расширить класс
FrameworkElement
, который обладает необходимой инфраструктурой, позволяющей содержать также и визуальные элементы. В целях иллюстрации вставьте в проект новый класс по имени CustomVisualFrameworkElement
.
Унаследуйте его от
FrameworkElement
и импортируйте пространства имен System
, System.Windows
, System.Windows.Input
, System.Windows.Media
и System.Windows.Media.Imaging
.
Класс
CustomVisualFrameworkElement
будет поддерживать переменную член типа VisualCollection
, которая содержит два фиксированных объекта DrawingVisual
(конечно, в эту коллекцию можно было бы добавлять члены с помощью мыши, но лучше сохранить пример простым). Модифицируйте код класса следующим образом:
public class CustomVisualFrameworkElement : FrameworkElement
{
// Коллекция всех визуальных объектов.
VisualCollection theVisuals;
public CustomVisualFrameworkElement()
{
// Заполнить коллекцию VisualCollection несколькими объектами DrawingVisual.
// Аргумент конструктора представляет владельца визуальных объектов.
theVisuals = new VisualCollection(this)
{AddRect(),AddCircle()};
}
private Visual AddCircle()
{
DrawingVisual drawingVisual = new DrawingVisual();
// Получить объект DrawingContext для создания нового содержимого.
using DrawingContext drawingContext =
drawingVisual.RenderOpen()
// Создать круг и нарисовать его в DrawingContext.
drawingContext.DrawEllipse(Brushes.DarkBlue, null,
new Point(70, 90), 40, 50);
return drawingVisual;
}
private Visual AddRect()
{
DrawingVisual drawingVisual = new DrawingVisual();
using DrawingContext drawingContext =
drawingVisual.RenderOpen()
Rect rect =
new Rect(new Point(160, 100), new Size(320, 80));
drawingContext.DrawRectangle(Brushes.Tomato, null, rect);
return drawingVisual;
}
}
Прежде чем специальный элемент
FrameworkElement
можно будет использовать внутри Window
, потребуется переопределить два упомянутых ранее ключевых виртуальных члена, которые вызываются внутренне инфраструктурой WPF во время процесса визуализации. Метод GetVisualChild()
возвращает из коллекции дочерних элементов дочерний элемент по указанному индексу. Свойство VisualChildrenCount
, допускающее только чтение, возвращает количество визуальных дочерних элементов внутри визуальной коллекции. Оба члена легко реализовать, т.к. всю реальную работу можно делегировать переменной-члену типа VisualCollection
:
protected override int VisualChildrenCount
=> theVisuals.Count;
protected override Visual GetVisualChild(int index)
{
// Значение должно быть больше нуля, поэтому разумно это проверить.
if (index < 0 || index >= theVisuals.Count)
{
throw new ArgumentOutOfRangeException();
}
return theVisuals[index];
}
Теперь вы располагаете достаточной функциональностью, чтобы протестировать специальный класс. Модифицируйте описание XAML элемента Window, добавив в существующий контейнер
StackPanel
один объект CustomVisualFrameworkElement
. Это потребует создания специального пространства имен XML, которое отображается на пространство имен .NET Core.
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RenderingWithVisuals"
Title="Fun with the Visual Layer" Height="350" Width="525"
Loaded="Window_Loaded" WindowStartupLocation="CenterScreen">
Результат выполнения программы показан на рис. 26.15.
Поскольку класс
DrawingVisual
не располагает инфраструктурой UIElement
или FrameworkElement
, необходимо программно добавить возможность реагирования на операции проверки попадания. Благодаря концепции логического и визуального деревьев на визуальном уровне делать это очень просто. Оказывается, что в результате написания блока XAML по существу строится логическое дерево элементов. Однако с каждым логическим деревом связано намного более развитое описание, известное как визуальное дерево, которое содержит низкоуровневые инструкции визуализации.
Упомянутые деревья подробно рассматриваются в главе 27, а сейчас достаточно знать, что до тех пор, пока специальные визуальные объекты не будут зарегистрированы в таких структурах данных, выполнять операции проверки попадания невозможно. К счастью, контейнер
VisualCollection
обеспечивает регистрацию автоматически (вот почему в аргументе конструктора необходимо передавать ссылку на специальный элемент FrameworkElement
).
Измените код класса
CustomVisualFrameworkElement
для обработки события MouseDown
в конструкторе класса с применением стандартного синтаксиса С#:
this.MouseDown += CustomVisualFrameworkElement_MouseDown;
Реализация данного обработчика будет вызывать метод
VisualTreeHelper.HitTest()
с целью выяснения, находится ли курсор мыши внутри границ одного из визуальных объектов. Для этого в одном из параметров метода HitTest()
указывается делегат HitTestResultCallback
, который будет выполнять вычисления. Добавьте в класс CustomVisualFrameworkElement
следующие методы:
void CustomVisualFrameworkElement_MouseDown(object sender, MouseButtonEventArgs e)
{
// Выяснить, где пользователь выполнил щелчок.
Point pt = e.GetPosition((UIElement)sender);
// Вызвать вспомогательную функцию через делегат, чтобы
// посмотреть, был ли совершен щелчок на визуальном объекте.
VisualTreeHelper.HitTest(this, null,
new HitTestResultCallback(myCallback), new PointHitTestParameters(pt));
}
public HitTestResultBehavior myCallback(HitTestResult result)
{
// Если щелчок был совершен на визуальном объекте, то
// переключиться между скошенной и нормальной визуализацией.
if (result.VisualHit.GetType() == typeof(DrawingVisual))
{
if (((DrawingVisual)result.VisualHit).Transform == null)
{
((DrawingVisual)result.VisualHit).Transform = new SkewTransform(7, 7);
}
else
{
((DrawingVisual)result.VisualHit).Transform = null;
}
}
// Сообщить методу HitTest() о прекращении углубления в визуальное дерево.
return HitTestResultBehavior.Stop;
}
Снова запустите программу. Теперь должна появиться возможность щелкать на любом из отображенных визуальных объектов и наблюдать за выполнением трансформации. Наряду с тем, что рассмотренный пример взаимодействия с визуальным уровнем WPF очень прост, не забывайте, что здесь можно использовать те же самые кисти, трансформации, перья и диспетчеры компоновки, которые обычно применяются в разметке XAML. Таким образом, вы уже знаете довольно много о работе с классами, производными от
Visual
.
На этом исследование служб графической визуализации WPF завершено. Несмотря на раскрытие ряда интересных тем, на самом деле мы лишь слегка затронули обширную область графических возможностей инфраструктуры WPF. Дальнейшее изучение фигур, рисунков, кистей, трансформаций и визуальных объектов вы можете продолжить самостоятельно (в оставшихся главах, посвященных WPF, еще встретятся дополнительные детали).
Поскольку Windows Presentation Foundation является настолько насыщенной графикой инфраструктурой для построения графических пользовательских интерфейсов, не должно вызывать удивления наличие нескольких способов визуализации графического вывода. Плава начиналась с рассмотрения трех подходов к визуализации (фигуры, рисунки и визуальные объекты ), а также разнообразных примитивов визуализации, таких как кисти, перья и трансформации.
Вспомните, что когда необходимо строить интерактивную двумерную визуализацию, то фигуры делают такой процесс очень простым. С другой стороны, статические, не интерактивные изображения могут визуализироваться в оптимальной манере с использованием рисунков и геометрических объектов, а визуальный уровень (доступный только в коде) обеспечит максимальный контроль и производительность.
В настоящей главе будут представлены три важные (и взаимосвязанные) темы, которые позволят углубить понимание API-интерфейса Windows Presentation Foundation (WPF). Первым делом вы изучите роль логических ресурсов. Вы увидите, что система логических ресурсов (также называемых объектными ресурсами) представляет собой способ ссылки на часто используемые объекты внутри приложения WPF. Хотя логические ресурсы нередко реализуются в разметке XAML, они могут быть определены и в процедурном коде.
Далее вы узнаете, как определять, выполнять и управлять анимационной последовательностью. Вопреки тому, что можно было подумать, применение анимации WPF не ограничивается видеоиграми или мультимедийными приложениями. В API-интерфейсе WPF анимация может использоваться, например, для подсветки кнопки, когда она получает фокус, или увеличения размера выбранной строки в
DataGrid
. Понимание анимации является ключевым аспектом построения специальных шаблонов элементов управления (как вы увидите позже в главе).
Затем объясняется роль стилей и шаблонов WPF. Подобно веб-странице, в которой применяются стили CSS или механизм тем ASP.NET, приложение WPF может определять общий вид и поведение для набора элементов управления. Такие стили можно определять в разметке и сохранять их в виде объектных ресурсов для последующего использования, а также динамически применять во время выполнения. В последнем примере вы научитесь строить специальные шаблоны элементов управления.
Первой задачей будет исследование темы встраивания и доступа к ресурсам приложения. Инфраструктура WPF поддерживает два вида ресурсов. Первый из них — двоичные ресурсы; эта категория обычно включает элементы, которые большинство программистов считают ресурсами в традиционном смысле (встроенные файлы изображений или звуковых клипов, значки, используемые приложением, и т.д.).
Вторая категория, называемая объектными ресурсами или логическими ресурсами, представляет именованные объекты .NET, которые можно упаковывать и многократно применять повсюду в приложении. Несмотря на то что упаковывать в виде объектного ресурса разрешено любой объект .NET, логические ресурсы особенно удобны при работе с графическими данными произвольного рода, поскольку можно определить часто используемые графические примитивы (кисти, перья, анимации и т.д.) и ссылаться на них по мере необходимости.
Прежде чем перейти к теме объектных ресурсов, давайте кратко проанализируем, как упаковывать двоичные ресурсы вроде значков и файлов изображений (например, логотипов компании либо изображений для анимации) внутри приложений. Создайте в Visual Studio новый проект приложения WPF по имени
BinaryResourcesApp
. Модифицируйте разметку начального окна для обработки события Loaded
элемента Window и применения DockPanel
в качестве корня компоновки:
Title="Fun with Binary Resources" Height="500" Width="649"
Loaded="MainWindow_OnLoaded">
Предположим, что приложение должно отображать внутри части окна один из трех файлов изображений, основываясь на пользовательском вводе. Элемент управления
Image
из WPF может использоваться для отображения не только типичного файла изображения (*.bmp
, *.gif
, *.ico
, *.jpg
, *.png
, *.wdp
или *.tiff
), но также данных объекта DrawingImage
(как было показано в главе 26). Можете построить пользовательский интерфейс окна, который поддерживает диспетчер компоновки DockPanel
, содержащий простую панель инструментов с кнопками Next (Вперед) и Previous (Назад). Ниже панели инструментов расположите элемент управления Image
, свойство Source
которого в текущий момент не установлено:
Margin="5" Content="Previous" Click="btnPreviousImage_Click"/>
Margin="5" Content="Next" Click="btnNextImage_Click"/>
Добавьте следующие пустые обработчики событий:
private void MainWindow_OnLoaded(
object sender, RoutedEventArgs e)
{
}
private void btnPreviousImage_Click(
object sender, RoutedEventArgs e)
{
}
private void btnNextImage_Click(
object sender, RoutedEventArgs e)
{
}
Во время загрузки окна изображения добавляются в коллекцию, по которой будет совершаться проход с помощью кнопок Next и Previous. Располагая инфраструктурой приложения, можно заняться исследованием разных вариантов ее реализации.
Один из вариантов предусматривает поставку файлов изображений в виде набора несвязанных файлов в каком-то подкаталоге внутри пути установки приложения. Начните с создания в проекте новой папки (по имени
Images
). Добавьте в папку несколько изображений, щелкнув правой кнопкой мыши внутри данной папки и выбрав в контекстном меню пункт Add►Existing Item (Добавить►Существующий элемент). В открывшемся диалоговом окне Add Existing Item (Добавление существующего элемента) измените фильтр файлов на *.*, чтобы стали видны файлы изображений. Вы можете добавлять собственные файлы изображений или задействовать три файла изображений с именами Deer.jpg
, Dogs.jpg
и Welcome.jpg
из загружаемого кода примеров.
Чтобы скопировать содержимое папки
\Images
в папку \bin\Debug
при компиляции проекта, выберите все изображения в окне Solution Explorer, щелкните правой кнопкой мыши и выберите в контекстном меню пункт Properties (Свойства); откроется окно Properties (Свойства). Установите свойство Build Action (Действие сборки) в Content (Содержимое), а свойство Copy Output Directory (Копировать в выходной каталог) в Copy always (Копировать всегда), как показано на рис. 27.1.
На заметку! Для свойства Copy Output Directory можно было бы также выбрать вариант Copy if Newer (Копировать, если новее), что позволит сократить время копирования при построении крупных проектов с большим объемом содержимого. В рассматриваемом примере варианта Copy always вполне достаточно.
После компиляции проекта появится возможность щелкнуть на кнопке Show all Files (Показать все файлы) в окне Solution Explorer и просмотреть скопированную папку
\Images
внутри \bin\Debug
(может также потребоваться щелкнуть на кнопке Refresh (Обновить)).
Инфраструктура WPF предоставляет класс по имени
BitmapImage
, определенный в пространстве имен System.Windows.Media.Imaging
. Он позволяет загружать данные из файла изображения, местоположение которого представлено объектом System.Uri
. Добавьте поле типа List
для хранения всех изображений, а также поле типа int
для хранения индекса изображения, показанного в текущий момент:
// Список файлов BitmapImage.
List _images=new List();
// Текущая позиция в списке.
private int _currImage=0;
Внутри обработчика события
Loaded
окна заполните список изображений и установите свойство Source
элемента управления Image
в первое изображение из списка:
private void MainWindow_OnLoaded(
object sender, RoutedEventArgs e)
{
try
{
string path=Environment.CurrentDirectory;
// Загрузить эти изображения из диска при загрузке окна.
_images.Add(new BitmapImage(new Uri($@"{path}\Images\Deer.jpg")));
_images.Add(new BitmapImage(new Uri($@"{path}\Images\Dogs.jpg")));
_images.Add(new BitmapImage(new Uri($@"{path}\Images\Welcome.jpg")));
// Показать первое изображение в списке.
imageHolder.Source=_images[_currImage];
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Реализуйте обработчики для кнопок Previous и Next, чтобы обеспечить проход по изображениям. Когда пользователь добирается до конца списка, происходит переход в начало и наоборот.
private void btnPreviousImage_Click(
object sender, RoutedEventArgs e)
{
if (--_currImage < 0)
{
_currImage=_images.Count - 1;
}
imageHolder.Source=_images[_currImage];
}
private void btnNextImage_Click(
object sender, RoutedEventArgs e)
{
if (++_currImage >=_images.Count)
{
_currImage=0;
}
imageHolder.Source=_images[_currImage];
}
Теперь можете запустить программу и переключаться между всеми изображениями.
Если файлы изображений необходимо встроить прямо в сборку .NET Core как двоичные ресурсы, тогда выберите файлы изображений в окне Solution Explorer (из папки
\Images
, а не \bin\Debug\Images
) и установите свойство Build Action в Resource (Ресурс), а свойство Copy to Output Directory — в Do not copy (He копировать), как показано на рис. 27.2.
В меню Build (Сборка) среды Visual Studio выберите пункт Clean Solution (Очистить решение), чтобы очистить текущее содержимое папки
\bin\Debug\Images
, и повторно скомпилируйте проект. Обновите окно Solution Explorer и удостоверьтесь в том, что данные в каталоге \bin\Debug\Images
отсутствуют. При текущих параметрах сборки графические данные больше не копируются в выходную папку, а встраиваются в саму сборку. Прием обеспечивает наличие ресурсов, но также приводит к увеличению размера скомпилированной сборки.
Вам нужно модифицировать код для загрузки изображений в список, извлекая их из скомпилированной сборки:
// Извлечь из сборки и затем загрузить изображения.
_images.Add(new BitmapImage(new Uri(@"/Images/Deer.jpg", UriKind.Relative)));
_images.Add(new BitmapImage(new Uri(@"/Images/Dogs.jpg", UriKind.Relative)));
_images.Add(new BitmapImage(new Uri(@"/Images/Welcome.jpg", UriKind.Relative)));
В таком случае больше не придется определять путь установки и можно просто задавать ресурсы по именам, которые учитывают название исходного подкаталога. Также обратите внимание, что при создании объектов
Uri
указывается значение Relative
перечисления UriKind
. В данный момент исполняемая программа представляет собой автономную сущность, которая может быть запущена из любого местоположения на машине, т.к. все скомпилированные данные находятся внутри сборки.
При построении приложения WPF часто приходится определять большой объем разметки XAML для использования во многих местах окна или возможно во множестве окон либо проектов. Например, пусть создана "безупречная" кисть с линейным градиентом, определение которой в разметке занимает 10 строк. Теперь кисть необходимо применить в качестве фонового цвета для каждого элемента
Button
в проекте, состоящем из 8 окон, т.е. всего получается 16 элементов Button
.
Худшее, что можно было бы предпринять — копировать и вставлять одну и ту же разметку XAML в каждый элемент управления
Button
. Очевидно, в итоге это могло бы стать настоящим кошмаром при сопровождении, т.к. всякий раз, когда нужно скорректировать внешний вид и поведение кисти, приходилось бы вносить изменения во многие места.
К счастью, объектные ресурсы позволяют определить фрагмент разметки XAML, назначить ему имя и сохранить в подходящем словаре для использования в будущем. Подобно двоичным ресурсам объектные ресурсы часто компилируются в сборку, где они требуются. Однако в такой ситуации нет необходимости возиться со свойством Build Action. При условии, что разметка XAML помещена в корректное местоположение, компилятор позаботится обо всем остальном.
Взаимодействие с объектными ресурсами является крупной частью процесса разработки приложений WPF. Вы увидите, что объектные ресурсы могут быть намного сложнее, чем специальная кисть. Допускается определять анимацию на основе XAML, трехмерную визуализацию, специальный стиль элемента управления, шаблон данных, шаблон элемента управления и многое другое, и упаковывать каждую сущность в многократно используемый ресурс.
Как уже упоминалось, для применения в приложении объектные ресурсы должны быть помещены в подходящий объект словаря. Каждый производный от
FrameworkElement
класс поддерживает свойство Resources
, которое инкапсулирует объект ResourceDictionary
, содержащий определенные объектные ресурсы. Объект ResourceDictionary
может хранить элементы любого типа,потому что оперирует экземплярами System.Object
и допускает манипуляции из разметки XAML или процедурного кода.
В инфраструктуре WPF все элементы управления плюс элементы
Window
, Page
(используемые при построении навигационных приложений) и UserControl
расширяют класс FrameworkElement
, так что почти все виджеты предоставляют доступ к ResourceDictionary
. Более того, класс Application
, хотя и не расширяет FrameworkElement
, но поддерживает свойство с идентичным именем Resources, которое предназначено для той же цели.
Чтобы приступить к исследованию роли объектных ресурсов, создайте в Visual Studio новый проект приложения WPF по имени
ObjectResourcesApp
и замените первоначальный элемент Grid
горизонтально выровненным диспетчером компоновки StackPanel
, внутри которого определите два элемента управления Button
(чего вполне достаточно для пояснения роли объектных ресурсов):
Выберите кнопку OK и установите в свойстве
Background
специальный тип кисти с применением интегрированного редактора кистей (который обсуждался в главе 26). Кисть помещается внутрь области между дескрипторами
и
:
Чтобы разрешить использовать эту кисть также и в кнопке Cancel (Отмена), область определения
RadialGradientBrush
должна быть расширена до словаря ресурсов родительского элемента. Например, если переместить RadialGradientBrush
в StackPanel
, то обе кнопки смогут применять одну и ту же кисть, т.к. они являются дочерними элементами того же самого диспетчера компоновки. Что еще лучше, кисть можно было бы упаковать в словарь ресурсов самого окна, в результате чего ее могли бы свободно использовать все элементы содержимого окна.
Когда необходимо определить ресурс, для установки свойства
Resources
владельца применяется синтаксис "свойство-элемент". Кроме того, элементу ресурса назначается значение х:Кеу
, которое будет использоваться другими частями окна для ссылки на объектный ресурс. Имейте в виду, что атрибуты х:Key
и х:Name
— не одно и то же! Атрибут х:Name
позволяет получать доступ к объекту как к переменной-члену в файле кода, в то время как атрибут х:Кеу
дает возможность ссылаться на элемент в словаре ресурсов.
Среда Visual Studio позволяет переместить ресурс на более высокий уровень с применением соответствующего окна Properties. Чтобы сделать это, сначала понадобится идентифицировать свойство, имеющее сложный объект, который необходимо упаковать в виде ресурса (свойство
Background
в рассматриваемом примере). Справа от свойства находится небольшой квадрат, щелчок на котором приводит к открытию всплывающего меню. Выберите в нем пункт Convert to New Resource (Преобразовать в новый ресурс), как продемонстрировано на рис. 27.3.
Будет запрошено имя ресурса (
myBrush
) и предложено указать, куда он должен быть помещен. Оставьте отмеченным переключатель This document (Этот документ), который выбирается по умолчанию (рис. 27.4).
В результате определение кисти переместится внутрь дескриптора
Window
.
Resources:
Свойство
Background
элемента управления Button
обновляется для работы с новым ресурсом:
FontSize="20" Background="{DynamicResource myBrush}"/>
Мастер создания ресурсов определил новый ресурс как динамический (
Dynamic Resource
). Динамические ресурсы рассматриваются позже, а пока поменяйте тип ресурса на статический (StaticResource
):
FontSize="20" Background="{StaticResource myBrush}"/>
Чтобы оценить преимущества, модифицируйте свойство
Background
кнопки Cancel (Отмена), указав в нем тот же самый ресурс StaticResource
, после чего можно будет видеть повторное использование в действии:
FontSize="20" Background="{StaticResource myBrush}"/>
Расширение разметки
{StaticResource}
применяет ресурс только один раз (при инициализации) ион остается "подключенным" к первоначальному объекту на протяжении всей времени жизни приложения. Некоторые свойства (вроде градиентных переходов) будут обновляться, но в случае создания нового элемента Brush
, например, элемент управления не обновится. Чтобы взглянуть на такое поведение в действии, добавьте свойство Name
и обработчик события Click
к каждому элементу управления Button
:
FontSize="20" Background="{StaticResource myBrush}" Click="Ok_OnClick"/>
FontSize="20" Background="{StaticResource myBrush}" Click="Cancel_OnClick"/>
Затем поместите в обработчик события
Ok_OnClick()
следующий код:
private void Ok_OnClick(object sender, RoutedEventArgs e)
{
// Получить кисть и внести изменение.
var b=(RadialGradientBrush)Resources["myBrush"];
b.GradientStops[1]=new GradientStop(Colors.Black, 0.0);
}
На заметку! Здесь для поиска ресурса по имени используется индексатор
Resources
. Тем не менее, имейте в виду, что если ресурс найти не удастся, тогда будет сгенерировано исключение времени выполнения. Можно также применять метод TryFindResource()
, который не приводит к генерации исключения, а просто возвращает null
, если указанный ресурс не найден.
Запустив программу и щелкнув на кнопке ОК, вы заметите,что градиенты соответствующим образом изменяются. Добавьте в обработчик события
Cancel_OnClick()
такой код:
private void Cancel_OnClick(object sender, RoutedEventArgs e)
{
// Поместить в ячейку myBrush совершенно новую кисть.
Resources["myBrush"]=new SolidColorBrush(Colors.Red);
}
Снова запустив программу и щелкнув на кнопке Cancel, вы обнаружите, что ничего не происходит!
Для свойства также можно использовать расширение разметки
DynamicResource
. Чтобы выяснить разницу, измените разметку для кнопки Cancel, как показано ниже:
FontSize="20" Background="{DynamicResource myBrush}" Click="Cancel_OnClick"/>
На этот раз в результате щелчка на кнопке Cancel цвет фона для кнопки Cancel изменяется, а цвет фона для кнопки ОК остается прежним. Причина в том, что расширение разметки
{DynamicResource}
способно обнаруживать замену внутреннего объекта, указанного посредством ключа, новым объектом. Как и можно было предположить, такая возможность требует дополнительной инфраструктуры времени выполнения, так что {StaticResource}
обычно следует использовать, только если не планируется заменять объектный ресурс другим объектом во время выполнения с уведомлением всех элементов, которые задействуют данный ресурс.
Когда в словаре ресурсов окна имеются объектные ресурсы, их могут потреблять все элементы этого окна, но не другие окна приложения. Решение совместно использовать объектные ресурсы в рамках приложения предусматривает их определение на уровне приложения, а не на уровне какого-то окна. В Visual Studio отсутствуют способы автоматизации такого действия, а потому необходимо просто вырезать имеющееся определение объекта кисти из области
Windows.Resource
и поместить его в область Application.Resources
файла Арр.xaml
.
Теперь любое дополнительное окно или элемент управления в приложении в состоянии работать с данным объектом кисти. Ресурсы уровня приложения доступны для выбора при установке свойства
Background
элемента управления (рис. 27.5).
На заметку! Помещение ресурса на уровень приложения и назначение его свойству элемента управления приводит к замораживанию ресурса, что препятствует изменению значений во время выполнения. Ресурс можно клонировать и модифицировать клон.
Ресурсов уровня приложения часто оказывается вполне достаточно, но они ничем не помогут, если ресурсы необходимо разделять между проектами. В таком случае понадобится определить то, что известно как объединенный словарь ресурсов. Считайте его библиотекой классов для ресурсов WPF; он представляет собой всего лишь файл
.xaml
, содержащий коллекцию ресурсов. Единственный проект может иметь любое требуемое количество таких файлов (один для кистей, один для анимации и т.д.), каждый из которых может быть добавлен в диалоговом окне Add New Item (Добавление нового элемента), открываемом через меню Project (рис. 27.6).
Вырежьте текущие ресурсы из области определения
Application.Resources
в новом файле МуBrushes.xaml
и перенесите их в словарь:
xmlns:local="clr-namespace:ObjectResourcesApp"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
Хотя данный словарь ресурсов является частью проекта, все словари ресурсов должны быть объединены (обычно на уровне приложения) в единый словарь ресурсов, чтобы их можно было использовать. Для этого применяется следующий формат в файле
Арр.xaml
(обратите внимание, что множество словарей ресурсов объединяются за счет добавления элементов ResourceDictionary
в область ResourceDictionary.MergedDictionaries
):
Проблема такого подхода в том, что каждый файл ресурсов потребуется добавлять в каждый проект, нуждающийся в ресурсах. Более удачный подход к разделению ресурсов заключается в определении библиотеки классов .NET Core для совместного использования проектами, чем мы и займемся.
Самый легкий способ построения сборки из одних ресурсов предусматривает создание проекта WPF User Control Library (.NET Core) (Библиотека пользовательских элементов управления WPF (.NETCore)). Создайте такой проект (по имени
MyBrushesLibrary
) в текущем решении,выбрав пункт меню Add►New Project (Добавить►Новый проект) в Visual Studio, и добавьте ссылку на него в проект ObjectResourcesApp
.
Теперь удалите файл
UserControll.xaml
из проекта. Перетащите файл MyBrushes.xaml
в проект MyBrushesLibrary
и удалите его из проекта ObjectResourcesApp
. Наконец, откройте файл MyBrushes.xaml
в проекте MyBrushesLibrary
и измените пространство имен х:local
на clr-namespace:MyBrushesLibrary
. Вот как должно выглядеть содержимое файла MyBrushes.xaml
:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyBrushesLibrary">
Скомпилируйте проект WPF User Control Library. Объедините имеющиеся двоичные ресурсы со словарем ресурсов уровня приложения из проекта
ObjectResourcesApp
. Однако такое действие требует использования довольно забавного синтаксиса:
Имейте в виду, что данная строка чувствительна к пробелам. Если возле символов двоеточия или косой черты будут присутствовать лишние пробелы, то возникнут ошибки времени выполнения. Первая часть строки представляет собой дружественное имя внешней библиотеки (без файлового расширения). После двоеточия идет слово
Component
, а за ним имя скомпилированного двоичного ресурса, которое будет идентичным имени исходного словаря ресурсов XAML.
На этом знакомство с системой управления ресурсами WPF завершено. Описанные здесь приемы придется часто применять в большинстве разрабатываемых приложений (а то и во всех). Теперь давайте займемся исследованием встроенного API-интерфейса анимации WPF.
В дополнение к службам графической визуализации, которые рассматривались в главе 26, инфраструктура WPF предлагает API-интерфейс для поддержки служб анимации. Встретив термин анимация, многим на ум приходит вращающийся логотип компании, последовательность сменяющих друг друга изображений (для создания иллюзии движения), подпрыгивающий текст на экране или программа специфического типа вроде видеоигры или мультимедийного приложения.
Наряду с тем, что API-интерфейсы анимации WPF определенно могли бы использоваться для упомянутых выше целей, анимация может применяться всякий раз, когда приложению необходимо придать особый стиль. Например, можно было бы построить анимацию для кнопки на экране, чтобы она слегка увеличивалась, когда курсор мыши находится внутри ее границ (и возвращалась к прежним размерам, когда курсор покидает границы). Или же можно было бы предусмотреть анимацию для окна, обеспечив его закрытие с использованием определенного визуального эффекта, такого как постепенное исчезновение до полной прозрачности. Применением, более ориентированным на бизнес-приложения, может быть постепенное увеличение четкости отображения сообщений об ошибках на экране, улучшая восприятие пользовательского интерфейса. Фактически поддержка анимации WPF может применяться в приложениях любого рода (бизнес-приложениях, мультимедийных программах, видеоиграх и т.д.) всякий раз, когда нужно создать более привлекательное впечатление у пользователей.
Как и для многих других аспектов WPF, с построением анимации не связано ничего нового. Единственная особенность заключается в том, что в отличие от других API-интерфейсов, которые вы могли использовать в прошлом (включая Windows Forms), разработчики не обязаны создавать необходимую инфраструктуру вручную. В WPF не придется заранее создавать фоновые потоки или таймеры, применяемые для продвижения вперед анимационной последовательности, определять специальные типы для представления анимации, очищать и перерисовывать изображения либо реализовывать утомительные математические вычисления. Подобно другим аспектам WPF анимацию можно строить целиком в разметке XAML, целиком в коде C# либо с использованием комбинации того и другого.
На заметку! В среде Visual Studio отсутствует поддержка создания анимации посредством каких-либо графических инструментов и потому разметку XAML необходимо вводить вручную. Тем не менее, поставляемый в составе Visual Studio 2019 продукт Blend на самом деле имеет встроенный редактор анимации, который способен существенно упростить решение задач.
Чтобы разобраться в поддержке анимации WPF, потребуется начать с рассмотрения классов анимации из пространства имен
System.Windows.Media.Animation
сборки PresentationCore.dll
. Здесь вы найдете свыше 100 разных классов, которые содержат слово Animation
в своих именах.
Все классы такого рода могут быть отнесены к одной из трех обширных категорий. Во-первых, любой класс, который следует соглашению об именовании вида ТипДанных
Animation
(ByteAnimation
, ColorAnimation
, DoubleAnimation
, Int32Animation
и т.д.), позволяет работать с анимацией линейной интерполяцией. Она обеспечивает плавное изменение значения во времени от начального к конечному.
Во-вторых, классы, следующие соглашению об именовании вида ТипДанных
AnimationUsingKeyFrames
(StringAnimationUsingKeyFrames
, DoubleAnimationUsingKeyFrames
, PointAnimationUsingKeyFrames
и т.д.), представляют анимацию ключевыми кадрами, которая позволяет проходить в цикле по набору определенных значений за указанный период времени. Например, ключевые кадры можно применять для изменения надписи на кнопке, проходя в цикле по последовательности индивидуальных символов.
В-третьих, классы, которые следуют соглашению об именовании вида ТипДанных
AnimationUsingPath
(DoubleAnimationUsingPath
, PointAnimationtJsingPath
и т.п.), представляют анимацию на основе пути, позволяющую перемещать объекты по определенному пути. Например, в приложении глобального позиционирования (GPS) анимацию на основе пути можно использовать для перемещения элемента по кратчайшему маршруту к месту, указанному пользователем.
Вполне очевидно, упомянутые классы не применяются для того, чтобы напрямую предоставить анимационную последовательность переменной определенного типа данных (в конце концов, как можно было бы выполнить анимацию значения 9, используя объект
Int32Animation?
).
В качестве примера возьмем свойства
Height
и Width
типа Label
, которые являются свойствами зависимости, упаковывающими значение double
. Чтобы определить анимацию, которая будет увеличивать высоту метки с течением времени, можно подключить объект DoubleAnimation
к свойству Height
и позволить WPF позаботиться о деталях выполнения действительной анимации. Или вот другой пример: если требуется реализовать переход цвета кисти от зеленого до желтого в течение 5 секунд, то это можно сделать с применением типа ColorAnimation
.
Следует уяснить, что классы
Animation
могут подключаться к любому свойству зависимости заданного объекта, которое имеет соответствующий тип. Как объяснялось в главе 25, свойства зависимости являются специальной формой свойств, которую требуют многие службы WPF, включая анимацию, привязку данных и стили.
По соглашению свойство зависимости определяется как статическое, доступное только для чтения поле класса, имя которого образуется добавлением слова
Property
к нормальному имени свойства. Например, для обращения к свойству зависимости для свойства Height
класса Button
в коде будет использоваться Button.HeightProperty
.
Во всех классах
Animation
определены следующие ключевые свойства, которые управляют начальным и конечным значениями, применяемыми для выполнения анимации:
•
То
— представляет конечное значение анимации;
•
From
— представляет начальное значение анимации;
•
By
— представляет общую величину, на которую анимация изменяет начальное значение.
Несмотря на тот факт, что все классы поддерживают свойства
То
, From
и By
, они не получают их через виртуальные члены базового класса. Причина в том, что лежащие в основе типы, упакованные внутри указанных свойств, варьируются в широких пределах (целые числа, цвета, объекты Thickness
и т.д.), и представление всех возможностей через единственный базовый класс привело бы к очень сложным кодовым конструкциям.
В связи со сказанным может возникнуть вопрос: почему не использовались обобщения .NET для определения единственного обобщенного класса анимации с одиночным параметром типа (скажем,
Animate
)? Опять-таки, поскольку существует огромное количество типов данных (цвета, векторы, целые числа, строки и т.д.), применяемых для анимации свойств зависимости, решение оказалось бы не настолько ясным, как можно было бы ожидать (не говоря уже о том, что XAML обеспечивает лишь ограниченную поддержку обобщенных типов).
Хотя для определения виртуальных свойств
То
, From
и By
не использовался единственный базовый класс, классы Animation
все же разделяют общий базовый класс — System.Windows.Media.Animation.Timeline
. Данный тип предлагает набор дополнительных свойств, которые управляют темпом продвижения анимации (табл. 27.1).
Вы построите окно, содержащее элемент
Button
, который обладает довольно странным поведением: когда на него наводится курсор мыши, он вращается вокруг своего левого верхнего угла. Начните с создания в Visual Studio нового проекта приложения WPF по имени SpinningButtonAnimationApp
. Модифицируйте начальную разметку, как показано ниже (обратите внимание на обработку события MouseEnter
кнопки):
MouseEnter="btnSpinner_MouseEnter" Click="btnSpinner_OnClick"/>
В файле отделенного кода импортируйте пространство имен
System.Windows.Media.Animation
и добавьте в файл C# следующий код:
private bool _isSpinning=false;
private void btnSpinner_MouseEnter(
object sender, MouseEventArgs e)
{
if (!_isSpinning)
{
_isSpinning=true;
// Создать объект DoubleAnimation и зарегистрировать
// его с событием Completed.
var dblAnim=new DoubleAnimation();
dblAnim.Completed +=(o, s)=> { _isSpinning=false; };
// Установить начальное и конечное значения.
dblAnim.From=0;
dblAnim.To=360;
// Создать объект RotateTransform и присвоить
// его свойству RenderTransform кнопки.
var rt=new RotateTransform();
btnSpinner.RenderTransform=rt;
// Выполнить анимацию объекта RotateTransform.
rt.BeginAnimation(RotateTransform.AngleProperty, dblAnim);
}
}
private void btnSpinner_OnClick(
object sender, RoutedEventArgs e)
{
}
Первая крупная задача метода
btnSpinner_MouseEnter()
связана с конфигурированием объекта DoubleAnimation
, который будет начинать со значения 0
и заканчивать значением 360
. Обратите внимание, что для этого объекта также обрабатывается событие Completed
, где переключается булевская переменная уровня класса, которая применяется для того, чтобы выполняющаяся анимация не была сброшена в начало.
Затем создается объект
RotateTransform
, который подключается к свойству RenderTransform
элемента управления Button
(btnSpinner
). Наконец, объект RenderTransform
информируется о начале анимации его свойства Angle
с использованием объекта DoubleAnimation
. Реализация анимации в коде обычно осуществляется путем вызова метода BeginAnimation()
и передачи ему лежащего в основе свойства зависимости, к которому необходимо применить анимацию (вспомните, что по соглашению оно определено как статическое поле класса), и связанного объекта анимации.
Добавьте в программу еще одну анимацию, которая заставит кнопку после щелчка плавно становиться невидимой. Для начала создайте обработчик события
Click
кнопки btnSpinner
с приведенным ниже кодом:
private void btnSpinner_OnClick(
object sender, RoutedEventArgs e)
{
var dblAnim=new DoubleAnimation
{
From=1.0,
To=0.0
};
btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}
В коде обработчика события
btnSpinner_Click()
изменяется свойство Opacity
, чтобы постепенно скрыть кнопку из виду. Однако в настоящий момент это затруднительно, потому что кнопка вращается слишком быстро. Как можно управлять ходом анимации? Ответ на вопрос ищите ниже.
По умолчанию анимация будет занимать приблизительно одну секунду для перехода между значениями, которые присвоены свойствам
From
и То
. Следовательно, кнопка располагает одной секундой, чтобы повернуться на 360 градусов, и в то же время в течение одной секунды она постепенно скроется из виду (после щелчка на ней).
Определить другой период времени для перехода анимации можно посредством свойства
Duration
объекта анимации, которому присваивается объект Duration
. Обычно промежуток времени устанавливается путем передачи объекта TimeSpan
конструктору класса Duration
. Взгляните на показанное далее изменение, в результате которого кнопке будет выделено четыре секунды на вращение:
private void btnSpinner_MouseEnter(
object sender, MouseEventArgs e)
{
if (!_isSpinning)
{
_isSpinning=true;
// Создать объект DoubleAnimation и зарегистрировать
// его с событием Completed.
var dblAnim=new DoubleAnimation();
dblAnim.Completed +=(o, s)=> { _isSpinning=false; };
// На завершение поворота кнопке отводится четыре секунды.
dblAnim.Duration=new Duration(TimeSpan.FromSeconds(4));
...
}
}
Благодаря такой модификации у вас должен появиться шанс щелкнуть на кнопке во время ее вращения, после чего она плавно исчезнет.
На заметку! Свойство
BeginTime
класса Animation
также принимает объект TimeSpan
. Вспомните, что данное свойство можно устанавливать для указания времени ожидания перед запуском анимационной последовательности.
За счет установки в
true
свойства AutoReverse
объектам Animation
указывается о необходимости запуска анимации в обратном порядке по ее завершении. Например, если необходимо, чтобы кнопка снова стала видимой после исчезновения, можно написать следующий код:
private void btnSpinner_OnClick(object sender, RoutedEventArgs e)
{
DoubleAnimation dblAnim=new DoubleAnimation
{
From=1.0,
To=0.0
};
// После завершения запустить в обратном порядке.
dblAnim.AutoReverse=true;
btnSpinner.BeginAnimation(Button.OpacityProperty, dblAnim);
}
Если нужно, чтобы анимация повторялась несколько раз (или никогда не прекращалась), тогда можно воспользоваться свойством
RepeatBehavior
, общим для всех классов Animation
. Передавая конструктору простое числовое значение, можно указать жестко закодированное количество повторений. С другой стороны, если передать конструктору объект TimeSpan
, то можно задать время, в течение которого анимация должна повторяться. Наконец, чтобы выполнять анимацию бесконечно, свойство RepeatBehavior
можно установить в RepeatBehavior.Forever
. Взгляните на следующие способы изменения поведения повтора одного из двух объектов DoubleAnimation
, применяемых в примере:
// Повторять бесконечно.
dblAnim.RepeatBehavior=RepeatBehavior.Forever;
// Повторять три раза.
dblAnim.RepeatBehavior=new RepeatBehavior(3);
// Повторять в течение 30 секунд.
dblAnim.RepeatBehavior=new RepeatBehavior(TimeSpan.FromSeconds(30));
Итак, исследование приемов добавления анимации к аспектам какого-то объекта с использованием кода C# и API-интерфейса анимации WPF завершено. Теперь посмотрим, как делать то же самое с помощью разметки XAML.
Реализация анимации в разметке подобна ее реализации в коде, по крайней мере, для простых анимационных последовательностей. Когда необходимо создать более сложную анимацию, которая включает изменение значений множества свойств одновременно, объем разметки может заметно увеличиться. Даже в случае применения какого-то инструмента для генерирования анимации, основанной на разметке XAML, важно знать основы представления анимации в XAML, поскольку тогда облегчается задача модификации и настройки сгенерированного инструментом содержимого.
На заметку! В подкаталоге
XamlAnimations
внутри Chapter_27
есть несколько файлов XAML. Скопируйте их содержимое в редактор Kaxaml, чтобы просмотреть результаты.
Большей частью создание анимации подобно всему тому, что вы уже видели: по-прежнему производится конфигурирование объекта
Animation
, который затем ассоциируется со свойством объекта. Тем не менее, крупное отличие связано с тем, что разметка XAML не является дружественной к вызовам методов. В результате вместо вызова BeginAnimation()
используется раскадровка как промежуточный уровень.
Давайте рассмотрим полный пример анимации, определенной в терминах XAML, и подробно ее проанализируем. Приведенное далее определение XAML будет отображать окно, содержащее единственную метку. После того как объект
Label
загрузился в память, он начинает анимационную последовательность, во время которой размер шрифта увеличивается от 12 до 100 точек за период в четыре секунды. Анимация будет повторяться столько времени, сколько объект остается загруженным в память. Разметка находится в файле GrowLabelFont.xaml
, так что его содержимое необходимо скопировать в редактор Kaxaml, нажать клавишу <F5> и понаблюдать за поведением.
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="200" Width="600" WindowStartupLocation="CenterScreen"
Title="Growing Label Font!">
RepeatBehavior="Forever"/>
А теперь подробно разберем пример.
При продвижении от самого глубоко вложенного элемента наружу первым встречается элемент
, обращающийся к тем же самым свойствам, которые устанавливались в процедурном коде(From
, То
, Duration
и RepeatBehavior
):
RepeatBehavior="Forever"/>
Как упоминалось ранее, элементы
Animation
помещаются внутрь элемента Storyboard
, применяемого для отображения объекта анимации на заданное свойство родительского типа через свойство TargetProperty
, которым в данном случае является FontSize
. Элемент Storyboard
всегда находится внутри родительского элемента по имени BeginStoryboard
:
RepeatBehavior="Forever"/>
После того как элемент
BeginStoryboard
определен, должно быть указано действие какого-то вида, которое приведет к запуску анимации. Инфраструктура WPF предлагает несколько разных способов реагирования на условия времени выполнения в разметке, один из которых называется триггером. С высокоуровневой точки зрения триггер можно считать способом реагирования на событие в разметке XAML без необходимости в написании процедурного кода.
Обычно когда ответ на событие реализуется в С#, пишется специальный код, который будет выполнен при поступлении события. Однако триггер — всего лишь способ получить уведомление о том, что некоторое событие произошло (загрузка элемента в память, наведение на него курсора мыши, получение им фокуса и т.д.).
Получив уведомление о появлении события, можно запускать раскадровку. В показанном ниже примере обеспечивается реагирование на факт загрузки элемента
Label
в память. Поскольку вас интересует событие Loaded
элемента Label
, элемент EventTrigger
помещается в коллекцию триггеров элемента Label
:
RepeatBehavior="Forever"/>
Рассмотрим еще один пример определения анимации в XAML, на этот раз анимации ключевыми кадрами.
В отличие от объектов анимации линейной интерполяцией, обеспечивающих только перемещение между начальной и конечной точками, объекты анимации ключевыми кадрами позволяют создавать коллекции специальных значений, которые должны достигаться в определенные моменты времени.
Чтобы проиллюстрировать применение типа дискретного ключевого кадра, предположим, что необходимо построить элемент управления
Button
, который выполняет анимацию своего содержимого так, что на протяжении трех секунд появляется значение ОК!
по одному символу за раз. Представленная далее разметка находится в файле StringAnimation.xaml
. Ее можно скопировать в редактор Kaxaml и просмотреть результаты.
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="100" Width="300"
WindowStartupLocation="CenterScreen" Title="Animate String Data!">
FontSize="16pt" FontFamily="Verdana" Width="100">
Storyboard.TargetProperty="Content"
Duration="0:0:3">
Первым делом обратите внимание, что для кнопки определяется триггер события, который обеспечивает запуск раскадровки при загрузке кнопки в память. Класс
StringAnimationUsingKeyFrames
отвечает за изменение содержимого кнопки через значение Storyboard.TargetProperty
.
Внутри элемента
StringAnimationUsingKeyFrames
определены четыре элемента DiscreteStringKeyFrame
, которые изменяют свойство Content
на протяжении двух секунд (длительность, установленная объектом StringAnimationUsingKeyFrames
, составляет в сумме три секунды, поэтому между финальным символом !
и следующим появлением О
будет заметна небольшая пауза).
Теперь, когда вы получили некоторое представление о том, как строятся анимации в коде C# и разметке XAML, давайте выясним роль стилей WPF, которые интенсивно задействуют графику, объектные ресурсы и анимацию.
При построении пользовательского интерфейса приложения WPF нередко требуется обеспечить общий вид и поведение для целого семейства элементов управления. Например, может понадобиться сделать так, чтобы все типы кнопок имели ту же самую высоту, ширину, цвет и размер шрифта для своего строкового содержимого. Хотя решить задачу можно было бы установкой идентичных значений в индивидуальных свойствах, такой подход затрудняет внесение изменений, потому что при каждом изменении придется переустанавливать один и тот же набор свойств во множестве объектов.
К счастью, инфраструктура WPF предлагает простой способ ограничения внешнего вида и поведения связанных элементов управления с использованием стилей. Выражаясь просто, стиль WPF — это объект, который поддерживает коллекцию пар "свойство-значение". С точки зрения программирования отдельный стиль представляется с помощью класса
System.Windows.Style
. Класс Style
имеет свойство по имени Setters
, которое открывает доступ к строго типизированной коллекции объектов Setter
. Именно объект Setter
обеспечивает возможность определения пар "свойство-значение".
В дополнение к коллекции
Setters
класс Style
также определяет несколько других важных членов, которые позволяют встраивать триггеры, ограничивать место применения стиля и даже создавать новый стиль на основе существующего (воспринимайте такой прием как "наследование стилей"). Ниже перечислены наиболее важные члены класса Style
:
•
Triggers
— открывает доступ к коллекции объектов триггеров, которая делает возможной фиксацию условий возникновения разнообразных событий в стиле;
•
BasedOn
— разрешает строить новый стиль на основе существующего;
•
TargetType
— позволяет ограничивать место применения стиля.
Почти в каждом случае объект
Style
упаковывается как объектный ресурс. Подобно любому объектному ресурсу его можно упаковывать на уровне окна или на уровне приложения, а также внутри выделенного словаря ресурсов (что замечательно, поскольку делает объект Style
легко доступным во всех местах приложения). Вспомните, что цель заключается в определении объекта Style
, который наполняет (минимум) коллекцию Setters
набором пар "свойство-значение".
Давайте построим стиль, который фиксирует базовые характеристики шрифта элемента управления в нашем приложении. Начните с создания в Visual Studio нового проекта приложения WPF по имени
WpfStyles
. Откройте файл App.xaml
и определите в нем следующий именованный стиль:
Обратите внимание, что объект
BasicControlStyle
добавляет во внутреннюю коллекцию три объекта Setter
. Теперь примените получившийся стиль к нескольким элементам управления в главном окне. Из-за того, что стиль является объектным ресурсом, элементы управления, которым он необходим, по-прежнему должны использовать расширение разметки {StackResource}
или {DynamicResource}
для нахождения стиля. Когда они находят стиль, то устанавливают элемент ресурса в идентично именованное свойство Style
. Замените стандартный элемент управления Grid
следующей разметкой:
Style="{StaticResource BasicControlStyle}" Width="150"/>
Style="{StaticResource BasicControlStyle}" Width="250"/>
Если вы просмотрите элемент
Window
в визуальном конструкторе Visual Studio (или запустите приложение), то обнаружите, что оба элемента управления поддерживают те же самые курсор, высоту и размер шрифта.
В то время как оба элемента управления подчиняются стилю, после применения стиля к элементу управления вполне допустимо изменять некоторые из определенных настроек. Например, элемент
Button
теперь использует курсор Help
(вместо курсора Hand
, определенного в стиле):
Cursor="Help" Style="{StaticResource BasicControlStyle}" Width="250" />
Стили обрабатываются перед настройками индивидуальных свойств элемента управления, к которому применен стиль; следовательно, элементы управления могут "переопределять" настройки от случая к случаю.
В настоящий момент наш стиль определен так, что его может задействовать любой элемент управления (и он должен делать это явно, устанавливая свое свойство
Style
), поскольку каждое свойство уточнено посредством класса Control
. Для программы, определяющей десятки настроек, в результате получился бы значительный объем повторяющегося кода. Один из способов несколько улучшить ситуацию предусматривает использование атрибута TargetType
. Добавление атрибута TargetType
к открывающему дескриптору Style
позволяет точно указать, где стиль может быть применен (в данном примере внутри файла Арр.xaml
):
На заметку! При построении стиля, использующего базовый класс, нет нужды беспокоиться о том, что значение присваивается свойству зависимости, которое не поддерживается производными типами. Если производный тип не поддерживает заданное свойство зависимости, то оно игнорируется.
Кое в чем прием помог, но все равно вы имеете стиль, который может применяться к любому элементу управления. Атрибут
TargetType
более удобен, когда необходимо определить стиль, который может быть применен только к отдельному типу элементов управления. Добавьте в словарь ресурсов приложения следующий стиль:
Такой стиль будет работать только с элементами управления
Button
(или подклассами Button
). Если применить его к несовместимому элементу, тогда возникнут ошибки разметки и компиляции. Добавьте элемент управления Button, который использует новый стиль:
Style="{StaticResource BigGreenButton}" Width="250" Cursor="Help"/>
Результирующий вывод представлен на рис. 27.7.
Еще один эффект от атрибута
TargetType
заключается в том, что стиль будет применен ко всем элементам данного типа внутри области определения стиля при условии, что свойство х:Key
отсутствует.
Вот еще один стиль уровня приложения, который будет автоматически применяться ко всем элементам управления
TextBox
в текущем приложении:
Теперь можно определять любое количество элементов управления
TextBox
, и все они автоматически получат установленный внешний вид. Если какому-то элементу управления TextBox
не нужен такой стандартный внешний вид, тогда он может отказаться от него, установив свойство StyleB {x:Null}
. Например, элемент txtTest
будет иметь неименованный стандартный стиль, а элемент txtTest2
сделает все самостоятельно:
BorderThickness="5" Height="60" Width="100" Text="Ha!"/>
Новые стили можно также строить на основе существующего стиля посредством свойства
BasedOn
. Расширяемый стиль должен иметь подходящий атрибут х:Кеу
в словаре, т.к. производный стиль будет ссылаться на него по имени, используя расширение разметки {StaticResource}
или {DynamicResource}
. Ниже представлен новый стиль, основанный на стиле BigGreenButton
, который поворачивает элемент управления Button
на 20 градусов:
BasedOn="{StaticResource BigGreenButton}">
Чтобы применить новый стиль, модифицируйте разметку для кнопки следующим образом:
Style="{StaticResource TiltButton}" Width="250" Cursor="Help"/>
Такое действие изменяет внешний вид изображения, как показано на рис. 27.8.
Стили WPF могут также содержать триггеры за счет упаковки объектов
Trigger
в коллекцию Triggers
объекта Style
. Использование триггеров в стиле позволяет определять некоторые элементы Setter
таким образом, что они будут применяться только в случае истинности заданного условия триггера. Например, возможно требуется увеличивать размер шрифта, когда курсор мыши находится над кнопкой. Или, скажем, нужно подсветить текстовое поле, имеющее фокус, с использованием фона указанного цвета. Триггеры полезны в ситуациях подобного рода, потому что они позволяют предпринимать специфические действия при изменении свойства, не требуя написания явной логики С# в файле отделенного кода.
Далее приведена модифицированная разметка для стиля элементов управления типа
TextBox
, где обеспечивается установка фона желтого цвета, когда элемент TextBox
получает фокус:
При тестировании этого стиля вы обнаружите, что по мере перехода с помощью клавиши <ТаЬ> между элементами
TextBox
текущий выбранный TextBox
получает фон желтого цвета (если только стиль не отключен путем присваивания {x:Null}
свойству Style
).
Триггеры свойств также весьма интеллектуальны в том смысле, что когда условие триггера не истинно, то свойство автоматически получает стандартное значение. Следовательно, как только
TextBox
теряет фокус, он также автоматически принимает стандартный цвет без какой-либо работы с вашей стороны. По контрасту с ними триггеры событий (которые исследовались при рассмотрении анимации WPF) не возвращаются автоматически в предыдущее состояние.
Триггеры могут быть спроектированы так, что определенные элементы
Setter
будут применяться, когда истинными должны оказаться многие условия. Пусть необходимо устанавливать фон элемента TextBox
в Yellow
только в случае, если он имеет активный фокус и курсор мыши находится внутри его границ. Для этого можно воспользоваться элементом MultiTriggern
определить в нем каждое условие:
Стили также могут содержать в себе триггеры, которые запускают анимационную последовательность. Ниже показан последний стиль, который после применения к элементам управления
Button
заставит их увеличиваться и уменьшаться в размерах, когда курсор мыши находится внутри границ кнопки:
Duration="0:0:2" AutoReverse="True"/>
Здесь коллекция
Triggers
наблюдает за тем, когда свойство IsMouseOver
возвратит значение true
. После того как это произойдет, определяется элемент Trigger.EnterActions
для выполнения простой раскадровки, которая заставляет кнопку за две секунды увеличиться до значения Height
, равного 200
(и затем возвратиться к значению Height
, равному 40
). Чтобы отслеживать другие изменения свойств, можно также добавить область Trigger.ExitActions
и определить в ней любые специальные действия, которые должны быть выполнены, когда IsMouseOver
изменяется на false
.
Вспомните, что стиль может применяться также во время выполнения. Прием удобен, когда у конечных пользователей должна быть возможность выбора внешнего вида для их пользовательского интерфейса, требуется принудительно устанавливать внешний вид и поведение на основе настроек безопасности (например, стиль
DisableAllButton
) или еще в какой-то ситуации.
В текущем проекте было определено порядочное количество стилей, многие из которых могут применяться к элементам управления
Button
. Давайте переделаем пользовательский интерфейс главного окна, чтобы позволить пользователю выбирать имена имеющихся стилей в элементе управления ListBox
. На основе выбранного имени будет применен соответствующий стиль. Вот финальная разметка для элемента DockPanel
:
SelectionChanged="comboStyles_Changed" />
Элемент управления
ListBox
(по имени IstStyles
) будет динамически заполняться внутри конструктора окна:
public MainWindow()
{
InitializeComponent();
// Заполнить окно со списком всеми стилями для элементов Button.
lstStyles.Items.Add("GrowingButtonStyle");
lstStyles.Items.Add("TiltButton");
lstStyles.Items.Add("BigGreenButton");
lstStyles.Items.Add("BasicControlStyle");}
}
Последней задачей является обработка события
SelectionChanged
в связанном файле кода. Обратите внимание, что в следующем коде имеется возможность извлечения текущего ресурса по имени с использованием унаследованного метода TryFindResouce()
:
private void comboStyles_Changed(object sender, SelectionChangedEventArgs e)
{
// Получить имя стиля, выбранное в окне со списком.
var currStyle=(Style)TryFindResource(lstStyles.SelectedValue);
if (currStyle==null) return;
// Установить стиль для типа кнопки.
this.btnStyle.Style=currStyle;
}
После запуска приложения появляется возможность выбора одного из четырех стилей кнопок на лету. На рис. 27.9 показано готовое приложение в действии.
Теперь, когда вы понимаете, что собой представляют стили и ресурсы, есть еще несколько тем, которые потребуется раскрыть, прежде чем приступать к изучению построения специальных элементов управления. В частности, необходимо выяснить разницу между логическим деревом, визуальным деревом и стандартным шаблоном. При вводе разметки XAML в Visual Studio или в редакторе вроде Kaxaml разметка является логическим представлением документа XAML. В случае написания кода С#, который добавляет в элемент управления
StackPanel
новые элементы, они вставляются в логическое дерево. По существу логическое представление отражает то, как содержимое будет позиционировано внутри разнообразных диспетчеров компоновки для главного элемента Window
(или другого корневого элемента, такого как Page
или NavigationWindow
).
Однако за каждым логическим деревом стоит намного более сложное представление, которое называется визуальным деревом и внутренне применяется инфраструктурой WPF для корректной визуализации элементов на экране. Внутри любого визуального дерева будут находиться полные детали шаблонов и стилей, используемых для визуализации каждого объекта, включая все необходимые рисунки, фигуры, визуальные объекты и объекты анимации.
Полезно уяснить разницу между логическим и визуальным деревьями, потому что при построении специального шаблона элемента управления на самом деле производится замена всего или части стандартного визуального дерева элемента управления собственным вариантом. Следовательно, если нужно, чтобы элемент управления
Button
визуализировался в виде звездообразной фигуры, тогда можно определить новый шаблон такого рода и подключить его к визуальному дереву Button
. Логически тип остается тем же типом Button
, поддерживая все ожидаемые свойства, методы и события. Но визуально он выглядит совершенно по-другому. Один лишь упомянутый факт делает WPF исключительно полезным API-интерфейсом, поскольку другие инструментальные наборы для создания кнопки звездообразной формы потребовали бы построения совершенно нового класса. В инфраструктуре WPF понадобится просто определить новую разметку.
На заметку! Элементы управления WPF часто описывают как лишенные внешности. Это относится к тому факту, что внешний вид элемента управления WPF совершенно не зависит от его поведения и допускает настройку.
Хотя анализ логического дерева окна во время выполнения — не слишком распространенное действие при программировании с применением WPF, полезно упомянуть о том, что в пространстве имен
System.Windows
определен класс LogicalTreeHelper
, который позволяет инспектировать структуру логического дерева во время выполнения. Для иллюстрации связи между логическими деревьями, визуальными деревьями и шаблонами элементов управления создайте новый проект приложения WPF по имени TreesAndTemplatesApp
.
Замените элемент
Grid
приведенной ниже разметкой, которая содержит два элемента управления Button
и крупный допускающий только чтение элемент TextBox
с включенными линейками прокрутки. Создайте в IDE-среде обработчики событий Click
для каждой кнопки. Вот результирующая разметка XAML:
Margin="4" BorderBrush="Blue" Height="40"
Click="btnShowLogicalTree_Click"/>
BorderBrush="Blue" Height="40" Click="btnShowVisualTree_Click"/>
Background="AliceBlue" IsReadOnly="True"
BorderBrush="Red" VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility
="Auto" />
Внутри файла кода C# определите переменную-член
_dataToShow
типа string
. В обработчике события Click
объекта btnShowLogicalTree
вызовите вспомогательную функцию,которая продолжит вызывать себя рекурсивно с целью заполнения строковой переменной логическим деревом Window
. Для этого будет вызван статический метод GetChildren()
объекта LogicalTreeHelper
. Ниже показан необходимый код:
private string _dataToShow=string.Empty;
private void btnShowLogicalTree_Click(object sender, RoutedEventArgs e)
{
_dataToShow="";
BuildLogicalTree(0, this);
txtDisplayArea.Text=_dataToShow;
}
void BuildLogicalTree(int depth, object obj)
{
// Добавить имя типа к переменной-члену _dataToShow.
_dataToShow +=new string(' ', depth) + obj.GetType().Name + "\n";
// Если элемент - не DependencyObject, тогда пропустить его.
if (!(obj is DependencyObject))
return;
// Выполнить рекурсивный вызов для каждого логического дочернего элемента.
foreach (var child in LogicalTreeHelper.GetChildren((DependencyObject)obj))
{
BuildLogicalTree(depth + 5, child);
}
}
private void btnShowVisualTree_Click(
object sender, RoutedEventArgs e)
{
}
После запуска приложения и щелчка на кнопке Logical Tree of Window (Логическое дерево окна) в текстовой области отобразится древовидное представление, которое выглядит почти как точная копия исходной разметки XAML (рис. 27.10).
Визуальное дерево объекта
Window
также можно инспектировать во время выполнения с использованием класса VisualTreeHelper
из пространства имен System.Windows.Media
. Далее приведена реализация обработчика события Click
для второго элемента управления Button
(btnShowVisualTree
), которая выполняет похожую рекурсивную логику с целью построения текстового представления визуального дерева:
using System.Windows.Media;
private void btnShowVisualTree_Click(object sender, RoutedEventArgs e)
{
_dataToShow="";
BuildVisualTree(0, this);
txtDisplayArea.Text=_dataToShow;
}
void BuildVisualTree(int depth, DependencyObject obj)
{
// Добавить имя типа к переменной-члену _dataToShow.
_dataToShow +=new string(' ', depth) + obj.GetType().Name + "\n";
// Выполнить рекурсивный вызов для каждого визуального дочернего элемента.
for (int i=0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
BuildVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));
}
}
На рис. 27.11 видно, что визуальное дерево открывает доступ к нескольким низкоуровневым агентам визуализации, таким как
ContentPresenter
, AdornerDecorator
, TextBoxLineDrawingVisual
и т.д.
Вспомните, что визуальное дерево применяется инфраструктурой WPF для выяснения, каким образом визуализировать элемент
Window
и все содержащиеся в нем элементы. Каждый элемент управления WPF хранит собственный набор команд визуализации внутри своего стандартного шаблона. С точки зрения программирования любой шаблон может быть представлен как экземпляр класса ControlTemplate
. Кроме того, стандартный шаблон элемента управления можно получить через свойство Template
:
// Получить стандартный шаблон элемента Button.
Button myBtn=new Button();
ControlTemplate template=myBtn.Template;
Подобным же образом можно создать в коде новый объект
ControlTemplate
и подключить его к свойству Template
элемента управления:
// Подключить новый шаблон для использования в кнопке.
Button myBtn=new Button();
ControlTemplate customTemplate=new ControlTemplate();
// Предположим, что этот метод добавляет весь код для звездообразного шаблона.
MakeStarTemplate(customTemplate);
myBtn.Template=customTemplate;
Наряду с тем, что новый шаблон можно строить в коде, намного чаще это делается в разметке XAML. Тем не менее, прежде чем приступить к построению собственных шаблонов, завершите текущий пример и добавьте возможность просмотра стандартного шаблона для элемента управления WPF во время выполнения, что может оказаться полезным способом ознакомления с общей структурой шаблона Добавьте в разметку окна новую панель
StackPanel
с элементами управления; она стыкована с левой стороной главной панели DockPanel
(находится прямо перед элементом
) и определена следующим образом:
BorderThickness="4"
Width="358">
FontWeight="DemiBold" />
Background="BlanchedAlmond" Height="22"
Text="System.Windows.Controls.Button" />
Height="40" Width="100" Margin="5" Click="btnTemplate_Click"
HorizontalAlignment="Left" />
Width="301" Margin="10" Background="LightGreen" >
Добавьте пустой обработчик события
btnTemplate_Click()
:
private void btnTemplate_Click(
object sender, RoutedEventArgs e)
{
}
Текстовая область слева вверху позволяет вводить полностью заданное имя элемента управления WPF, расположенного в сборке
PresentationFramework.dll
. После того как библиотека загружена, экземпляр элемента управления динамически создается и отображается в большом квадрате слева внизу. Наконец, в текстовой области справа будет отображаться стандартный шаблон элемента управления. Добавьте в класс C# новую переменную-член типа Control
:
private Control _ctrlToExamine=null;
Ниже показан остальной код, который требует импортирования пространств имен
System.Reflection.System.Xml
и System.Windows.Markup
:
private void btnTemplate_Click(
object sender, RoutedEventArgs e)
{
_dataToShow="";
ShowTemplate();
txtDisplayArea.Text=_dataToShow;
}
private void ShowTemplate()
{
// Удалить элемент, который в текущий момент находится
// в области предварительного просмотра.
if (_ctrlToExamine !=null)
stackTemplatePanel.Children.Remove(_ctrlToExamine);
try
{
// Загрузить PresentationFramework и создать экземпляр
// указанного элемента управления. Установить его размеры для
// отображения, а затем добавить в пустой контейнер StackPanel.
Assembly asm=Assembly.Load("PresentationFramework, Version=4.0.0.0," +
"Culture=neutral, PublicKeyToken=31bf3856ad364e35");
_ctrlToExamine=(Control)asm.CreateInstance(txtFullName.Text);
_ctrlToExamine.Height=200;
_ctrlToExamine.Width=200;
_ctrlToExamine.Margin=new Thickness(5);
stackTemplatePanel.Children.Add(_ctrlToExamine);
// Определить настройки XML для предохранения отступов.
var xmlSettings=new XmlWriterSettings{Indent=true};
// Создать объект StringBuilder для хранения разметки XAML.
var strBuilder=new StringBuilder();
// Создать объект XmlWriter на основе имеющихся настроек.
var xWriter=XmlWriter.Create(strBuilder, xmlSettings);
// Сохранить разметку XAML в объекте XmlWriter на основе ControlTemplate.
XamlWriter.Save(_ctrlToExamine.Template, xWriter);
// Отобразить разметку XAML в текстовом поле.
_dataToShow=strBuilder.ToString();
}
catch (Exception ex)
{
_dataToShow=ex.Message;
}
}
Большая часть работы связана с отображением скомпилированного ресурса BAML на строку разметки XAML. На рис. 27.12 демонстрируется финальное приложение в действии на примере вывода стандартного шаблона для элемента управления
System.Windows.Controls.DatePicker
. Здесь отображается календарь, который доступен по щелчку на кнопке в правой части элемента управления.
К настоящему моменту вы должны лучше понимать взаимосвязь между логическими деревьями, визуальными деревьями и стандартными шаблонами элементов управления. Остаток главы будет посвящен построению специальных шаблонов и пользовательских элементов управления.
Специальный шаблон для элемента управления можно создавать с помощью только кода С#. Такой подход предусматривает добавление данных к объекту
ControlTemplate
и затем присваивание его свойству Template
элемента управления. Однако большую часть времени внешний вид и поведение ControlTemplate
будут определяться с использованием разметки XAML и фрагментов кода (мелких или крупных) для управления поведением во время выполнения.
В оставшемся материале главы вы узнаете, как строить специальные шаблоны с применением Visual Studio. Попутно вы ознакомитесь с инфраструктурой триггеров WPF и научитесь использовать анимацию для встраивания визуальных подсказок конечным пользователям. Применение при построении сложных шаблонов только IDE-среды Visual Studio может быть связано с довольно большим объемом клавиатурного набора и трудной работы. Конечно, шаблоны производственного уровня получат преимущество от использования продукта Blend, устанавливаемого вместе с Visual Studio. Тем не менее, поскольку текущее издание книги не включает описание Blend, время засучить рукава и приступить к написанию некоторой разметки.
Для начала создайте новый проект приложения WPF по имени
ButtonTemplate
. Основной интерес в данном проекте представляют механизмы создания и применения шаблонов, так что замените элемент Grid
следующей разметкой:
В обработчике события
Click
просто отображается окно сообщения (посредством вызова MessageBox.Show()
) с подтверждением щелчка на элементе управления. При построении специальных шаблонов помните, что поведение элемента управления неизменно, но его внешний вид может варьироваться.
В настоящее время этот элемент
Button
визуализируется с использованием стандартного шаблона, который представляет собой ресурс BAML внутри заданной сборки WPF, как было проиллюстрировано в предыдущем примере. Определение собственного шаблона по существу сводится к замене стандартного визуального дерева своим вариантом. Для начала модифицируйте определение элемента Button
, указав новый шаблон с применением синтаксиса "свойство-элемент". Шаблон придаст элементу управления округлый вид.
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontWeight="Bold" FontSize="20" Content="OK!"/>
Здесь определен шаблон, который состоит из именованного элемента
Grid
, содержащего именованные элементы Ellipse
и Label
. Поскольку в Grid
не определены строки и столбцы, каждый дочерний элемент укладывается поверх предыдущего элемента управления, позволяя центрировать содержимое. Если вы теперь запустите приложение, то заметите, что событие Click
будет инициироваться только в ситуации, когда курсор мыши находится внутри границ элемента Ellipse
(т.е. не на углах, окружающих эллипс). Это замечательная возможность архитектуры шаблонов WPF, т.к. нет нужды повторно вычислять попадание курсора, проверять граничные условия или предпринимать другие низкоуровневые действия. Таким образом, если шаблон использует объект Polygon
для отображения какой-то необычной геометрии, тогда можно иметь уверенность в том, что детали проверки попадания курсора будут соответствовать форме элемента управления, а не более крупного ограничивающего прямоугольника.
В текущий момент ваш шаблон внедрен в специфический элемент управления
Button
, что ограничивает возможности его многократного применения. В идеале шаблон круглой кнопки следовало бы поместить в словарь ресурсов, чтобы его можно было использовать в разных проектах, или как минимум перенести в контейнер ресурсов приложения для многократного применения внутри проекта. Давайте переместим локальный ресурс Button
на уровень приложения, вырезав определение шаблона из разметки Button
и вставив его в дескриптор Application.Resources
внутри файла Арр.xaml
. Добавьте атрибуты Key
и TargetType
:
HorizontalAlignment="Center"
FontWeight="Bold" FontSize="20" Content="OK!"/>
Модифицируйте разметку для
Button
, как показано далее:
Click="myButton_Click"
Template="{StaticResource RoundButtonTemplate}">
Из-за того, что этот ресурс доступен всему приложению, можно определять любое количество круглых кнопок, просто применяя имеющийся шаблон. В целях тестирования создайте два дополнительных элемента управления
Button
, которые используют данный шаблон (обрабатывать событие Click
для них не нужно):
Click="myButton_Click"
Template="{StaticResource RoundButtonTemplate}">
Template="{StaticResource RoundButtonTemplate}">
Template="{StaticResource RoundButtonTemplate}">
При определении специального шаблона также удаляются все визуальные подсказки стандартного шаблона. Например, стандартный шаблон кнопки содержит разметку,которая задает внешний вид элемента управления при возникновении определенных событий пользовательского интерфейса, таких как получение фокуса, щелчок кнопкой мыши, включение (или отключение) и т.д. Пользователи довольно хорошо приучены к визуальным подсказкам подобного рода, т.к. они придают элементу управления некоторую осязаемую реакцию. Тем не менее, в шаблоне
RoundButtonTemplate
разметка такого типа не определена и потому внешний вид элемента управления остается идентичным независимо от действий мыши. В идеальном случае элемент должен выглядеть немного по-другому, когда на нем совершается щелчок (возможно, за счет изменения цвета или отбрасывания тени), чтобы уведомить пользователя об изменении визуального состояния.
Задачу можно решить с применением триггеров, как вы только что узнали. Для простых операций триггеры работают просто великолепно. Существуют дополнительные способы достижения цели, которые выходят за рамки настоящей книги, но больше информации доступно по адресу
https://docs.microsoft.com/ru-ru/dotnet/desktop/wpf/controls/how-to-create-apply-template
.
В качестве примера обновите шаблон
RoundButtonTemplate
разметкой, которая добавляет два триггера. Первый триггер будет изменять цвет фона на синий, а цвет переднего плана на желтый, когда курсор находится на поверхности элемента управления. Второй триггер уменьшит размеры элемента Grid
(а также его дочерних элементов) при нажатии кнопки мыши, когда курсор расположен в пределах элемента.
FontSize="20" FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
Value="Blue"/>
Property="Foreground" Value="Yellow"/>
Property="RenderTransformOrigin" Value="0.5,0.5"/>
Property="RenderTransform">
Проблема с шаблоном элемента управления связана с тем, что каждая кнопка выглядит и содержит тот же самый текст. Следующее обновление разметки не оказывает никакого влияния:
Background="Red" Content="Howdy!" Click="myButton_Click"
Template="{StaticResource RoundButtonTemplate}" />
Background="LightGreen" Content="Cancel!"
Template="{StaticResource RoundButtonTemplate}" />
Background="Yellow" Content="Format"
Template="{StaticResource RoundButtonTemplate}" />
Причина в том, что стандартные свойства элемента управления (такие как
BackGround
и Content
) переопределяются в шаблоне. Чтобы они стали доступными, их потребуется отобразить на связанные свойства в шаблоне. Решить такие проблемы можно за счет использования расширения разметки {TemplateBinding}
при построении шаблона. Оно позволяет захватывать настройки свойств, которые определены элементом управления, применяющим шаблон, и использовать их при установке значений в самом шаблоне.
Ниже приведена переделанная версия шаблона
RoundButtonTemplate
, в которой расширение разметки {TemplateBinding}
применяется для отображения свойства Background
элемента Button
на свойство Fill
элемента Ellipse
; здесь также обеспечивается действительная передача значения Content
элемента Button
свойству Content
элемента Label
:
FontSize="20" FontWeight="Bold" HorizontalAlignment="Center"
VerticalAlignment="Center" />
После такого обновления появляется возможность создания кнопок с разными цветами и текстом. Результат обновления разметки XAML представлен на рис.27.13.
При проектировании шаблона для отображения текстового значения элемента управления использовался элемент
Label
. Подобно Button
он поддерживает свойство Content
. Следовательно, если применяется расширение разметки {TemplateBinding}
, тогда можно определять элемент Button
со сложным содержимым, а не только с простой строкой.
Но что, если необходимо передать сложное содержимое члену шаблона, который не имеет свойства
Content
? Когда в шаблоне требуется определить обобщенную область отображения содержимого, то вместо элемента управления специфического типа (Label
или TextBox
) можно использовать класс ContentPresenter
. Хотя в рассматриваемом примере в этом нет нужды, ниже показана простая разметка, иллюстрирующая способ построения специального шаблона, который применяет класс ContentPresenter
для отображения значения свойства Content
элемента управления, использующего шаблон:
В данный момент наш шаблон просто определяет базовый внешний вид и поведение элемента управления
Button
. Тем не менее, за процесс установки базовых свойств элемента управления (содержимого, размера шрифта, веса шрифта и т.д.) отвечает сам элемент Button
:
FontWeight="Bold"
Template="{StaticResource RoundButtonTemplate}"
Click="myButton_Click"/>
При желании значения базовых свойств можно устанавливать в шаблоне. В сущности, таким способом фактически создаются стандартный внешний вид и поведение. Как вам уже должно быть понятно, это работа стилей WPF. Когда строится стиль (для учета настроек базовых свойств), можно определить шаблон внутри стиля! Ниже показан измененный ресурс приложения внутри файла
App.xaml
, которому назначен ключ RoundButtonSyle
:
После такого обновления кнопочные элементы управления можно создавать с установкой свойства
Style
следующим образом:
Click="myButton_Click" Style="{StaticResource RoundButtonStyle}"/>
Несмотря на то что внешний вид и поведение кнопки остаются такими же, преимущество внедрения шаблонов внутрь стилей связано с тем, что появляется возможность предоставить готовый набор значений для общих свойств. На этом обзор применения Visual Studio и инфраструктуры триггеров при построении специальных шаблонов для элемента управления завершен. Хотя об инфраструктуре WPF можно еще много чего сказать, теперь у вас имеется хороший фундамент для дальнейшего самостоятельного изучения.
Первой в главе рассматривалась система управления ресурсами WPF. Мы начали с исследования работы с двоичными ресурсами и роли объектных ресурсов. Вы узнали, что объектные ресурсы представляют собой именованные фрагменты разметки XAML, которые могут быть сохранены в разнообразных местах с целью многократного использования содержимого.
Затем был описан API-интерфейс анимации WPF. В приведенных примерах анимация создавалась с помощью кода С#, а также посредством разметки XAML. Для управления выполнением анимации, определенной в разметке, применяются элементы
Storyboard
и триггеры. Далее был продемонстрирован механизм стилей WPF, который интенсивно использует графику, объектные ресурсы и анимацию.
После этого вы прояснили отношение между логическим и визуальным деревьями. В своей основе логическое дерево является однозначным соответствием разметке, которая создана для описания корневого элемента WPF. Позади логического дерева находится гораздо более глубокое визуальное дерево, содержащее детальные инструкции визуализации.
Кроме того, вы изучили роль стандартного шаблона. Не забывайте, что при построении специальных шаблонов вы по существу заменяете все визуальное дерево элемента управления (или часть дерева) собственной реализацией.
В настоящей главе исследование программной модели WPF завершается рассмотрением возможностей, которые поддерживаются паттерном "модель-представление-модель представления" (Model View ViewModel — MWM). Вы также узнаете о системе уведомлений WPF и ее реализации паттерна "Наблюдатель" (Observer) через наблюдаемые модели и коллекции. Обеспечение автоматического отображения пользовательским интерфейсом текущего состояния данных значительно улучшает его восприятие конечными пользователями и сокращает объем ручного кодирования, требуемого для получения того же результата с помощью более старых технологий (вроде Windows Forms).
Во время разработки на основе паттерна "Наблюдатель" вы ознакомитесь с механизмами добавления проверки достоверности в свои приложения. Проверка достоверности — жизненно важная часть любого приложения, которая позволяет не только сообщать пользователю о том, что что-то пошло не так, но и указывать, в чем именно заключается проблема. Вы научитесь встраивать проверку достоверности в разметку представления для информирования пользователя о возникающих ошибках.
Затем вы более глубоко погрузитесь в систему команд WPF и создадите специальные команды для инкапсуляции программной логики почти так, как поступали в главе 25 со встроенными командами. С созданием специальных команд связано несколько преимуществ, включая (помимо прочего) возможность многократного использования кода, инкапсуляцию логики и разделение обязанностей.
Наконец, вы задействуете все это в примере приложения MWM.
Прежде чем приступить к детальному исследованию уведомлений, проверки достоверности и команд в WPF, было бы неплохо пролить свет на конечную цель настоящей главы, которой является паттерн "модель-представление-модель представления" (MWM). Будучи производным от паттерна проектирования "Модель представления" (Presentation Model) Мартина Фаулера, паттерн MWM задействует обсуждаемые в главе возможности, специфичные для XAML, чтобы сделать процесс разработки приложений WPF более быстрым и ясным. Само название паттерна отражает его основные компоненты: модель (Model), представление (View) и модель представления (ViewModel).
Модель — это объектное представление имеющихся данных. В паттерне MWM модели концептуально совпадают с моделями внутри нашего уровня доступа к данным (Data Access Layer — DAL). Иногда они являются теми же физическими классами, но поступать так вовсе не обязательно. По мере чтения главы вы узнаете, каким образом решать, применять ли модели DAL или же создавать новые модели.
Модели обычно используют в своих интересах встроенную (либо специальную) проверку достоверности через аннотации данных и интерфейс
INotifyDataErrorInfo
и сконфигурированы как наблюдаемые классы для связывания с системой уведомлений WPF. Все упомянутые темы рассматриваются позже в главе.
Представление — это пользовательский интерфейс приложения, который спроектирован так, чтобы быть чрезвычайно легковесным. Вспомните о стенде меню в ресторане для автомобилистов. На стенде отображаются позиции меню и цены, а также имеется механизм взаимодействия клиента с внутренними системами. Однако в стенд не внедрены какие-либо интеллектуальные возможности, разве что он может быть снабжен специальной логикой пользовательского интерфейса, такой как включение освещения в темное время суток.
Представления MWM должны разрабатываться с учетом аналогичных целей. Любые интеллектуальные возможности необходимо встраивать в какие-то другие места приложения. Иметь прямое отношение к манипулированию пользовательским интерфейсом может только код в файле отделенного кода (например, в
MainWindow.xaml.cs
). Он не должен быть основан на бизнес-правилах или на чем-то еще, что нуждается в предохранении для будущего применения. Хотя это не является главной целью MWM, хорошо разработанные приложения MWM обычно имеют совсем небольшой объем отделенного кода.
В WPF и других технологиях XAML модель представления служит двум целям.
• Модель представления предлагает единственное местоположение для всех данных, необходимых представлению. Это вовсе не означает, что модель представления отвечает за получение действительных данных; взамен она является просто транспортным механизмом для перемещения данных из хранилища в представление. Обычно между представлениями и моделями представлений имеется отношение "один к одному", но существуют архитектурные отличия, которые в каждом конкретном случае могут варьироваться.
• Вторая цель модели представления касается ее действия в качестве контроллера для представления. Почти как стенд меню модель представления принимает указание от пользователя и передает их соответствующему коду для выполнения подходящих действий. Довольно часто такой код имеет форму специальных команд.
На заре развития WPF, когда разработчики все еще были в поиске лучшей реализации паттерна MWM, велись бурные (а временами и жаркие) дискуссии о том, где реализовывать элементы, подобные проверке достоверности и паттерну "Наблюдатель". Один лагерь (сторонников анемичной (иногда называемой бескровной) модели) аргументировал, что все элементы должны находиться в моделях представлений, поскольку добавление таких возможностей к модели нарушает принцип разделения обязанностей. Другой лагерь (сторонников анемичной модели представления) утверждал, что все элементы должны находиться в моделях, т.к. тогда сокращается дублирование кода.
Конечно, фактический ответ зависит от обстоятельств. Реализация классами моделей интерфейсов
INotifyPropertyChanged
, IDataErrorInfо
и INotifyDataErrorInfo
гарантирует, что соответствующий код близок к своей цели (как вы увидите далее в главе) и реализован только однократно для каждой модели. Другими словами, есть ситуации, когда сами классы моделей представлений необходимо разрабатывать как наблюдаемые. По большому счету вы должны самостоятельно выяснить, что имеет больший смысл для приложения, не приводя к чрезмерному усложнению кода и не принося в жертву преимущества MWM.
На заметку! Для WPF доступны многочисленные инфраструктуры MWM, такие как MWMLite, Caliburn.Micro и Prism (хотя Prism — нечто намного большее, чем просто инфраструктура MWM). В настоящей главе обсуждается паттерн MWM и функциональные средства WPF, которые поддерживают его реализацию. Исследование других инфраструктур и выбор среди них наиболее подходящей для нужд приложения остается за вами как разработчиком.
Значительным недостатком системы привязки Windows Forms является отсутствие уведомлений. Если находящиеся внутри представления данные модифицируются в коде, то пользовательский интерфейс также должен обновляться программно, чтобы оставаться в синхронном состоянии с ними. Итогом будет большое количество вызовов метода
Refresh()
на элементах управления, обычно превышающее абсолютно необходимое для обеспечения безопасности. Наряду с тем, что наличие слишком многих обращений к Refresh()
обычно не приводит к серьезной проблеме с производительностью, недостаточное их число может отрицательно повлиять на пользовательский интерфейс.
Система привязки, встроенная в приложения на основе XAML, устраняет указанную проблему за счет того, что позволяет привязывать объекты данных и коллекции к системе уведомлений, разрабатывая их как наблюдаемые. Всякий раз, когда изменяется значение свойства в наблюдаемой модели либо происходит изменение в наблюдаемой коллекции (например, добавление, удаление или переупорядочение элементов), инициируется событие (
NotifyPropertyChanged
либо NotifyCollectionChanged
). Инфраструктура привязки автоматически прослушивает такие события и в случае их появления обновляет привязанные элементы управления. Более того, разработчики имеют контроль над тем, для каких свойств выдаются уведомления. Выглядит безупречно, не так ли? На самом деле все не настолько безупречно. Настройка наблюдаемых моделей вручную требует написания довольно большого объема кода. К счастью, как вы вскоре увидите, существует инфраструктура с открытым кодом, которая значительно упрощает работу.
В этом разделе вы построите приложение, в котором используются наблюдаемые модели и коллекции. Для начала создайте новый проект приложения WPF по имени
WpfNotifications
. В приложении будет применяться форма "главная-подробности", которая позволит пользователю выбирать объект автомобиля в элементе управления ComboBox
и просматривать детальную информацию о нем в расположенных ниже элементах управления TextBox
. Поместите в файл MainWindow.xaml
следующую разметку:
SharedSizeGroup="CarLabels"/>
DisplayMemberPath="PetName" />
SharedSizeGroup="CarLabels"/>
HorizontalAlignment="Right" Orientation="Horizontal" Margin="0,5,0,5">
Padding="4, 2"/>
Окно должно напоминать показанное на рис. 28.1.
Свойство
IsSharedSizeScope
элемента управления Grid
заставляет дочерние сетки разделять размеры. Элемент ColumnDefinitions
, помеченный как SharedSizeGroup
, автоматически получит ту же самую ширину без каких-либо потребностей в программировании. В рассматриваемом примере, если размер метки Pet Name (Дружественное имя) изменяется из-за более длинного значения, тогда соответствующим образом корректируется и размер колонки Vehicle (Автомобиль), который находится в другом элементе управления Grid
, сохраняя аккуратный внешний вид окна.
Щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer, выберите в контекстном меню пункт Add►New Folder (Добавить►Новая папка) и назначьте новой папке имя
Models
. Создайте в новой папке файл класса Car.cs
. Первоначально код класса выглядит так:
public class Car
{
public int Id { get; set; }
public string Make { get; set; }
public string Color { get; set; }
public string PetName { get; set; }
}
Следующий шаг заключается в создании операторов привязки для элементов управления. Вспомните, что конструкции привязки данных вращаются вокруг контекста данных, который может быть установлен в самом элементе управления или в родительском элементе управления. Здесь контекст будет установлен в элементе
DetailsGrid
, так что каждый содержащийся внутри него элемент управления унаследует результирующий контекст данных.
Установите свойство
DataContext
в свойство SelectedItem
элемента ComboBox
. Модифицируйте определение элемента Grid
, содержащего элементы управления с информацией об автомобиле, следующим образом:
DataContext="{Binding ElementName=cboCars, Path=SelectedItem}">
Текстовые поля в элементе
DetailsGrid
будут отображать индивидуальные характеристики выбранного автомобиля. Добавьте подходящие атрибуты Text
и привязки к элементам управления TextBox
:
Наконец, поместите нужные данные в элемент управления
ComboBox
. В файле MainWindow.xaml.cs
создайте новый список записей Car
и присвойте его свойству ItemsSource
элемента ComboBox
. Кроме того, добавьте оператор using
для пространства имен WpfNotifications.Models
.
using WpfNotifications.Models;
// Для краткости код не показан.
public partial class MainWindow : Window
{
readonly IList _cars = new List();
public MainWindow()
{
InitializeComponent();
_cars.Add(new Car {Id = 1, Color = "Blue", Make = "Chevy",
PetName = "Kit"});
_cars.Add(new Car {Id = 2, Color = "Red", Make = "Ford",
PetName = "Red Rider"});
cboCars.ItemsSource = _cars;
}
}
Запустите приложение. Вы увидите, что в поле со списком Vehicle для выбора доступны два варианта автомобилей. Выбор одного из них приводит к автоматическому заполнению текстовых полей сведениями об автомобиле. Измените цвет одного из автомобилей, выберите другой автомобиль и затем возвратитесь к автомобилю, запись о котором редактировалась. Вы обнаружите, что новый цвет по-прежнему связан с автомобилем. Здесь нет ничего примечательного, просто демонстрируется мощь привязки данных XAML.
Несмотря на то что предыдущий пример работает ожидаемым образом, когда данные изменяются программно, пользовательский интерфейс не отразит изменения до тех пор, пока в приложении не будет предусмотрен код для обновления данных. Чтобы проиллюстрировать сказанное, добавьте обработчик события
Click
для кнопки btnChangeColor
:
Button x:Name="btnChangeColor" Content="Change Color" Margin="5,0,5,0"
Padding="4, 2" Click="BtnChangeColor_OnClick"/>
Внутри обработчика события
BtnChangeColor_OnClick()
с помощью свойства SelectedItem
элемента управления ComboBox
отыщите выбранную запись в списке автомобилей и измените ее цвет на Pink
:
private void BtnChangeColor_OnClick(object sender, RoutedEventArgs e)
{
_cars.First(x => x.Id == ((Car)cboCars.SelectedItem)?.Id).Color = "Pink";
}
Запустите приложение, выберите автомобиль и щелкните на кнопке Change Color (Изменить цвет). Никаких видимых изменений не произойдет. Выберите другой автомобиль и затем снова первоначальный. Теперь вы заметите обновленное значение. Для пользователя такое поведение не особенно подходит.
Добавьте обработчик события
Click
для кнопки btnAddCar
:
Click="BtnAddCar_OnClick" />
В обработчике события
BtnAddCar_OnClick()
добавьте новую запись в список Car
:
private void BtnAddCar_Click(object sender, RoutedEventArgs e)
{
var maxCount = _cars?.Max(x => x.Id) ?? 0;
_cars?.Add(new Car { Id=++maxCount,Color="Yellow",Make="VW",PetName="Birdie"});
}
Запустите приложение, щелкните на кнопке Add Car (Добавить автомобиль) и просмотрите содержимое элемента управления
ComboBox
. Хотя известно, что в списке имеется три автомобиля, в элементе ComboBox
отображаются только два! Чтобы устранить обе проблемы, вы превратите класс Car
в наблюдаемую модель и будете использовать наблюдаемую коллекцию для хранения всех экземпляров Car
.
Проблема с тем, что изменение значения свойства модели не отображается в пользовательском интерфейсе, решается за счет реализации классом модели
Car
интерфейса INotifyPropertyChanged
. Интерфейс INotifyPropertyChanged
содержит единственное событие PropertyChangedEvent
. Механизм привязки XAML прослушивает это событие для каждого привязанного свойства в классах, реализующих интерфейс INotifyPropertyChanged
. Вот как определен интерфейс INotifyPropertyChanged
:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
Добавьте в файл
Car.cs
следующие операторы using
:
using System.ComponentModel;
using System.Runtime.CompilerServices;
Затем обеспечьте реализацию классом
Car
интерфейса INotifyPropertyChanged
:
public class Car : INotifyPropertyChanged
{
// Для краткости код не показан.
public event PropertyChangedEventHandler PropertyChanged;
}
Событие
PropertyChanged
принимает объектную ссылку и новый экземпляр класса PropertyChangedEventArgs
:
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs("Model"));
Первый параметр представляет собой объект, который инициирует событие. Конструктор класса
PropertyChangedEventArgs
принимает строку, указывающую свойство, которое было изменено и нуждается в обновлении. Когда событие инициировано, механизм привязки ищет элементы управления, привязанные к именованному свойству данного объекта. В случае передачи конструктору PropertyChangedEventArgs
значения String.Empty
обновляются все привязанные свойства объекта.
Вы сами управляете тем, какие свойства вовлечены в процесс автоматического обновления. Автоматически обновляться будут только те свойства, которые генерируют событие
PropertyChanged
внутри блока set
. Обычно в перечень входят все свойства классов моделей, но в зависимости от требований приложения некоторые свойства можно опускать. Вместо инициирования события PropertyChanged
непосредственно в блоке set
для каждого задействованного свойства распространенный подход предусматривает написание вспомогательного метода (как правило, называемого OnPropertyChanged()
), который генерирует событие от имени свойств обычно в базовом классе для моделей. Добавьте в класс Car
следующий метод:
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
Модифицируйте каждое автоматическое свойство класса
Car
, чтобы оно имело полноценные блоки get
и set
, а также поддерживающее поле. В случае если значение изменилось, вызовите вспомогательный метод OnPropertyChanged()
. Вот обновленное свойство Id
:
private int _id;
public int Id
{
get => _id;
set
{
if (value == _id) return;
_id = value;
OnPropertyChanged();
}
}
Проделайте аналогичную работу со всеми остальными свойствами в классе и снова запустите приложение. Выберите автомобиль и щелкните на кнопке Change Color. Изменение немедленно отобразится в пользовательском интерфейсе. Первая проблема решена!
В версии C# 6 появилась операция
nameof
, которая возвращает строковое имя переданного ей элемента. Ее можно применять в вызовах метода OnPropertyChanged()
внутри блоков set
, например:
public string Color
{
get { return _color; }
set
{
if (value == _color) return;
_color = value;
OnPropertyChanged(nameof(Color));
}
}
Обратите внимание на то, что в случае использования операции
nameof
удалять атрибут [CallerMemberName]
из метода OnPropertyChanged()
необязательно (хотя он становится излишним). В конце концов, выбор между применением операции nameof
или атрибута CallerMemberName
зависит от личных предпочтений.
Следующей проблемой, которую необходимо решить, является обновление пользовательского интерфейса при изменении содержимого коллекции, что достигается путем реализации интерфейса
INotifyCollectionChanged
. Подобно INotifyPropertyChanged
данный интерфейс открывает доступ к единственному событию CollectionChanged
. В отличие от INotifyPropertyChanged
реализация интерфейса INotifyCollectionChanged
вручную предполагает больший объем действий, чем просто вызов метода в блоке set
свойства. Понадобится создать реализацию полного списка объектов и генерировать событие CollectionChanged
каждый раз, когда он изменяется.
К счастью, существует намного более легкий способ, чем создание собственных классов коллекций. Класс
ObservableCollection
реализует интерфейсы INotifyCollectionChanged
, INotifyPropertyChanged
и Collection
и входит в состав .NET Core. Никакой дополнительной работы делать не придется. Чтобы продемонстрировать его применение, добавьте оператор using
для пространства имен System.Collections.ObjectModel
и модифицируйте закрытое поле _cars
следующим образом:
private readonly IList _cars =
new ObservableCollection();
Снова запустите приложение и щелкните на кнопке Add Car. Новые записи будут должным образом появляться.
Еще одним преимуществом наблюдаемых моделей является способность отслеживать изменения состояния. Отслеживать флаги изменения (т.е. когда изменяется одно и более значений объекта) в WPF довольно легко. Добавьте в класс
Car
свойство типа bool
по имени IsChanged
. Внутри его блока set
вызовите метод OnPropertyChanged()
, как поступали с другими свойствами класса Car
.
private bool _isChanged;
public bool IsChanged {
get => _isChanged;
set
{
if (value == _isChanged) return;
_isChanged = value;
OnPropertyChanged();
}
}
Свойство
IsChanged
необходимо устанавливать в true
внутри метода OnPropertyChanged()
. Важно не устанавливать свойство IsChanged
в true
в случае изменения его самого, иначе сгенерируется исключение переполнения стека! Модифицируйте метод OnPropertyChanged()
следующим образом (здесь используется описанная ранее операция nameof
):
protected virtual void OnPropertyChanged(
[CallerMemberName] string propertyName = "")
{
if (propertyName != nameof(IsChanged))
{
IsChanged = true;
}
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
}
Откройте файл
MainWindow.xaml
и добавьте в DetailsGrid
дополнительный элемент RowDefinition
. Поместите в конец элемента Grid
показанную ниже разметку, которая содержит элементы управления Label
и Checkbox
, привязанные к свойству IsChanged
:
Margin="10,0,0,0" IsEnabled="False" IsChecked="{Binding Path=IsChanged}" />
Если вы запустите приложение прямо сейчас, то увидите, что каждая отдельная запись отображается как измененная, хотя пока ничего не изменялось! Дело в том, что во время создания объекта устанавливаются значения свойств, а установка любых значений приводит к вызову метода
OnPropertyChanged()
, который и устанавливает свойство IsChanged
объекта. Чтобы устранить проблему, установите свойство IsChanged
в false
последним в коде инициализации объекта. Откройте файл MainWindow.xaml.cs
и модифицируйте код создания списка:
_cars.Add(
new Car {Id = 1, Color = "Blue", Make = "Chevy",
PetName = "Kit", IsChanged = false});
_cars.Add(
new Car {Id = 2, Color = "Red", Make = "Ford",
PetName = "Red Rider", IsChanged =
false});
Снова запустите приложение, выберите автомобиль и щелкните на кнопке Change Color. Флажок Is Changed (Изменено) становится отмеченным наряду с изменением цвета.
Во время выполнения приложения можно заметить, что при вводе в текстовых полях флажок Is Changed не становится отмеченным до тех пор, пока фокус не покинет элемент управления, где производился ввод. Причина кроется в свойстве
UpdateSourceTrigger
привязок элементов TextBox
.
Свойство
UpdateSourceTrigger
определяет, какое событие (изменение значения, переход фокуса и т.д.) является основанием для обновления пользовательским интерфейсом лежащих в основе данных. Перечисление UpdateSourceTrigger
принимает значения, описанные в табл. 28.1.
Стандартным событием обновления для элементов управления
TextBox
является LostFocus
. Измените его на PropertyChanged
, модифицировав привязку для элемента TextBox
, который отвечает за ввод цвета:
Если вы запустите приложение и начнете ввод в текстовом поле Color (Цвет), то флажок Is Changed немедленно отметится. Может возникнуть вопрос о том, почему для элементов управления
TextBox
в качестве стандартного выбрано значение LostFocus
. Дело в том, что проверка достоверности (рассматриваемая вскоре) для модели запускается в сочетании с UpdateSourceTrigger
. В случае TextBox
это может потенциально вызывать ошибки, которые будут постоянно возникать до тех пор, пока пользователь не введет корректное значение. Например, если правила проверки достоверности не разрешают вводить в элементе TextBox
менее пяти символов, тогда сообщение об ошибке будет отображаться при каждом нажатии клавиши, пока пользователь не введет пять или более символов. В таких случаях с обновлением источника лучше подождать до момента, когда пользователь переместит фокус из элемента TextBox
(завершив изменение текста).
Применение интерфейсов
INotifyPropertyChanged
в моделях и классов ObservableCollection
для списков улучшает пользовательский интерфейс приложения за счет поддержания его в синхронизированном состоянии с данными. В то время как ни один из интерфейсов не является сложным, они требуют обновлений кода. К счастью, в инфраструктуре предусмотрен класс ObservableCollection
, поддерживающий все необходимое для создания наблюдаемых коллекций. Также удачей следует считать обновление проекта Fody с целью автоматического добавления функциональности INotifyPropertyChanged
. При наличии под рукой упомянутых двух инструментов нет никаких причин отказываться от реализации наблюдаемых моделей в своих приложениях WPF.
Теперь, когда интерфейс
INotifyPropertyChanged
реализован и задействован класс ObservableCollection
, самое время заняться добавлением в приложение средств проверки достоверности. Приложениям необходимо проверять пользовательский ввод и обеспечивать обратную связь с пользователем, если введенные им данные оказываются некорректными. В настоящем разделе будут раскрыты наиболее распространенные механизмы проверки достоверности для современных приложений WPF, но это лишь часть возможностей, встроенных в инфраструктуру WPF.
Проверка достоверности происходит, когда привязка данных пытается обновить источник данных. В дополнение к встроенным проверкам, таким как исключения в блоках
set
для свойств, можно создавать специальные правила проверки достоверности. Если любое правило проверки достоверности (встроенное или специальное) нарушается, то в игру вступает класс Validation
, который обсуждается позже в главе.
На заметку! В каждом разделе главы можно продолжить работу с проектом из предыдущего раздела или создать копию проекта, специально предназначенную для нового раздела. Всем последующим разделам соответствуют отдельные проекты, которые доступны в каталоге с кодом для настоящей главы внутри хранилища GitHub.
В каталоге для этой главы внутри хранилища GitHub новый проект (скопированный из предыдущего примера) называется
WpfValidations
. Если вы работаете с тем же самым проектом, созданным в предыдущем разделе, то при копировании в свой проект кода из примеров, приведенных в текущем разделе, просто должны обращать внимание на изменения пространств имен.
Прежде чем добавлять проверку достоверности в проект, важно понять назначение класса
Validation
. Он входит в состав инфраструктуры проверки достоверности и предоставляет методы и присоединяемые свойства, которые могут применяться для отображения результатов проверки. При обработке ошибок проверки обычно используются три основных свойства класса Validation
, кратко описанные в табл. 28.2. Они будут применяться далее в разделе.
Как упоминалось ранее, технологии XAML поддерживают несколько механизмов для встраивания логики проверки достоверности внутрь приложения. В последующих разделах рассматриваются три самых распространенных варианта проверки.
Хотя исключения не должны использоваться для обеспечения выполнения бизнес-логики, они могут (и будут) возникать, а потому требуют надлежащей обработки. Если исключения не обработаны в коде, тогда пользователь должен получить визуальную обратную связь об имеющейся проблеме. В отличие от Windows Forms в инфраструктуре WPF исключения привязки (по умолчанию) не распространяются до пользователя как собственно исключения. Тем не менее, они указываются визуально с применением декоратора (визуального уровня, который находится над элементами управления).
Запустите приложение, выберите запись в элементе
ComboВох
и очистите значение в текстовом поле Id
. Поскольку свойство Id
определено как имеющее тип int
(не тип int
, допускающий null
), требуется числовое значение. После покидания поля Id
по нажатию клавиши <ТаЬ> механизм привязки отправляет свойству CarId
пустую строку, но из-за того, что пустая строка не может быть преобразована в значение int
, внутри блока set
генерируется исключение. В нормальных обстоятельствах необработанное исключение привело бы к отображению окна сообщения пользователю, но в данном случае ничего подобного не происходит. Взглянув на порцию Debug (Отладка) окна Output (Вывод), вы заметите следующие строки:
System.Windows.Data Error: 7 : ConvertBack cannot convert value '' (type 'String').
BindingExpression:Path=Id; DataItem='Car' (HashCode=52579650); target element is
'TextBox' (Name=''); target property is 'Text' (type 'String') FormatException:'System.
FormatException: Input string was not in a correct format.
Ошибка System.Windows.Data: 7 : ConvertBack не может преобразовать (типа String).
BindingExpression : Path=Id; DataItem='Car' (HashCode=52579650);
целевой элемент - TextBox (Name=''); целевое свойство - Text
(типа String) FormatExceptionSystem.FormatException:
Входная строка не имела корректный формат.
Визуально исключение представляется с помощью тонкого прямоугольника красного цвета вокруг элемента управления (рис. 28.2).
Прямоугольник красного цвета — это свойство
ErrorTemplate
объекта Validation
, которое действует в качестве декоратора для связанного элемента управления. Несмотря на то что стандартный внешний вид говорит о наличии ошибки, нет никакого указания на то, что именно пошло не так. Хорошая новость в том, что шаблон отображения ошибки в свойстве ErrorTemplate
является полностью настраиваемым, как вы увидите позже в главе.
Интерфейс
IDataErrorInfo
предоставляет механизм для добавления специальной проверки достоверности в классы моделей. Данный интерфейс добавляется прямо в классы моделей (или моделей представлений), а код проверки помещается внутрь классов моделей (предпочтительно в частичные классы). Такой подход централизует код проверки достоверности в проекте, что совершенно не похоже на инфраструктуру Windows Forms, где проверка обычно делалась в самом пользовательском интерфейсе.
Показанный далее интерфейс
IDataErrorInfo
содержит два свойства: индексатор и строковое свойство по имени Error
. Следует отметить, что механизм привязки WPF не задействует свойство Error
.
public interface IDataErrorInfo
{
string this[string columnName] { get; }
string Error { get; }
}
Вскоре вы добавите частичный класс
Car
, но сначала необходимо модифицировать класс в файле Car.cs
, пометив его как частичный. Добавьте в папку Models
еще один файл по имени CarPartial.cs
. Переименуйте этот класс в Car
, пометьте его как partial
и обеспечьте реализацию классом интерфейса IDataErrorInfo
. Затем реализуйте члены интерфейса IDataErrorInfo
. Вот начальный код:
public partial class Car : IDataErrorInfo
{
public string this[string columnName] => string.Empty;
public string Error { get;}
}
Чтобы привязанный элемент управления мог работать с интерфейсом
IDataErrorInfo
, в выражение привязки потребуется добавить ValidatesOnDataErrors
. Модифицируйте выражение привязки для текстового поля Make
следующим образом (и аналогично обновите остальные конструкции привязки):
Text="{Binding Path=Make, ValidatesOnDataErrors=True}" />
После внесения изменений в конструкции привязки индексатор вызывается на модели каждый раз, когда возникает событие
PropertyChanged
. В качестве параметра columnName
индексатора используется имя свойства из события. Если индексатор возвращает string.Empty
, то инфраструктура предполагает, что все проверки достоверности прошли успешно и какие-либо ошибки отсутствуют. Если индексатор возвращает значение, отличающееся от string.Empty
, тогда в свойстве для данного объекта присутствует ошибка, из-за чего каждый элемент управления, привязанный к этому свойству специфического экземпляра класса, считается содержащим ошибку. Свойство HasError
объекта Validation
устанавливается в true
и активизируется декоратор ErrorTemplate
для элементов управления, на которые повлияла ошибка.
Добавьте простую логику проверки достоверности к индексатору в файле
CorePartial.cs
. Правила проверки элементарны :
• если
Make
равно ModelT
, то установить сообщение об ошибке в "Too Old"
(слишком старая модель);
• если
Make
равно Chevy
и Color
равно Pink
, то установить сообщение об ошибке в $" {Make}'s don't come in {Color}"
(модель в таком цвете не поставляется).
Начните с добавления оператора
switch
для каждого свойства. Во избежание применения "магических" строк в операторах case
вы снова будете использовать операцию nameof
. В случае сквозного прохода через оператор switch
возвращается string.Empty
. Далее добавьте правила проверки достоверности. В подходящих операторах case
реализуйте проверку значения свойства на основе приведенных выше правил. В операторе case
для свойства Make
первым делом проверьте, равно ли значение ModelT
. Если это так, тогда возвратите сообщение об ошибке. В случае успешного прохождения проверки в следующей строке кода вызовите вспомогательный метод, который возвратит сообщение об ошибке, если нарушено второе правило, или string.Empty
, если нет. В операторе case
для свойства Color
просто вызовите тот же вспомогательный метод. Ниже показан код:
public string this[string columnName]
{
get
{
switch (columnName)
{
case nameof(Id):
break;
case nameof(Make):
return Make == "ModelT"
? "Too Old"
: CheckMakeAndColor();
case nameof(Color):
return CheckMakeAndColor();
case nameof(PetName):
break;
}
return string.Empty;
}
}
internal string CheckMakeAndColor()
{
if (Make == "Chevy" && Color == "Pink")
{
return $"{Make}'s don't come in {Color}";
}
return string.Empty;
}
Запустите приложение, выберите автомобиль
Red Rider
(Ford
) и измените значение в поле Make (Производитель) на ModelT
. После того, как фокус покинет поле, появится декоратор ошибки красного цвета. Выберите в поле со списком автомобиль Kit
(Chevy
) и щелкните на кнопке Change Color, чтобы изменить его цвет на Pink
. Вокруг поля Color
незамедлительно появится декоратор ошибки красного цвета, но возле поля Make он будет отсутствовать. Измените значение в поле Make на Ford
и переместите фокус из этого поля; декоратор ошибки красного цвета не появляется!
Причина в том, что индексатор выполняется, только когда для свойства сгенерировано событие
PropertyChanged
. Как обсуждалось в разделе "Система уведомлений привязки WPF" ранее в главе, событие PropertyChanged
инициируется при изменении исходного значения свойства объекта, что происходит либо через код (вроде обработчика события Click
для кнопки Change Color), либо через взаимодействие с пользователем (синхронизируется с помощью UpdateSourceTrigger
). При изменении цвета свойство Make не изменяется, а потому событие PropertyChanged
для него не генерируется. Поскольку событие не генерируется, индексатор не вызывается и проверка достоверности для свойства Make
не выполняется.
Решить проблему можно двумя путями. Первый предусматривает изменение объекта
PropertyChangedEventArgs
, которое обеспечит обновление всех привязанных свойств, за счет передачи его конструктору значения string.Empty
вместо имени поля. Как упоминалось ранее, это заставит механизм привязки обновить каждое свойство в данном экземпляре. Добавьте метод OnPropertyChanged()
со следующим кодом:
protected virtual void OnPropertyChanged([CallerMemberName]
string propertyName = "")
{
if (propertyName != nameof(IsChanged))
{
IsChanged = true;
}
//PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(string.Empty));
}
Теперь при выполнении того же самого теста текстовые поля Make и Color декорируются с помощью шаблона отображения ошибки, когда одно из них обновляется. Так почему бы ни генерировать событие всегда в такой манере? В значительной степени причиной является производительность. Вполне возможно, что обновление каждого свойства объекта приведет к снижению производительности. Разумеется, без тестирования об этом утверждать нельзя, и конкретные ситуации могут (и вероятно будут) варьироваться.
Другое решение предполагает генерацию события
PropertyChanged
для зависимого поля (полей), когда одно из полей изменяется. Недостаток такого приема в том, что вы (или другие разработчики, сопровождающие ваше приложение) должны знать о взаимосвязи между свойствами Make
и Color
через код проверки достоверности.
Интерфейс
INotifyDataErrorInfo
, появившийся в версии .NET 4.5, построен на основе интерфейса IDataErrorInfo
и предлагает дополнительные возможности для проверки достоверности. Конечно, возросшая мощь сопровождается дополнительной работой! По разительному контрасту с предшествующими приемами проверки достоверности, которые вы видели до сих пор, свойство привязки ValidatesOnNotifyDataErrors
имеет стандартное значение true
, поэтому добавлять его к операторам привязки не обязательно.
Интерфейс
INotifyDataErrorInfo
чрезвычайно мал, но, как вскоре будет показано, для обеспечения своей эффективности требует написания порядочного объема связующего кода. Ниже приведено определение интерфейса INotifyDataErrorInfo
:
public interface INotifyDataErrorInfo
{
bool HasErrors { get; }
event EventHandler
ErrorsChanged;
IEnumerable GetErrors(string propertyName);
}
Свойство
HasErrors
используется механизмом привязки для выяснения, есть ли какие-нибудь ошибки в любых свойствах экземпляра. Если метод GetErrors()
вызывается со значением null
или пустой строкой в параметре propertyName
, то он возвращает все ошибки, существующие в экземпляре. Если методу передан параметр propertyName
, тогда возвращаются только ошибки, относящиеся к конкретному свойству. Событие ErrorsChanged
(подобно событиям PropertyChanged
и CollectionChanged
) уведомляет механизм привязки о необходимости обновления пользовательского интерфейса для текущего списка ошибок.
При реализации
INotifyDataErrorInfo
большая часть кода обычно помещается в базовый класс модели, поэтому она пишется только один раз. Начните с замены IDataErrorInfo
интерфейсом INotifyDataErrorInfo
в файле класса CarPartial.cs
(код для IDataErrorInfo
в классе можете оставить; вы обновите его позже).
public partial class Car: INotifyDataErrorInfo, IDataErrorInfo
{
...
public IEnumerable GetErrors(string propertyName)
{
throw new NotImplementedException();
}
public bool HasErrors { get; }
public event
EventHandler ErrorsChanged;
}
Добавьте закрытое поле типа
Dictionary>
, которое будет хранить сведения о любых ошибках, сгруппированные по именам свойств. Понадобится также добавить оператор using
для пространства имен System.Collections.Generic
. Вот как выглядит код:
using System.Collections.Generic;
private readonly Dictionary> _errors
= new Dictionary>();
Свойство
HasErrors
должно возвращать true
, если в словаре присутствуют любые ошибки, что легко достигается следующим образом:
public bool HasErrors => _errors.Any();
Создайте вспомогательный метод для инициирования события
ErrorsChanged
(подобно инициированию события PropertyChanged
):
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this,
new DataErrorsChangedEventArgs(propertyName));
}
Как упоминалось ранее, метод
GetErrors()
должен возвращать любые ошибки в словаре, когда в параметре передается пустая строка или null
. Если передается допустимое значение propertyName
, то возвращаются ошибки, обнаруженные для указанного свойства. Если параметр не соответствует какому-либо свойству (или ошибки для свойства отсутствуют), тогда метод возвратит null
.
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
{
return _errors.Values;
}
return _errors.ContainsKey(propertyName)
? _errors[propertyName]
: null;
}
Финальный набор вспомогательных методов будет добавлять одну или большее число ошибок для свойства либо очищать все ошибки для свойства (или всех свойств). Не следует забывать о вызове вспомогательного метода
OnErrorsChanged()
каждый раз, когда словарь изменяется.
private void AddError(string propertyName, string error)
{
AddErrors(propertyName, new List { error });
}
private void AddErrors(
string propertyName, IList errors)
{
if (errors == null || !errors.Any())
{
return;
}
var changed = false;
if (!_errors.ContainsKey(propertyName))
{
_errors.Add(propertyName, new List());
changed = true;
}
foreach (var err in errors)
{
if (_errors[propertyName].Contains(err)) continue;
_errors[propertyName].Add(err);
changed = true;
}
if (changed)
{
OnErrorsChanged(propertyName);
}
}
protected void ClearErrors(string propertyName = "")
{
if (string.IsNullOrEmpty(propertyName))
{
_errors.Clear();
}
else
{
_errors.Remove(propertyName);
}
OnErrorsChanged(propertyName);
}
Возникает вопрос: когда приведенный выше код активизируется? Механизм привязки прослушивает событие
ErrorsChanged
и обновляет пользовательский интерфейс, если в коллекции ошибок для выражения привязки возникает изменение. Но код проверки по-прежнему нуждается в триггере для запуска. Доступны два механизма, которые обсуждаются далее.
Одним из мест выполнения проверки на предмет ошибок являются блоки
set
для свойств, как демонстрируется в показанном ниже примере, упрощенном до единственной проверки на равенство свойства Make
значению ModelT
:
public string Make
{
get { return _make; }
set
{
if (value == _make) return;
_make = value;
if (Make == "ModelT")
{
AddError(nameof(Make), "Too Old");
}
else
{
ClearErrors(nameof(Make));
}
OnPropertyChanged(nameof(Make));
OnPropertyChanged(nameof(Color));
}
}
Основная проблема такого подхода состоит в том, что вам приходится сочетать логику проверки достоверности с блоками
set
для свойств, что делает код труднее в чтении и сопровождении.
В предыдущем разделе было показано, что реализацию интерфейса
IDataErrorInfo
можно добавить к частичному классу, т.е. обновлять блоки set
не понадобится. Кроме того, индексатор автоматически вызывается при возникновении события PropertyChanged
в свойстве. Комбинирование IDataErrorInfo
и INotifyDataErrorInfo
предоставляет дополнительные возможности для проверки достоверности из INotifyDataErrorInfo
, а также отделение от блоков set
, обеспечиваемое IDataErrorInfo
.
Цель применения
IDataErrorInfo
не в том, чтобы запускать проверку достоверности, а в том, чтобы гарантировать вызов кода проверки, который задействует INotifyDataErrorInfo
, каждый раз, когда для объекта генерируется событие PropertyChanged
. Поскольку интерфейс IDataErrorInfo
не используется для проверки достоверности, необходимо всегда возвращать string.Empty
из индексатора. Модифицируйте индексатор и вспомогательный метод CheckMakeAndColor()
следующим образом:
public string this[string columnName]
{
get
{
ClearErrors(columnName);
switch (columnName)
{
case nameof(Id):
break;
case nameof(Make):
CheckMakeAndColor();
if (Make == "ModelT")
{
AddError(nameof(Make), "Too Old");
hasError = true;
}
break;
case nameof(Color):
CheckMakeAndColor();
break;
case nameof(PetName):
break;
}
return string.Empty;
}
}
internal bool CheckMakeAndColor()
{
if (Make == "Chevy" && Color == "Pink")
{
AddError(nameof(Make), $"{Make}'s don't come in {Color}");
AddError(nameof(Color),
$"{Make}'s don't come in {Color}");
return true;
}
return false;
}
Запустите приложение, выберите автомобиль
Chevy
и измените цвет на Pink
. В дополнение к декораторам красного цвета вокруг текстовых полей Make и Model будет также отображаться декоратор в виде красного прямоугольника, охватывающего целиком всю сетку, в которой находятся поля с детальной информацией об автомобиле (рис. 28.3).
Это еще одно преимущество применения интерфейса
INotifyDataErrorInfo
. В дополнение к элементам управления, которые содержат ошибки, элемент управления, определяющий контекст данных, также декорируется шаблоном отображения ошибки.
Свойство
Errors
класса Validation
возвращает все ошибки проверки достоверности для конкретного объекта в форме объектов ValidationError
. Каждый объект ValidationError
имеет свойство ErrorContent
, которое содержит список сообщений об ошибках для свойства. Это означает, что сообщения об ошибках, которые нужно отобразить, находятся в списке внутри списка. Чтобы вывести их надлежащим образом, понадобится создать элемент ListBox
, содержащий еще один элемент ListBox
. Звучит слегка запутанно, но вскоре все прояснится.
Первым делом добавьте одну строку в
DetailsGrid
и удостоверьтесь в том, что значение свойства Height
элемента Window
составляет не менее 300
. Поместите в последнюю строку элемент управления ListBox
и привяжите его свойство ItemsSource
к DetailsGrid
, используя Validation.Errors
для Path
:
ItemsSource="{Binding ElementName=DetailsGrid, Path=(Validation.Errors)}">
Добавьте к
ListBox
элемент DataTemplate
, а в него — элемент управления ListBox
, привязанный к свойству ErrorContent
. Контекстом данных для каждого элемента ListBoxItem
в этом случае является объект ValidationError
, так что устанавливать контекст данных не придется, а только путь. Установите путь привязки в ErrorContent
:
Запустите приложение, выберите автомобиль
Chevy
и установите цвет в Pink
. В окне отобразятся ошибки (рис. 28.4).
Мы лишь слегка коснулись поверхности того, что можно делать при проверке достоверности и отображении сообщений об ошибках, но представленных сведений должно быть вполне достаточно для выработки вами способа разработки информативных пользовательских интерфейсов, которые улучшают восприятие.
Вероятно, вы заметили, что в настоящий момент в классе
CarPartial
присутствует много кода. Поскольку в рассматриваемом примере есть только один класс модели, проблемы не возникают. Но по мере появления новых моделей в реальном приложении добавлять весь связующий код в каждый частичный класс для моделей нежелательно. Гораздо эффективнее поместить поддерживающий код в базовый класс, что и будет сделано.
Создайте в папке
Models
новый файл класса по имени BaseEntity.cs
. Добавьте в него операторы using
для пространств имен System.Collections
и System.ComponentModel
. Пометьте класс как открытый и обеспечьте реализацию им интерфейса INotifyDataErrorInfor
.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace Validations.Models
{
public class BaseEntity : INotifyDataErrorInfo
}
Переместите в новый базовый класс весь код, относящийся к
INofityDataErrorInfo
, из файла CarPartial.cs
. Любые закрытые методы понадобится сделать защищенными. Удалите реализацию интерфейса INotifyDataErrorInfo
из класса в файле CarPartial.cs
и добавьте BaseEntity
в качестве базового класса:
public partial class Car : BaseEntity, IDataErrorInfo
{
// Для краткости код не показан.
}
Теперь любые создаваемые классы моделей будут наследовать весь связующий код
INotifyDataErrorInfo
.
Для проверки достоверности в пользовательских интерфейсах инфраструктура WPF способна также задействовать аннотации данных. Давайте добавим несколько аннотаций данных к модели
Car
.
Откройте файл
Car.cs
и поместите в него оператор using
для пространства имен System.ComponentModel.DataAnnotations
. Добавьте к свойствам Make
, Color
и PetName
атрибуты [Required]
и [StringLength(50)]
. Атрибут [Required]
определяет правило проверки достоверности, которое регламентирует, что значение свойства не должно быть null
(надо сказать, оно избыточно для свойства Id
, т.к. свойство не относится к типу int
, допускающему null
). Атрибут [StringLength(50)]
определяет правило проверки достоверности, которое ограничивает длину значения свойства 50 символами.
В WPF вы должны программно контролировать наличие ошибок проверки достоверности на основе аннотаций данных. Двумя основными классами, отвечающими за проверку достоверности на основе аннотаций данных, являются
ValidationContext
и Validator
. Класс ValidationContext
предоставляет контекст для контроля за наличием ошибок проверки достоверности. Класс Validator
позволяет проверять, есть ли в объекте ошибки, связанные с аннотациями данных, в ValidationContext
.
Откройте файл
BaseEntity.cs
и добавьте в него следующие операторы using
:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
Далее создайте новый метод по имени
GetErrorsFromAnnotations()
. Это обобщенный метод, который принимает в качестве параметров строковое имя свойства и значение типа Т
, а возвращает строковый массив. Он должен быть помечен как protected
. Вот его сигнатура:
protected string[] GetErrorsFromAnnotations(
string propertyName, T value)
{}
Внутри метода
GetErrorsFromAnnotations()
создайте переменную типа List
, которая будет хранить результаты выполненных проверок достоверности, и объект ValidationContext
с областью действия, ограниченной именем переданного методу свойства. Затем вызовите метод Validate.TryValidateProperty()
, который возвращает значение bool
. Если все проверки (на основе аннотаций данных) прошли успешно, тогда метод возвращает true
. В противном случае он возвратит false
и наполнит List
информацией о возникших ошибках. Полный код выглядит так:
protected string[] GetErrorsFromAnnotations(
string propertyName, T value)
{
var results = new List();
var vc = new ValidationContext(this, null, null)
{ MemberName = propertyName };
var isValid = Validator.TryValidateProperty(
value, vc, results);
return (isValid)
? null
: Array.ConvertAll(
results.ToArray(), o => o.ErrorMessage);
}
Теперь можете модифицировать метод индексатора в файле
CarPartial.cs
, чтобы проверять наличие любых ошибок, основанных на аннотациях данных. Обнаруженные ошибки должны добавляться в коллекцию ошибок, поддерживаемую интерфейсом INotifyDataErrorInfo
. Это позволяет привести в порядок обработку ошибок. В начале индексаторного метода очистите ошибки для столбца. Затем обработайте результаты проверок достоверности и в заключение предоставьте специальную логику для сущности. Ниже показан обновленный код индексатора:
public string this[string columnName]
{
get
{
ClearErrors(columnName);
var errorsFromAnnotations =
GetErrorsFromAnnotations(columnName,
typeof(Car)
.GetProperty(columnName)?.GetValue(this,null));
if (errorsFromAnnotations != null)
{
AddErrors(columnName, errorsFromAnnotations);
}
switch (columnName)
{
case nameof(Id):
break;
case nameof(Make):
CheckMakeAndColor();
if (Make == "ModelT")
{
AddError(nameof(Make), "Too Old");
}
break;
case nameof(Color):
CheckMakeAndColor();
break;
case nameof(PetName):
break;
}
return string.Empty;
}
}
Запустите приложение, выберите один из автомобилей и введите в поле Color текст, содержащий более 50 символов. После превышения порога в 50 символов аннотация данных
StringLength
создает ошибку проверки достоверности, которая сообщается пользователю (рис. 28.5).
Финальной темой является создание стиля, который будет применяться, когда элемент управления содержит ошибку, а также обновление
ErrorTemplate
для отображения более осмысленного сообщения об ошибке. Как объяснялось в главе 27, элементы управления допускают настройку посредством стилей и шаблонов элементов управления.
Начните с добавления в раздел
Window.Resources
файла MainWindow.xaml
нового стиля с целевым типом TextBox
. Добавьте к стилю триггер, который устанавливает свойства, когда свойство Validation.HasError
имеет значение true
. Свойствами и устанавливаемыми значениями являются Background(Pink)
, Foreground(Black)
и ToolTip(ErrorContent)
. В элементах Setter
для свойств Background
и Foreground
нет ничего нового, но синтаксис установки свойства ToolTip
требует пояснения. Привязка (Binding
) указывает на элемент управления, к которому применяется данный стиль, в этом случае TextBox
. Путь (Path
) представляет собой первое значение ErrorContent
в коллекции Validation.Errors
. Разметка выглядит следующим образом:
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
Запустите приложение и создайте условие для ошибки. Результат будет подобен тому, что показан на рис. 28.6, и укомплектован всплывающей подсказкой с сообщением об ошибке.
Определенный выше стиль изменяет внешний вид любого элемента управления
TextBox
, который содержит ошибку. Далее мы создадим специальный шаблон элемента управления с целью обновления свойства ErrorTemplate
класса Validation
, чтобы отобразить восклицательный знак красного цвета и установить всплывающие подсказки для восклицательного знака. Шаблон ErrorTemplate
является декоратором, который располагается поверх элемента управления. Хотя только что созданный стиль обновляет сам элемент управления, шаблон ErrorTemplate
будет размещаться поверх элемента управления.
Поместите элемент
Setter
непосредственно после закрывающего дескриптора Style.Triggers
внутри созданного стиля. Вы будете создавать шаблон элемента управления, состоящий из элемента TextBlock
(для отображения восклицательного знака) и элемента BorderBrush
, который окружает TextBox
, содержащий сообщение об ошибке (или несколько сообщений). В языке XAML предусмотрен специальный дескриптор для элемента управления, декорированного с помощью ErrorTemplate
, под названием AdornedElementPlaceholder
. Добавляя имя такого элемента управления, можно получить доступ к ошибкам, которые ассоциированы с элементом управления. В рассматриваемом примере вас интересует доступ к свойству Validation.Errors
с целью получения ErrorContent
(как делалось в Style.Trigger
). Вот полная разметка для элемента Setter
:
ToolTip="{Binding ElementName=controlWithError,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>
Запустите приложение и создайте условие для возникновения ошибки. Результат будет подобен представленному на рис. 28.7.
На этом исследование методов проверки достоверности в WPF завершено. Разумеется, с их помощью можно делать намного большее. За дополнительными сведениями обращайтесь в документацию по WPF.
Как и в разделе, посвященном проверке достоверности, можете продолжить работу с тем же проектом или создать новый проект и скопировать в него весь код из предыдущего проекта. Вы создадите новый проект по имени
WpfCommands
. В случае работы с проектом из предыдущего раздела обращайте внимание на пространства имен в примерах кода и корректируйте их по мере необходимости.
В главе 25 объяснялось, что команды являются неотъемлемой частью WPF. Команды могут привязываться к элементам управления WPF (таким как
Button
и MenuItem
) для обработки пользовательских событий, подобных щелчку. Вместо создания обработчика события напрямую и помещения его кода в файл отделенного кода при возникновении события выполняется метод Execute()
команды. Метод CanExecute()
используется для включения или отключения элемента управления на основе специального кода. В дополнение к встроенным командам, которые применялись в главе 25, можно создавать собственные команды, реализуя интерфейс ICommand
. Когда вместо обработчиков событий используются команды, появляются преимущества инкапсуляции кода приложения, а также автоматического включения и отключения элементов управления с помощью бизнес-логики.
Как было показано в главе 25, интерфейс
ICommand
определен следующим образом:
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
Обработчики событий для элементов управления
Button
вы замените командами, начав с кнопки Change Color. Создайте в проекте новую папку по имени Cmds
. Добавьте в нее новый файл класса ChangeColorCornmand.cs
. Сделайте класс открытым и реализующим интерфейс ICommand
. Добавьте приведенные ниже операторы using
(первый может варьироваться в зависимости от того, создавался ли новый проект для данного примера):
using WpfCommands.Models;
using System.Windows.Input;
Код класса должен выглядеть примерно так:
public class ChangeColorCommand : ICommand
{
public bool CanExecute(object parameter)
{
throw new NotImplementedException();
}
public void Execute(object parameter)
{
throw new NotImplementedException();
}
public event EventHandler CanExecuteChanged;
}
Если метод
CanExecute()
возвращает true
, то привязанные элементы управления будут включенными, а если false
, тогда они будут отключенными. Если элемент управления включен (CanExecute()
возвращает true
)и на нем совершается щелчок, то запустится метод Execute()
. Параметры, передаваемые обоим методам, поступают из пользовательского интерфейса и основаны на свойстве CommandParameter
, устанавливаемом в конструкциях привязки. Событие CanExecuteChanged
предусмотрено в системе привязки и уведомлений для информирования пользовательского интерфейса о том, что результат, возвращаемый методом CanExecute()
, изменился (почти как событие PropertyChanged
).
В текущем примере кнопка Change Color должна работать, только если параметр отличается от
null
ипринадлежит типу Car
. Модифицируйте метод CanExecute()
следующим образом:
public bool CanExecute(object parameter)
=> (parameter as Car) != null;
Значение параметра для метода
Execute()
будет таким же, как и для метода CanExecute()
. Поскольку метод Execute()
может выполняться лишь в случае, если object
имеет тип Car
, аргумент потребуется привести к типу Car
и затем обновить значение цвета:
public void Execute(object parameter)
{
((Car)parameter).Color="Pink";
}
Финальное обновление класса команды связано с присоединением команды к диспетчеру команд (
CommandManager
). Метод CanExecute()
запускается при загрузке окна в первый раз и затем в ситуации, когда диспетчер команд инструктирует его о необходимости перезапуска. Каждый класс команды должен быть присоединен к диспетчеру команд, для чего нужно модифицировать код, относящийся к событию CanExecuteChanged
:
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
Следующее изменение связано с созданием экземпляра класса
ChangeColorCommand
, к которому может иметь доступ элемент управления Button
. В настоящий момент вы будете делать это в файле отделенного кода для MainWindow
(позже в главе код переместится в модель представления). Откройте файл MainWindow.xaml.cs
и удалите обработчик события Click
для кнопки Change Color. Поместите в начало файла следующие операторы using
(пространство имен может варьироваться в зависимости от того, работаете вы с предыдущим проектом или начали новый):
using WpfCommands.Cmds;
using System.Windows.Input;
Добавьте открытое свойство по имени
ChangeColorCmd
типа ICommand
с поддерживающим полем. В теле выражения для свойства возвратите значение поддерживающего поля (создавая экземпляр ChangeColorCommand
, если поддерживающее поле равно null
):
private ICommand _changeColorCommand = null;
public ICommand ChangeColorCmd
=> _changeColorCommand ??= new ChangeColorCommand());
Как было показано в главе 25, элементы управления WPF, реагирующие на щелчки (вроде
Button
), имеют свойство Command
, которое позволяет назначать элементу управления объект команды. Для начала присоедините объект команды, созданный в файле отделенного кода, к кнопке btnChangeColor
. Поскольку свойство для команды находится в классе MainWindow
, с помощью синтаксиса привязки RelativeSource
получается окно, содержащее необходимую кнопку:
Command="{Binding Path=ChangeColorCmd,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
Кнопка также нуждается в передаче объекта
Car
в качестве параметра для методов CanExecute()
и Execute()
, что делается через свойство CommandParameter
. Установите свойство Path
для CommandParameter
в свойство SelectedItem
элемента ComboBox
по имени cboCars
:
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"
Вот завершенная разметка для кнопки:
Padding="4, 2" Command="{Binding Path=ChangeColorCmd,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"/>
Запустите приложение. Кнопка Change Color не будет доступной (рис. 28.8), т.к. автомобиль еще не выбран.
Теперь выберите автомобиль; кнопка Change Color становится доступной, а щелчок на ней обеспечивает изменение цвета, как и ожидалось!
Если распространить такой шаблон на
AddCarCommand.cs
, то итогом стал бы код, повторяющийся среди классов. Это хороший знак о том, что необходим базовый класс. Создайте внутри папки Cmds
новый файл класса по имени CommandBase.cs
и добавьте оператор using
для пространства имен System.Windows.Input
. Сделайте класс CommandBase
открытым и реализующим интерфейс ICommand
. Превратите класс и методы Execute()
и CanExecute()
в абстрактные. Наконец, добавьте обновление в событие CanExecuteChanged
из класса ChangeColorCommand
. Ниже показана полная реализация:
using System;
using System.Windows.Input;
namespace WpfCommands.Cmds
{
public abstract class CommandBase : ICommand
{
public abstract bool CanExecute(object parameter);
public abstract void Execute(object parameter);
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}
Добавьте в папку
Cmds
новый файл класса по имени AddCarCommand.cs
. Сделайте класс открытым и укажите CommandBase
в качестве базового класса. Поместите в начало файла следующие операторы using
:
using System.Collections.ObjectModel;
using System.Linq;
using WpfCommands.Models;
Ожидается, что параметр должен иметь тип
ObservableCollection
, поэтому предусмотрите в методе CanExecute()
соответствующую проверку. Если параметр относится к типу ObservableCollection
, тогда метод Execute()
должен добавить дополнительный объект Car
подобно обработчику события Click
.
public class AddCarCommand :CommandBase
{
public override bool CanExecute(object parameter)
=> parameter is ObservableCollection;
public override void Execute(object parameter)
{
if (parameter is not ObservableCollection cars)
{
return;
}
var maxCount = cars.Max(x => x.Id);
cars.Add(new Car
{
Id = ++maxCount,
Color = "Yellow",
Make = "VW",
PetName = "Birdie"
});
}
}
Добавьте открытое свойство типа
ICommand
по имени AddCarCmd
с поддерживающим полем. В теле выражения для свойства возвратите значение поддерживающего поля (создавая экземпляр AddCarCommand
, если поддерживающее поле равно null
):
private ICommand _addCarCommand = null;
public ICommand AddCarCmd
=> _addCarCommand ??= new AddCarCommand());
Модифицируйте разметку XAML, удалив атрибут
Click
и добавив атрибуты Command
и CommandParameter
. Объект AddCarCommand
будет получать список автомобилей из поля со списком cboCars
. Ниже показана полная разметка XAML для кнопки:
Command="{Binding Path=AddCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=ItemsSource}"/>
В результате появляется возможность добавления автомобилей и обновления их цветов (пока с весьма ограниченной функциональностью) с помощью многократно используемого кода, содержащегося в автономных классах.
Финальным шагом будет обновление класса
ChangeColorCommand
, чтобы он стал унаследованным от CommandBase
. Замените интерфейс ICommand
классом CommandBase
, добавьте к обоим методам ключевое слово override
и удалите код события CanExecuteChanged
. Все оказалось действительно настолько просто! Вот как выглядит новый код:
public class ChangeColorCommand : CommandBase
{
public override bool CanExecute(object parameter)
=> parameter is Car;
public override void Execute(object parameter)
{
((Car)parameter).Color = "Pink";
}
}
Еще одной реализацией паттерна "Команда" (Command) в WPF является
RelayCommand
. Вместо создания нового класса, представляющего каждую команду, данный паттерн применяет делегаты для реализации интерфейса ICommand
. Реализация легковесна в том, что каждая команда не имеет собственного класса. Объекты RelayCommand
обычно используются, когда нет необходимости в многократном применении реализации команды.
Как правило, объекты
RelayCommand
реализуются в двух классах. Базовый класс RelayCommand
используется при отсутствии каких-либо параметров для методов CanExecute()
и Execute()
, а класс RelayCommand
применяется, когда требуется параметр. Начните с базового класса RelayCommand
, который задействует класс CommandBase
. Добавьте в папку Cmds
новый файл класса по имени RelayCommand.cs
. Сделайте его открытым и укажите CommandBase
в качестве базового класса. Добавьте две переменные уровня класса для хранения делегатов Execute()
и CanExecute()
:
private readonly Action _execute;
private readonly Func _canExecute;
Создайте три конструктора. Первый — стандартный конструктор (необходимый для производного класса
RelayCommand
), второй — конструктор, который принимает параметр Action
, и третий — конструктор, принимающий параметры Action
и Func
:
public RelayCommand(){}
public RelayCommand(Action execute) : this(execute, null) { }
public RelayCommand(Action execute, Func canExecute)
{
_execute = execute
?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
Наконец, реализуйте переопределенные версии
CanExecute()
и Execute()
. Метод CanExecute()
возвращает true
, если параметр Func
равен null
; если же параметр Func
не null
, то он выполняется и возвращается true
. Метод Execute()
выполняет параметр типа Action
.
public override bool CanExecute(object parameter)
=> _canExecute == null || _canExecute();
public override void Execute(object parameter) { _execute(); }
Добавьте в папку
Cmds
новый файл класса по имени RelayCommandT.cs
. Класс RelayCommandT
является почти полной копией базового класса, исключая тот факт, что все делегаты принимают параметр. Сделайте класс открытым и обобщенным, а также унаследованным от базового класса RelayCommand
:
public class RelayCommand : RelayCommand
Добавьте две переменные уровня класса для хранения делегатов
Execute()
и CanExecute()
:
private readonly Action _execute;
private readonly Func _canExecute;
Создайте два конструктора. Первый из них принимает параметр
Action
, а второй — параметры Action
и Func
:
public RelayCommand(Action execute):this(execute, null) {}
public RelayCommand(
Action execute, Func canExecute)
{
_execute = execute
?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
Наконец, реализуйте переопределенные версии
CanExecute()
и Execute()
. Метод CanExecute()
возвращает true
, если Func
равно null
, а иначе выполняет Func
и возвращает true
. Метод Execute()
выполняет параметр типа Action
.
public override bool CanExecute(object parameter)
=> _canExecute == null || _canExecute((T)parameter);
public override void Execute(object parameter)
{ _execute((T)parameter); }
Когда используются объекты
RelayCommand
, при конструировании новой команды должны указываться все методы для делегатов. Это вовсе не означает, что код нуждается в помещении внутрь файла отделенного кода (как показано здесь); он просто должен быть доступным из файла отделенного кода. Код может находиться в другом классе (или даже в другой сборке), что дает преимущества инкапсуляции, связанные с созданием специального класса команды.
Добавьте новую закрытую переменную типа
RelayCommand
и открытое свойство по имени DeleteCarCmd
;
private RelayCommand _deleteCarCommand = null;
public RelayCommand DeleteCarCmd
=> _deleteCarCommand ??=
new RelayCommand(DeleteCar,CanDeleteCar));
Также потребуется создать методы
DeleteCar()
и CanDeleteCar()
:
private bool CanDeleteCar(Car car) => car != null;
private void DeleteCar(Car car)
{
_cars.Remove(car);
}
Обратите внимание на строгую типизацию в методах — одно из преимуществ применения
RelayCommand
.
Последним шагом будет добавление кнопки Delete Car (Удалить автомобиль) и установка привязок
Command
и CommandParameter
. Добавьте следующую разметку:
Command="{Binding Path=DeleteCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"/>
Теперь, запустив приложение, вы можете удостовериться в том, что кнопка Delete Car доступна, только если в раскрывающемся списке выбран автомобиль, и щелчок на ней приводит к удалению записи об автомобиле.
На этом краткий экскурс в команды WPF завершен. За счет перемещения кода обработки событий из файла отделенного кода в индивидуальные классы команд появляются преимущества инкапсуляции, многократного использования и улучшенной возможности сопровождения. Если настолько большое разделение обязанностей не требуется, тогда можно применять легковесную реализацию
RelayCommand
. Цель заключается в том, чтобы улучшить возможность сопровождения и качество кода, так что выбирайте подход, который лучше подходит для вашей ситуации.
Как и в разделе "Проверка достоверности WPF", вы можете продолжить работу с тем же самым проектом или создать новый и скопировать в него весь код. Вы создадите новый проект по имени
WpfViewModel
. В случае работы с проектом из предыдущего раздела обращайте внимание на пространства имен в примерах кода и корректируйте их по мере необходимости.
Создайте в проекте новую папку под названием
ViewModels
и поместите в нее новый файл класса MainWindowViewModel.cs
. Добавьте операторы using
для следующих пространств имен и сделайте класс открытым:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Input;
using WpfViewModel.Cmds;
using WpfViewModel.Models;
На заметку! Популярное соглашение предусматривает именование моделей представлений в соответствие с окном, которое их поддерживает. Обычно имеет смысл следовать такому соглашению, поэтому оно соблюдается в настоящей главе. Тем не менее, как и любой паттерн или соглашение, это не норма, и на данный счет вы найдете широкий спектр мнений.
В модель представления будет перемещен почти весь код из файла отделенного кода. В конце останется только несколько строк, включая вызов метода
InitializeComponent()
и код установки контекста данных для окна в модель представления.
Создайте открытое свойство типа
IList
по имени Cars
:
public IList Cars { get; } =
new ObservableCollection();
Создайте стандартный конструктор и перенесите в него весь код создания объектов
Car
из файла MainWindow.xaml.cs
, обновив имя списковой переменной. Можете также удалить переменную _cars
из MainWindow.xaml.cs
. Ниже показан конструктор модели представления:
public MainWindowViewModel()
{
Cars.Add(
new Car { Id = 1, Color = "Blue", Make = "Chevy",
PetName = "Kit", IsChanged = false });
Cars.Add(
new Car { Id = 2, Color = "Red", Make = "Ford",
PetName = "Red Rider", IsChanged = false });
}
Далее переместите весь код, относящийся к командам, из файла отделенного кода окна в модель представления, заменив ссылку на переменную
_cars
ссылкой на Cars
. Вот измененный код:
// Для краткости остальной код не показан
private void DeleteCar(Car car)
{
Cars.Remove(car);
}
Из файла
MainWindow.xaml.cs
кода была удалена большая часть кода. Удалите строку, которая устанавливает ItemsSource
для поля со списком, оставив только вызов InitializeComponent()
. Код должен выглядеть примерно так:
public MainWindow()
{
InitializeComponent();
}
Добавьте в начало файла следующий оператор
using
:
using WpfViewModel.ViewModels;
Создайте строго типизированное свойство для хранения экземпляра модели представления:
public MainWindowViewModel ViewModel { get; set; }
= new MainWindowViewModel();
Добавьте свойство
DataContext
к объявлению окна в разметке XAML:
DataContext="{Binding ViewModel, RelativeSource={RelativeSource Self}}"
Теперь, когда свойство
DataContext
для Window
установлено в модель представления, потребуется обновить привязки элементов управления в разметке XAML. Начиная с поля со списком, модифицируйте разметку за счет добавления свойства ItemsSource
:
ItemsSource="{Binding Cars}" />
Прием работает, т.к. контекстом данных для
Window
является MainWindowViewModel
, a Cars
— открытое свойство модели представления. Вспомните, что конструкции привязки обходят дерево элементов до тех пор, пока не найдут контекст данных. Далее понадобится обновить привязки для элементов управления Button
. Задача проста; поскольку привязки уже установлены на уровне окна, нужно лишь модифицировать конструкции привязки, чтобы они начинались со свойства DataContext
:
Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=DataContext.AddCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=ItemsSource}"/>
Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=DataContext.DeleteCarCmd,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}" />
Margin="5,0,5,0" Padding="4, 2"
Command="{Binding Path=DataContext.ChangeColorCmd,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type Window}}}"
CommandParameter="{Binding ElementName=cboCars, Path=SelectedItem}"/>
Верите или нет, но вы только что закончили построение первого WPF-приложения MWM. Вы можете подумать: "Это не реалистичное приложение. Как насчет данных? Данные в примере жестко закодированы". И вы будете совершенно правы. Это не реальное приложение, а лишь демонстрация. Однако в ней легко оценить всю прелесть паттерна MWM. Представлению ничего не известно о том, откуда поступают данные; оно просто привязывается к свойству модели представления. Реализации модели представления можно менять, скажем, использовать версию с жестко закодированными данными во время тестирования и версию, обращающуюся к базе данных, в производственной среде.
Можно было бы обсудить еще немало вопросов, в том числе разнообразные инфраструктуры с открытым кодом, паттерн "Локатор модели представления" (View Model Locator) и множество разных мнений на предмет того, как лучше реализовывать паттерн MWM. В том и заключается достоинство паттернов проектирования программного обеспечения — обычно существует много правильных способов их реализации, и вам необходимо лишь отыскать стиль, который наилучшим образом подходит к имеющимся требованиям.
Если вы хотите обновить проект
AutoLot.Dal
для MWM, то должны применить изменения, которые вносились в Car
, ко всем сущностям в проекте AutoLot.Dal.Models
, включая BaseEntity
.
В главе рассматривались аспекты WPF, относящиеся к поддержке паттерна MWM. Сначала было показано, каким образом связывать классы моделей и коллекции с помощью системы уведомлений в диспетчере привязки. Демонстрировалась реализация интерфейса
INotifyPropertyChanged
и применение классов наблюдаемых коллекций для обеспечения синхронизации пользовательского интерфейса и связанных с ним данных.
Вы научились добавлять код проверки достоверности к модели с применением интерфейсов
IDataErrorInfo
и INotifyDataErrorInfо
, а также проверять наличие ошибок, основанных на аннотациях данных. Было показано, как отображать обнаруженные ошибки проверки достоверности в пользовательском интерфейсе, чтобы пользователь знал о проблеме и мог ее устранить. Вдобавок был создан стиль и специальный шаблон элементов управления для визуализации ошибок более эффективным способом.
В заключение вы узнали, каким образом собирать все компоненты вместе за счет добавления модели представления, а также приводить в порядок разметку и отделенный код пользовательского интерфейса, чтобы усилить разделение обязанностей.