2

I have a wpf window where I am using xml data through an XMLDataProvider. The screen is based on a grid and all the data is being displayed correctly, having defined the xml as follows...

<Grid.DataContext>
    <XmlDataProvider x:Name="Data" XPath="Book/Page" />
</Grid.DataContext>

With the xml source being set in code behind as follows...

InitializeComponent();

string appPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
Data.Source = new Uri(appPath + @"\SNG.xml");

All so good so far. But now I have a need to read one of the elements from the xml file in the code behind. All my searching and the only way I've found to do it is to bind it to an invisible control then read the data out of the control. e.g. to read the BookRef from the xml I have the following in the xaml...

TextBlock Name="BookRefTextBox" Visibility="Hidden" Text="{Binding XPath=@BookRef}"/>

Then in the code behind...

string bookRef = BookRefTextBox.Text;

This works, I can then use the data that came from the xml file... but it really feels like a fudge. Is there a better way to get the value of parts of the xml file from within the code behind section.

EDIT:

Forgot to say that I've also tried putting the XmlDataProvider in Windows.Resources instead of in Grid.DataContext as some examples I've found do.

However I then can't find a way to set the path to the xml file in code behind. Added to which putting it in Windows.Resource does not make it any easier to find how to access the data from the Xml file.

EDIT2: Here is an example of the XML file. Note there are multiple books.

<Books>
  <Book Id="1" BookRef="12345" Name="My Book Name" Author="Author" Pages="2" >
    <Page PageId="1"/>
    <Page PageId="2"/>
  </Book>
  <Book Id="1" BookRef="67890" Name="My Second Book Name" Author="Author 2" Pages="1" >
    <Page PageId="1"/>
  </Book>
</Books>
RosieC
  • 649
  • 2
  • 11
  • 27

2 Answers2

2

OK, here is another way, more complicated, though.

You are correct that you need to synchronize the currently displayed page with the text of the BookRefTextBox. So on top of XmlDataProvider, I define a CollectionViewSource, which can be used to keep track of the current displayed page.

In the XAML code below, both BookRefTextBox and listBox1 (I use ListBox to display pages) are bound to the same CollectionViewSource, and by setting IsSynchronizedWithCurrentItem="True", the current item is updated when the selected page is changed.

The interesting point is the XPath expression for BookRefTextBox.Test, XPath=../@BookRef means the Text of BookRefTextBox is Find the parent element of the current page- which is Book, and display its BookRef attribute".

The whole XAML of the window.

<Window x:Class="MyNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <XmlDataProvider x:Key="userDataXmlDataProvider1" Source="/Data/XMLFile1.xml" XPath="Books/Book/Page"/>
        <CollectionViewSource x:Key="userDataCollectionViewSource1" Source="{StaticResource userDataXmlDataProvider1}"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource userDataXmlDataProvider1}">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="3*"/>
        </Grid.RowDefinitions>
        <TextBlock x:Name="BookRefTextBox" Grid.Row="0" Text="{Binding XPath=../@BookRef}" />
        <ListBox x:Name="listBox1" Grid.Row="1" 
                 IsSynchronizedWithCurrentItem="True"
                 ItemsSource="{Binding}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" Margin="8,0,8,0">
                        <Label Content="{Binding XPath=@PageId}" Width="100" Margin="5" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

And from code behind, you can get the text of the BookRefTextBox.

Edit:

In order to set the source from code behind:

If XmlDataProvider is declared in Window.Resources, it has a x:Key attribute, you access a resource via Key, not Name.

