Настройка отображения данных с привязкой данных и WPF

ОГЛАВЛЕНИЕ

Когда платформа Windows Presentation Foundation (WPF) впервые появилась в поле зрения .NET, большинство статей и демонстрационных приложений превозносили ее первоклассный механизм визуализации и возможности работы с трехмерной графикой. Хотя читать их и баловаться с ними интересно, такие примеры не отражают широких реальных возможностей WPF. Большинству из нас не нужно создавать приложения с вращающимися видеокубами, которые взрываются фейерверком, если их щелкнуть. Большинство из нас зарабатывают на жизнь, создавая программное обеспечение для отображения и правки больших объемов сложных научных или деловых данных.

Хорошие новости состоят в том, что WPF предлагает отличную поддержку для управления отображением и правкой сложных данных. В номере журнала MSDN® Magazine за декабрь 2007 года Джон Папа (John Papa) написал статью «Привязка данных в WPF» (msdn.microsoft.com/magazine/cc163299), отлично объясняющую ключевые концепции привязки данных в WPF. Здесь я рассмотрю более сложные случаи привязки данных, отталкиваясь от того, что Джон представил в вышеупомянутом выпуске рубрики «Точки данных». По прочтении читатели будут осведомлены о различных путях реализации распространенных требований к привязке данных, наблюдаемых в большинстве бизнес-приложений.

Привязка в коде

Одним из крупнейших изменений, представляемых WPF для разработчиков настольных приложений, является широкое использование и поддержка декларативного программирования. Интерфейсы пользователя и ресурсы WPF можно объявлять с помощью расширяемого языка разметки приложений (XAML), стандартного языка разметки, основанного на XML. Большинство объяснений привязки данных WPF показывают, лишь как работать с привязками в XAML. Поскольку всего, что можно сделать в XAML, также можно достичь в коде, важно, чтобы профессиональные разработчики под WPF знали, как работать с привязкой данных не только декларативно, но и программно.

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

Элементы WPF наследуют методы SetBinding и GetBindingExpression либо от FrameworkElement, либо от FrameworkContentElement. Это просто повышающие удобство работы методы, которые вызывают методы с теми же именами в служебном классе BindingOperations. В приведенном ниже коде показано, как использовать класс BindingOperations для привязки свойства Text текстового окна к свойству на другом объекте:

