Опубликован: 13.12.2011 | Доступ: свободный | Студентов: 1021 / 34 | Оценка: 4.29 / 4.57 | Длительность: 13:56:00
Лекция 6:

Привязка данных в технологиях WPF и Silverlight

< Лекция 5 || Лекция 6: 12 || Лекция 7 >

Основные принципы связывания

Связывание – это просто способ синхронизации двух элементов данных. Элемент данных (data point) – абстрактное понятие, выражающее идею "точки" в пространстве данных. Описать элемент данных можно разными способами; чаще всего он представляется источником данных и запросом. Например, элемент данных "свойство" состоит из объекта и имени свойства. Имя свойства определяет само свойство, а объект служит источником данных для этого свойства.

В WPF элемент данных представлен классом Binding. Для конструирования привязки мы указываем источник (данных) и путь (запрос). В следующем примере создается элемент данных, ссылающийся на свойство Text объекта TextBox:

Binding bind = new Binding();
bind.Source = textBox1;
bind.Path = new PropertyPath("Text");

Нужен еще второй элемент данных, который будет синхронизован с первым. Поскольку связывание в WPF ограничивается только деревом элементов, то для определения какого-либо элемента данных нужно вызвать метод SetBinding. Этот метод вызывается для источника данных, а данные привязываются к запросу (в данном примере к свойству ContentControl.ContentProperty):

contentControl1.SetBinding(ContentControl.Content, bind);

В этом примере свойство Text объекта textBox1 связывается со свойством Content объекта contentControl1. То же самое можно было бы выразить на XAML

<Window ... Title=’ExampleBind’>
    <StackPanel>
        <TextBox x:Name=’textBox1’ />
        <ContentControl 
              Margin=’5’ x:Name=’contentControl1’
              Content=’{Binding ElementName=textBox1,Path=Text}’ />
    </StackPanel>
</Window>

Когда привязка объявляется в разметке, для задания источника можно использовать свойство ElementName.

Как мы только что увидели, механизм связывания можно применить для привязки свойства Text (строкового) элемента TextBox к свойству Content (имеющему тип object). Так же можно привязать свойство Text к свойству FontFamily:

<Window ... Title=’ExampleBind2’ >
    <StackPanel>
        <TextBox x:Name=’textBox1’ />
        <TextBox x:Name=’textBox2’ />
        <ContentControl
            Margin=’5’
            Content=’{Binding ElementName=textBox1,Path=Text}’
            FontFamily=’{Binding ElementName=textBox2,Path=Text}’/>
    </StackPanel>
</Window>

Существует два механизма преобразования: класс TypeConverter, существующий в .NET, начиная с версии 1.0, и новый интерфейс IValueConverter. В нашем случае с классом FontFamily ассоциирован конвертер типов TypeConverter, поэтому преобразование выполняется автоматически.

Чтобы выполнить нужное преобразование, можно воспользоваться конвертерами значений, ассоциированными с привязкой. Для этого берется источник (строка из свойства Text) и преобразуется в какой-то объект, который понимает получатель (свойство Content).

Начнем с создания простого типа:

public class Human 
{
    private string _name;
    public string Name 
    {
        get { return _name; }
        set { _name = value; }
    }
}

Тип мог бы быть любым: встроенным, библиотечным, разработанным вами. Идея в том, что мы хотим преобразовать свойство Text в объект конкретного типа. Для этого произведем конвертер от интерфейса IValueConverter и реализуем два метода:

public class HumanConverter : IValueConverter 
{
    public object Convert(
        object value, Type targetType, object parameter, CultureInfo culture)
    {
        Human h = new Human();
        h.Name = (string)value;
        return h;
    }
    public object ConvertBack(
        object value, Type targetType, object parameter, CultureInfo culture) 
    {
         return ((Human)value).Name;
    }
}

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

<ContentControl
    Margin=’5’
    FontFamily=’{Binding ElementName=textBox2,Path=Text}’>
    <ContentControl.Content>
        <Binding ElementName=’textBox1’ Path=’Text’>
            <Binding.Converter>
                <l:HumanConverter xmlns:l=’clr_namespace:ExampleBind’/>
            </Binding.Converter>
        </Binding>
    </ContentControl.Content>
