Reputation:
I have spend a lot of time trying to figure out how to bind data in my XML file to the TreeView control but I do not know where to start. I even tried going through Two-way binding of Xml data to the WPF TreeView and Josh Smith's code sample on codeproject, but still can't understand how to begin!!!
I have XML in in a file "C:\SPDependencies.xml" (I can change the format if required)!!!:
<node type="SPDependencies" Name="SPDependencies">
<node type="StoredProc" Name="SP1">
<node type="OperationType" Name="Type1">
<node type="TableName" Name="Table1"/>
<node type="TableName" Name="Table2"/>
</node>
<node type="OperationType" Name="Type2">
<node type="TableName" Name="Table1"/>
<node type="TableName" Name="Table2"/>
</node>
.....
</node>
<node type="StoredProc" Name="SP2">
<node type="OperationType" Name="Type1">
...
...
</node>
</node>
I need to display this in the Treeview control in the following format:
<SP1>
<Type1>
<Table1>
<Table2>
<Table3>
<Type2>
<Table1>
<Table2>
<Table3>
<SP2>
<Type1>
........
Thanks, Abhi.
Upvotes: 3
Views: 17486
Reputation: 37066
I've figured out how to do this without stepping on TreeView.DataContext
. Binding is easy. Codebehind is nearly as easy, but with one little gotcha.
You get nothing if you bind or assign XmlDataProvider
to ItemsSource
. It's not an IEnumerable
(though it is INotifyPropertyChanged
) and there's no implicit conversion. What you want is XmlDataProvider.Data
, which is declared as Object
, but I'm seeing a runtime type of XmlDataCollection
(which inherits from ReadOnlyObservableCollection<XmlNode>
).
Binding Data
is easy. I don't know if it's pure MVVM to put XmlDataProvider
in your viewmodel, maybe not.
Viewmodel:
public XmlDataProvider ViewModelXMLDataProp { ... }
XAML
<TreeView
ItemsSource="{Binding ViewModelXMLDataProp.Data}"
...
/>
Done -- that is, unless you need to use the XPath
property of the Binding
. If you do, you have to use the DataContext
kludge. You can't set both Path
and XPath
on the same binding.
The XPath property of the XmlDataProvider
does the same thing. If you can work with that, you're good.
You'd think Binding.Source
would work, because that works when your XmlDataProvider
is a static resource. When Binding.Source
is a DataSourceProvider
and Path
is unspecified, Path
defaults to Data
:
<TreeView
ItemsSource="{Binding Source={StaticResource MyXmlDataProviderResource}}"
...
/>
...but that works because you're giving it a static resource. The following actually binds to the string "ViewModelXMLDataProp"
rather than looking to the DataContext
for a property by that name. That's no good.
<TreeView
ItemsSource="{Binding Source=ViewModelXMLDataProp}"
...
/>
Maybe you could write a MarkupExtension
that'd make that work, but there's no need.
You should learn and use MVVM, but things happen for a lot of reasons and you didn't come here for a sermon.
Codebehind is a little trickier. TreeView.ItemsSource
requires nothing more than that the object you give it must implement System.Collections.IEnumerable
, so cast provider.Data
to System.Collections.IEnumerable
and don't worry about what the exact runtime type is.
Now here's the gotcha: XmlDataProvider.Data
is populated asynchronously.
protected void LoadXML(String path)
{
var provider =
new XmlDataProvider()
{
Source = new Uri(path, UriKind.Absolute),
XPath = "./*"
};
// FAIL: provider.Data is still null
treeView.ItemsSource = (IEnumerable)provider.Data;
}
I found this to be an issue even when I create an XmlDocument
, call XmlDocument.Load()
, and assign the document to XmlDataProvider.Document
. A Binding
will still be hanging around when the Data
property finally gets set, and it'll update ItemsSource
then. But an assignment to ItemsSource
in your code behind file does no such thing.
Contrary to ubiquitous folk belief at Stack Overflow, in the following code there is no binding happening, and in no sense has anything been bound:
// NOT A BINDING
treeView.ItemsSource = someRandomCollectionOfStuff;
If nobody's creating an instance of System.Windows.Data.Binding
or x:Bind
, it's not a binding. This distinction matters: "Use the current value of x
" is not the same concept as "indefinitely update with future values of y.x
every time y
raises PropertyChanged
".
You could programmatically create a Binding
or even handle PropertyChanged
, but they went ahead and gave you a much simpler option. Just handle the XmlDataProvider.DataChanged
event.
protected void LoadXML(String path)
{
var provider =
new XmlDataProvider()
{
Source = new Uri(path, UriKind.Absolute),
XPath = "./*"
};
provider.DataChanged += (s,e)
=> treeView.ItemsSource = (IEnumerable)provider.Data;
}
And that does it. You could even keep that provider around, load new XML in it, and have the DataChanged
event keep the treeview current. Seems like a waste of effort though.
Upvotes: 1
Reputation: 17049
Given the following xml file:
<node type="SPDependencies" Name="SPDependencies">
<node type="StoredProc" Name="SP1">
<node type="OperationType" Name="Type1">
<node type="TableName" Name="Table1"/>
</node>
<node type="OperationType" Name="Type2">
<node type="TableName" Name="Table1"/>
</node>
</node>
<node type="StoredProc" Name="SP2">
<node type="OperationType" Name="Type1">
</node>
</node>
</node>
View:
<Window x:Class="Tree.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Tree"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<HierarchicalDataTemplate x:Key="template">
<TextBlock Text="{Binding XPath=@Name}" />
<HierarchicalDataTemplate.ItemsSource>
<Binding XPath="node" />
</HierarchicalDataTemplate.ItemsSource>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid DataContext="{Binding Path=XmlData}">
<TreeView ItemsSource="{Binding}" ItemTemplate="{StaticResource template}">
</TreeView>
</Grid>
</Window>
View model:
public class ViewModel
{
public XmlDataProvider XmlData { get; set; }
public ViewModel()
{
XmlData = new XmlDataProvider();
XmlData.Source = new Uri(@"C:\input.xml");
XmlData.XPath = "node";
}
}
Output:
If you only want to show the nodes below the root, simply change the XPath to:
XmlData.XPath = "/node/node";
Upvotes: 5
Reputation: 4065
Heres the tree:
<Window.Resources>
<HierarchicalDataTemplate DataType="node"
ItemsSource="{Binding XPath=node}">
<TextBox Width="Auto"
Text="{Binding XPath=@Name, UpdateSourceTrigger=PropertyChanged}" />
</HierarchicalDataTemplate>
<XmlDataProvider
x:Key="xmlDataProvider"
XPath="node" Source="C:\Data.XML">
</XmlDataProvider>
</Window.Resources>
<Grid>
<StackPanel>
<Button Click="Button_Click">Save</Button>
<TreeView
Width="Auto"
Height="Auto"
Name="treeview"
ItemsSource="{Binding Source={StaticResource xmlDataProvider}, XPath=.}"/>
</StackPanel>
</Grid>
I've added a simple button to save changes. So for your Button_Click method in code behind:
XmlDataProvider dataProvider = this.FindResource("xmlDataProvider") as XmlDataProvider;
dataProvider.Document.Save(dataProvider.Source.LocalPath);
See here for an article about data binding and WPF.
Upvotes: 3