Гибкая и мощная привязка данных в WPF (продолжение)

Автор: Уолт Ричер (Walt Ritscher)

Уведомление об изменении

Почти любое свойство .NET может служить в качестве источника данных. В предыдущем примере источником данных было свойство Value элемента Slider, которое возвращает значение типа Double. Данные загружаются в целевой объект данных при инициализации. Если устанавливается двусторонняя привязка (подробности ниже в этой статье), то данные будут возвращаться назад в источник данных. Система зависимостей WPF может также сохранять обратную синхронизацию данных в целевом объекте данных в случае, когда изменяется свойство источника. Однако этот процесс не автоматический. Чтобы это выполнялось, свойство источника должно реализовывать некоторую форму уведомления об изменении. Выбранный тип должен сообщать инфраструктуре, что базовые данные изменены. Существует несколько способов осуществить это в источнике данных.

  • INotifyPropertyChanged. Реализуйте этот интерфейс .NET 2.0 и создайте событие PropertyChanged . В методах задания свойств следует вызывать событие PropertyChanged всякий раз при изменении значения. Этот способ является основным в WPF.
  • DependencyProperty. Получите из DependencyObject и создайте DependencyProperty. Все уведомления об изменении обрабатываются системой свойств зависимости WPF. Побочный эффект такого подхода состоит в том, что создается зависимость от сборок WPF в сборке бизнес-класса.
  • События xxxPropertyChanged. В .NET версий 1.x можно создавать событие с тем же именем, что и свойство (WidthChanged), и порождать событие при изменении свойства (Width). Майкрософт обеспечивает работу этого сценария в WPF благодаря возможности привязки к старым типам .NET. Хотя это удобно, разработчики найдут, что работать с INotifyPropertyChanged проще.

Пример бизнес-объекта, реализующего INotifyPropertyChanged, см. в листинге 1, а пример подхода с использованием DependencyProperty см. в листинге 2.

Контекст данных

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

Для каждой привязки необходим объект источника данных и путь к свойству источника. Контекст данных позволяет инфраструктуре искать исходный объект далее по логическому дереву. Все элементы FrameworkElements и FrameworkContentElements имеют свойство DataContext. Например:

<TextBlock   Text='{Binding Path=Salary}' />

Что же здесь происходит? Свойство Path указывает на свойство Salary, но где XAML, задающий источник данных? Чтобы ответить на этот вопрос, рассмотрим следующий код.

В C#:

// in the Loaded event procedure
  var emp = new Employee();
  emp.FirstName = "Johnny";
  emp.LastName = "Appleseed";
  emp.Salary = 15000M;

  // sets the Datacontext for the Page
  this.DataContext = emp;

В Visual Basic:

' in the Loaded event procedure
  Dim emp = New Employee()
  emp.FirstName = "Johnny"
  emp.LastName = "Appleseed"
  emp.Salary = 15000D
  ' sets the Datacontext for the Page
  Me.DataContext = emp

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

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

В C#:

// setting different data contexts
   var emp1 = new Employee();
   emp1.LastName = "Appleseed";
   
   stackPanel1.DataContext = emp1;

   var emp2 = new Employee();
   emp2.LastName = "Washington";
   dockPanel1.DataContext = emp2;

В Visual Basic:

' setting different data contexts
  Dim emp1 = New Employee()
  emp1.LastName = "Appleseed"
  stackPanel1.DataContext = emp1
  Dim emp2 = New Employee()
  emp2.LastName = "Washington"
  dockPanel1.DataContext = emp2

Класс Binding имеет конструктор, принимающий строку Path. Это означает, что можно полностью исключить свойство Path полностью и использовать короткий синтаксис, как показано ниже.

<TextBlock   Text='{Binding Path=Salary}' />
<!-This binding works without the
   explicit Path property-->
<TextBlock   Text='{Binding Salary}' />

Расширение привязки

Класс Binding — это основной действующий персонаж в истории привязки данных. Он имеет простой и понятный API. Далее приводятся некоторые наиболее часто используемые свойства привязки.

  • ElementName. Источник данных является другим элементом. Элемент-источник должен иметь заданное имя, обычно с помощью атрибута x:Name.
  • Source. Предоставляет ссылку на допустимый источник объекта. Привязка настраивается с помощью атрибута Source, который имеет преимущество перед любым примененным контекстом данных.
  • RelativeSource. Источник — это свойство в связанном объекте.
  • Path. Предоставляет навигационные инструкции для поиска свойства источника данных. Простейший путь — это всего лишь имя свойства:
<!-- Path to sub properties-->
 <TextBlock xx='{Binding Path=UserName.Length}' />
 <!-- Path to attached property -->
 <TextBlock xx='{Binding Path=(Canvas.Top)}' />
 <!-- Path to Indexer property-->
 <TextBlock xx='{Binding Path='{Brushes[2]}' />
  • XPath. Предоставляет навигационные инструкции XPath для поиска узла источника XML.

Что может быть источником данных

В качестве источника данных можно использовать любой тип, если он имеет общий конструктор без параметров и хотя бы одно общее свойство. Ниже приводится пример привязки к System.String. В этом примере оба элемента TextBlock имеют имя, что делает их доступными в CS-файле и в VB-файле. Привязка textBox2 устанавливается в файле XAML, а контекст данных — в коде программной части.