</ContentControl>

Элементы данных и преобразования – две базовые конструкции механизма связывания. Познакомившись с основными ингредиентами данных, мы можем заняться деталями привязки к объектам CLR.

Привязка к объектам CLR

Данные привязываются к объектам CLR с помощью свойств и списков (списком считается любой тип, реализующий интерфейс IEnumerable). Связывание устанавливает связь между источником и получателем.

Идентификатор имени свойства для связывания с объектом может записываться в двух видах: для простых свойств CLR и для зависимых свойств, производных от класса DependencyProperty. Чтобы понять, в чем разница, начнем с простого примера:

<Window
    xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’
    xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’>
    <StackPanel>
        <TextBox Name=’text1’>Hello</TextBox>
        <TextBox Text=’{Binding ElementName=text1, Path=Text}’ />
    </StackPanel>
</Window>

Здесь свойство Text объекта TextBox связывается со свойством Text другого объекта. С очень похожим примером мы уже встречались выше. Поскольку Text –зависимое свойство, то этот пример в точности эквивалентен такому:

<Window
    xmlns=’http://schemas.microsoft.com/winfx/2006/xaml/presentation’
    xmlns:x=’http://schemas.microsoft.com/winfx/2006/xaml’>
    <StackPanel>
        <TextBox Name=’text1’>Hello</TextBox>
        <TextBox Text=’{Binding ElementName=text1, Path=(TextBox.Text)}’ />
    </StackPanel>
</Window>

Во втором случае мы воспользовались формой идентификатора свойства, "классифицированной классом". Результат в обоих случаях одинаков, но во втором примере удается избежать применения отражения для разрешения имени "Text" в выражении привязки. Эта оптимизация полезна с двух точек зрения: во-первых, чтобы избежать накладных расходов на отражение, а, во-вторых, чтобы обеспечить привязку к присоединенным свойствам. Например, если бы мы захотели привязать объект TextBox к свойству Grid.Row, то могли бы написать,

<SomeControl SomeProperty=’{Binding ElementName=text1, Path=(Grid.Row)}’ />.

Чтобы лучше понять, как работают пути к свойствам, мы можем взять чуть более сложный объект. Определим класс Person, в котором есть составные свойства Address и Name. В совокупности три класса – Person, Name и Address – образуют небольшую объектную модель, на которой можно продемонстрировать некоторые интересные задачи, возникающие в связи со связыванием. Для начала организуем простое отображение данных о человеке

<!— Window1.xaml —>
<Window ...  Title=’Object Binding’>
    <StackPanel>
        <ContentControl Content=’{Binding Path=Name}’ />
        <TextBlock Text=’{Binding Path=Addresses[0].AddressName}’ />
        <TextBlock Text=’{Binding Path=Addresses[0].Street1}’ />
        <TextBlock Text=’{Binding Path=Addresses[0].City}’ />
    </StackPanel>
</Window>

// Window1.xaml.cs
public partial class Window1: Window 
{
    public Window1()
    {
        InitializeComponent();
        DataContext = new Person();
                             new Name("Иван", "Иванов"),
                             new Address("Интуит",
                                         "Интуит.ru",
                                         "Москва");
    }
}

Здесь иллюстрируется привязка к простому свойству (Path=Name) и более сложные пути к свойствам (Path=Addresses[0].AddressName). Квадратные скобки позволяют добраться до отдельных элементов набора. Обратите также внимание, что мы можем составить сколь угодно сложный путь из имен свойств и индексов в списке. Привязка к списку производится точно так же, как к свойству. Путь к свойству должен приводить к объекту, который реализует интерфейс IEnumerable, но в остальном никаких отличий нет. Можно было бы отображать не один адрес, а завести список и привязаться к его свойству ItemsSource (разумеется, тогда мы смогли бы определить шаблон данных для типа адреса):

<ListBox ItemsSource=’{Binding Path=Addresses}’ />

У человека есть единственное имя и нуль или более адресов. До сих пор нас интересовало в основном отображение данных. Если у свойства, к которому мы привязываемся, есть метод установки, то возможна и двусторонняя привязка.