static void BindText(TextBox textBox, string property)
{
    DependencyProperty textProp = TextBox.TextProperty;
    if (!BindingOperations.IsDataBound(textBox, textProp))
    {
      Binding b = new Binding(property);
      BindingOperations.SetBinding(textBox, textProp, b);
    }

Привязку свойства несложно отменить, использовав следующий код:

static void UnbindText(TextBox textBox)
{
   DependencyProperty textProp = TextBox.TextProperty;
   if (BindingOperations.IsDataBound(textBox, textProp))
   {
     BindingOperations.ClearBinding(textBox, textProp);
   }
}

При очистке привязки привязанное значение также удаляется из целевого свойства.

Объявление привязки данных в XAML скрывает некоторые из лежащих в основе деталей. При начале работы с привязками в коде эти детали начинают всплывать на поверхность. Одна из них заключается в том, что отношения между источником привязки и ее целью на деле поддерживаются экземпляром класса BindingExpression, а не самим классом Binding. Класс Binding («Привязка») содержит высокоуровневую информацию, которую могут совместно использовать несколько классов BindingExpression, но обеспечение связи между двумя связанными свойствами производится лежащим в основе выражением. Нижеследующий код показывает, как BindingExpression можно использовать, чтобы программно удостовериться в том, проверяется ли свойство Text («Текст») текстового окна:

static bool IsTextValidated(TextBox textBox)
{
   DependencyProperty textProp = TextBox.TextProperty;

   var expr = textBox.GetBindingExpression(textProp);
   if (expr == null)
     return false;

   Binding b = expr.ParentBinding;
   return b.ValidationRules.Any();

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


Работа с шаблонами

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

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

Предположим, что у нас имеется простой класс, именуемый FullName, в котором хранится имя некоего лица. Необходимо отобразить список имен, в котором фамилия каждого лица выделялась бы по сравнению с прочим. Чтобы сделать это, можно создать шаблон DataTemplate, описывающий, как визуализовать объект FullName. Код, показанный наРис. 1, отображает класс FullName и фоновый код для окна, в котором отобразится список имен.

Рис. 1. Отображение объектов FullName с помощью DataTemplate

public class FullName
{
   public string FirstName { get; set; }
   public char MiddleInitial { get; set; }
   public string LastName { get; set; }
}

public partial class WorkingWithTemplates : Window
{
   // This is the Window's constructor.
   public WorkingWithTemplates()
   {
     InitializeComponent();

     base.DataContext = new FullName[]
     {
       new FullName 
       { 
         FirstName = "Johann", 
         MiddleInitial = 'S', 
         LastName = "Bach" 
       },
       new FullName 
       { 
         FirstName = "Gustav",
         MiddleInitial = ' ',
         LastName = "Mahler" 
       },
       new FullName 
       { 
         FirstName = "Alfred", 
         MiddleInitial = 'G', 
         LastName = "Schnittke" 
       }
     };
   }
}

Как можно увидеть наРис. 2, в файле XAML окна имеется элемент управления ItemsControl. Он создает простой список элементов, которые пользователь не может выбирать или удалять. У элемента ItemsControl имеется наблон DataTemplate, присвоенный его свойству ItemTemplate, с помощью которого он визуализирует каждый экземпляр FullName, созданный в конструкторе окна. Можно заметить, что у большинства элементов TextBlock в DataTemplate свойство Text («Текст») привязано к представляемым ими свойствам объекта FullName.

Рис. 2. Отображение объектов FullName с помощью DataTemplate

<!-- This displays the FullName objects. -->
<ItemsControl ItemsSource="{Binding Path=.}">
  <ItemsControl.ItemTemplate>
   <DataTemplate>
    <StackPanel Orientation="Horizontal">
     <TextBlock FontWeight="Bold" Text="{Binding LastName}" />
     <TextBlock Text=", " />
     <TextBlock Text="{Binding FirstName}" />
     <TextBlock Text=" " />
     <TextBlock Text="{Binding MiddleInitial}" />
    </StackPanel>
   </DataTemplate>
  </ItemsControl.ItemTemplate>
</ItemsControl>

При запуске этого демонстрационного приложения оно выглядит, как показано наРис. 3. Использование DataTemplate для визуализации имени позволяет легко выделить фамилию каждого лица, поскольку параметр шрифта FontWeight соответствующего TextBlock является полужирным. Этот простой пример демонстрирует суть взаимоотношений между привязкой данных WPF и шаблонами. По мере углубления в тему я буду совмещать эти функции, создавая способы визуализации сложных объектов со все более широкими возможностями.

 

Рис. 3. Объекты FullName, визуализованные DataTemplate


Работа с унаследованным DataContext

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

Для ссылки на объект источника данных не обязательно устанавливать свойство DataContext. Если свойству DataContext элемента-предка в дереве элементов (технически говоря, логическом дереве) дано значение для его DataContext, то значение автоматически будет унаследовано каждым производным элементом в интерфейсе пользователя. Другими словами, если DataContext установлен так, чтобы ссылаться на объект Foo, то, по умолчанию, DataContext каждого элемента в окне будет ссылаться на тот же объект Foo. Любому элементу в окне можно легко дать свое значение DataContext, что заставит все элементы, производные от этого элемента, унаследовать новое значение DataContext. Это напоминает внешнее свойство в Windows Forms.

В предыдущем разделе я рассмотрел использование DataTemplates для создания визуализаций объектов данных. Свойства элементов, созданных шаблоном наРис. 2, привязаны к свойствам объекта FullName. Эти элементы неявно привязывают к их свойству DataContext. Свойство DataContext элементов, созданных ншаблоном DataTemplate, ссылается на объект данных, для которого используется шаблон, такой как объект FullName.

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

Другими примером унаследованного свойства зависимости является имеющееся у всех элементов свойство Font­Size. Если установить свойство зависимости FontSize на окне, то по умолчанию все элементы в этом окне будут изображаться текстом в указанном им размере шрифта. Инфраструктура, что используется для распространения значения FontSize вниз по дереву элементов, распространяет и DataContext.

Здесь термин «наследование» используется в значении, отличающемся от его объектно-ориентированного смысла, где подкласс наследует члены родительского класса. Наследование значений свойств относится только к распространению значений вниз по дереву элементов во время выполнения. Естественно, класс может унаследовать свойство зависимости для поддержки наследования значений в объектно-ориентированном смысле.


Работа с представлениями коллекции

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

На представление коллекции возложено несколько задач. Оно отслеживает текущий элемент в коллекции, каковым обычно является выбранный/активный элемент в элементе управления «список». Представления коллекций также предлагают общие способы упорядочения, фильтрации и разбиения на группы элементов в списке. Несколько элементов управления могут привязывать к одному и тому же представлению вокруг коллекции, обеспечивая свою координацию друг с другом. В приведенном ниже коде показаны некоторые возможности ICollectionView:

// Get the default view wrapped around the list of Customers.
ICollectionView view = CollectionViewSource.GetDefaultView(allCustomers);

// Get the Customer selected in the UI.
Customer selectedCustomer = view.CurrentItem as Customer;

// Set the selected Customer in the UI.
view.MoveCurrentTo(someOtherCustomer);

У всех элементов управления типа списка, в том числе у списка, поля со списком и представления списка, свойство IsSynchronizedWithCurrentItem должно быть установлено на true для сохранения синхронизации со свойством представления коллекции CurrentItem. Это свойство определяет абстрактный класс Selector («Селектор»). Если оно не установлено на true, то выбор элемента в списочном элементе управления не обновит CurrentItem представления коллекции, и присваивание CurrentItem нового значения не будет отображено в списочном элементе управления.

Работа с иерархическими данными

Реальный мир полон иерархических данных. Клиент размещает несколько заказов, молекула состоит из множества атомов, отдел состоит из множества сотрудников, а солнечная система содержит группу небесных тел. Читатели, без сомнения, знакомы с такой схемой «основной/подробности».
WPF предоставляет различные способы работы с иерархическими структурами данных, каждый из которых подходит для своих ситуаций. По сути, альтернатива сводится либо к использованию нескольких элементов управления для отображения данных, либо к отображение нескольких уровней иерархии данных в одном элементе управления. Здесь я разберу оба эти подхода.


Использование нескольких элементов управления для отображения данных XML

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

На Рис. 4, основанном на описанной выше ситуации, показан упрощенный пример данных, с которыми может работать приложение, обернутых в компонент XmlDataProvider платформы WPF. Эти данные согут отображаться в интерфейсе пользователя, подобном показанному наРис. 5. Обратите внимание, что клиентов и заказы можно выбирать, но сведения о заказе существуют в форме списка только для чтения. Этому есть причина – возможность выбирать визуальный объект следует предоставлять только тогда, когда он воздействует на состояние приложения или является изменяемым.

Рис. 4. Иерархия клиентов заказов и сведений о заказах в формате XML

<XmlDataProvider x:Key="xmlData">
  <x:XData>
   <customers >
    <customer name="Customer 1">
     <order desc="Big Order">
      <orderDetail product="Glue" quantity="21" />
      <orderDetail product="Fudge" quantity="32" />
      </order>
      <order desc="Little Order">
       <orderDetail product="Ham" quantity="1" />
       <orderDetail product="Yarn" quantity="2" />
      </order>
     </customer>
     <customer name="Customer 2">
      <order desc="First Order">
       <orderDetail product="Mousetrap" quantity="4" />
      </order>
     </customer>
    </customers>
   </x:XData>
</XmlDataProvider>

 

Рис. 5 Один из способов отображения данных XML

В коде XAML наРис. 6 описано, как использовать эти различные элементы управления для отображения только что показанных иерархических данных. Это окно не требует кода; оно существует полностью в коде XAML.

Рис. 6. Код XAML для привязки иерархических данных XML к интерфейсу пользователя

<Grid DataContext=
   "{Binding Source={StaticResource xmlData},
   XPath=customers/customer}"
   Margin="4" 
  >
  <Grid.RowDefinitions>
   <RowDefinition Height="Auto" />
   <RowDefinition />
   <RowDefinition />
  </Grid.RowDefinitions>

  <!-- CUSTOMERS -->
  <DockPanel Grid.Row="0">
   <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers" />
   <ComboBox
    IsSynchronizedWithCurrentItem="True"
    ItemsSource="{Binding}"
    >
    <ComboBox.ItemTemplate>
     <DataTemplate>
      <TextBlock Text="{Binding XPath=@name}" />
     </DataTemplate>
    </ComboBox.ItemTemplate>
   </ComboBox>
  </DockPanel>

  <!-- ORDERS -->
  <DockPanel Grid.Row="1">
   <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
   <ListBox
    x:Name="orderSelector" 
    DataContext="{Binding Path=CurrentItem}"
    IsSynchronizedWithCurrentItem="True" 
    ItemsSource="{Binding XPath=order}"
    >
    <ListBox.ItemTemplate>
     <DataTemplate>
      <TextBlock Text="{Binding XPath=@desc}" />
     </DataTemplate>
    </ListBox.ItemTemplate>
   </ListBox>
  </DockPanel>

  <!-- ORDER DETAILS -->
  <DockPanel Grid.Row="2">
   <TextBlock DockPanel.Dock="Top" FontWeight="Bold" 
    Text="Order Details" />
   <ItemsControl 
    DataContext=
     "{Binding ElementName=orderSelector, Path=SelectedItem}"
    ItemsSource="{Binding XPath=orderDetail}">
    <ItemsControl.ItemTemplate>
     <DataTemplate>
      <TextBlock>
       <Run>Product:</Run>
       <TextBlock Text="{Binding XPath=@product}" />
       <Run>(</Run>
       <TextBlock Text="{Binding XPath=@quantity}" />
       <Run>)</Run>
      </TextBlock>
     </DataTemplate>
    </ItemsControl.ItemTemplate>
   </ItemsControl>
  </DockPanel>
</Grid>

Обратите внимание на широкое использование коротких запросов XPath для указания платформе WPF на место, где нужно получить привязанные значения. Класс Binding предоставляет свойство XPath, которому можно выделить любой запрос XPath, поддерживаемый методом XmlNode.SelectNodes. Внутренние механизмы WPF используют этот метод для исполнения запросов XPath. Увы, это значит, что поскольку XmlNode.SelectNodes не поддерживает на данный момент использование функций XPath, их не поддерживает и привязка данных WPF.

Поле со списком клиентов и список заказов производят привязку к получающемуся набору узлов запроса XPath, выполняемого запросом DataContext корневого элемента Grid (таблица). DataContext списка автоматически возвратит CurrentItem представления коллекции, являющегося оберткой для коллекции XmlNodes, создаваемой для DataContext таблицы. Другими словами, DataContext списка – это выбранный в настоящий момент клиент. Поскольку ItemsSource списка неявно привязан к собственному DataContext (потому что не было указано другого источника) и его привязка ItemsSource исполняет запрос XPath для получения элементов <order> из DataContext, то ItemsSource фактически привязан к списку заказов выбранного клиента.

Помните, что при привязке к данным XML реально привязка происходит к объектам, созданным вызовом к XmlNode.SelectNodes. При неосторожности можно получить несколько элементов управления, выполняющих привязку к логически эквивалентным, но физически различным наборам XmlNodes. Это обусловлено тем, что при каждом вызове к XmlNode.SelectNodes создается новый набор узлов XmlNode, даже если каждый раз передавать тот же запрос XPath тому же узлу XmlNode. Это особая проблема привязки к данным XML, так что при привязке к бизнес-объектам ее можно спокойно игнорировать.


Использование множества элементов управления для отображения бизнес-объектов

Теперь предположим, что требуется провести привязку к данным из предыдущего примера, но данные существуют в виде бизнес-объектов, а не в коде XML. Как это изменит способ привязки к различным уровням иерархии данных? Насколько похожим или отличающимся будет прием?

В коде наРис. 7 показаны простые классы, используемые для создания бизнес-объектов, сохраняющих данные, к которым произойдет привязка. Эти классы формируют ту же логическую схему, что и данные XML, использованные в предыдущем разделе.

Рис. 7. Классы для создания иерархии бизнес-объектов

public class Customer
{
   public string Name { get; set; }
   public List<Order> Orders { get; set; }

   public override string ToString()
   {
     return this.Name;
   }
}

public class Order
{
   public string Desc { get; set; }
   public List<OrderDetail> OrderDetails { get; set; }

   public override string ToString()
   {
     return this.Desc;
   }
}

public class OrderDetail
{
   public string Product { get; set; }
   public int Quantity { get; set; }
}

Код XAML окна, отображающего эти объекты, показан наРис. 8. Он очень похож на код XAML сРис. 6, но между существуют важные отличия, на которые стоит обратить внимание. Чего в коде XAML не наблюдается, так это конструктора окна, создающего объекты данных и устанавливающего DataContext, вместо установки кодом XAML ссылки на него, как на ресурс. Обратите внимание, что ни в одном из элементов управления свойство DataContext не установлено прямо. Все они наследуют то же свойство DataContext, являющееся экземпляром List<Customer>.

Рис. 8. Код XAML для привязки иерархических бизнес-объектов к интерфейсу пользователя

<Grid Margin="4">
  <Grid.RowDefinitions>
   <RowDefinition Height="Auto" />
   <RowDefinition />
   <RowDefinition />
  </Grid.RowDefinitions>

  <!-- CUSTOMERS -->
  <DockPanel Grid.Row="0">
   <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Customers"
   />
   <ComboBox 
    IsSynchronizedWithCurrentItem="True" 
    ItemsSource="{Binding Path=.}" 
    />
  </DockPanel>

  <!-- ORDERS -->
  <DockPanel Grid.Row="1">
   <TextBlock DockPanel.Dock="Top" FontWeight="Bold" Text="Orders" />
   <ListBox 
    IsSynchronizedWithCurrentItem="True" 
    ItemsSource="{Binding Path=CurrentItem.Orders}" 
    />
  </DockPanel>

  <!-- ORDER DETAILS -->
  <DockPanel Grid.Row="2">
   <TextBlock DockPanel.Dock="Top" FontWeight="Bold" 
     Text="Order Details" />
   <ItemsControl
    ItemsSource="{Binding Path=CurrentItem.Orders.CurrentItem.
    OrderDetails}"
    >
    <ItemsControl.ItemTemplate>
     <DataTemplate>
      <TextBlock>
       <Run>Product:</Run>
       <TextBlock Text="{Binding Path=Product}" />
       <Run>(</Run>
       <TextBlock Text="{Binding Path=Quantity}" />
       <Run>)</Run>
      </TextBlock>
     </DataTemplate>
    </ItemsControl.ItemTemplate>
   </ItemsControl>
  </DockPanel>