XmlDataProvider xdp = this.Resources["userDataXmlDataProvider1"]  as XmlDataProvider; 
xdp.Source = new Uri(...);
kennyzx
  • 12,845
  • 6
  • 39
  • 83
  • Thanks, that looks the sort of thing I want. However I'm guessing that the 'record' that I would be reading here would not be synchronized with the one currently being displayed on the screen, am I correct in that guess? My XML contains multiple 'books' and multiple 'pages' and I really need to read the 'BookRef' from the one currently on screen. – RosieC Dec 24 '14 at 09:02
  • wow, the question is not that simple, I thought that there is only a Book element (which is the root element). – kennyzx Dec 24 '14 at 09:16
  • show your xml sample in your question, that would be helpful. – kennyzx Dec 24 '14 at 09:29
  • Added an xml sample to the question. I've been trying to search if there is a way to link XDocument to the XMLDataProvider but not come up with anything. Maybe the answer is to do as I'm doing and read the info out of a hidden textblock in the xaml, however it really does seem like a fudge and that I aught to be able to read the info directly in the codebehind :( – RosieC Dec 24 '14 at 09:52
  • I have updated my answer, in order to get the currently displayed page, you need to do it in XAML. I add a textblock in XAML to display the `BookRef` attribute of the book of the currently diplayed page. – kennyzx Dec 24 '14 at 14:49
  • Thanks again, I've given that a try and it solves both that problem and another I had. However it causes me a new problem... I can't work out how to set the source of the xmldataprovider in the code behind when using this method. This has moved the xmldataprovider into the Windows.Resources (I had it in Grid.DataContext) and when I do that the xmldataprovider is not accessible from the code behind, even if it's given an x:Name (at least I can't find it if it is). I need to set the filename/source in code behind because it is not always the same. – RosieC Dec 24 '14 at 16:42
  • Just realized that this has actually brought me back full circle to my first solution... of using a hidden textbox/textblock in the xaml which I then read the contents of in the codebehind. You don't actually need the CollectionViewSource to do that, in my earlier code I had the xmldataprovier in the grid.datacontext and all the controls within the grid were synchronized to the same record. CollectionViewSource has come up in searches as a potential solution to another problem I have... changing page/record from code behind... but still can't find how to set Source when using that. – RosieC Dec 24 '14 at 16:51
  • Resource is accessed via Key, not Name, see my edit, this should solve one of the question in your Edit. But yes, still doing so does not make it easier to access the data from xml, the hard part is I need to depend on XAML to know which page is currently displayed. – kennyzx Dec 25 '14 at 01:24
  • Thanks, that's got me further. I'm sure in my searches I found something about using CollectionViewSource to control which page is currently displayed (but I could't use it at the time because of the problem setting the filename, which you've now solved). I'm going to search for the stuff I found before and see where that takes me. – RosieC Jan 01 '15 at 15:26
  • Well my problem is sort of solved now... or rather avoided. CollectionViewSource combined with your help setting the filename in codebehind when using a resource, has allowed me to change page in a much better way, and doing so has actually removed my need to read the data in code behind (at least for now). Thanks for all the help and for sticking with me @kennyzx. – RosieC Jan 01 '15 at 16:39
0

I believe I have finally found the answer that avoids the use of a hidden control. First off many thanks to kennyzx for his answer which while it still used a hidden control was invaluable in leading me to this answer.

Instead of putting the XmlDataProvider in the Grid.Context it has been moved to the Window.Resources and a CollectionViewSource added to accompany it.

<Window.Resources>        
    <XmlDataProvider x:Name="books" x:Key="bookData" XPath="Books/Book/Page"/>
    <CollectionViewSource x:Key="collBookData" Source="{StaticResource bookData}"/>
</Window.Resources>

A new XmlDataProvider is defined in the code behind and in the constructor of the window is set to be a reference to the one defined in the XAML Windows.Resources.

    XmlDataProvider bookData;

    public BookPage()
    {
        InitializeComponent();

        string appPath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase);
        bookData = (XmlDataProvider)this.Resources["bookData"];
        bookData.Source = new Uri(appPath + @"\SNG.xml");
    }

The DataContext of the Grid is set to be the CollectionViewSource.

    <Grid.DataContext>
        <Binding Source="{StaticResource collBookData}"/>
    </Grid.DataContext>

The above is not 100% necessary as it could be specified on each control instead, but this way makes for simpler binding on each control on the form. (No hidden controls in this solution, only the ones I want to actually show). For example...

    <TextBlock Name="myTextBlockName" Style="{StaticResource MyTextBlockStyle}" Text="{Binding XPath=../@BookRef}" />

Finally the bit to read the data from the XML in code behind.

        XmlNode currentXmlNode = bookData.Document.SelectNodes("Books/Book/Page").Item(collBookData.View.CurrentPosition);
        string currentBookRef = currentXmlNode.ParentNode.Attributes["BookRef"].Value;

Just as an aside, this solution also allows me to use MoveCurrentToPrevious and MoveCurrentToNext against collBookData.View to change the current page being displayed (previously had a hidden listbox control to do that and wasn't happy with that solution either).

RosieC
  • 649
  • 2
  • 11
  • 27