Редактирование

Чтобы редактировать значения, должен быть какой-то способ узнать, что значение изменилось. Помимо разрешения изменять свойство, существует несколько интерфейсов, которые позволяют объекту или списку рассылать извещения об изменении. Если источник данных уведомляет об изменении, то система связывания сможет отреагировать на модификацию данных. Чтобы наделить наш класс Person способностью извещать об изменениях, у нас есть три возможности:

  • реализовать интерфейс INotifyPropertyChanged;
  • добавить события, с помощью которых мы будем сообщать об изменении;
  • создать свойства, производные от класса DependencyProperty.

Использование событий для извещения об изменениях свойств впервые было применено в .NET 1.0 и поддерживается механизмами связывания в Windows Forms и ASP.NET. Интерфейс INotifyPropertyChanged появился в .NET 2.0. Он оптимизирован для привязки к данным, обладает более высокой производительностью и проще как для авторов объектов, так и для самой системы связывания. Но использовать этот интерфейс в обычных сценариях, когда необходимо извещать об изменениях, несколько сложнее.

Применение свойств, производных от DependencyProperty, относительно просто, позволяет объекту воспользоваться преимуществами, которые дает разреженное хранилище, и хорошо сопрягается с другими службами WPF (например, с динамической привязкой к ресурсам и стилизацией). Подробно создание объектов со свойствами, производными от DependencyProperty, обсуждается в лекции о User/Custom control’ах.

Все три способа во время выполнения демонстрируют одинаковое поведение. Вообще говоря, при создании модели данных лучше реализовать интерфейс INotifyPropertyChanged. Для использования свойств, производных от DependencyProperty, требуется, чтобы класс объекта наследовал DependencyObject, а это, в свою очередь, означает, что объект данных должен работать в STA-потоке. Применение событий для извещения об изменениях свойств приводит к разбуханию объектной модели; в общем случае этот механизм лучше оставить для свойств, которые разработчики и так привыкли отслеживать:

public class Name: INotifyPropertyChanged
{
    ...
    public string First
    {
        get { return _first; }
        set 
        {
            _first = value;
             NotifyChanged("First");
        }
    }
...
    public event PropertyChangedEventHandler PropertyChanged;
    void NotifyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}

Раз мы применяем один и тот же паттерн ко всем трем объектам данных, то можем изменить пользовательский интерфейс и воспользоваться преимуществами новой реализации объектов. Мы заведем два неизменяемых текстовых поля (TextBlock) для отображения текущего имени и два объекта TextBox для редактирования значений. Поскольку класс Name реализует интерфейс INotifyPropertyChanged, то при изменении значений система связывания получит извещение, что приведет к обновлению объектов TextBlock:

<Window ... Text=’Object Binding’>
    <StackPanel>
        <TextBlock Text=’{Binding Path=Name.First}’ />
        <TextBlock Text=’{Binding Path=Name.Last}’ />
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Label Grid.Row=’0’ Grid.Column=’0’>First</Label>
            <TextBox Grid.Row=’0’ Grid.Column=’1’
                Text=’{Binding Path=Name.First}’ />
            <Label Grid.Row=’1’ Grid.Column=’0’>Last</Label>
            <TextBox Grid.Row=’1’ Grid.Column=’1’
                Text=’{Binding Path=Name.Last}’ />
        </Grid>
    </StackPanel>
</Window>

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

<TextBox Grid.Row=’1’ Grid.Column=’1’
    Text=’{Binding Path=Name.Last, UpdateSourceTrigger=PropertyChanged}’/>

Списки сложнее простого изменения свойств. Для связывания необходимо знать, какие элементы были добавлены или удалены. С этой целью в WPF введен интерфейс INotifyCollectionChanged, в котором определено единственное событие CollectionChanged с аргументом такого типа:

public class NotifyCollectionChangedEventArgs: EventArgs
{
    public NotifyCollectionChangedAction Action { get; }
    public IList NewItems { get; }
    public int NewStartingIndex { get; }
    public IList OldItems { get; }
    public int OldStartingIndex { get; }
}

