Introduction
This article is targeted for Beginner to Intermediate level WPF programmers. This article assumes that you have some basic knowledge of WPF in regards to layout. If you have no experience with WPF, I strongly suggest reading Sacha Barber’s “WPF: A Beginner’s Guide” series.
The Windows Presentation Foundation (WPF) provides developers with a vast amount of data binding functionality that has been praised by many, including yours truly. However, one area that I’ve found where the data binding falls short is with the XmlDataProvider
. Unlike most data binding options in WPF, the XmlDataProvider
does not natively support two-way binding. Meaning, if you make modifications to the data via a bound control, the changes are not persisted to the source XML file. This article will explain how to overcome that shortcoming and mimic two-way binding with the XmlDataProvider
.
Background
The application in this article was developed with Visual Studio 2008, and targets the 3.5 version of the .NET framework.
Overview
The attached demo code consists of a WPF application with a single window and an XML file. Since I enjoy fantasy football, my XML file consists of NFL teams and their associated conferences. An example of the data within the Teams.xml file looks like this:
<Teams xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Team>
<Id>1</Id>
<Name>Arizona Cardinals</Name>
<Conference>NFC West</Conference>
</Team>
<Team>
<Id>2</Id>
<Name>Atlanta Falcons</Name>
<Conference>NFC South</Conference>
</Team>
</Teams>
The single window in the application consists of several controls: a ListBox
, three Label
s, and three TextBox
es. The ListBox
displays the NFL team name. The three TextBox
controls display the team data associated with the currently selected team within the ListBox
.
The WPF window looks like this:
One Way Binding
I won’t get into the details of layout here. However, I want to point out the items within the XAML that make the binding from the source to the control possible. The first, and the most important, item is the XmlDataProvider
. The XmlDataProvider
specifies the underlying source XML file through its Source
attribute. The XPath
attribute is used to specify the level at which the data binding occurs. The x:Name
attribute exposes the XmlDataProvider
to the code-behind.
<Grid.DataContext>
<XmlDataProvider x:Name="TeamData" Source="Teams.xml" XPath="Teams/Team" />
</Grid.DataContext>
You’ll notice in the example above that the XmlDataProvider
is contained within Grid.DataContext
tags. The layout of the application’s single window is a Grid
. Since the XPath statement starts the binding at the team level and the XmlDataProvider
is defined within the Grid
’s DataContext
, the Grid
is bound to the data at the team level.
Note: The binding does not have to be done at the Grid
level. We could bind the ListItem
and other controls to the data within the XmlDataProvider
individually. However, there is a synchronization issue when multiple controls bind directly to the XmlDataProvider
and use the XPath statement. Explanation of the issue is out of the scope for this article, but can be found at Ian Griffiths’ blog. The approach recommended by Ian was incorporated into this article and the demo code.
The next item in the binding architecture is the ListBox
. Since the ListBox
is contained within the Grid
, and the Grid
is bound to the data at the team level, the bindings can be inherited. We can accomplish this simply by setting the ListBox
's ItemsSource
property to the current bindings.
<ListBox x:Name="TeamsListBox" Margin="0,0,0,20" DockPanel.Dock="Left"
ItemsSource="{Binding}"
ItemTemplate="{StaticResource teamItemTemplate}"
IsSynchronizedWithCurrentItem="True"
Visibility="Visible" SelectionMode="Single">
</ListBox>
Each ListBox
item is now bound to a Team
element in the XmlDataProvider
. To display the team name in the place of the ListBox
item, we utilize the teamItemTemplate
DataTemplate
. The DataTemplate
displays a Label
control and utilizes the XPath
attribute to traverse down to the Name
element.
<DataTemplate x:Key="teamItemTemplate">
<Label Content="{Binding XPath=Name}"/>
</DataTemplate>
The final items in the binding architecture are the TextBox
controls to display the selected team’s data. For layout purposes, all of the Label
and TextBox
controls for the team’s individual items are displayed within a StackPanel
. The StackPanel
, like the previously mentioned ListBox
, is contained within the Grid
. We again use the inherited bindings, and by way of the XPath
attribute, traverse down to the corresponding element.
<StackPanel Grid.Column="1" Margin="0,0,5,0">
<StackPanel Orientation="Horizontal">
<Label Style="{StaticResource labelStyle}">ID:</Label>
<TextBox Style="{StaticResource textboxStyle}" Text="{Binding XPath=Id}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Style="{StaticResource fieldsetStyle}">
<Label Style="{StaticResource labelStyle}">Team:</Label>
<TextBox Style="{StaticResource textboxStyle}" Text="{Binding XPath=Name}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Style="{StaticResource fieldsetStyle}">
<Label Style="{StaticResource labelStyle}">Conference:</Label>
<TextBox Style="{StaticResource textboxStyle}" Text="{Binding XPath=Conference}" />
</StackPanel>
</StackPanel>
This is all that is needed to provide one way binding of WPF controls to an XML file.
Two-Way Binding
Now that we can consume an XML file and bind it to our GUI, it would be really nice if we could persist any changes that we make to the data. Here is where we realize the shortcomings of the XmlDataProvider
. If you modify the data within the Team Name textbox, you will see that the changes are persisted within the ListBox
. Unfortunately, these changes are only in memory and are not persisted to the underlying XML file. If you stop and restart the application, the original data will be reloaded from the unchanged XML file. Luckily, we can very easily write some code to mimic the binding from the GUI back to the XML source file.
Before I discuss the code, there is one configuration option we must modify. Since the Teams.xml file will need to be deployed with the application’s executable, it is easier to copy it to the output directory upon a successful build. We can right click the Teams.xml file, choose Properties, and select the “Copy if newer” option for the Copy to Output Directory configuration. This will copy the XML file to the same location as the application’s executable. Now, anytime we need to reference the file, we can simply append “Teams.xml” to the path of the currently executing assembly.
There are two code blocks that must be added to the window’s code-behind file to finalize the two-way binding architecture. First, we add code to set the Source
property of the XmlDataProvider
to the copy of the XML file located in the output directory. This ensures that we are reading from and writing to the same instance of the XML file. Second, we utilize an event handler to execute the code to persist the in-memory data to the XML file. In the demo, I used the Click
event handler for the Save button. This could easily be modified to execute within a different event handler such as a TextBox.LostFocus
or a Window.Closing
event. To persist the changes, we simply call the Save
method on the XmlDataProvider
’s Document
property and specify the XmlDataProvider
’s Source
as the destination file.
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
string appPath = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
TeamData.Source = new Uri(appPath + @"\Teams.xml");
}
private void SaveTeamsButton_Click(object sender, RoutedEventArgs e)
{
string source = TeamData.Source.LocalPath;
TeamData.Document.Save(source);
}
}
In Conclusion
Although the XmlDataProvider
does have its limitations, they are easily overcome with a few lines of code. Hopefully, this article will benefit those searching for an easy solution.
History
- June 9, 2008 – Version 1.0 – Initial release.