</Grid>

Другим существенным различием при привязке к бизнес-объектам вместо кода XML является то, что элементу ItemsControl, размещающему сведения о заказе, не нужно проводить привязку к SelectedItem списка заказа. Этот подход был необходим в случае привязки XML по причине отсутствия универсального способа сослаться на текущий элемент списка, элементы которого происходят из локального запроса XPath.

При привязке к бизнес-объектам вместо XML привязка ко вложенным уровням выбранных элементов является тривиальной задачей. Принадлежащая элементу ItemsControl привязка ItemsSource использует эту удобную функцию, дважды указывая CurrentItem в пути привязки: один раз для выбранного клиента, второй раз для выбранного заказа. Свойство CurrentItem является членом базового представления ICollectionView, являющегося оберткой источника данных, как уже говорилось выше.

Существует еще один интересный момент, относящийся к различию в подходах к работе XML и бизнес-объекта. Поскольку пример XML привязывает к XmlElements, необходимо предоставить шаблоны DataTemplate, чтобы объяснить, как визуализировать клиентов и заказы. При привязке к специальным бизнес-объектам можно избежать этого лишнего труда, просто переопределив метод ToString классов Customer («Клиент») и Order («Заказ») и позволив WPF отобразить выходные данные этого метода для данных объектов. Этого фокуса достаточно лишь для объектов, у которых могут быть простые текстовые представления. При работе со сложными объектами данных использование этой удобного приема может не иметь смысла.


Один элемент управления для отображения всей иерархии

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

Заполнить представление TreeView в WPF элементами можно одним из двух способов. Первый способ – добавление элементов вручную в коде либо в коде XAML, а второй – создание их через привязку данных.

В приведеном ниже коде XAML показано, как можно добавлять элементы TreeViewItem к представлению TreeView в коде XAML:

<TreeView>
  <TreeViewItem Header="Item 1">
   <TreeViewItem Header="Sub-Item 1" />
   <TreeViewItem Header="Sub-Item 2" />
  </TreeViewItem>
  <TreeViewItem Header="Item 2" />
</TreeView>

Прием создания элементов в TreeView вручную имеет смысл в ситуациях, когда элемент управления всегда будет отображать небольшой и статичный набор элементов. При необходимости отображать большие объемы данных, которые могут меняться со временем, становится необходимым использование более динамического подхода. На этом этапе вариантов есть два. Можно написать код, который проходит по структуре данных, создает элементы TreeViewItem, основанные на найденных им объектах данных, и добавляет эти элементы к TreeView. В качестве альтернативы можно воспользоваться иерархическими шаблонами данных и возложить всю работу на WPF.

Использование иерархических шаблонов данных

То, как WPF следует визуализировать иерархические данные через иерархические шаблоны данных, можно выразить декларативно. Класс HierarchicalDataTemplate является средством, наводящим мост между сложной структурой данных и визуальным представлением этих данных. Он очень похож на нормальный DataTemplate, но также позволяет указать, откуда происходят дочерние элементы объекта данных. Также можно предоставить классу HierarchicalDataTemplate шаблон для визуализации этих дочерних элементов.