public enum NotifyCollectionChangedAction
{
    Add,
    Remove,
    Replace,
    Move,
    Reset,
}

В нашем примере мы можем поддержать динамическое добавление и удаление адресов человека. Проще всего это реализуется в помощью класса ObservableCollection<T>, который наследует Collection<T> и дополнительно реализует интерфейс INotifyCollectionChanged:

public class Person: INotifyPropertyChanged
{
    IList<Address> _addresses = new ObservableCollection<Address>();
...
}

Теперь можно модифицировать наше приложение так, чтобы все адреса отображались в списковом поле, и реализовать пользовательский интерфейс для ввода новых адресов и добавления их в список:

<!— window1.xaml —>
...
<StackPanel>
    <ListBox ItemsSource=’{Binding Path=Addresses}’/>
    <Button Click=’Add’ Grid.Row=’4’>Add</Button>
</StackPanel>
…
// window1.xaml.cs
void Add(object sender, RoutedEventArgs e)
{
    Address a = new Address();
    a.Street1 = _street.Text;
    a.City = _city.Text;
    ((Person)DataContext).Addresses.Add(a);
}

В текстовые поля можно вводить новую информацию, а при нажатии кнопки Add список адресов обновляется. Поскольку свойство Addresses реализует интерфейс INotifyCollectionChanged, система связывания получает извещение о новом адресе и корректно обновляет пользовательский интерфейс.

Привязка к объектам CLR – процедура по большей части автоматическая. Реализовав интерфейсы INotifyPropertyChanged и INotifyCollectionChanged, мы наделяем источник данных "интеллектом".

Ключевые термины

Data access object (DAO) — это объект, который предоставляет абстрактный интерфейс к какому-либо типу базы данных или механизму хранения. Определённые возможности предоставляются независимо от того, какой механизм хранения используется и без необходимости специальным образом соответствовать этому механизму хранения. Этот шаблон проектирования применим ко множеству языков программирования, большинству программного обеспечения, нуждающемуся в хранении информации и к большей части баз данных, но традиционно этот шаблон связывают с приложениями на платформе Java Enterprise Edition, взаимодействующими с реляционными базами данных через интерфейс JDBC, потому что он появился в рекомендациях от фирмы Sun Microsystems.

Remote Data Objects (RDO) — технология доступа к базам данных компании Microsoft. Представляет собой набор COM-объектов инкапсулирующих ODBCAPI, а также клиентскую курсорную библиотеку. Технология RDO появилась в 1995 году одновременно с выходом продукта Visual Basic 4.0. RDO позиционировалась как технология более простая чем прямое использование вызовов ODBC и в то же время более эффективная чем технология DAO. RDO была ориентирована на обработку данных на стороне сервера БД (такого как MS SQL Server, Oracle и т.д.) в отличие от DAO ориентированной в основном на обработку данных на стороне клиента.

ActiveX Data Objects (ADO)интерфейс программирования приложений для доступа к данным, разработанный компанией Microsoft (MS Access, MS SQL Server) и основанный на технологии компонентов ActiveX. ADO позволяет представлять данные из разнообразных источников (реляционных баз данных, текстовых файлов и т.д.) в объектно-ориентированном виде.

ADO.NETосновная модель доступа к данным для приложений, основанных на Microsoft .NET. Не является развитием более ранней технологии ADO. Скорее представляет собой совершенно самостоятельную технологию. Компоненты ADO.NET входят в поставку оболочки .NET Framework; таким образом, ADO.NET является одной из главных составных частей .NET.

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

Динамическое связываниесвязывание цели вызова и вызываемого метода на этапе выполнения, когда с сущностью связывается метод класса объекта, связанного с сущностью в момент выполнения.

Краткие итоги

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

Набор для практики

Вопросы:

  1. Охарактеризуйте особенности статического и динамического связывания
  2. Особенности класса ObservableCollection<T>
  3. Интерфейсы INotifyCollectionChanged и INotifyPropertyChanged
  4. Использование Converter
  5. Способы наделить пользовательский класс способностью извещать об изменениях
  6. Основные свойства привязки
< Лекция 5 || Лекция 6: 12 || Лекция 7 >