воскресенье, 6 марта 2011 г.

Коротко о связываниях (binding) в WPF

Байдинги или привязки в WPF очень обширная тема, очень много статей в интернете рзаного размера побольше и поменьше, иногда попадаются удобные шпоргалки. Тем не менее на освоение этой простой технологии ушло без малого пара месяцев, хотя все очень просто.


Основные учасники привязки

Байндинги WPF связываю два объекта: Источник(Source) и Подписчик(Target). Все взаимодействие между source и target объектами укладывается в элементарную схему

На уровне xaml разметки подписчиком всегда будет контрол к которому прикручивается binding, а истоником будет оставшийся объект. Например:

<TextBlock Text="{Binding ElementName=Person}"/>

Здесь TextBlock является подписчиком, а источником будет некий элемент с именем Person.
Источником  может быть любой объект, главное правильно и корректно сосласться на него. Чаще всего приходится пользоваться такими конструкциями:
  • {Binding} В этом случае источником будет взят у контрола из поля DataContext
  • {Binding ElementName=Person} Привязываемся к контролу из xaml разметки с именем Person
  • {Binding RelativeSource={RelativeSource...}} Такая привязка используется, когда хотят привязаться к объекту по относительной связи. Подробнее здесь и здесь.
Чаще всего приходится привязываться не ко всему объекту, а к его свойствам. Для этого существует поле Path. Используется так же как и в коде. Например у нас есть класс

class Citizen
{
public string Name { get; set; }
public string Surname { get; set; }
public string Patronymic { get; set; }
}

Экземпляр лежит в DataContext контрола и мы хотим отобразить свойство Name, binding будет таким


<TextBlock Text="{Binding Path=Name}"/>

А если хотим вывести на экран длину имени, то это делается вот так

<TextBlock Text="{Binding Path=Name.Length}"/>


Направление привязки

В зависимости от ситуации может оказаться, что binding должен работать не в обе стороны, а в какую-то одну, за это поведение отвечает поле Mode:

<TextBlock Text="{Binding ElementName=Person, Mode=Default}"/>

Mode принемает одно из пяти значений:
  • Mode=TwoWay. Все изменения подписчика будут отправленны источнику и, наоборот, все изменения источника повлияют на подписчика. 

  • Mode=OneWay. Данные будут только браться из источника, но изменения подписчика никак не будут влиять на источник.


  • Mode=OneWayToSource. Данные будут браться от подписчика и устанавливаться источнику.  Будут игнорироваться изменения источника.

  • Mode=OneTime. Значение будет считано один раз из источника, дальнейшие изменения источника и подписчика игнорируются.



  • Mode=Default. Устанавливает в Mode значение по умолчанию, одно из четырех: TwoWay, OneWay, OneWayToSource или OneTime. Надо отметить, что для различных контролов в WPF значение поумолчанию разное.

Зачем нужен INotifyPropertyChanged

После всего вышенаписанного осталось непонятно, как источник сообщает о том, что он  изменился. Для этого достаточно отнаследовать класс модели от интерфейса INotifyPropertyChanged. Класс Citizen будет выглядеть примерно так


class Citizen : INotifyPropertyChanged
{
string _name;
public string Name
{
get
{
return _name;
}
set
{
_name = value;
OnPropertyChanged("Name");
}
}

//Surname и Patronymic будут переписаны аналогично Name
public string Surname { ... }
public string Patronymic { ... }

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}

Теперь когда мы изменим поле Name будет вызвано событие о том, что в этом объекте поле Name изменилось. По этому сообщению binding поймет в какой момент надо будет обновить интерфейс.


Когда изменения от подписчика дойдут до источника

С тем как подписчик узнает об изменении источника разобрались. Теперь посмотрим обратную задачу. Когда подписчик сообщает источнику о внесенных изменениях. За это в привязке отвечает поле UpdateSourceTrigger, который может принемать ондно из четырех значений.

  • UpdateSourceTrigger=PropertyChanged. Источник получит новое значение сразу же после изменения подписчика. Далеко не всегда удобный вариант. Например, если мы изменяем текст, то источнику будет присваиваться новое значение всякий раз когда мы вводим/удаляем символ. 
  • UpdateSourceTrigger=LostFocus. Все изменения передадутся источнику после того, как подписчик потеряет фокус. 
  • UpdateSourceTrigger=Default. В этом случае нужно будет лезть в документацию и смотреть какое значение ставиться. Совершенно бесполезное значение, почти всегда устанавливается как UpdateSourceTrigger=PropertyChanged
  • UpdateSourceTrigger=Explicit. Источник узнает об изменении подписчика только после вызова метода UpdateSource. Для этого необходимо иметь binding не в xaml разметке, а непосредственно в коде. подробнее рассказывать нехочу, так как используется редко. Здесь написано подробнее.


Конверторы в привязке

Совершенно прозрачна работа привязок с простыми типами данных (int, double, string). Однако этого недостаточно. Например, мы работаем с валютными операциями. По какой-то причине храним все в рублях, но пользователю нужно выдавать сумму в долларах. Привязки в WPF позволяют конвертировать сырые значения из модели в требуемые для представления. Для этого достаточно написать класс-наследник интерфейса IValueConverter. А потом подключить его в xaml разметке. Интерфейс IValueConverter требует обязательного наличия в классе двух методов.


object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)


Отвечает за преобразование значения из источника в нужный подписчику вид. И второй

object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)


Отвечает за обратное преобразование, значение от подписчику переводит в корректный для источника формат. На картинке это выглядит так


Для примера, впростой реализации конвертер валюты из рублей в доллары будет выглядеть так

class RublesToDollarsConverter : IValueConverter

{
//такой курс долллара был на момент написания поста
const double RublesPerDollar = 28.1717;

#region IValueConverter Members

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is double)
{
return (double)value / RublesPerDollar;
}
return value;
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is double)
{
return (double)value * RublesPerDollar;
}
return value;
}

#endregion
}


Подключить конвертер можно несколькими способами. Самый простой путь: добавить конвертер в список ресурсов xaml разметки контрола и задать этому ресурсу имя, например "ManeyConverter". Теперь там где мы хотим пользоваться конвертером в binding добавляем поле Converter={StaticResource ManeyConverter}. Привязка будет выглядеть примерно так:


{Binding ... Converter={StaticResource ResourceKey=}}.

Вот совершенно чудесная статья по конверторам.


Проверка корректности данных


Тут я ничего писать не буду, ибо в этой статье все прекрасно и коротко описано.

Ложка дегтя

Неcмотря на всю прелесть связываний, есть очень существенное ограничение. При формировании связывания нельзя ссылаться на динамические объекты. То есть внутри биндинга нельзя пользоваться еще одним биндингом или DynamicResource. Однако, это ограничение частично можно обойти через StaticResource, но об этом потом.

2 комментария:

  1. Спасибо большое за статью! помогла в нужный момент!

    ОтветитьУдалить
  2. "Интерфейс IValueConverter требует обязательного наличия в классе двух методов.


    1 - object Convert
    2 - object Convert"
    у вас опечатка, 2 - object ConvertBack

    ОтветитьУдалить