4

I am attempting to rewrite my ForestPad application utilizing WPF for the presentation layer. In WinForms, I am populating each node programmatically but I would like to take advantage of the databinding capabilities of WPF, if possible.

In general, what is the best way to two-way databind the WPF TreeView to an Xml document?

A generic solution is fine but for reference, the structure of the Xml document that I am trying to bind to looks like this:

<?xml version="1.0" encoding="utf-8"?>
<forestPad
    guid="6c9325de-dfbe-4878-9d91-1a9f1a7696b0"
    created="5/14/2004 1:05:10 AM"
    updated="5/14/2004 1:07:41 AM">
<forest 
    name="A forest node"
    guid="b441a196-7468-47c8-a010-7ff83429a37b"
    created="01/01/2003 1:00:00 AM"
    updated="5/14/2004 1:06:15 AM">
    <data>
    <![CDATA[A forest node
        This is the text of the forest node.]]>
    </data>
    <tree
        name="A tree node"
        guid="768eae66-e9df-4999-b950-01fa9be1a5cf"
        created="5/14/2004 1:05:38 AM"
        updated="5/14/2004 1:06:11 AM">
        <data>
        <![CDATA[A tree node
            This is the text of the tree node.]]>
        </data>
        <branch
            name="A branch node"
            guid="be4b0993-d4e4-4249-8aa5-fa9c940ae2be"
            created="5/14/2004 1:06:00 AM"
            updated="5/14/2004 1:06:24 AM">
            <data>
            <![CDATA[A branch node
                This is the text of the branch node.]]></data>
                <leaf
                name="A leaf node"
                guid="9c76ff4e-3ae2-450e-b1d2-232b687214aa"
                created="5/14/2004 1:06:26 AM"
                updated="5/14/2004 1:06:38 AM">
                <data>
                <![CDATA[A leaf node
                    This is the text of the leaf node.]]>
                </data>
            </leaf>
        </branch>
    </tree>
</forest>
</forestPad>
Timothy Lee Russell
  • 3,719
  • 1
  • 35
  • 43

3 Answers3

9

Well, it would be easier if your element hierarchy was more like...

<node type="forest">
    <node type="tree">
        ...

...rather than your current schema.

As-is, you'll need 4 HierarchicalDataTemplates, one for each hierarchical element including the root, and one DataTemplate for leaf elements:

<Window.Resources>
    <HierarchicalDataTemplate
        DataType="forestPad"
        ItemsSource="{Binding XPath=forest}">
        <TextBlock
            Text="a forestpad" />
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate
        DataType="forest"
        ItemsSource="{Binding XPath=tree}">
        <TextBox
            Text="{Binding XPath=data}" />
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate
        DataType="tree"
        ItemsSource="{Binding XPath=branch}">
        <TextBox
            Text="{Binding XPath=data}" />
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate
        DataType="branch"
        ItemsSource="{Binding XPath=leaf}">
        <TextBox
            Text="{Binding XPath=data}" />
    </HierarchicalDataTemplate>
    <DataTemplate
        DataType="leaf">
        <TextBox
            Text="{Binding XPath=data}" />
    </DataTemplate>

    <XmlDataProvider
        x:Key="dataxml"
        XPath="forestPad" Source="D:\fp.xml">
    </XmlDataProvider>
</Window.Resources>

You can instead set the Source of the XmlDataProvider programmatically:

dp = this.FindResource( "dataxml" ) as XmlDataProvider;
dp.Source = new Uri( @"D:\fp.xml" );

Also, re-saving your edits is as easy as this:

dp.Document.Save( dp.Source.LocalPath );

The TreeView itself needs a Name and an ItemsSource bonded to the XmlDataProvider:

<TreeView
    Name="treeview"
    ItemsSource="{Binding Source={StaticResource dataxml}, XPath=.}">

I this example, I did TwoWay binding with TextBoxes on each node, but when it comes to editing just one node at a time in a separate, single TextBox or other control, you would be binding it to the currently selected item of the TreeView. You would also change the above TextBoxes to TextBlocks, as clicking in the TextBox does not actually select the corresponding TreeViewItem.

<TextBox
    DataContext="{Binding ElementName=treeview, Path=SelectedItem}"
    Text="{Binding XPath=data, UpdateSourceTrigger=PropertyChanged}"/>

The reason you must use two Bindings is that you cannot use Path and XPath together.

Edit:

Timothy Lee Russell asked about saving CDATA to the data elements. First, a little on InnerXml and InnerText.

Behind the scenes, XmlDataProvider is using an XmlDocument, with it's tree of XmlNodes. When a string such as "stuff" is assigned to the InnerXml property of an XmlNode, then those tags are really tags. No escaping is done when getting or setting InnerXml, and it is parsed as XML.

However, if it is instead assigned to the InnerText property, the angle brackets will be escaped with entities &lt; and &gt;. The reverse happens when the value is retreived. Entities (like &lt;) are resolved back into characters (like <).

Therefore, if the strings we store in the data elements contain XML, entities have been escaped, and we need to undo that simply by retrieving InnerText before adding a CDATA section as the node's child...

XmlDocument doc = dp.Document;

XmlNodeList nodes = doc.SelectNodes( "//data" );

foreach ( XmlNode node in nodes ) {
    string data = node.InnerText;
    node.InnerText = "";
    XmlCDataSection cdata = doc.CreateCDataSection( data );
    node.AppendChild( cdata );
}

doc.Save( dp.Source.LocalPath );

If the node already has a CDATA section and the value has not been changed in any way, then it still has a CDATA section and we essentially replace it with the same. However, through our binding, if we change the value of the data elements contents, it replaces the CDATA in favor of an escaped string. Then we have to fix them.

Joel B Fant
  • 24,406
  • 4
  • 66
  • 67
  • Thanks Joel, that works. One question though. I surround the content in the data element with a CDATA section so that it is possible to store Xml. Is there a way to control how the XmlDataProvider writes out the data element? – Timothy Lee Russell Oct 09 '08 at 22:09
  • If there is XML as a string, it would escape angle brackets with entities (they start with &). This can be reversed because the Document property returns an XmlDocument. I'll edit and add the code for doing CDATA in the data elements. – Joel B Fant Oct 10 '08 at 13:45
  • Great -- that works. The performance is really bad for the size of documents that I am working with but instead of updating every node, I will add an IsDirty flag and only update nodes that have been edited. – Timothy Lee Russell Oct 10 '08 at 17:12
  • I also don't want a root node so I changed the XPath in the ItemsSource of the TreeView to "XPath=forest" instead of "XPath=." which works perfectly. – Timothy Lee Russell Oct 10 '08 at 17:15
  • I realize this is a very old Q&A, but this answer helped me a lot today, so I wanted to thank you. I'm curious, though, how would the solution differ if the XML were the `node` tree, as you suggested? – Naikrovek Mar 06 '13 at 01:24
  • @Naikrovek: Normalization. Each element has the same attributes, so they might as well be the same data type. Then you're not limited to a particular depth by your data structure, or don't have to deal with both leaves and branches within branches. You simply have nodes within nodes. Now, he may have reasons for that design, but it does mean having several HierarchicalDataTemplates instead of one. – Joel B Fant Mar 06 '13 at 19:49
  • The rationale (for better or worse) is described in the article after the jump. As with a lot of decisions, it was somewhat arbitrary. In the web version of ForestPad which I'm currently building with ASP.NET WebAPI and AngularJS, I am using the same data type for each node with unlimited depth. Link to old ForestPad article: http://www.codeproject.com/Articles/7255/ForestPad-a-method-for-storing-and-retrieving-text – Timothy Lee Russell Jan 09 '14 at 21:19
  • Actually, you only need to use one HierarchicalDataTemplate. You just need to use an XPath expression that selects all of the nodes that you want it to use: XPath=tree|branch|leaf – B. Fuller Mar 03 '22 at 20:59
2

We had a similar issue. You may find reading this article helpful. We used the ViewModel pattern described and it simplified everything.

Shaun Bowe
  • 9,840
  • 11
  • 50
  • 71
1

I know this is an old post, but there's a more elegant solution. You can indeed use a single HierarchicalDataTemplate, if you use an XPath expression that selects all of the nodes that you want the template to use: XPath=tree|branch|leaf.

<HierarchicalDataTemplate x:Key="forestTemplate"
        ItemsSource="{Binding XPath=tree|branch|leaf}">
    <TextBlock Text="{Binding XPath=data}" />
</HierarchicalDataTemplate>

Here's a full Page example with XData embedded in the XmlDataProvider1:

<Page 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Page.Resources>
    <XmlDataProvider x:Key="sampleForestPad" XPath="forestPad/forest">
        <x:XData>
            <forestPad xmlns=""
                guid="6c9325de-dfbe-4878-9d91-1a9f1a7696b0"
                created="5/14/2004 1:05:10 AM"
                updated="5/14/2004 1:07:41 AM">
                <forest 
                    name="A forest node"
                    guid="b441a196-7468-47c8-a010-7ff83429a37b"
                    created="01/01/2003 1:00:00 AM"
                    updated="5/14/2004 1:06:15 AM">
                    <data>
                    <![CDATA[A forest node
                        This is the text of the forest node.]]>
                    </data>
                    <tree
                        name="A tree node"
                        guid="768eae66-e9df-4999-b950-01fa9be1a5cf"
                        created="5/14/2004 1:05:38 AM"
                        updated="5/14/2004 1:06:11 AM">
                        <data>
                        <![CDATA[A tree node
                            This is the text of the tree node.]]>
                        </data>
                        <branch
                            name="A branch node"
                            guid="be4b0993-d4e4-4249-8aa5-fa9c940ae2be"
                            created="5/14/2004 1:06:00 AM"
                            updated="5/14/2004 1:06:24 AM">
                            <data>
                            <![CDATA[A branch node
                                This is the text of the branch node.]]></data>
                                <leaf
                                name="A leaf node"
                                guid="9c76ff4e-3ae2-450e-b1d2-232b687214aa"
                                created="5/14/2004 1:06:26 AM"
                                updated="5/14/2004 1:06:38 AM">
                                <data>
                                <![CDATA[A leaf node
                                    This is the text of the leaf node.]]>
                                </data>
                            </leaf>
                        </branch>
                    </tree>
                </forest>
            </forestPad>
        </x:XData>
    </XmlDataProvider>

    <HierarchicalDataTemplate x:Key="forestTemplate"
        ItemsSource="{Binding XPath=tree|branch|leaf}">
      <TextBlock Text="{Binding XPath=data}" />
    </HierarchicalDataTemplate>

    <Style TargetType="TreeViewItem">
      <Setter Property="IsExpanded" Value="True"/>
    </Style>
  </Page.Resources>

    <TreeView ItemsSource="{Binding Source={StaticResource sampleForestPad}}"
      ItemTemplate="{StaticResource forestTemplate}"/>
</Page>

This will render as:

ForestTreeView

B. Fuller
  • 157
  • 12