Статьи

Связывание данных с WPF: связывание с XML

Эта статья взята из WPF в действии с Visual Studio 2008 от Manning Publications. Это демонстрирует, как привязка к XML в WPF чрезвычайно проста. Оглавление, форум авторов и другие ресурсы можно найти по адресу http://www.manning.com/feldman2/ .

Это воспроизводится здесь с разрешения Manning Publications . Книги Мэннинга продаются исключительно через Мэннинга. Посетите страницу книги для получения дополнительной информации.

Мягкая печать: ноябрь 2008 | 520 страниц
ISBN: 1933988223

 

Используйте код «dzone30», чтобы получить 30% скидку на любую версию этой книги.

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

Долгое время у каждого приложения был свой подход к привязке данных к пользовательскому интерфейсу. Со временем, однако, различные структуры пытались обобщить проблему — с различной степенью успеха. Windows Forms была первой технологией пользовательского интерфейса Microsoft, которая действительно имела надежную модель привязки данных, что было сделано путем внедрения привязки глубоко в среду. WPF делает это еще дальше. Привязка данных имеет статус «гражданин первого класса» в WPF, а поддержка носит повсеместный и гибкий характер.

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

Привязка к XML

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

уязвимости и уязвимости в компьютерных системах, и именно так получается, что список публикуется в виде файла XML. Последняя версия XML на момент написания этой статьи весила около 30 МБ. Это звучит как хороший кусок XML, чтобы дать механизм привязки, чтобы жевать. По сути, XML станет нашей моделью. MITER — это исследовательская лаборатория, финансируемая из федерального бюджета Один из проектов, над которым работает MITER, называется списком общих уязвимостей и уязвимостей (CVE). Этот список предоставляет единый источник для идентификации и описания

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

Тем не менее, иногда все, что вам нужно, — это легкое наложение на XML или SQL. В связи с этим мы собираемся создать небольшое приложение для просмотра данных в этих файлах XML (рисунок 11.6).

Рисунок 11.6. Готовая утилита CVE Viewer

CVE XML также предоставляет нам некоторые вложенные данные, что нам и нужно для этого примера. Однако, прежде чем мы зайдем слишком далеко, нам понадобится файл данных XML — список CVE от MITRE. Основной сайт для CVE находится по адресу:

http://cve.mitre.org/

и загрузки списка CVE доступны по адресу:

http://cve.mitre.org/data/downloads/index.html

Есть три файла: Все, CAN и Записи. Файл Entries меньше (около 2 МБ), тогда как файлы All и CAN ближе к 30 МБ. В этом упражнении мы хотим увидеть, как привязка данных будет выдерживать некоторое давление, поэтому мы загрузили файл «All» (allitems.xml) для нашего эксперимента. Не стесняйтесь выбирать меньший файл, если хотите. Вот пример записи из allitems.xml:

<cve>    <item type="CVE" name="CVE-1999-0002" seq="1999-0002">        <status>Entry</status>        <desc>Buffer overflow in NFS mountd gives root access to remote attackers, mostly in Linux systems.</desc>        <refs>            <ref source="CERT">CA-98.12.mountd</ref>            <ref source="BID" url="http://www.securityfocus.com/bid/121">121</ref>            <ref source="XF">linux-mountd-bo</ref>        </refs>    </item>    …A billion more items here…</cve>

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

Создание приложения CVE Viewer

Получив файлы, создайте новый проект приложения WPF с именем «CVE Browser» и, как обычно, удалите Window1.xaml, создайте новое окно с именем CveViewer.xaml и укажите на него StartupUri. Макет здесь будет более сложным, чем ProcessMonitor, поэтому нам нужно сделать немного больше настроек, чем раньше. Окончательный макет показан на рисунке 11.7.

Рисунок 11.7. Основная схема приложения CVE Viewer

Чтобы настроить этот макет, сделайте следующее:

  • Разделите сетку на три столбца шириной 120, 5 и 1 *
  • В первом столбце создайте DockPanel
  • Добавьте TextBox с последующим ListControl в DockPanel
  • Во втором столбце добавьте GridSplitter. Хотя имена CVE в настоящее время имеют предсказуемую ширину, мы не хотим, чтобы приложение работало плохо, если используются более подробные имена.
  • В третьем столбце добавьте GroupBox
  • В GroupBox нам нужны некоторые области для описания, ссылок и комментариев. StackPanel даст нам «эффект» документа для этой части пользовательского интерфейса, и мы будем стилизовать элементы TextBlock так, чтобы они выглядели как заголовки.

Наконец, нам нужны некоторые элементы TextBox и списки для отображения вложенных списков данных для ссылок и комментариев. В листинге 11.4 показан XAML для макета вместе с необходимыми элементами управления.

Листинг 11.4. XAML для CVE Viewer

<Window x:Class="CVE_Viewer.CveViewer" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:debug="clr-namespace:System.Diagnostics;assembly=WindowsBase" Title="Common Vulnerabilities and Exposures Viewer" Width="600" Height="400"><Grid>  <Grid.ColumnDefinitions>    <ColumnDefinition Width="120" />    <ColumnDefinition Width="3" />    <ColumnDefinition Width="1*" />  </Grid.ColumnDefinitions>  <DockPanel> #1    <TextBox Name="filter" DockPanel.Dock="Top" />    <ListBox Name="listBox1" />  </DockPanel>  <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" /> #2  <GroupBox Grid.Column="2" Header="CVE Details"> #3    <StackPanel>      <WrapPanel>        <Label Height="23">Name:</Label>        <Label FontWeight="Bold" Height="23" MinWidth="100" /> #4        <Label Height="23">Status:</Label>        <Label FontWeight="Bold" Height="23" MinWidth="80" />      </WrapPanel>      <TextBlock FontSize="12" FontWeight="Bold" Background="SteelBlue" Foreground="White" Padding="10,2,2,2">Description</TextBlock> #5      <TextBlock TextWrapping="Wrap" Margin="10,10,10,20" />      <TextBlock FontSize="12" FontWeight="Bold" Background="SteelBlue" Foreground="White" Padding="10,2,2,2">References</TextBlock>      <ListBox Margin="10,10,10,20" BorderThickness="0" />      <TextBlock FontSize="12" FontWeight="Bold" Background="SteelBlue" Foreground="White" Padding="10,2,2,2">Comments</TextBlock>      <ListView Margin="10,10,10,20" BorderThickness="0" />    </StackPanel>  </GroupBox></Grid></Window>

(аннотация) # 1 Элементы управления слева
(аннотация) # 2 Splitter
(аннотация) # 3 Подробные данные
(аннотация) # 5 Баннеры просто широкие Blue TextBlocks

Вы можете заметить, что есть ряд элементов управления, которые не имеют никакого значения (# 4). Это потому, что мы собираемся в конечном итоге связать их значения с нашим исходным XML.

Привязка элементов управления к XML

Для следующей задачи мы будем использовать XmlDataProvider. Как и ObjectDataProvider, XmlDataProvider позволяет простое объявление на основе XAML ресурсов XML для использования в приложении WPF. В этом случае мы собираемся объявить его как ресурс элемента верхнего уровня Window. Кроме того, не забудьте ввести пространство имен, чтобы включить атрибут PresentationTraceSources для самого элемента окна:

xmlns:debug="clr-namespace:System.Diagnostics;assembly=WindowsBase"

Теперь мы добавим XmlDataProvider к элементу Window (листинг 11.4).

Листинг 11.4. Добавление XMLDataProvider

<Window.Resources>  <XmlDataProvider x:Key="cve"            Source="X:\Path\to\allitems.xml" #1            XPath="/cve/item" #2            IsAsynchronous="False" #3            IsInitialLoadEnabled="True" #4            debug:PresentationTraceSources.TraceLevel="High"  /></Window.Resources>

Убедитесь, что вы указали правильный путь к XML-файлу в атрибуте Source (# 1). Также обратите внимание на несколько интересных атрибутов этого DataProvider, которые позволяют асинхронную загрузку XML-документа (# 3). Мы также говорим провайдеру, чтобы он автоматически загружал XML при создании окна (# 4). Последняя строка — включение отладки на провайдере, чтобы облегчить нашу жизнь позже.

Это почти все, что нам нужно сделать, чтобы сделать XML доступным для нашего приложения. Мы могли бы так же легко указать провайдеру действительный URI или ввести XmlDocument или XmlReader. Один атрибут, который мы не упомянули, — это атрибут XPath (# 2).

XPath — это стандарт для определения выборок в XML. Стандарт поддерживается W3C и является одним из наиболее распространенных способов выбора элементов в документе XML. Здесь определенное выражение / cve / item говорит, что нужно выбрать все элементы item под корневым элементом cve. Это наш начальный набор данных.

Обозначение привязки XPath

В предыдущем разделе мы использовали Path, чтобы указать конкретное свойство, к которому мы хотели привязаться. С XmlDataProvider, путь все еще в игре, но дополнительное свойство, называемое XPath, будет более интересным. Первое связывание, которое мы хотим, находится на левой стороне ListBox. Это отобразит все CVE в источнике данных XML:

<ListBox ItemsSource="{Binding Source={StaticResource cve}}">

Пока единственное различие между привязкой объекта и привязкой XML заключается в конфигурации источника данных. Вы также можете заметить в конструкторе, что ваш ListBox теперь содержит элементы из реального XML-файла. Иногда это может раздражать, особенно если ваш пользовательский интерфейс привязан к удаленному источнику данных во время разработки. В то же время довольно удобно видеть эффекты связывания без необходимости запуска программы (рисунок 11.8).

Рисунок 11.8. Привязка выполняется в реальном времени к нашим данным в редакторе. Это выглядит только как куча ошибок, потому что это список ошибок.

Теперь у нас есть довольно уродливый список, так как это список InnerText XmlElements. То, что мы действительно хотим в списке слева, это значения из атрибутов имени каждого тега элемента. Как и раньше, нам нужно настроить DataTemplate. Введите следующий XAML в теги ListBox:

<ListBox.ItemTemplate>  <DataTemplate>    <TextBlock Text="{Binding XPath=@name}" />  </DataTemplate></ListBox.ItemTemplate>

« @Name » — это синтаксис XPath для запроса атрибута с именем name из текущего элемента. Рисунок 11.9 показывает ListBox после того, как шаблон был определен.

Рисунок 11.9 Теперь у нас есть шаблон данных, данные ListBox гораздо удобнее для чтения.

Гораздо лучше — теперь наш список стал намного разумнее. Это хорошее время, чтобы поближе взглянуть на то, что происходит между источником (XMLDataProvider) и целью (ListBox) в этом примере. XML является особенно хорошим средством для изучения взаимосвязи между источниками и целями.

При такой настройке XPath, который мы указали в XmlDataProvider, представляет XML-документ как коллекцию XmlElements — XPath определяет набор узлов элементов под корневым узлом cve. Итак, наш источник — это коллекция элементов XMLE типа item. Поскольку ListBoxes может обрабатывать коллекции, все хорошо.

Однако, если бы мы захотели, мы могли бы изменить источник, удалив выражение XPath. Идем дальше и удалим атрибут XPath = «/ cve / item» из XmlDataProvider. Обратите внимание, что список в конструкторе теперь пуст. Причина в том, что без какого-либо XPath XmlDataProvider будет предоставлять корневой элемент (элемент cve) документа. Таким образом, ListBox будет пытаться отобразить коллекцию с одним элементом, но поскольку элемент cve не имеет атрибута name, он вообще ничего не отобразит.

Чтобы это исправить, мы можем изменить атрибут ItemsSource объекта ListBox:

<ListBox ItemsSource="{Binding Source={StaticResource cve}, XPath=/cve/item}">

Теперь у нас снова есть элементы. Это связано с тем, что теперь мы говорим ListBox для привязки к указанному XPath в данных, предоставленных источником данных. Это возвращает нас к тем же данным, которые мы имели раньше.

Все, что мы действительно демонстрируем здесь, — это то, что, в частности, с помощью XPath, не существует единственного «правильного» способа выполнить какое-либо конкретное связывание. Это сама привязка, которая понимает обе стороны отношения и выполняет сопоставление, так что вы можете преобразовать источник в список узлов XmlNode и использовать их в качестве привязки по умолчанию или сделать так, чтобы цель выполняла работу, применяя XPath к чему-либо: XPathable «.

Теперь давайте посмотрим, как вы используете Path против XPath.

Путь против XPath

Как Path, так и XPath предоставляют способ ссылаться на «бит данных», который мы хотим получить из нашего текущего элемента, но у них несколько разные приложения. Например, вы можете думать о нашем ListBox как о отображении списка XmlNodes. Мы используем нотацию XPath, чтобы выбрать атрибут имени из каждого из этих узлов. Однако XmlNode — это объект со свойствами. Если мы хотим получить доступ к значению свойства объекта XmlNode (игнорируя тот факт, что он содержит XML), мы можем использовать нотацию Path. Например, если мы хотим получить OuterXml (свойство XmlNode), мы можем сделать это, указав:

<TextBlock Text="{Binding Path=OuterXml}" />

Это то, что было бы трудно сделать с помощью XPath.

Теперь у вас должен быть список, показывающий каждый элемент ref XML в списке. Среди прочего, это удобный способ быстро визуализировать, какие XML-элементы связаны в определенном контексте и что доступно для них. Когда мы впервые настраивали привязки XML, мы привязывались к OuterXml повсюду, чтобы наблюдать за изменением контекста данных. Прежде чем мы перейдем к следующему разделу, вернемся и вернем привязку к использованию XPath:

<TextBlock Text="{Binding XPath=@name}" />

Одна вещь, которая может быть не совсем понятна, это то, как привязка знает, против чего нужно выполнить Path или XPath. Как это работает, основано на текущем DataContext, о чем мы расскажем далее.

Понимание и использование DataContexts

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

Мы рассмотрим это немного дальше, подключив некоторые элементы управления на правой панели — подробности о выбранном в данный момент элементе в списке. Все элементы UIE имеют свойство DataContext , которое указывает, куда они будут искать данные, если явный источник не указан как часть операции Binding. Мы могли бы установить DataContext для каждого из элементов управления, которые мы хотим связать, но, поскольку DataContext наследуется, если мы установим его в GroupBox, который содержит все элементы управления, они автоматически будут иметь одинаковый контекст:

<GroupBox.DataContext>  <Binding ElementName="listBox1" Path="SelectedItem" /></GroupBox.DataContext>

Это говорит о том, что DataContext для GroupBox (и его дочерних элементов) является свойством SelectedItem в элементе управления ListBox listBox1. Теперь, когда мы связываем отдельные элементы, нам просто нужно указать привязку относительно этого контекста данных. На рисунке 11.10 показано визуальное представление этого. Если бы у нас была еще более глубокая иерархия, мы могли бы повторить этот процесс до тошноты.

Рисунок 11.10 Поскольку цель привязки также может быть источником, подробное представление может связываться с SelectedItem Списка пользовательского интерфейса, вместо того, чтобы выяснить, как отслеживать активный элемент в самом источнике XML.

We have four labels set up across a WrapPanel to show the name and status of each item as we click on it. Without defining any sources on the Label controls themselves, we can specify Path or XPath bindings as if we specified the XML element. Add the following Content tags to the Labels:

<WrapPanel>  <Label Height="23">Name:</Label>  <Label FontWeight="Bold" Height="23" Content="{Binding XPath=@name}" MinWidth="100" />  <Label Height="23">Status:</Label>  <Label FontWeight="Bold" Height="23" Content="{Binding XPath=status}" MinWidth="80" /></WrapPanel>

We are binding the first label to the value from the name attribute and the second label to the value of the status element (since there is no @ sign in front of status, XPath interprets that to mean that we want the contents of a child element). Directly after the WrapPanel, we can now bind our description as well:

<TextBlock Margin="10,10,10,20" TextWrapping="Wrap" Text="{Binding XPath=desc}" />

Since there’s no selected item in the designer, the property will be null and we won’t see anything as we set all these up. However, when you run the application, you should be able to click through the list and see the name, description and status fields all populated. When the SelectedItem changes, the Binding we set on the DataContext property of the GroupBox catches the PropertyChanged event fired from the first ListBox and sets the DataContext accordingly. When the DataContext changes, the subsequent controls are then notified and all the bindings we just defined are re-evaluated and updated. Beautiful.

The next thing we want to do is to populate the ListBox that shows all of the “refs” from the item xml –hyperlinks to related data. Listing 11.5 shows the XAML for this.

Listing 11.5 Binding to the list of refs

<ListBox ItemsSource="{Binding XPath=refs/ref} #1 "Margin="10,10,10,20" BorderThickness="0" BorderBrush="Transparent">  <ListBox.ItemTemplate>    <DataTemplate>      <WrapPanel>        <TextBlock MinWidth="50" Text="{Binding XPath=@source}" /> #2        <TextBlock>          <Hyperlink NavigateUri="{Binding XPath=@url}" #3                          RequestNavigate="Hyperlink_RequestNavigate">            <TextBlock Text="{Binding Path=InnerText}" />          </Hyperlink>        </TextBlock>      </WrapPanel>    </DataTemplate>  </ListBox.ItemTemplate></ListBox>

(annotation) #1 Bind to list of refs
(annotation) #2 Source from under ref

There is a fair amount going on here, so we’ll take it slow. First of all, we are setting the ItemsSource for the ListBox to use the XPath “refs/ref” (#1). Since we are inside of the DataContext set on the GroupBox, this XPath will be relative to that, so will return all of the ref elements under the refs element under the current item (no, really). Further, because we are setting the Source, we are implicitly setting a new DataContext that will apply to all of the items in the ListBox. Any binding that we do within an item will be relative to the current ref object.

The first control we are putting in our template is a TextBlock that is bound to the source attribute (#2). This is an attribute on ref elements. The next thing we want to do is create a hyperlink based on the data in the ref tag (#3). This is tricky because not everything inside of a Hyperlink can be directly bound. So, let’s take the pieces one at a time:

NavigateUri="{Binding XPath=@url}"

This is the easy one – we want the value from the URL attribute in the ref to be where the hyperlink will navigate us to.

RequestNavigate="Hyperlink_RequestNavigate"

This is just an event handler. The Hyperlink_RequestNavigate method gets the NavigateUri from the passed Hyperlink, and then does a Process.Start(). We haven’t bothered showing the code, but it is in the on-line version.

<TextBlock Text="{Binding Path=InnerText}" />

Because you can’t bind to the contents of a Hyperlink directly, we have to put something inside of the Hyperlink that will display the text we want to display. So we are putting a TextBlock inside of the Hyperlink (which is inside of a TextBlock) so that we can bind the TextBlock’s Text property. Notice that we are using Path instead of XPath here, since we want the InnerText of the XmlElement.

The binding for the comments ListBox is pretty similar, albeit simpler (listing 11.6):

Listing 11.6 Binding the list of comments

<ListView ItemsSource="{Binding XPath=comments/comment}" #1                                               Margin="10,10,10,20" BorderThickness="0" />  <ListView.ItemTemplate>    <DataTemplate>      <TextBlock Text="{Binding Path=InnerText}"/> #2    </DataTemplate>  </ListView.ItemTemplate></ListView>

(annotation) #1 Collecting all the comments from the item
(annotation) #2 Just bind the comment text, nothing fancy.

At this point, we have a functional CVE viewer that binds XML remarkably fast. With the XML support in WPF, creating custom editors for XML is extremely easy, and can be used to mitigate the pain of manually editing XML configuration files.

Master-Detail Binding

As you saw in figure 11.10, the list is driven from the data source, but the detail view is driven off of the list, rather than the data. From the user’s perspective, this is irrelevant, but there are certainly situations where you really want to make sure that what the user is viewing is tied to the data, and not a selected control. For example, if there are multiple controls that can potentially control the current record. Also, from a purist’s perspective, it is more “correct” to tie to data if possible (although not, perhaps as simple).

The nice thing is that WPF Data Binding has automatic handling for master-detail binding. If you bind a list data source to a list control, as we do above—tying the list of Items to the ListBox—then the list control shows a list of values. However, if you bind a non-list control to a list, like a TextBox, the binding mechanism automatically associates the binding with the current record in the list. So, instead of doing this:

<GroupBox.DataContext>  <Binding ElementName="listBox1" Path="SelectedItem" /></GroupBox.DataContext>

We could do this:

<GroupBox.DataContext>  <Binding Source="{StaticResource cve}"/></GroupBox.DataContext>

Which means that our individual controls are bound to exactly the same thing as the list. If you run the application with this binding, you will notice two things. First of all, the controls on the right of the application will all be populated even before you select a record in the list and second, changing the current selection in the list does not change what is displayed on the right.

So, the cool thing is that the binding code automatically knew what to do as far as figuring out to hand the current record to all of our controls on the right. The reason that we have data automatically is that the binding automatically assumes that the first record is the selected record. However, since we are no longer binding to the selected item in the ListBox, we need to somehow let the binding know that the “current” record has changed when the value changes in the ListBox. This is easily done by setting a property on the ListBox:

<ListBox Name="listBox1" ItemsSource="{Binding Source={StaticResource cve}}" IsSynchronizedWithCurrentItem="True">

What IsSynchronizedWithCurrentItem does is tell the ListBox to update the binding source with the currently selected item—assuming that the binding source is one that can handle that (which is most). Now, when you run, everything works as it did before, except that we are tied to the data source for the current item, rather than the ListBox. Figure 11.11 shows how this binding is working.

Figure 11.11 — It is perfectly legal to bind controls that can only handle a single item to a multiple-item data source. The master-detail support in WPF Binding will automatically associate those controls with the currently selected item.

Both approaches (binding to the selected item in a list or relying on master-detail support to automatically bind to the data source) produce the same results. For simple UI, the first approach makes it easier to see what is going on, while the second approach is more “correct.” For more complex situations, this correctness can often help make things work a little more cleanly.

 

(1) Of course, if you were writing an XML editor, these would be ideal domain objects.