Привязка данных (Data Binding) в Silverlight

ОГЛАВЛЕНИЕ

Привязка данных (Data binding) является соединением между пользовательским интерфейсом (User Interface) и бизнес-объектом либо другим провайдером данных. Объект пользовательского интерфейса называется приемником (target), провайдер данных называется источником (source).

Привязка данных помогает в разделении уровня пользовательского интерфейса вашего приложения от других уровней вашего приложения (бизнес-объектов, информации и т.д.). Разделение ответственности усиливается уменьшением связанности между приемником пользовательского интерфейса и его источником посредством использования объекта привязки (Binding).

Объект привязки может быть воспринят как черный ящик с универсальными коннекторами: на одной стороне - для приемника, и на другой стороне - для источника. Сверху расположены переключатели, где самым важным является переключатель режима привязки данных (Data Binding Mode), который определяет способ передачи данных.

 

Рис. 2-1. Объект привязки (Binding) в качестве универсального коннектора между приемником и источником 

Режимы привязки данных

Режим (Mode) , который имеет тип BindingMode, является перечислением, обладающим тремя возможными значениями, как это показано ниже,

 

Рис. 2-2. Вырезка из документации о BindingMode

OneTime (одноразовая) привязка устанавливает приемник и после этого привязка завершается. Это отлично подходит для отображения информации, которая изменяется очень редко, либо вообще никогда.

OneWay (односторонняя) привязка устанавливает приемник и обновляет его по ходу изменения источника. Это отлично подходит для отображения информации, которую пользователю не разрешено изменять.

TwoWay (двусторонняя) привязка устанавливает приемник и обновляет его по мере изменения источника, а также изменяет источник, в том случае если пользовательизменит приемник либо что-то другое в приложении, что может вызвать изменение источника.

Если вы создаете интернет-магазин по продаже книг и отображаете информацию о книге, то вы можете использовать одноразовую привязку данных об авторе (Author) и для заголовка (Title), как только вы получите данную информацию (эта информация ведь не будет изменяться впоследствии ), и одностороннюю привязку для цены на книгу ( ведь кто-то может понизить цену за день) , а также вам, скорее всего, понадобится двусторонняя привязка данных для количества книг на складе.

Приемником вашей привязки может быть любое общедоступное свойство (public) виртуального или CLR-объекта.

Вы можете проверить это на небольшом примере; но даже в этом примере мы будем придерживаться трехуровневого подхода, при этом соблюдая строгое разделение между

  • Уровнем пользовательского интерфейса (User Interface Layer)
  • Бизнес-уровнем (Business Layer)
  • Уровнем сохраняемости (Persistence Layer).

Уровень пользовательского интерфейса будет состоять из элементов управления, которыми мы завладеем из инструментария и будем использовать такими, какие они есть. Тем не менее следующая статья расскажет о способе изменения их внешнего вида при помощи стилей и шаблонов. Бизнес-уровень будет представлен классом Book. Мы опустим пока уровень сохраняемости.


Бизнес объект и INotifyPropertyChanged

Создайте новое приложение Silverlight, которое создаст Page.xaml и Page.xaml.cs.

Добавьте к приложению файл Book.cs, который будет представлять собой бизнес уровень.

Бизнес-объект Silverlight и тот объект, который был создан (т.е., ASP.NET) , отличаются тем, что нам необходимо заставить наш бизнес-объект учавствовать в односторонней и двусторонней привязке с уровнем пользовательского интерфейса.

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

Данный интерфейс требует только одного: чтобы класс имел событие типа PropertyChangedEventHandler (называнный PropertyChanged по конвенции), подразумевающий (Implicit) поддержку привязки, тем не менее, ваш бизнес-объект, согласно конвенции, должен вызывать событие PropertyChanged когда любое свойство, привязанное к элементу управления пользовательским интерфейсом будет изменено (и самым простым способом изменить его является установка его значения).

Давайте начнем с упрощенной версии класса Book:

public class Book : INotifyPropertyChanged
{
     private string bookTitle;
     public event PropertyChangedEventHandler PropertyChanged;

     public string Title
     {
       get
       {
         return bookTitle;
       }
       set
       {
         bookTitle = value;
         if (PropertyChanged != null)
         {
           PropertyChanged(this, new PropertyChangedEventArgs("Title"));
         }
       }
     }
}

Первое на что вы должны обратить внимание, так это использование выражения для System.ComponentModel, которое необходимо для интерфейса INotifyPropertyChanged. Класс декларирует то, что он выполнит данный интерфейс в объявлении класса,

public class Book : INotifyPropertyChanged

От него требуется иметь в наличии экземпляр события типа PropertyChangedEventHandler, которое согласно шаблону должно быть названо PropertyChanged. В дапнном упрощенном примере класса Book мы имеем только одно поле указанное в качестве свойства заголовка (Title) - это bookTitle. Мы не можем использовать новый упрощенный синтаксис свойства из C# 3.0 :

