Содержание
Привязка в WPF — это механизм предоставления данных и взаимодействия с ними в приложении. Без использования привязки сегодня не обходится ни одно сколько-нибудь серьезное приложение WPF, поэтому здесь мы рассмотрим основные моменты работы привязки в WPF и научимся использовать её в наших приложениях. Прежде всего, стоит разобраться с основами привязки и тем, как она организуется в приложениях WPF.
Привязка в WPF. Основные сведения
Практически в любом приложении, будь то приложение WPF, .NET MAUI, Blazor или любое другое, можно выделить две части — это интерфейс пользователя (UI), используя который, пользователь взаимодействует с приложением (frontend) и часть, в которой происходит сбор, обработка и хранение данных (backend). Одна из задач, которая стоит перед разработчиком приложения заключается в том, чтобы организовать взаимодействие этих частей — когда пользователь вводит какие-либо данные, то эти данные должны быть каким-либо образом использованы приложением и наоборот — если приложение производит какие-либо изменения в данных, то эти данные должны отобразиться в интерфейсе пользователя.
Одним из распространенных решений этой задачи является использование событий: один объект сообщает посредствам событий об изменениях, другой (или другие объекты) — производит какую-либо работу по обработке этих изменений. Однако, наличие большого количества разнообразных объектов приводит к такому же разнообразию обработчиков событий в приложении и, как следствие, код приложения становится трудночитаемым и поддерживаемым, содержит большой объем однотипного стандартного кода. Механизм привязки позволяет автоматизировать процесс передачи данных от одного объекта к другому.
Вне зависимости от того, что и к чему привязывается, привязка в WPF работает по одному механизму, представленному на рисунке ниже:
Здесь мы можем выделить следующие составные части:
- целевой объект привязки (Binding Target). Обычно, в качестве цели привязки выступает какой-либо элемент управления, например,
Button,TextBoxи т.д. - целевое свойство (Dependency Property, свойство зависимостей). Например, в
TextBoxтаким свойством можем выступать свойство содержащегося в элементе управления текста (Text) - источник привязки (Binding Source). В качестве источника привязки могут выступать любые объекты. Например, это может быть наш собственный объект, содержащий какую-либо информацию.
- путь к значению в источнике привязки для использования (Binding Object) — то, как данные из источника передаются в цель, например, преобразуются из одного типа данных в другой, форматируются и т.д..
Во всех составляющих этой диаграммы новым для нас являются свойства зависимостей (Dependency Property). До сих пор, рассматривая различные элементы управления WPF мы не затрагивали это понятие и не рассматривали то, как такие свойства реализуются, а между тем, свойства зависимостей — это, можно сказать, краеугольный камень всего механизма привязки в WPF, а также использования стилей. Рассмотрим, что из себя представляют свойства зависимостей.
Свойства зависимостей (Dependency Property) в WPF
Когда мы взаимодействуем в нашем приложении с какими-либо элементами управления и их отдельными свойствами, то мы, по сути, используем свойства зависимостей. Например, свойство Text у TextBox выглядит для нас как стандартное свойство C#, но, по сути, оно скрывает за собой свойство зависимостей с именем TextProperty. Вот как на самом деле выглядит это свойство:
public class TextBox : TextBoxBase, IAddChild, ITextBoxViewHost
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text", // Property name
typeof(string), // Property type
typeof(TextBox), // Property owner
new FrameworkPropertyMetadata( // Property metadata
string.Empty, // default value
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | // Flags
FrameworkPropertyMetadataOptions.Journal,
new PropertyChangedCallback(OnTextPropertyChanged), // property changed callback
new CoerceValueCallback(CoerceText),
true, // IsAnimationProhibited
UpdateSourceTrigger.LostFocus // DefaultUpdateSourceTrigger
));
[DefaultValue("")]
[Localizability(LocalizationCategory.Text)]
public string Text
{
get { return (string) GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
Статическое свойство TextProperty — это свойство зависимостей, представленное объектом DependencyProperty. По соглашению, все свойства зависимостей представляют статические публичные поля (public static) к именам которых прибавляется суффикс Property.
Регистрация свойства зависимостей осуществляется с помощью метода DependencyProperty.Register(), в который передаются следующие параметры:
| Параметр | Тип | Описание |
name |
string |
Имя регистрируемого свойства зависимостей. В нашем примере — это Text |
propertyType |
Type |
Тип свойства зависимостей. В нашем случае — это string |
ownerType |
Type |
Владелец свойства зависимостей. В нашем случае — это TextBox |
typeMetadata |
PropertyMetadata |
Необязательный параметр. Дополнительные настройки свойства зависимостей |
validateValueCallback |
ValidateValueCallback |
Необязательный параметр. Функция обратного вызова, используемая для валидации свойства. |
Что касается первых трех параметров, то здесь вопросов не возникает — это довольно простые свойства с типами, которые изучаются в основах C#. Более подробно остановимся на четвертом параметре — typeMetadata. Несмотря на то, что PropertyMetadata— это вполне конкретный тип данных, на практике, при работе со свойствами зависимостей наиболее часто используются объекты, являющиеся наследниками этого типа, а именно FrameworkPropertyMetadata(как в примере выше). Объекты этого типа содержат ряд свойств для конфигурации свойства зависимостей, а именно:
| Свойство | Тип | Описание |
AffectsArrange |
bool |
если имеет значение true, то свойство зависимостей будет влиять на процесс компоновки элемента |
AffectsMeasure |
bool |
если имеет значение true, то свойство зависимостей будет учитываться при установке размеров элемента при компоновке |
AffectsParentArrange |
bool |
если имеет значение true, то свойство зависимостей будет влиять на процесс компоновки в родительском элементе |
AffectsParentMeasure |
bool |
если имеет значение true, то свойство зависимостей будет учитываться при установке размеров родительского элемента при его компоновке |
AffectsRender |
bool |
если имеет значение true, то свойство зависимостей будет влиять на рендеринг и визуализацию элемента |
BindsTwoWayByDefault |
bool |
если имеет значение true, то свойство зависимостей будет использовать двустороннюю привязку данных по умолчанию |
CoerceValueCallback |
CoerceValueCallback |
ссылка на метод, который применяется для проверки допустимости значения до его валидации. Если значение не допустимо, то оно может корректироваться, чтобы соответствовать допустимым диапазонам |
DefaultValue |
object |
значение по умолчанию для свойства зависимостей |
Inherits |
bool |
если имеет значение true, то вложенные элементы применительно к себе могут изменять значение свойства зависимостей. Например, если контейнер Windows задает свойствоFontSize, то TextBlock автоматически применяет это значение, если в нем самом свойство FontSizeне установлено |
IsAnimationProhibited |
bool |
если имеет значение true, то свойство зависимостей не применяется при анимации |
IsNotDataBindable |
bool |
если имеет значение true, то свойство зависимостей не будет поддерживать привязку данных |
Journal |
bool |
если имеет значение true, то значение свойства зависимостей будет журналироваться (сохраняться) |
PropertyChangedCallback |
PropertyChangedCallback |
хранит ссылку на метод, который вызывается при изменении значения свойства |
SubPropertiesDoNotAffectRender |
bool |
если имеет значение true, то элемент не будет перерисовываться, если если какое-то подсвойство у свойства зависимостей изменит свое значение |
В нашем примере, для регистрации свойства зависимостей применяется один из конструкторов:
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text", // Property name
typeof(string), // Property type
typeof(TextBox), // Property owner
new FrameworkPropertyMetadata( // Property metadata
string.Empty, // default value
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault | // Flags
FrameworkPropertyMetadataOptions.Journal,
new PropertyChangedCallback(OnTextPropertyChanged), // property changed callback
new CoerceValueCallback(CoerceText),
true, // IsAnimationProhibited
UpdateSourceTrigger.LostFocus // DefaultUpdateSourceTrigger
));
Этот конструктор устанавливает в качестве значения по умолчанию пустую строку, указывает, что будет поддерживаться двухсторонняя привязка, а свойство будет журналироваться. При изменении значения свойства Text будут вызываться методы OnTextChanged() и CoerceText().
После регистрации свойства определяется обертка — это обычное свойство .NET, которое имеет сеттер и геттер и вызывает методы GetValue() и SetValue() для получения и установки значения свойства зависимостей. Эти методы определены в базовом классе DependencyObject, который является базовым для всех элементов WPF, в том числе и для TextBoх.
После того, как свойство зависимостей зарегистрировано, мы можем его использовать в приложении для привязки данных.
Вычисление свойств зависимостей. Провайдеры свойств
В WPF определение значения свойств зависимостей — это многоэтапный процесс. На различных стадиях этого процесса применяются различные провайдеры свойств, которые помогают получить значение для свойства зависимостей. Так, при извлечении значения свойства система использует десять различных провайдеров, а именно:
- Получение локального значение свойства (которое установлено разработчиком через XAML или через код C#)
- Вычисление значения с помощью триггеров из шаблона родительского элемента (про триггеры мы ещё поговорим далее)
- Вычисление значения из шаблона родительского элемента
- Вычисление значения с помощью триггеров из применяемых стилей
- Вычисление значения с помощью триггеров из применяемого шаблона
- Получение значения из сеттеров применяемых стилей
- Вычисление значения с помощью триггеров из применяемых тем
- Получение значения из сеттеров применяемых тем
- Получение унаследованного значения (если свойство
FrameworkPropertyMetadata.Inheritsимеет значениеtrue) - Извлечение значения по умолчанию, которое устанавливается через объект
FrameworkPropertyMetadata
Эти этапы выполняются последовательно сверху вниз. Причем, если на предыдущем этапе было получено значение, то следующие этапы не выполняются. Все десять перечисленных этапов обычно объединяются в одну стадию — получение базового значения. Что касается процесса установки значения, то здесь также выполняется ряд последовательных шагов:
- Получение базового значения (см. выше)
- Если значение свойства, полученное на шаге 1, представляет собой сложное выражение (например, выражение привязки данных), то WPF вычисляет значение этого выражения и получает конкретный результат
- Если для свойства применяется анимация, то далее она используется для получения нового значения
- После получения значения WPF применяет делегат
CoerceValueCallback, который задается в объектеFrameworkPropertyMetadataпри регистрации свойства. С помощью метода, на который указывает этот делегат, проверяется, входит ли значение в диапазон допустимых значений. Если не входит, то в зависимости от логики может задаваться новое значение - В конце применяется делегат
ValidateValueCallback, который выполняет валидацию. Метод, на который ссылается делегат, возвращаетtrueпри прохождении валидации. Иначе возвращаетсяfalseи генерируется исключение.
Конечно, рассмотрение свойств зависимостей без конкретного примера не имеет смысла. Поэтому далее, мы продолжим рассматривать основы привязки данных в WPF и продемонстрируем то, как мы можем использовать свойства зависимостей в наших собственных элементах управления.
Выражение привязки в WPF
Для определения привязки используется выражение, которое в одном из простых случаев имеет следующий вид:
{Binding Path=Свойство_объекта_источника}
Выражение привязки может содержать и другие параметры, которые влияют на вычисления — о них мы поговорим далее, например, если мы привязываем свойство одного элемента управления к другому, то выражение привязки принимает два параметра:
ElementName— для указания имени источникаPath— для указания свойства источника
Это выражение применяется к свойству цели, которое, как мы уже знаем, должно представлять из себя свойство зависимостей, чтобы платформа могла вычислять и устанавливать значения для этого свойства.
Рассмотрим простой пример привязки WPF:
<StackPanel>
<TextBox x:Name="ButtonName" Margin="10"/>
<Button Content="{Binding ElementName=ButtonName, Path=Text}" Width="200"/>
</StackPanel>
Этот пример можно представить в виде схемы:
То есть элемент TextBox является источником (его значение применяется к свойству Content кнопки), а Button — приемником привязки. Свойство Content элемента Button привязывается к свойству Text элемента TextBox. В итоге при вводе текста в текстовое поле синхронно будут происходить изменения и на кнопке:
Как было сказано выше, выражения привязки могут содержать различные параметры, однако в этой части полученной информации о выражениях привязки нам будет достаточно, чтобы продемонстрировать пример того, как мы можем использовать свойства зависимостей и привязку в WPF при разработке собственных элементов управления.
Пример использования относительной привязки данных и свойств зависимостей в WPF
Достаточно часто в приложениях используются поля ввода текста с описаниями (метками). Стандартное поле ввода (TextBox) не содержит свойства, которое отвечает за текст метки, поэтому, если мы хотим использовать описание поля, то должны использовать минимум два компонента:
- непосредственно само поле ввода —
TextBox - метку для описания поля, например,
Label.
В XAML это выглядит следующим образом:
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Target="{Binding ElementName=userName}"
Content="_Имя пользователя"
VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="userName"
VerticalAlignment="Center"
Text="Введите имя пользователя" />
</Grid>
Таких меток может быть десятки в приложении и каждый раз дублировать один и тот же код XAML — лишняя трата времени и захламление кода. Намного выгоднее вынести этот код в отдельный пользовательский компонент WPF и, используя свойства зависимостей и привязки позволить пользователю компонента задавать два значения: текст метки и текст поля ввода по умолчанию.
Создадим в проекте новую папку Components и разместим в ней новый пользовательский элемент управления WPF:
который назовем TextBoxExtended. Наш элемент управления, как и обычное представление WPF, например, страница или окно, состоит из двух файлов — разметки XAML и файла отдельного кода:
Перенесем в разметку XAML код из предыдущего примера (пока как есть)
<UserControl x:Class="WpfApp1.Components.TextBoxExtended"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1.Components"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Target="{Binding ElementName=userName}"
Content="_Имя пользователя"
VerticalAlignment="Center"/>
<TextBox Grid.Column="1" x:Name="userName"
VerticalAlignment="Center"
Text="Введите имя пользователя" />
</Grid>
</UserControl>
Теперь нам необходимо определить два свойства в нашем компоненте, которые будут отвечать за текст метки и поля ввода. Сделать мы это можем, например, в файле отдельного кода, помня, что свойство цели — это всегда свойство зависимостей. Перепишем код файла TextBoxExtended.xaml.cs следующим образом:
namespace WpfApp1.Components
{
/// <summary>
/// Логика взаимодействия для TextBoxExtended.xaml
/// </summary>
public partial class TextBoxExtended : UserControl
{
public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register("LabelText",
typeof(string),
typeof(TextBoxExtended),
new FrameworkPropertyMetadata() {
DefaultValue="Метка"
});
public string LabelText
{
get {
return (string)GetValue(LabelTextProperty);
}
set {
SetValue(LabelTextProperty, value);
}
}
public static readonly DependencyProperty DefaultTextProperty = DependencyProperty.Register("DefaultText",
typeof(string),
typeof(TextBoxExtended),
new FrameworkPropertyMetadata()
{
DefaultValue = "Значение по умолчанию"
});
public string DefaultText
{
get
{
return (string)GetValue(LabelTextProperty);
}
set
{
SetValue(LabelTextProperty, value);
}
}
public TextBoxExtended()
{
InitializeComponent();
}
}
}
Здесь мы создали для компонента два свойства зависимостей:
LabelTextProperty— текст меткиDefaultTextProperty— текст по умолчанию для поля ввода.
Обоим свойствам мы также добавили значения по умолчанию при их регистрации. Теперь нам необходимо каким-либо образом передавать полученные значения свойств в разметку XAML, чтобы обновлять соответствующие свойства элементов управления из которых состоит наш компонент. Для этого мы должны, по сути, привязать компонент к самом себе — свойство зависимостей, которое мы создали привязывается к другому свойству зависимостей в элементе управления, входящего в состав нашего компонента. В WPF для этого может использоваться выражение для относительной привязки. Изменим код TextBoxExtended.xaml следующим образом:
<UserControl x:Class="WpfApp1.Components.TextBoxExtended"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1.Components"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Target="{Binding ElementName=userName}"
Content="{Binding LabelText, RelativeSource={RelativeSource AncestorType=local:TextBoxExtended}}"
VerticalAlignment="Center"/>
<TextBox Grid.Column="1"
x:Name="userName"
VerticalAlignment="Center"
Text="{Binding DefaultText, RelativeSource={RelativeSource AncestorType=local:TextBoxExtended}}" />
</Grid>
</UserControl>
Обратите внимание на то, как привязываются свойства Content и Text. Здесь мы воспроизвели следующую схему привязки:
О том, как работает относительная привязка и какие параметры может принимать выражение привязки мы также отдельно поговорим в следующих частях пособия. А пока мы можем воспользоваться нашим компонентом и посмотреть на результат. Перейдем в файл MainWindow.xaml и изменим его следующим образом:
<Window x:Class="WpfApp1.MainWindow"
...
xmlns:components="clr-namespace:WpfApp1.Components"
...>
<StackPanel>
<components:TextBoxExtended/>
<components:TextBoxExtended LabelText="Второе поле ввода" DefaultText="Текст по умолчанию"/>
</StackPanel>
</Window>
Здесь мы подключили новое пространство имен в котором содержится наш компонент, а также добавили два новых компонента в приложение. Внешний вид главного окна приложения теперь будет таким:
Итого
В этой части мы познакомились в общих чертах с тем, что из себя представляет привязка в WPF. Привязка в WPF — это мощный механизм предоставления данных и взаимодействия с ними в приложении. Для работы привязки необходим источник и цель привязки, при этом, свойство цели к которому осуществляется привязка обязательно должно быть свойством зависимостей, благодаря чему платформа WPF производит автоматическое обновление данных. Чтобы осуществить привязку в коде XAML используется выражение привязки, которое может содержать различные параметры.