Предположим, что теперь требуется отобразить данные, представленные наРис. 7 внутри одного элемента управления TreeView. Получившийся TreeView может выглядеть примерно так, как показано наРис. 9. Реализация этого включает использование двух HierarchicalDataTemplate и одного DataTemplate.

 

Рис. 9. Изображение целой иерархии данных в TreeView

Два иерархических шаблона отображают объекты Customer и Order. Поскольку у объектов OrderDetail нет дочерних элементов, их можно визуализировать с помощью неиерархического DataTemplate. Свойство ItemTemplate элемента TreeView использует шаблон для объектов типа Customer, поскольку объекты типа Customer и объекты данных содержатся в корневом уровне TreeView. В коде XAML, приведенном наРис. 10, показано, как собираются все части этой головоломки.

Рис. 10. Код XAML в основе отображения TreeView

<Grid>
  <Grid.DataContext>
   <!-- 
   This sets the DataContext of the UI
   to a Customers returned by calling
   the static CreateCustomers method. 
   -->
   <ObjectDataProvider 
    xmlns:local="clr-namespace:VariousBindingExamples"
    ObjectType="{x:Type local:Customer}"
    MethodName="CreateCustomers"
    />
  </Grid.DataContext>

  <Grid.Resources>
   <!-- ORDER DETAIL TEMPLATE -->
   <DataTemplate x:Key="OrderDetailTemplate">
    <TextBlock>
     <Run>Product:</Run>
     <TextBlock Text="{Binding Path=Product}" />
     <Run>(</Run>
     <TextBlock Text="{Binding Path=Quantity}" />
     <Run>)</Run>
    </TextBlock>
   </DataTemplate>

   <!-- ORDER TEMPLATE -->
   <HierarchicalDataTemplate 
    x:Key="OrderTemplate"
    ItemsSource="{Binding Path=OrderDetails}"
    ItemTemplate="{StaticResource OrderDetailTemplate}"
    >
    <TextBlock Text="{Binding Path=Desc}" />
   </HierarchicalDataTemplate>

   <!-- CUSTOMER TEMPLATE -->
   <HierarchicalDataTemplate 
    x:Key="CustomerTemplate"
    ItemsSource="{Binding Path=Orders}"
    ItemTemplate="{StaticResource OrderTemplate}"
    >
    <TextBlock Text="{Binding Path=Name}" />
   </HierarchicalDataTemplate>
  </Grid.Resources>

  <TreeView
   ItemsSource="{Binding Path=.}"
   ItemTemplate="{StaticResource CustomerTemplate}"
   />

</Grid>

Я выделяю коллекцию объектов Customer для DataContext таблицы (Grid), которая содержит представление TreeView. В коде XAML это можно проделать, используя ObjectDataProvider, являющийся удобным способом вызова метода из XAML. Поскольку DataContext наследуется вниз по дереву элементов, DataContext представления TreeView дает ссылку на этот набор объектов Customer. Именно по этой причине мы можем дать его свойству ItemsSource привязку "{Binding Path=.}", которая является способом указать, что свойство ItemsSource привязано к DataContext элемента TreeView.

Если свойству ItemTemplate представления TreeView не было присвоено значение, то TreeView будет отображать только объекты Customer верхнего уровня. Поскольку WPF не известно, как визуализировать Customer, оно вызовет ToString на каждом Customer и отобразит этот текст для каждого элемента. У него не будет возможности выяснить, что у каждого Customer имеется список связанных с ним объектов Order, а у каждого объекта Order список объектов OrderDetail. Поскольку платформа WPF не может волшебным образом разобраться в существующей схеме данных, необходимо объяснить эту схему платформе WPF, чтобы она могла правильно визуализировать структуру данных.

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

Важно обратить внимание на порядок объявления шаблонов. Шаблон необходимо объявить, прежде чем на него можно ссылаться через выражение StaticResource. Это требование, навязываемое средством чтения XAML, и оно относится ко всем ресурсам, а не только к шаблонам.

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


Работа со вводимыми пользователем данными

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

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

Проверка ввода через ValidationRules

В первой версии платформы WPF, являвшейся частью Microsoft® .NET Framework 3.0, имелась лишь ограниченная поддержка проверки ввода. У класса Binding имелось свойство ValidationRules, в котором могло храниться любое число классов, производных от ValidationRule. Каждое из этих правил могло содержать некоторую логику, проверяющую, является ли привязанное значение допустимым.

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

Предположим, что у нас имеется класс, представляющий эпоху времени, такой, как простой класс Era, показанный здесь:

public class Era
{
   public DateTime StartDate { get; set; }
   public TimeSpan Duration { get; set; }
}

Если необходимо позволить пользователю изменять дату начала и продолжительность эпохи, можно использовать два элемента управления «текстовое поле» и привязать их свойства Text к свойствам экземпляра Era. Поскольку пользователь может вводить в текстовое поле всё, что пожелает, нельзя быть уверенным, что введенный текст окажется преобразуемым в экземпляр DateTime или TimeSpan. В данном случае можно использовать ExceptionValidationRule для сообщения об ошибках в преобразовании данных и затем отобразить эти ошибки в интерфейсе пользователя. В коде XAML, приведенном наРис. 11, показано, как можно выполнить эту задачу.

Рис. 11. Простой класс, представляющий эпоху времени

<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
  <TextBox.Text>
   <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
    <Binding.ValidationRules>
     <ExceptionValidationRule />
    </Binding.ValidationRules>
   </Binding>
  </TextBox.Text>
</TextBox>

<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox 
  Grid.Row="3" 
  Text="{Binding 
     Path=Duration, 
     UpdateSourceTrigger=PropertyChanged, 
     ValidatesOnExceptions=True}" 
  />

Эти два текстовых поля демонстрируют два способа, которыми ExceptionValidationRule можно добавить к свойству ValidationRules привязки в XAML. В текстовом поле Start Date («Дата начала») использован развернутый синтаксис элемента свойства, чтобы прямо добавить правило. В текстовом поле Duration («Продолжительность») использован сокращенный синтаксис, который просто устанавливает свойство привязки ValidatesOnExceptions на true. У обеих привязок свойство UpdateSourceTrigger установлено на PropertyChanged, чтобы ввод проверялся каждый раз, когда свойству текстового поля Text дается новое значение, вместо ожидания момента, когда элемент управления потеряет фокус. Снимок экрана программы показан наРис. 12.

 

Рис. 12. ExceptionValidationRule отображает ошибки проверки

Отображение ошибок проверки

Как показано наРис. 13, текстовое поле Duration содержит неверное значение. Содержащаяся в нем строка не преобразуема в экземпляр TimeSpan. Во всплывающем сообщении текстового поля отображается сообщение об ошибке, и маленький красный значок ошибки появляется на правой стороне элемента управления. Это поведение не является автоматическим, но его легко реализовать и подогнать под конкретный случай.

Рис. 13. Визуализация ошибок, выявленных при проверке ввода, для пользователя

<!-- 
The template which renders a TextBox 
when it contains invalid data. 
-->
<ControlTemplate x:Key="TextBoxErrorTemplate">
  <DockPanel>
   <Ellipse 
    DockPanel.Dock="Right" 
    Margin="2,0"
    ToolTip="Contains invalid data"
    Width="10" Height="10"  
    >
    <Ellipse.Fill>
     <LinearGradientBrush>
      <GradientStop Color="#11FF1111" Offset="0" />
      <GradientStop Color="#FFFF0000" Offset="1" />
     </LinearGradientBrush>
    </Ellipse.Fill>
   </Ellipse>
   <!-- 
   This placeholder occupies where the TextBox will appear. 
   -->
   <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

<!-- 
The Style applied to both TextBox controls in the UI.
-->
<Style TargetType="TextBox">
  <Setter Property="Margin" Value="4,4,10,4" />
  <Setter 
   Property="Validation.ErrorTemplate" 
   Value="{StaticResource TextBoxErrorTemplate}" 
   />
  <Style.Triggers>
   <Trigger Property="Validation.HasError" Value="True">
    <Setter Property="ToolTip">
     <Setter.Value>
      <Binding 
       Path="(Validation.Errors)[0].ErrorContent"
       RelativeSource="{x:Static RelativeSource.Self}"
       />
     </Setter.Value>
    </Setter>
   </Trigger>
  </Style.Triggers>
</Style>

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

Style («Стиль») на Рис. 13 нацелен на все экземпляры текстового поля в интерфейсе пользователя Он применяет к текстовому полю три параметра. Первый, Setter («Метод установки») влияет на свойство Margin («Поля») текстового поля. Свойство Margin устанавливается на значение, которое предоставляет достаточно пространства для отображения значка ошибки с правой стороны.

Следующее свойство Setter в Style присваивает ControlTemplate, используемый для визуализации текстового поля, когда то содержит неверные данные. Оно устанавливает присоединенное свойство Validation.ErrorTemplate на шаблон ControlTemplate, объявленный над Style. Когда класс Validation («Проверка») сообщает, что проверка выявила в текстовом поле одну или несколько ошибок, текстовое поле визуализирует сообщение с помощью этого шаблона. Именно это создает красный значок ошибки, показанный на Рис. 12.

Style также содержит Trigger («Триггер»), отслеживающий прикрепленное к текстовому полю свойство Validation.HasError. Когда класс Validation устанавливает прикрепленное свойство HasError на true для определенного текстового поля, Trigger в Style активируется и присваивает текстовому полю всплывающее сообщение. Содержимое всплывающего сообщения привязано к сообщению об ошибке исключения, выданного при попытке преобразовать текст из текстового поля в экземпляр типа данных свойства источника данных.


Проверка ввода через IDataErrorInfo

С появлением Microsoft .NET Framework 3.5 поддержка проверки ввода в WPF радикально улучшилась. Подход ValidationRule полезен для простых приложений, но реальные приложения имеют дело со всей сложностью реальных данных и бизнес-правил. Кодирование бизнес-правил в объекты ValidationRule не только привязывает этот код к платформе WPF, но также и не позволяет бизнес-логике быть там, где ей и положено быть: в бизнес-объектах!

У многих приложений имеется бизнес-слой, где все сложности обработки бизнес-правил заключены в набор бизнес-объектов. При компиляции в Microsoft .NET Framework 3.5 можно воспользоваться интерфейсом IDataErrorInfo, чтобы заставить WPF запрашивать бизнес-объекты о том, находятся ли они в допустимом состоянии или нет. Это избавляет от необходимости размещать бизнес-логику в объектах, отдельных от бизнес-слоя, и позволяет создавать бизнес-объекты, независимые от платформы интерфейса пользователя. Поскольку интерфейс IDataErrorInfo используется уже несколько лет, это также упрощает повторное использование бизнес-объектов из старых приложений Windows Forms или ASP.NET.

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

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

Рис. 14. IDataErrorInfo предоставляет сообщения о выявленных проверкой ошибках

public class SmartEra 
   : System.ComponentModel.IDataErrorInfo
{
   public DateTime StartDate { get; set; }
   public TimeSpan Duration { get; set; }

   #region IDataErrorInfo Members

   public string Error
   {
     get { return null; }
   }

   public string this[string property]
   {
     get 
     {
       string msg = null;
       switch (property)
       {
         case "StartDate":
           if (DateTime.Now < this.StartDate)
             msg = "Start date must be in the past.";
           break;

         case "Duration":
           if (this.Duration.Ticks == 0)
             msg = "An era must have a duration.";
           break;

         default:
           throw new ArgumentException(
             "Unrecognized property: " + property);
       }
       return msg;
     }
   }

   #endregion // IDataErrorInfo Members
}

Употребить поддержку проверки класса SmartEra из интерфейса пользователя WPF очень просто. Нужно лишь указать привязкам, что тем следует принять интерфейс IDataErrorInfo на объекте, к которому они привязаны. Это можно сделать одним из двух способов, как показано наРис. 15.

Рис. 15. Употребление логики проверки

<!-- START DATE -->
<TextBlock Grid.Row="0">Start Date:</TextBlock>
<TextBox Grid.Row="1">
  <TextBox.Text>
   <Binding Path="StartDate" UpdateSourceTrigger="PropertyChanged">
    <Binding.ValidationRules>
     <ExceptionValidationRule />
     <DataErrorValidationRule />
    </Binding.ValidationRules>
   </Binding>
  </TextBox.Text>
</TextBox>

<!-- DURATION -->
<TextBlock Grid.Row="2">Duration:</TextBlock>
<TextBox 
  Grid.Row="3" 
  Text="{Binding 
     Path=Duration, 
     UpdateSourceTrigger=PropertyChanged, 
     ValidatesOnDataErrors=True,
     ValidatesOnExceptions=True}" 
  />

Подобно тому, как правило ExceptionValidationRule можно добавить к коллекции ValidationRules привязки явно или неявно, правило DataErrorValidationRule можно добавить прямо к ValidationRules привязки, или же можно установить свойство ValidatesOnDataErrors на true. Оба подхода дают один и тот же конечный эффект – система привязки запрашивает интерфейс IDataErrorInfo источника данных на предмет выявленных проверкой ошибок.


Подводя итоги

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

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

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

Автор: Джош Смит (Josh Smith)
Источник: http://msdn.microsoft.com/ru-ru/magazine/