简体   繁体   中英

Dynamically updating a DataGrid using a background worker

I have an ObservableCollection of custom class that holds a string and an int:

public class SearchFile
{
    public string path { set; get; }
    public int occurrences { set; get; }
}

I want to display the collection in a dataGrid . The collection has methods that notify whenever it has been updated, so so far it's only a matter of linking it to the DataGrid.ItemsSource (correct?). Here's the grid XAML (with dataGrid1.ItemsSource = files; in the C# codebehind):

        <DataGrid AutoGenerateColumns="False" Height="260" Name="dataGrid1" VerticalAlignment="Stretch" IsReadOnly="True" ItemsSource="{Binding}" >
            <DataGrid.Columns>
                <DataGridTextColumn Header="path" Binding="{Binding path}" />
                <DataGridTextColumn Header="#" Binding="{Binding occurrences}" />
            </DataGrid.Columns>
        </DataGrid>

Now things are more complicated. I first want to display the path s with the default values of occurrence of zero. Then, I want to go through every SearchFile and update it with a calculated value of occurrence . Here's the helper function:

    public static void AddOccurrences(this ObservableCollection<SearchFile> collection, string path, int occurrences)
    {
        for(int i = 0; i < collection.Count; i++)
        {
            if(collection[i].path == path)
            {
                collection[i].occurrences = occurrences;
                break;
            }
        }
    }

And here's the placeholder worker function:

    public static bool searchFile(string path, out int occurences)
    {
        Thread.Sleep(1000);
        occurences = 1;
        return true; //for other things; ignore here     
    }

I'm using a BackgroundWorker as the background thread. Here's how:

    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {           
        List<string> allFiles = new List<string>();
        //allFiles = some basic directory searching

        this.Dispatcher.Invoke(new Action(delegate
        {
            searchProgressBar.Maximum = allFiles.Count;
            files.Clear(); // remove the previous file list to build new one from scratch
        }));

        /* Create a new list of files with the default occurrences value. */
        foreach(var file in allFiles)
        {
            SearchFile sf = new SearchFile() { path=file, occurrences=0 };
            this.Dispatcher.Invoke(new Action(delegate
            {
                files.Add(sf);
            }));
        }

        /* Add the occurrences. */
        foreach(var file in allFiles)
        {
            ++progress; // advance the progress bar
            this.Dispatcher.Invoke(new Action(delegate
            {
                searchProgressBar.Value = progress;
            }));

            int occurences;
            bool result = FileSearcher.searchFile(file, out occurences);

            files.AddOccurrences(file, occurences);
        }
    }

Now when I run it, there are two problems. First, updating the progress bar's value throws the The calling thread cannot access this object because a different thread owns it. exception. Why? It's in a dispatcher, so it should work just fine. And second, the foreach loop breaks on the bool result =... line. I commenting it out and tried setting int occurences = 1 , and then the loop goes around, but there's something weird going on: whenever I call the method, it's either all zeroes, all ones, or a between state, with onez beginning after a seemingly random number of zeroes).

Why's that?

The Dispatcher is not quite as simple as it sounds. Using a dispatcher means the code will be executed on the same thread which created the containing object, but that is not necessarily the UI thread. Make sure the worker_DoWork method is defined in an actual UI element that appears on the screen (which is just an easy way to eliminate elements created by background threads).

Really, looking at your code, I'm not sure why you're using a background worker at all. This looks like it will actually be slower, because of the constant dispatching to the UI thread. Instead, I think your better option would be to just put the long-running portions in a task, and update the UI on a single callback. For example, in your code it looks like the only thing that is actually too slow for the UI thread might be the FileSearcher call. That would be easy enough to put in a background task which returns the number of results found.

As for the problem on FileSearcher, your method definitions don't match up. The method you posted takes just a path and the out int, but when you call it you pass 4 parameters. Without seeing the overload you're actually calling it's tough to guess what's going wrong.

EDIT: Let me back up a little more. Your root problem is just that WPF doesn't support binding to a collection which is modified from a background thread. That's the reason for all of the dispatching and other complicated code there. My original suggestion (a single task for the long work) was one way to work around the problem. but you can probably do even better using the ObservableList class . This lets you update the collection from the background thread, and automatically notifies the UI without having to use a dispatcher. That is likely to make most of the complicated threading issues go away. I'd recommend reading through the first three articles there for reference, too, there's a lot of good information there.

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