<StackPanel Margin='20'>
    <Label>Via Code:</Label>
    <TextBlock x:Name='tb1'
               Margin='20'
               Background='LightGray'
               Text="" />
<Label>Via Binding:</Label>
    <TextBlock x:Name='tb2'
               Margin='20'
               Background='LightGray'
               Text="{Binding}" />
    </StackPanel>

В файле кода программной части C#:

string sample;
public BindingToString()
{
  InitializeComponent();
  sample = "ABC DEF GHI JKL MNO PQR";

  // assign directly to the Text property
  tb1.Text = sample;

  // register with the binding system
  tb2.DataContext = sample;
  // there must also be a binding in the XAML
  // or here in code

}

В файле кода программной части Visual Basic:

Private sample As String
Public Sub New()
  InitializeComponent()
  sample = "ABC DEF GHI JKL MNO PQR"
  ' assign directly to the Text property
  textBox1.Text = sample
  ' register with the binding system
  textBox2.DataContext = sample
  ' there must also be a binding in the XAML
  ' or here in code
End Sub

Хранение элементов в ресурсах

Я собираюсь за несколько минут показать, как устанавливать привязку без написания какого-либо кода. Да, я говорю только о привязке XAML. Однако, прежде чем приступить к этому предмету, я хочу потратить несколько минут на обсуждение ресурсов XAML. Инфраструктура ресурсов существует для предоставления многократно используемых элементов в XAML. Я начну с рассмотрения средства синтаксического анализа XAML.

Когда средство синтаксического анализа XAML обнаруживает элемент WPF в файле XAML, оно создает экземпляр базового типа. Это возможно, поскольку.в пространстве имен https://schemas.microsoft.com/winfx/2006/xaml/presentation имеется однозначное (один-к-одному) сопоставление между всеми его элементами, и в сборке .NET существует класс WPF. Таким образом, средство синтаксического анализа знает, как включить элемент <Button> в экземпляр класса System.Windows.Controls.Button. При использовании редактора XAML Visual Studio для написания XAML это в значительной степени скрыто. IntelliSense показывает разрешенные элементы, и компилятор помечает все элементы, которые он не может распознать.

При написании кода часто создаются переменные для хранения экземпляров классов, чтобы можно было ссылаться на них в любом месте кода. Архитекторы WPF считали, что в XAML также важно реализовать многократно используемые элементы. Так родилась инфраструктура ресурсов. Чтобы создать переменную XAML, ее следует добавить в ResourceDictionary. Для ссылки на хранимый элемент используется расширение разметки StaticResource или DynamicResource. Теперь необходимо только узнать, как создать словарь ресурсов в XAML.

Краткое изучение API WPF показывает, что каждый FrameworkElement имеет словарь ресурсов, доступный в его свойстве Resources. Возьмите любой элемент инфраструктуры в файле XAML, добавьте элемент в его свойство Resources, и этот элемент будет доступен в данном элементе инфраструктуры и во всех его дочерних объектах. Поскольку ResourceDictionary хранит все элементы в простой хэш-таблице, все, что требуется для добавления элемента, это ключ и экземпляр объекта.

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

В C#:

// target type for style
// is the TextBox
var s = new Style(typeof(TextBox));
var backSet = new Setter();
 
backSet.Property = BackgroundProperty;
backSet.Value = 
   new SolidColorBrush(Colors.SteelBlue);

   s.Setters.Add(backSet);

// apply the style to 
// the TextBox
textBox1.Style = s;
textBox2.Style = s;

В Visual Basic:

' target type for style
' is the TextBox
Dim s = New Style(GetType(TextBox))
Dim backSet = New Setter()
backSet.Property = BackgroundProperty
backSet.Value = New SolidColorBrush(Colors.SteelBlue)
   s.Setters.Add(backSet)
' apply the style to 
' the TextBox
textBox1.Style = s
textBox2.Style = s

В этом примере сначала создается переменная стиля, а затем добавляется несколько методов задания свойств. После добавления методов задания стиля в коллекцию Setters стиль применяется к отдельным текстовым полям.

Чтобы выполнить то же в XAML, необходимо создать элемент (переменную) в разделе Resources. Затем с помощью расширения разметки StaticResource следует назначить этот ресурс текстовым полям.

В XAML:

<!-- You must provide each element in a 
     Resources section with a key.
     The key - element pair are added
     to the ResourceDictionary's hashtable
     -->
<Page.Resources>
  
  <Style x:Key='demoStyle'
         TargetType='TextBox'>
    <Setter Property='Background'
            Value='SteelBlue' />
  </Style>
</Page.Resources>
<StackPanel>
  <!-- reference the resource with the
       StaticResource markup extension -->
  <TextBox Style='{StaticResource demoStyle}'
           Text='Setting Style from a Page Resource' />
  <TextBox Style='{StaticResource demoStyle}'
           Text='Setting Style from a Page Resource' />
</StackPanel>

Стили — не единственный элемент, который можно хранить в словаре ресурсов. Обычно в словаре ресурсов также сохраняются кисти и шаблоны. Фактически, как можно будет видеть в следующем разделе, можно создавать и хранить в словаре ресурсов почти любой тип .NET. На данный момент показано, как выполнять привязку в коде. В следующем выпуске CoDe Magazine я буду рассматривать привязку XAML.