Monday, November 21, 2011

WPF - Working with XML and XmlDataProvider

Working with XML or XML related data is not always easy in C#. With .NET Framework 3.0 Microsoft introduced the XmlDataProvider as part of the data provider infrastructure. Using XmlDataProvider inside WPF applications together with some XPath expressions makes it really easy to bind whatever kind of Xml fragment you have to some user interface controls. This blog entry shows how to use the provider by showing a simple example.

The XML Data
First of all, we need some sample data. I used the data from the XmlDataProvider example at the according
MSDN site:


<Inventory xmlns="">
  <Books>
    <Book ISBN="0-7356-0562-9" Stock="in" Number="9">
      <Title>XML in Action</Title>
      <Summary>XML Web Technology</Summary>
    </Book>
    <Book ISBN="0-7356-1370-2" Stock="in" Number="8">
      <Title>Programming Microsoft Windows With C#</Title>
      <Summary>C# Programming using the .NET Framework</Summary>
    </Book>
    <Book ISBN="0-7356-1288-9" Stock="out" Number="7">
      <Title>Inside C#</Title>
      <Summary>C# Language Programming</Summary>
    </Book>
    <Book ISBN="0-7356-1377-X" Stock="in" Number="5">
      <Title>Introducing Microsoft .NET</Title>
      <Summary>Overview of .NET Technology</Summary>
    </Book>
    <Book ISBN="0-7356-1448-2" Stock="out" Number="4">
      <Title>Microsoft C# Language Specifications</Title>
      <Summary>The C# language definition</Summary>
    </Book>
  </Books>
  <CDs>
    <CD Stock="in" Number="3">
      <Title>Classical Collection</Title>
      <Summary>Classical Music</Summary>
    </CD>
    <CD Stock="out" Number="9">
      <Title>Jazz Collection</Title>
      <Summary>Jazz Music</Summary>
    </CD>
  </CDs>
</Inventory>

We have an inventory containing books and CDs. Every book and CD has two child elements: title and summary. Books got an additional attribute for their ISBN number. The data is stored in a separate XML file.

The View
We will create a very simple WPF view for displaying our data. It will contain a combo box in order to select books or CDs. A list box will display the items of a category and the user will be able to select a single item and to display its properties. Furthermore the user may change the summary of the selected item. Here is a screenshot of the view:



Binding the Data
XmlDataProvider exposes several ways to access xml data
- inline xml with the
x:XData element
- set the Source property to an
URI
- set the Document property to a XmlDocument object

I will use the Source property and set it to the path of the xml file. I do this in code behind, inside the constructor:

var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if(!string.IsNullOrEmpty(directory))
{
    provider.Source = new Uri(Path.Combine(directory, "Inventory.xml"));
}

With the provider set up, we can now define the Binding inside the view:
<Window x:Class="XmlDataProviderSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="280" Width="600">
  <Window.Resources>
    <XmlDataProvider x:Key="dataProvider"/>
  </Window.Resources>
  <StackPanel>
    <ComboBox x:Name="inventoryTypeComboBox" ItemsSource="{Binding Source={StaticResource dataProvider}, XPath=/Inventory/*}">
      <ComboBox.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Name}" />
        </DataTemplate>
      </ComboBox.ItemTemplate>
    </ComboBox>
    <StackPanel Margin="0,9,0,0" Orientation="Horizontal" DataContext="{Binding ElementName=inventoryTypeComboBox, Path=SelectedItem}">
      <ListBox x:Name="listBox" ItemsSource="{Binding Path=ChildNodes}">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <TextBlock Text="{Binding XPath=Title}"/>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
      <Grid Margin="6,0,0,0" DataContext="{Binding ElementName=listBox, Path=SelectedItem}">
        <Grid.ColumnDefinitions>
          <ColumnDefinition/>
          <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition/>
          <RowDefinition/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Text="ISBN: "></TextBlock>
        <TextBlock Grid.Column="1" Text="{Binding XPath=@ISBN}"></TextBlock>
        <TextBlock Grid.Row="1" Text="Summary: "></TextBlock>
        <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding XPath=Summary}"/>
      </Grid>
    </StackPanel>
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="60"/>
        <ColumnDefinition Width="60"/>
      </Grid.ColumnDefinitions>
      <Button Margin="3,6,3,0" Content="Save" Grid.Column="1" Click="OnSaveClicked"/>
      <Button Margin="3,6,6,0" Content="Exit" Grid.Column="2" Click="OnExitClicked"/>
    </Grid>
  </StackPanel>
</Window>

Let’s start by examining the combo box. Its ItemsSource property is bound to the XmlDataProvider. The root node is selected by using an XPath expression. It selects all direct children of the inventory node, which are in that case books and CDs. In order to show some friendly text inside the combo box it defines an ItemTemplate, which has a TextBlock bound to the Name property of the current XmlNode. The XPath expression returns a list of XmlNode objects. Therefore you can bind to any property of that object. You could also bind to another XPath expression based on the current node.

A ListBox shall show all the items of the selected category. The data context of the container containing the list box is bound to the currently selected item of the combo box. That item is an XmlNode object, so the list box can be bound to the nodes ChildNodes collection. You don’t need to specify the Path attribute, but it makes the code more readable. Again we’re defining an ItemTemplate for the list. This time we use an XPath expression to bind to the title node in order to display the inventory item title inside the list. We could have also bound to the InnerText property of the XmlNode. Use whatever you like.

When the user selects an item in the list, its properties shall be shown next to the list and at least the summary shall be editable. The data context of the Grid container element is therefore bound to the selected item of the list box. Inside the Grid are TextBlocks for displaying the ISBN and the summary of the currently selected inventory item. The summary is displayed inside a TextBox so the user may change it. Note, that the ISBN is an attribute of the book node, so you have to use an @ symbol inside the XPath expression.

Changing and Saving Data
XmlDataProvider keeps an XmlDocument with an XmlNode object collection of the xml document in memory. Using the xml API in .NET you can do any modifications you want. What’s cool about using XmlDataProvider in WPF is that when you change the value of a bound XmlNode inside a user control, the new value will be written to the XmlNode object. That means it supports two-way binding. That makes it extremely easy to edit the summary node in our example. In order to save the changes back to the file system, you just need to call the save method of the underlying XmlDocument:

private void OnSaveClicked(object sender, RoutedEventArgs e)
{
    if(provider != null && provider.Document != null)
    {
        provider.Document.Save(provider.Source.AbsolutePath);
    }
}

Summary
The XmlDataProvider together with WPF binding makes it very easy to access, display, and modify Xml data. You can navigate through the data with just some simple XPath expressions. For small documents it is extremely fast. For larger documents consider loading the document in a background thread and assign it manually to the provider. Always remember  that the XmlDataProvider is just a wrapper around an XmlDocument.

1 comment:

  1. You're binding to a value called 'Name,' yet I don't see that tag/param set up anywhere in the XML. What's up?

    ReplyDelete