Pull to refresh

WPF Binding: Когда нужно использовать ObjectDataProvider?

Reading time 11 min
Views 18K
Original author: Beatriz Costa
Существует множество способов создать объект, который будет использоваться как data source для binding'а. Многие люди создают объект в коде и присваивают свойству DataContext у Window этот объект. Вообще, это хороший способ. Вы могли заметить, что я добавляла объект-источник в Resource Dictionary класса Window в большинстве моих постов, и это работало довольно хорошо. Однако, у нас есть класс ObjectDataProvider в data binding'е, который так же может быть использован для создания вашего source-объекта в XAML. В этом посте я попытаюсь объяснить различия между добавлением объекта-источника непосредственно в resources и использованием ObjectDataProvider. Надеюсь, я предоставлю вам руководство о том, как оценить вашу задачу и выбрать наилучшее решение.

По мере того, как я буду описывать эти возможности, мы создадим небольшую программу, которая позволит людям указать свой вес на Земле и узнать, сколько они будут весить на Юпитере.

Когда мы добавляем объект-источник непосредственно в resources, data binding engine вызывает дефолтный конструктор для типа данного объекта. Объект, добавленный в словарь ресурсов, использует ключ, определенный через x:Key. Вот пример кода при таком подходе:
  1. <Window.Resources>
  2.   <local:MySource x:Key=”source/>
  3.   (…)
  4. </Window.Resources>
* This source code was highlighted with Source Code Highlighter.

Как вариант, вы можете добавить объект класса ObjectDataProvider в resources и использовать его как источник при binding'е. Класс ObjectDataProvider это обертка для вашего объекта-источника, которая предоставляет некоторую дополнительную функциональность. Далее я расскажу о следующих примечательных возможностях ObjectDataProvider'а:
  • Передача параметров в конструктор;
  • Binding на метод (который может принимать параметры);
  • Замена source-объекта;
  • Асинхронное создание source-объекта.

Передача параметров в конструктор


Когда мы добавляем объект-источник непосредственно в resources, WPF всегда вызывает дефолтный конструктор для этого типа. Может случиться так, что у вас нет контроля над объектом-источником, и у того класса, который вы добавляете, нет дефолтного конструктора. Например, это класс из сторонней библиотеки и он объявлен как sealed. В этой ситуации вы можете создать экземпляр класса в XAML через использование ObjectDataProvider’а следующим способом:
  1. <ObjectDataProvider ObjectType=”{x:Type local:MySource}” x:Key=”odp1>
  2.   <ObjectDataProvider.ConstructorParameters>
  3.     <system:String>Jupiter</system:String>
  4.   </ObjectDataProvider.ConstructorParameters>
  5. </ObjectDataProvider>
* This source code was highlighted with Source Code Highlighter.

Эта разметка создает экземпляр класса MySource через вызов его конструктора и передачи ему в качестве параметра строки “Jupiter”. При этом так же создается объект класса ObjectDataProvider, который будет обертывать объект класса MySource.

У MySource есть public-свойство под названием “Planet”, которое предоставляет объект класса Planet, чье имя соответствует строке, переданной в конструктор. В нашем случае, это “Jupiter”. Я хочу, чтобы в программе был Label, который связывался бы со свойством Name у планеты. Binding на подсвойства может быть осуществлен в WPF с помощью “dot notation”. В общем случае, это выглядит следующим образом: Path = Property.SubProperty. Вы можете увидеть это в следующем коде:
  1. <Label Content=”{Binding Source={StaticResource odp1}, Path=Planet.Name}” Grid.ColumnSpan=”2HorizontalAlignment=”CenterFontWeight=”BoldForeground=”IndianRedFontSize=”13Margin=”5,5,5,15/>
* This source code was highlighted with Source Code Highlighter.

Вы могли посмотреть на это определение binding’а и подумать, что оно не имеет смысла. Оно выглядит, как будто мы связываемся с подсвойством Name свойства Planet класса ObjectDataProvider. Но выше я упоминала, что у MySource есть свойство Planet, у ObjectDataProvider’а же его нет. Причина, по которой этот код работает, это то, что binding engine обращается к ObjectDataProvider’у особым образом, когда свойство Path к source-объекту будет заключено в оболочку (т.е. будет wrapped). Заметьте, что этот специальный способ обращения так же может быть применен при связывании с XmlDataProvider’ом или CollectionViewSource’ом.

Binding на метод


У MySource есть метод, который принимает в качестве аргумента вес человека на Земле и рассчитывает вес этого человека на планете, которая была передана в конструктор. Я хочу передать некоторое значение веса в метод в XAML и связаться с его результатом. Это может быть сделано с помощью ObjectDataProvider’а:
  1. <ObjectDataProvider ObjectInstance=”{StaticResource odp1}” MethodName=”WeightOnPlanetx:Key=”odp2>
  2.   <ObjectDataProvider.MethodParameters>
  3.     <system:Double>95</system:Double>
  4.   </ObjectDataProvider.MethodParameters>
  5. </ObjectDataProvider>
* This source code was highlighted with Source Code Highlighter.

Заметьте, что вместо установки свойства ObjectType, в этот раз я установила свойство ObjectInstance. Это позволяет нам многократно использовать экземпляр класса MySource, который мы создали в предыдущем ObjectDataProvider ‘е. Я также установила свойство MethodName и передала параметр в этот метод с помощью свойства MethodParameters. Отобразить результат, возвращаемый из метода, так же просто, как создать binding с этим вторым ObjectDataProvider’ом:
  1. <Label Content=”{Binding Source={StaticResource odp2}}” Grid.Row=”2Grid.Column=”1Grid.ColumnSpan=”2/>
* This source code was highlighted with Source Code Highlighter.

Это хорошее начало, но я хотела бы позволить пользователям вводить свой собственный вес в TextBox, и чтобы после этого Label показывал новое значение. Однако, проблема в том, что я не могу поместить описание Binding’а в свойство MethodParameters потому, что это свойство не является DependencyProperty. В действительности, ObjectDataProvider тоже не является DependencyObject’ом. Как вы помните, целью Binding’а должно быть Dependency-свойство, хотя источник (source)может быть любым. К счастью, выход из положения существует, когда вы хотите связать CLR-свойство с Dependency-свойством: вы можете поменять местами источник и цель, поместив ваш binding в Dependency-свойство и присвоив свойству Mode Binding’а значение TwoWay или OneWayToSource. Этот код показывает, как создать TextBox, который изменяет аргумент, который был передан в метод WeightOnPlanet:
  1. <Window.Resources>
  2.   (…)
  3.   <local:DoubleToString x:Key=”doubleToString/>
  4.   (…)
  5. </Window.Resources>
  6.   
  7. (…)
  8.   
  9. <TextBox Grid.Row=”1Grid.Column=”1Name=”tbStyle=”{StaticResource tbStyle}”>
  10.   <TextBox.Text>
  11.     <Binding Source=”{StaticResource odp2}” Path=”MethodParameters[0]” BindsDirectlyToSource=”trueUpdateSourceTrigger=”PropertyChangedConverter=”{StaticResource doubleToString}”>
  12.     (…)
  13.     </Binding>
  14.   </TextBox.Text>
  15. </TextBox>
* This source code was highlighted with Source Code Highlighter.

В данной ситуации мне не нужно что-либо делать для того, чтобы связывание было Two-Way, потому что я создаю связывание со свойством Text у объекта класса TextBox; оно [связывание] уже является Two-Way по умолчанию. По умолчанию, связывания будут One-Way в большинстве Dependency-свойствах и Two-Way в тех свойствах, где мы ожидаем, что данные будут изменяться пользователем. Это дефолтное поведение может быть переопределено путем изменения свойства Mode у Binding’а.

Как я говорила выше, когда мы создаем binding на ObjectDataProvider, binding engine автоматически смотрит на источник, который обертывается, а не на сам ObjectDataProvider. В этой ситуации, это представляет для нас некоторую проблему, потому что мы хотим связаться со свойством MethodParameters у ObjectDataProvider’а. Чтобы изменить стандартное поведение, мы должны присвоить свойству BindsDirectlyToSource значение true.

Свойство MethodParameters является IList’ом, и в нашей ситуации мы хотим привязаться к первому элементу списка, так как метод WeightOnPlanet принимает только один параметр. Мы можем сделать это, используя индексатор, так же, как мы сделали бы это в C#-коде.

Я присвоила свойству UpdateSourceTrigger значение PropertyChanged, поэтому, когда метод вызывается, мы получаем новое значение всякий раз, когда пользователь печатает что-нибудь в TextBox’е. Другие возможные значения свойства UpdateSourceTrigger это “Explicit” (мы должны явно вызывать метод UpdateSource() у binding’а) и “LostFocus” (источник будет обновлен, когда элемент управления потеряет фокус), который является дефолтным.

Если бы мы осуществляли привязку к свойству с типом double, binding engine пытался бы автоматически преобразовывать свойство Text TextBox’а к типу double. Однако, из-за того, что мы привязываемся к методу, нам надо написать собственный конвертер. Без него binding будет пытаться вызвать метод WeightOnPlanet, который принимает в качестве параметра строку, что будет приводить к ошибке, т.к. подобного метода не существует. Если мы посмотрим на окно Output в Visual Studio, то мы увидим там отладочное сообщение, говорящее о том, что не удалось найти метод, который принимает те параметры, которые мы передаем. Вот код для данного конвертера:
  1. public class DoubleToString : IValueConverter
  2. {
  3.   public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  4.   {
  5.     if (value != null)
  6.     {
  7.       return value.ToString();
  8.     }
  9.     return null;
  10.   }
  11.   
  12.   public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  13.   {
  14.     string strValue = value as string;
  15.     if (strValue != null)
  16.     {
  17.       double result;
  18.       bool converted = Double.TryParse(strValue, out result);
  19.       if (converted)
  20.       {
  21.         return result;
  22.       }
  23.     }
  24.     return null;
  25.   }
  26. }
* This source code was highlighted with Source Code Highlighter.

Некоторые из вас могут быть немного озадачены этим конвертером: должен ли он быть StringToDouble или же DoubleToString? Метод Convert вызывается, когда происходит передача данных от источника (double) к цели (string), а метод ConvertBack вызывается, когда происходит передача в обратном направлении. И так, нам нужен DoubleToString конвертер, а не какой-либо другой.

А что будет, если пользователь введет неправильное значение веса? Он может ввести отрицательное число, или строку, содержащую не только цифры, или же он может вообще ничего не вводить. И, если ситуация такова, я не хочу позволять binding’у начинать передачу данных источнику. Я хочу создать свою собственную логику, которая не только не позволит binding’у передавать данные, но и предупредит пользователя о том, что введенное им значение неверно. Это может быть сделано с помощью функции Validation в связывании данных. Я написала ValidationRule, который проверяет правильность значений и добавила его в свойство следующим способом:
  1. <Binding Source=”{StaticResource odp2}” Path=”MethodParameters[0]” BindsDirectlyToSource=”trueUpdateSourceTrigger=”PropertyChangedConverter=”{StaticResource doubleToString}”>
  2.   <Binding.ValidationRules>
  3.     <local:WeightValidationRule />
  4.   </Binding.ValidationRules>
  5. </Binding>
* This source code was highlighted with Source Code Highlighter.

WeightValidationRule является наследником от ValidationRule и переопределяет метод Validate, в котором я описала нужную мне логику:
  1. public class WeightValidationRule : ValidationRule
  2. {
  3.   public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
  4.   {
  5.     // Value is not a string
  6.     string strValue = value as string;
  7.     if (strValue == null)
  8.     {
  9.       // not going to happen
  10.       return new ValidationResult(false, “Invalid Weight - Value is not a string”);
  11.     }
  12.     
  13.     // Value can not be converted to double
  14.     double result;
  15.     bool converted = Double.TryParse(strValue, out result);
  16.     if (!converted)
  17.     {
  18.       return new ValidationResult(false, “Invalid Weight - Please type a valid number”);
  19.     }
  20.     
  21.     // Value is not between 0 and 1000
  22.     if ((result < 0) || (result > 1000))
  23.     {
  24.       return new ValidationResult(false, “Invalid Weight - You’re either too light or too heavy”);
  25.     }
  26.     
  27.     return ValidationResult.ValidResult;
  28.   }
  29. }
* This source code was highlighted with Source Code Highlighter.

Теперь, ввод неправильного значения вызывает появления красной границы вокруг TextBox’а. Однако, я хочу уведомлять пользователя о сообщении, которое содержится в ValidationResult. Более того, я хотела бы, чтобы появлялась всплывающая подсказка с сообщением об ошибке, когда пользователь делает что-то неправильно. Это все может быть сделано в XAML, с помощью стилей и триггеров:
  1. <Style x:Key=”tbStyleTargetType=”{x:Type TextBox}”>
  2.   <Style.Triggers>
  3.     <Trigger Property=”Validation.HasErrorValue=”true>
  4.       <Setter Property=”ToolTip
  5.           Value=”{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}”/>
  6.     </Trigger>
  7.   </Style.Triggers>
  8. </Style>
* This source code was highlighted with Source Code Highlighter.

Validation.HasError это вложенное (attached) Dependency-свойство, которое становится равно true всякий раз, когда хотя бы один ValidationRule возвращает ошибку. Validation.Errors также является вложенным Dependency-свойством, которое содержит список ошибок у определенного элемента. В данном случае, мы знаем, что TextBox может иметь только одну ошибку (так как у него есть только одно правило), значит, мы можем спокойно связать ToolTip с первой ошибкой в этом списке. “{RelativeSource Self}” просто означает, что источником нашего связывания является сам TextBox. Заметьте, что в описании свойство Path используются скобки – они должны использоваться всякий раз, когда мы осуществляем связывание с вложенным Dependency-свойством. По-русски, “Path=(Validation.Errors)[0].ErrorContent” означает, что мы ищем вложенное свойство Validation.Errors у источника (т.е. у TextBox’а), получаем оттуда первый элемент в списке и обращаемся к подсвойству ErrorContent у полученного ValidationError ‘а.

Если вы попробуете вписать в TextBox что-то, отличное от числа в диапазоне от 0 до 1000, то вы должны увидеть всплывающую подсказку с сообщением об ошибке.

Я написала пример (который входит в SDK), в котором показаны некоторые другие аспекты валидации. Можно еще много чего сказать об этой возможности, но я оставлю более подробное объяснение для последующих постов.

Замена source-объекта


У вас может возникнуть необходимость, когда вам надо заменить текущий источник всех ваших привязок на какой-нибудь другой объект. Если в resource dictionary у вас есть объект, который используется как источник при связывании, нет никакой возможности заменить этот объект каким-нибудь другим объектом и вызвать обновление всех привязок. Удаление этого объекта из словаря ресурсов и добавление нового с таким же x:Key не приведет к тому, что привязки будут оповещены об этом.

Если это ваш случай, вы можете решить использовать ObjectDataProvider. Если вы измените свойство ObjectType, все связывания на этот ObjectDataProvider будут оповещены о том, что объект-источник обновился и надо обновить данные.

Заметьте, что если вы присваиваете свойству DataContext элемента, который находиться выше в дереве элементов, ваш source-объект программно, присвоение другого объекта также вызовет обновление всех привязок.

Асинхронное создание source-объекта


У ObjectDataProvider’а есть свойство под названием IsAsynchronous, которое позволяет вам контролировать, будут ли данные загружаться в том же потоке, что и ваша программа, или же в другом. По умолчанию, у ObjectDataProvider‘а это свойство выставлено в false, а у XmlDataProvider’а в true.

Я планирую более детально обсудить это в одном из моих будущих постов, так что будьте готовы.

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


Здесь вы можете найти проект для Visual Studio с кодом, который был использован в статье.
Tags:
Hubs:
+9
Comments 6
Comments Comments 6

Articles