Всем привет!
По разным причинам большинство из нас использует десктопные приложения, как минимум, браузер :) А у некоторых из нас возникает необходимость в написании своих. В этой статье я хочу пробежаться по процессу разработки несложного десктопного приложения с использованием технологии Windows Presentation Foundation (WPF) и применением паттерна MVVM. Желающих продолжить чтение прошу под кат.
Думаю, необязательно говорить, что WPF — это разработка Microsoft :) Технология эта предназначена для разработки десктопных приложений под Windows, начиная с Windows XP. Почему именно так? Это связано с тем, что WPF работает поверх платформы .NET, минимальные требования которой — Windows XP и новее. К сожалению, WPF не работает на других платформах, хотя есть шансы, что в ближайшее время это изменится: в стадии разработки находится WPF-based фреймворк Avalonia.
В чём особенность WPF?
Два основных отличия WPF от других средств построения десктопных приложений:
- Язык разметки XAML, предназначенный для разметки самого интерфейса окна.
- Рендеринг посредством DirectX, аппаратное ускорение графики.
Я не буду углубляться в подробности, т.к. это не совсем тема статьи. Если интересно, то гуглить XAML, WPF rendering, milcore.dll и DirectX :)
О чём эта статья?
Эта статья содержит пример приложения, построенного на технологии WPF:
Я постараюсь ориентировать материал статьи в практическую сторону в стиле "повторяй за мной" с пояснениями.
Что нам понадобится для повторения статьи?
Небольшой опыт разработки на C# :) Как минимум, нужно хорошо понимать синтаксис языка. Также понадобится Windows-машина (в примерах будет Win 10) с установленной на ней Visual Studio (в примерах будет 2017, есть бесплатная Community версия). При установке VS необходимо будет включить поддержку десктопной разработки под платформу .NET
Так же в этом разделе я опишу создание проекта.
Запускаем VS, создаём новый проект, тип приложения выбираем WPF App (.NET Framework) (можно ввести в строке поиска справа вверху), называем как угодно.
После создания нового проекта откроется окно редактора интерфейса, у меня оно выглядит так
Внизу находится редактор разметки, вверху — предпросмотр интерфейса окна, но можно поменять относительное расположение редактора кода и предпросмотра интерфейса так, что они будут располагаться в горизонтальном порядке, с помощью вот этих кнопок (справа на границе двух областей):
Перед тем, как начать
Элементы окна (их ещё называют контрОлами от слова Control) должны размещаться внутри контейнера или внутри другого элемента типа ContentControl. Контейнер — это специальный контрол, позволяющий разместить внутри себя несколько дочерних контролов и организовать их взаимное расположение. Примеры контейнеров:
- Grid — позволяет организовать элементы по столбцам и строкам, ширина каждого столбца или строки настраивается индивидуально.
- StackPanel — позволяет расположить дочерние элементы в одну строку или столбец.
Есть и другие контейнеры. Поскольку контейнер тоже является контролом, то внутри контейнера могут быть вложенные контейнеры, содержащие вложенные контейнеры и так далее. Это позволяет гибко располагать контролы относительно друг друга. Так же с помощью контейнеров мы можем не менее гибко управлять поведением вложенных контролов при изменении размеров окна.
MVVM и интерфейс INotifyPropertyChanged. Копия текста.
Итогом этого примера станет приложение с двумя контролами, в одном из которых можно редактировать текст, а в другом только просматривать. Изменения из одного в другой будут переходить синхронно без явного копирования текста с помощью привязки (binding).
Итак, у нас есть свежесозданный проект (я назвал его Ex1), перейдём в редактор разметки и первым делом заменим контейнер, указанный по умолчанию (<Grid></Grid>) на <StackPanel></StackPanel>. Этого контейнера будет достаточно, т.к. нам понадобится расположить всего лишь два контрола один над другим. Укажем явно, каким образом будут располагаться компоненты, добавив свойство Orientation="Vertical". Добавим внутрь стек панели парочку элементов: поле для ввода текста и поле для отображения текста. Поскольку эти контролы не будут содержать вложенного кода, можно описать их самозакрывающимся тегом (см. код ниже). После всех вышеописанных процедур код описания контейнера и вложенных контролов должен принять такой вид:
<StackPanel Orientation="Vertical">
<TextBox />
<TextBlock />
</StackPanel>
Теперь сосредоточимся на цели этого примера. Мы хотим, чтобы при наборе текста в текстбоксе этот же текст синхронно отображался в текстблоке, избежав при этом явной операции копирования текста. Нам понадобится некая связующая сущность, и вот тут-то мы и подошли к такой штуке, как привязка (binding), о которой было сказано выше. Привязка в терминологии WPF — это механизм, позволяющий связывать некоторые свойства контролов с некоторыми свойствами объекта C#-класса и выполнять взаимное обновление этих свойств при изменении одной из частей связки (это может работать в одну, в другую или в обе стороны сразу). Для тех, кто знаком с Qt, можно провести аналогию слотов и сигналов. Чтобы не растягивать время, перейдём к коду.
Итак, для организации привязки нужны свойства контролов и некое свойство некоего C#-класса. Для начала разберёмся с XAML-кодом. Текст обоих контролов хранится в свойстве Text, поэтому добавим привязку для этих свойств. Делается это так:
<TextBox Text="{Binding}"/>
<TextBlock Text="{Binding}"/>
Мы сделали привязку, но пока непонятно к чему :) Нам нужен объект какого-то класса и какое-то свойство в этом объекте, к которому будет выполнена привязка (как ещё говорят, на которое нужно забиндиться).
Так что это за класс? Этот класс называется вьюмоделью (view model) и служит как раз связующим звеном между view (интерфейсом или его частями) и model (моделью, т.е. теми частями кода, которые отвечают за логику приложения. Это позволяет отделить (в какой-то степени) логику приложения от интерфейса (представления, view) и называется паттерном Model-View-ViewModel (MVVM). В рамках WPF этот класс также называется DataContext.
Однако, просто написать класс вьюмодели недостаточно. Нужно ещё как-то оповещать механизм привязки о том, что свойство вьюмодели или свойство вью изменилось. Для этого существует специальный интерфейс INotifyPropertyChanged, который содержит событие PropertyChanged. Реализуем этот интерфейс в рамках базового класса BaseViewModel. В дальнейшем все наши вьюмодели мы будем наследовать от этого базового класса, чтобы не дублировать реализацию интерфейса. Итак, добавим в проект каталог ViewModels, а в этот каталог добавим файл BaseViewModel.cs. Получим такую структуру проекта:
Код реализации базовой вьюмодели:
using System.ComponentModel;
namespace Ex1.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Создадим для нашего класса MainWindow свою вьюмодель, унаследовавшись от базовой. Для этого в том же каталоге ViewModels создадим файл MainWindowViewModel.cs, внутри которого будет такой код:
namespace Ex1.ViewModels
{
public class MainWindowViewModel : BaseViewModel
{
}
}
Шикарно! Теперь нужно добавить в эту вьюмодель свойство, на которое будем биндить текст наших контролов. Поскольку это текст, тип этого свойства должен быть string:
public string SynchronizedText { get; set; }
В итоге получим такой код
namespace Ex1.ViewModels
{
public class MainWindowViewModel : BaseViewModel
{
public string SynchronizedText { get; set; }
}
}
Так, кажется, справились. Осталось забиндиться на это свойство из вьюхи и готово. Давайте сделаем это прямо сейчас:
<TextBox Text="{Binding Path=SynchronizedText}"/>
<TextBlock Text="{Binding Path=SynchronizedText}"/>
Ништяк, запускаем проект, набираем текст в текстбокс иииии… ничего не происходит))) Ну, ничего страшного, на самом деле мы идём правильной дорогой, просто пока ещё не дошли до нужной точки.
Предлагаю на минутку остановиться и подумать, чего же нам не хватает. Вьюха у нас есть. Вьюмодель тоже. Свойства вроде забиндили. Нужный интерфейс реализовали. Проделали кучу работы ради копирования жалкой строчки текста, за что нам это???!?!111
Ладно, шутки в сторону. Мы забыли создать объект вьюмодели и кое-что ещё (об этом позже). Сам класс мы описали, но это ничего не значит, ведь у нас нет объектов этого класса. Ок, где нужно хранить ссылку на этот объект? Ближе к началу примера я упомянул некий DataContext, используемый в WPF. Так вот, у любой вью есть свойство DataContext, которому мы можем присвоить ссылку на нашу вьюмодель. Сделаем это. Для этого откроем файл MainWindow.xaml и нажмём F7, чтобы открыть код этой вьюхи. Он практически пустой, в нём есть только конструктор класса окна. Добавим в него создание нашей вьюмодели и поместим её в DataContext окна (не забываем добавить using с нужным неймспейсом):
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
Это было просто, но этого всё равно не хватает. По-прежнему при запуске приложения никакой синхронизации текста не происходит. Что ещё нужно сделать?
Нужно вызвать событие PropertyChanged при изменении свойства SynchronizedText и сообщить вьюхе о том, что она должна следить за этим событием. Итак, чтобы вызвать событие, модифицируем код вьюмодели:
public class MainWindowViewModel : BaseViewModel
{
private string _synchronizedText;
public string SynchronizedText
{
get => _synchronizedText;
set
{
_synchronizedText = value;
OnPropertyChanged(nameof(SynchronizedText));
}
}
}
Что мы тут сделали? Добавили скрытое поле для хранения текста, обернули его в уже существующее свойство, а при изменении этого свойства не только меняем скрытое поле, но и вызываем метод OnPropertyChanged, определённый в базовой вьюмодели и вызывающий событие PropertyChanged, объявленное в интерфейсе INotifyPropertyChanged, так же реализованное в базовой вьюмодели. Получается, что при каждом изменении текста возникает событие PropertyChanged, которому передаётся имя свойства вьюмодели, которое было изменено.
Ну, почти всё, финишная прямая! Осталось указать вьюхе, что оно должно слушать событие PropertyChanged:
<TextBox Text="{Binding Path=SynchronizedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}"/>
<TextBlock Text="{Binding Path=SynchronizedText, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"/>
Помимо того, что мы указали, по какому триггеру должно происходить обновление, мы так же указали, в какую сторону это обновление отслеживается: от вью к вьюмодели или наоборот. Поскольку в текстбоксе мы вводим текст, то нам интересны изменения только во вью, поэтому выбираем режим OneWayToSource. В случае с текстблоком всё ровно наоборот: нам интересны изменения во вьюмодели, чтобы отобразить их во вью, поэтому выбираем режим OneWay. Если бы нам нужно было, чтобы изменения отслеживались в обе стороны, можно было не указывать Mode вообще, либо указать TwoWay явно.
Итак, запускаем программу, набираем текст и voi-la! Текст синхронно меняется, и мы нигде ничего не копировали!
Спасибо за внимание, продолжение следует. Будем разбираться с DataTemplate и паттерном Command.
Автор: GREGOR_812