简体   繁体   中英

How to refresh xmlDataProvider when xml document changes at runtime in WPF?

I am trying to make a image viewer/album creator in visual studio, wpf. The image paths for each album is stored in an xml document which i bind to to show the images from each album in a listbox. The problem is when i add a image or an album at runtime and write it to the xml document. I can't seem to make the bindings to the xml document update so they show the new images and albums aswell. Calling Refresh() on the XmlDataProvider doesn't change anything. I don't wish to redo the binding of the XmlDataProvider, just make it read from the same source again.

XAML:

...
<Grid.DataContext>
            <XmlDataProvider x:Name="Images" Source="Data/images.xml" XPath="/albums/album[@name='no album']/image" />
</Grid.DataContext>
...
<Label Grid.Row="1" Grid.Column="0" HorizontalAlignment="Right" VerticalAlignment="Bottom" Padding="0" Margin="0,0,0,5" Content="{x:Static resx:Resource.AddImageLabel}"/>
<TextBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Name="newImagePath" Margin="0" />
<Button Grid.Row="1" Grid.Column="2" HorizontalAlignment="Left" VerticalAlignment="Bottom" Name="newImagePathButton" Content="{x:Static resx:Resource.BrowseImageButton}" Click="newImagePathButton_Click" />
...
<ListBox Grid.Column="0" Grid.ColumnSpan="4" Grid.Row="3" HorizontalAlignment="Stretch" Name="thumbnailList" VerticalAlignment="Bottom" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding BindingGroupName=Images}" SelectedIndex="0" Background="#FFE0E0E0" Height="110">
...

Code behind:

private void newImagePathButton_Click(object sender, RoutedEventArgs e)
{
    string imagePath = newImagePath.Text;

    albumCreator.addImage(imagePath, null);

    //Reset import image elements to default
    newImagePath.Text = "";

    //Refresh thumbnail listbox
    Images.Refresh();

    Console.WriteLine("Image added!");
}

public void addImage(string source, XmlElement parent)
{
    if (parent == null)
    {
        //Use default album
        parent = (XmlElement)root.FirstChild;
    }

    //Create image element with source element within
    XmlElement newImage = xmlDoc.CreateElement(null, "image", null);
    XmlElement newSource = xmlDoc.CreateElement(null, "source", null);
    newSource.InnerText = source;
    newImage.AppendChild(newSource);

    //Add image element to parent
    parent.AppendChild(newImage);

    xmlDoc.Save(xmlFile);

}

Thank you very much for any help!

The right way in this situation I beleive is to use ObservableCollection and bind it to ItemsSource property of your ListView . So, just play with objects and no tricks with XML files.

Edit:

Entire concept is work with Refresh() . Next sample is works. Check if Refresh() call is made after document saving.

<ListView x:Name="uiList" ItemsSource="{Binding}">
    <ListView.DataContext>
        <XmlDataProvider x:Name="DataSource" Source="c:\XMLFile.xml" XPath="/root/item"  />
    </ListView.DataContext>
    <ListView.ItemTemplate>
        <DataTemplate>
            <Border Width="40" Height="40" Background="Gray">
                <Label Content="{Binding Attributes[0]}" />
            </Border>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

...

public MainWindow()
{
    InitializeComponent();
    uiList.SelectionChanged += new SelectionChangedEventHandler(uiList_SelectionChanged);
}

void uiList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    string sFile = @"c:\XMLFile.xml";
    XDocument oDoc = XDocument.Load(sFile);
    oDoc.Root.Add(
        new XElement("item", new XAttribute("name", "test3"))
    );
    oDoc.Save(sFile);

    XmlDataProvider oProv = uiList.DataContext as XmlDataProvider;
    oProv.Refresh();
}

Possible Problem and Solution #1

Do you have this XmlDataProvider resource declared in the Application.Resources block as well?

If you do, the XAML UI ListBox element "thumbnailList" refers to the Grid panel's instance of the XmlDataProvider. I'm guessing, since I can't see the code in your Window CS file constructor, that you refer to the Application-level instance of the XmlDataProvider when you address the XmlDataProvider there as in

XmlDataProvider xmlDataProvider = Application.Current.FindResource("Images") as XmlDataProvider;

XmlDocument xDoc = xmlDataProvider.Document;

If this is the case, remove the XmlDataProvider resource from the Grid element. Now when your code-behind updates the XML file the UI will automatically update.


Possible Problem and Solution #2

I see from your addImage() method that you refer to an instance variable named "xDoc".