public class Book : INotifyPropertyChanged
{
   public event PropertyChangedEventHandler PropertyChanged;
   public string Title { get; set; }
}       

Это эффективно только в случаях, когда вы ничего не делаете в механизме получения, кроме как возврат значения, и когда вы ничего не делаете в механизме установки, кроме как установку значения. В нашем случае установщик делает несколько действий: он проверяет всех на регистрацию в событии, и если найдет регистрацию, то он вызывает зарегистрированный метод посредством делегата события.


Вынесение общего кода во время добавления новых свойств

В то время как мы добавляем новые свойства (bookAuthor, quantityOnHand и т.д.), каждый механизм установки должен проверить на наличие кого-либо, зарегистрированного в данном событии, и если найдет, то должен осуществить вызов. Копирование кода не очень приятная процедура: такой код сложнее масштабировать, и в данном случае высока вероятность ошибок. Мы вынесем ответственность в отдельный метод и заставим каждое свойство вызывать данный метод.

public class Book : INotifyPropertyChanged
{
     private string bookTitle;
     private string bookAuthor;
     private int quantityOnHand;
     private bool multipleAuthor;
     private string authorURL;
     private string authorWebPage;
     private List<string> myChapters;
  
     // реализация необходимого события для интерфейса
     public event PropertyChangedEventHandler PropertyChanged;
 
     public string Title
     {
       get { return bookTitle; }
       set
       {
         bookTitle = value;
         NotifyPropertyChanged("Title");
       }    // завершение установки
     }      // конец свойства
 
     public string Author
     {
       get { return bookAuthor; }
       set
       {
         bookAuthor = value;
         NotifyPropertyChanged("Author");
       }    // завершение установки
 
     }
 
     public List<string> Chapters
     {
       get { return myChapters; }
       set
       {
         myChapters = value;
         NotifyPropertyChanged("Chapters");
       }
}
  
public bool MultipleAuthor
{
       get { return multipleAuthor; }
       set
       {
         multipleAuthor = value;
         NotifyPropertyChanged("MultipleAuthor");
       }    // завершение установки
     }
     public int QuantityOnHand
     {
       get { return quantityOnHand; }
       set
       {
         quantityOnHand = value;
         NotifyPropertyChanged("QuantityOnHand");
       }    // завершение установки
     }

     // factoring out the call to the event
     public void NotifyPropertyChanged(string propertyName)
     {
       if (PropertyChanged != null)
       {
         PropertyChanged(this,
           new PropertyChangedEventArgs(propertyName));
       }
 
     }

Вынесением вызова PropertyChanged мы можем теперь передавать название любого свойства и повторно использовать данный шаблон для вызова делегата.
Отображение информации

Для того чтобы ясно и просто отобразить информацию, мы создадим форму с двумя колонками. Левая будет содержать подсказки, а правая - информацию:

 

Рис. 2-3. Отображаем свойства книги (Book)

На данный момент мы не будет беспокоиться о том, как мы получаем информацию о книге, и лишь сфокусируемся на ее отображении. Все ярлыки слева будут созданы при помощи TextBlocks. Два первых ярлыка справа также будут блоками текста, а за ними идут элементы ListBox, СheckBox и затем TextBox. Обратите внимание на разницу между TextBlock (являющейся ярлыком) и TextBox, который используется для ввода данных.

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

<UserControl x:Class="BookProperties.Page"
   xmlns="http://schemas.microsoft.com/client/2007"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Width="400" Height="300">
 
   <Grid x:Name="LayoutRoot" Background="White">
     <Grid.RowDefinitions>
       <RowDefinition MaxHeight="30" />
       <RowDefinition MaxHeight="30" />
       <RowDefinition MaxHeight="70" />
       <RowDefinition MaxHeight="30" />
       <RowDefinition MaxHeight="40" />
       <RowDefinition MaxHeight="50" />
     </Grid.RowDefinitions>
     <Grid.ColumnDefinitions>
       <ColumnDefinition MaxWidth="150"/>
       <ColumnDefinition MaxWidth="200" />
     </Grid.ColumnDefinitions>
 
     <TextBlock x:Name="TitlePrompt" Text="Title: "
       VerticalAlignment="Bottom"
       HorizontalAlignment="Right"
       Grid.Row="0" Grid.Column="0" />
    <TextBlock x:Name="Title"
       Text="?"  
       VerticalAlignment="Bottom"
       HorizontalAlignment="Left"
       Grid.Row="0" Grid.Column="1" />
 
     <TextBlock x:Name="AuthorPrompt" Text="Author: "
       VerticalAlignment="Bottom"
       HorizontalAlignment="Right"
       Grid.Row="1" Grid.Column="0" />
    <TextBlock x:Name="Author"
       Text="?"  
       VerticalAlignment="Bottom"
       HorizontalAlignment="Left"
       Grid.Row="1" Grid.Column="1" />
 
    <TextBlock x:Name="ChapterPrompt" Text="Chapters: " 
       VerticalAlignment="Bottom"
       HorizontalAlignment="Right"
       Grid.Row="2" Grid.Column="0" />
       
     <ListBox x:Name="Chapters"
       ItemsSource="?"
       VerticalAlignment="Bottom"
       HorizontalAlignment="Left"
       Height="60" Width="200"
       Grid.Row="2" Grid.Column="1" />
 
     <TextBlock x:Name="MultipleAuthorPrompt"
       Text="Multiple authors?: "  
       VerticalAlignment="Bottom"
       HorizontalAlignment="Right"
       Grid.Row="3" Grid.Column="0" />
       
     <CheckBox x:Name="MultipleAuthor"
       IsChecked="?"  
       VerticalAlignment="Bottom"
       HorizontalAlignment="Left"
       Grid.Row="3" Grid.Column="1"/>      
    
     <TextBlock x:Name="QOHPrompt"
     Text="Quantity On Hand: "
     VerticalAlignment="Bottom"
     HorizontalAlignment="Right"
     Grid.Row="4" Grid.Column="0" />
       
     <TextBox x:Name="QuantityOnHand"  
       Text="?"  
      VerticalAlignment="Bottom"
       HorizontalAlignment="Left"
       Height="30" Width="90"
       Grid.Row="4" Grid.Column="1" />
   </Grid>
</UserControl> 


Привязка к элементу TextBlock

Давайте начнем с простого и привяжем два первых блока с текстом. Основа заключается в том, что необходимо сообщить текстовому блоку о том, что вы привязываете и что даете название свойству. Вы также сообщаете Binding о режиме, который будете использовать (односторонний, двусторонний или одноразовый). По умолчанию установлен режим в одну сторону, но хорошим тоном программирования будет явная установка данного свойства. Данная привязка выполняется в текстовом поле и обрамлена фигурными скобками,

Text="{Binding Title, Mode=OneWay }"

Тем самым, первые две строки выглядят так

<TextBlock x:Name="TitlePrompt" Text="Title: "
   VerticalAlignment="Bottom"
   HorizontalAlignment="Right"
   Grid.Row="0" Grid.Column="0" />
  
<TextBlock x:Name="Title"
   Text="{Binding Title, Mode=OneWay }"
   VerticalAlignment="Bottom"
   HorizontalAlignment="Left"
   Grid.Row="0" Grid.Column="1" />
  
  <TextBlock x:Name="AuthorPrompt" Text="Author: "
   VerticalAlignment="Bottom"
   HorizontalAlignment="Right"
   Grid.Row="1" Grid.Column="0" />
  
<TextBlock x:Name="Author"
   Text="{Binding Author, Mode=OneWay }"
   VerticalAlignment="Bottom"
   HorizontalAlignment="Left"
   Grid.Row="1" Grid.Column="1" /> 

Пока что мы не знаем к свойствам Title и Author которого элемента мы осуществляем привязку, но тем не менее, мы подразумеваем наличие объекта с данными свойствами.

Тоже самое выполняется для всех других элементов управления. Для ListBox привязывается ItemSource, но в данном случае оно ожидает привязку к коллекции (collection ; в частности, к IEnumerable).

<ListBox x:Name="Chapters"
   ItemsSource="{Binding Chapters, Mode=OneWay}"
   VerticalAlignment="Bottom"
   HorizontalAlignment="Left"
   Height="60" Width="200"
   Grid.Row="2" Grid.Column="1" />

Checkbox подразумевает привязку к объекту, который будет иметь тип Boolean.

<CheckBox x:Name="MultipleAuthor" IsChecked="{Binding MultipleAuthor, Mode=TwoWay}"
   VerticalAlignment="Bottom"
   HorizontalAlignment="Left"
   Grid.Row="3" Grid.Column="1"/>       


DataContext

Когда вы осуществляете привязку данных во время разработки, вы оповещаете приемник о том, что ему необходимо знать (т.е. вы можете сообщить элементу TextBlock о том, что он будет отображать заголовок (Title) книги) , но вам не нужно говорить ему о конкретной книге, заголовок которой он будет отображать. DataContext является определенной книгой, выбранной во время выполнения и назначенной свойству DataContext элемента структуры (Framework Element; в данном случае - TextBlock) . Таким образом, он считает: "Я получаю значение заголовка (Title) о данной (this) книге". Мы детально рассмотрим это позже.

DataContext может быть необработанной информацией, но лучше назначить ему объект типа Binding. Объект Binding "знает" о том, как приемник должен получить необходимую информацию из источника. Это и является нашим универсальным коннектором с переключателем режима (Mode).

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

Вы можете заменить данный блок

Title.DataContext = currentBook;
Author.DataContext = currentBook;
Chapters.DataContext = currentBook;
MultipleAuthor.DataContext = currentBook;
QuantityOnHand.DataContext = currentBook; 

следующей строкой

LayoutRoot.DataContext = currentBook;


Пошаговая логика привязки данных

  1. Создайте объект приемника (т.е., TextBlock) и определите свойство (Property), которое будет связано (Bound)
  2. Определите что будет источником и свойством в данном источнике, чье значение будет привязано к приемнику
  3. Преобразуйте свойство приемника (Target) к свойству источника (Source) используя DataContext

Это очень легко выполнить, особенно во второй раз.

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

Для того чтобы продемонстрировать это мы добавим новую кнопку, Change, которая будет осуществлять переход между двумя книгами

<Button x:Name="Change"
Content="Change Book" 
Height="30" Width="80"
HorizontalAlignment="Right"
Grid.Row="5" Grid.Column="0" /> 

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

private Book b1;     
private Book b2;
private Book currentBook; 

Вы проинициализируете две книги в памяти, и к первой будете ссылаться при помощи определителя b1, а ко второй - при помощи b2, и currentBook будет переключаться между ними. Признаюсь, что это уловка, но все же это будет работать, при этом код будет очень простым.

Целью является то, что нам необходимо сымитировать "реальную" систему, где вы будете выбирать не между двумя книгами, а между множеством,

 

Рис. 2-4. Получение одной книги из библиотеки

Вам понадобится инициализировать каждую книгу при запуске программы и затем перемещаться по ним.

b1 = new Book();
InitializeBleak(b1);
currentBook = b2 = new Book()
InitializeProgramming(b2); 

Инициализация устанавливает свойства книги.

private void InitializeProgramming(Book b)
 {
   b.Title = "Programming Silverlight";
   b.Author = "Jesse Liberty, Tim Heuer";
   b.MultipleAuthor = true;
   b.QuantityOnHand = 20;
   b.Chapters = new List<string>()
     { "Introduction", "Controls", "Events", "Styles" };
 }
 
 private void InitializeBleak(Book b)
 {
   b.Title = "Bleak House";
   b.Author = "Charles Dickens";
   b.MultipleAuthor = false;
   b.QuantityOnHand = 150;
   b.Chapters = new List<string>()
   {
     "In Chancery",
     "In Fashion",
     "A Progress",
     "Telescoopic Philanthropy",
     "A Morning Adventure",
     "Quite at Home",
     "The Ghosts Walk",
     "Covering Sins",
     "Signs and Tokens",
     "The Law Writer"
   };
 } 


TextBox

В дополнение к добавлению CheckBox, мы добавляем второе поле Read/Write (чтения/записи), TextBox, которое будет отображать и позволит пользователю обновлять количество книг.

<TextBox x:Name="QuantityOnHand"  
   Text="{Binding QuantityOnHand, Mode=TwoWay}"
   VerticalAlignment="Bottom"
   HorizontalAlignment="Left"
   Height="30" Width="90"
   Grid.Row="4" Grid.Column="1" /> 

В нашем случае, каждая книга "знает" свое количество в магазине; конечно, существуют и другие способы реализации этого.


ListBox и привязка к списку

Наконец, мы подошли к главам (Chapters). Объект Book определяет главы (Chapters) в качестве списка строк

public List<string> Chapters
{
   get { return myChapters; }
   set
   {
     myChapters = value;
     NotifyPropertyChanged("Chapters");
   }

При отображении книги главы будут отображены в элементе ListBox. При выборе новой книги будут отображены и ее главы. Вы можете произвести итерацию по главам книги и создать новый набор ListItems, но и в этом случае есть вероятность допустить ошибку. Опять же, привязка данных (Data-binding) будет более подходящим решением.

Поскольку DataSource для Title и Author (для текущей книги) также будет иметь правильный список глав (Chapters), то вы можете назначить свойству Chapter объекта Book свойство ItemSource элемента List Box, и когда будет изменен DataSource, то главы будут изменены соответственно,

<ListBox x:Name="Chapters"
   ItemsSource="{Binding Chapters, Mode=OneWay}"
   VerticalAlignment="Bottom"
   HorizontalAlignment="Left"
   Height="60" Width="200"
   Grid.Row="2" Grid.Column="1" /> 

Следующие два изображения демонстрируют новую информацию о двух книгах, отображенную во время перемещения пользователя между ними.

Рис. 2-5. Отображение первой книги

Рис. 2-6. Отображение второй книги

Скачать исходники примеров 

Источник