The other possibility is that you are creating a NEW XmlDocument in your Window constructor, instead of referencing the XAML created XmlDocument object. If so, get the instance of the current XmlDocument instead of creating a new instance. Make sure to declare the resource at the Application level and remove the resource declaration from the Grid element

XmlDataProvider xmlDataProvider = Application.Current.FindResource("Images") as XmlDataProvider;

Or reference the resource at the Grid element (you will need to add a Name to the Grid) and do not declare the resource in the Application.Resources block

XmlDataProvider xmlDataProvider = grid.FindResource("Images") as XmlDataProvider;

XmlDocument xDoc = xmlDataProvider.Document;

Now when your code-behind updates the XML file the UI will automatically update.


Conclusions

If you declare these two class instance variables in your code-behind

XmlDataProvider xmlDataProvider;

XmlDataProvider gridXmlDataProvider;

and have this code in your Window constructor

xmlDataProvider = Application.Current.FindResource("Images") as XmlDataProvider;

gridXmlDataProvider = grid.FindResource("Images") as XmlDataProvider;

Put a stop in the addImage event handler right you add a Node and save the XML Document changes. Assuming you originally loaded oDoc from xmlDataProvider as shown above. Run in Debug Mode and open a Watch window and inspect the contents of xmlDataProvider and gridXmlDataProvider. Open the Document property on each, and compare the contents of the InnerXml property. On the xmlDataProvider (the Application-level's resource) you will find the latest node changes to the XML file are reflected. Not so on the gridXmlDataProvider (the XAML UI element's resource). InnerXml property shows no changes. No changes, not need to update the UI.

FYI I had Problem #1 - the same XmlDataProvider resource declared in the Application.Resources block AND in the Window.Resources block. I started out with the latter declaration, ran into an exception error after I referred to the XmlDataProvider instance via Application.Current.FindResource("name"), copy and pasted the declaration into the Application.Resources block, LEAVING the resource declared in the Window.Resources block, creating a TWO REFERENCE problem. The XAML UI used the Window data context, while my code-behind updated the XML file with the Application data context! Whenever I added or removed nodes from the XML file the UI (ListBox) did not get updated!

BTW XmlDataProvider already implements its own notification mechanism, no need use an ObservableCollection. oProv.Refresh() does not cause the refresh of the bound UI because it may point to a different instance of the XmlDataProvider (the Grid element's), and as far as that instance is concerned, no changes have happened.

This answer probably comes too late for you, but I just found this stuff out, thought I share it.

in xml

    <XmlDataProvider Source="XMLFile1.xml" XPath="Data"  DataChanged="XmlDataProvider_DataChanged"></XmlDataProvider>
</Window.DataContext>

in cs

  private void XmlDataProvider_DataChanged(object sender, EventArgs e)
        {
            Dispatcher.BeginInvoke((Action)(() =>
            {
                XmlDataProvider oProv = this.DataContext as XmlDataProvider;
                oProv.Refresh();
            }));
        }

Taken from.. http://www.infosysblogs.com/microsoft/2008/03/wpf_updating_xmldataprovider_w.html

Ive used below;

        XmlDataProvider xdp = this.Resources["userDataXmlDataProvider1"]  as XmlDataProvider;
        xdp.Source = new Uri(MyPath + @"\Projects.xml");

        FileSystemWatcher watcher = new FileSystemWatcher();
        //set the path of the XML file appropriately as per your requirements
        watcher.Path = MyPath;

        //name of the file i am watching
        watcher.Filter = "Projects.xml";

        //watch for file changed events so that we can refresh the data provider
        watcher.Changed += new FileSystemEventHandler(file_Changed);

        //finally, don't forget to enable watching, else the events won't fire           
        watcher.EnableRaisingEvents = true;

and

    void file_Changed(object sender, FileSystemEventArgs e)
    {
        XmlDataProvider xdp = this.Resources["userDataXmlDataProvider1"] as XmlDataProvider;
        xdp.Refresh();

    }

and in my UserControl;

    <UserControl.Resources>
    <XmlDataProvider x:Key="userDataXmlDataProvider1" XPath="Projects/Project" IsAsynchronous="True" />
    <CollectionViewSource x:Key="userDataCollectionViewSource1" Source="{StaticResource userDataXmlDataProvider1}"/>
    </UserControl.Resources>

    <Grid DataContext="{StaticResource userDataXmlDataProvider1}">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="3*"/>
    </Grid.RowDefinitions>
    <ListBox x:Name="listBox1" Grid.Row="1"
             ItemsSource="{Binding}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal" Margin="8,0,8,0">
                    <Label Content="{Binding XPath=ProjectName}" Width="100" Margin="5" />
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

</Grid>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM