Reputation: 102
I have a program that has a WPF UI and gathers and formats a bunch of data from a server. I want the user to be able to press a button to start data collection and not have the UI freeze up while that runs. When the data collection is done, I want all the data collected to be dumped to the UI to be displayed.
I'm very new to c#, multithreading, and MVVM so feel free to correct any mistakes I make.
Originally I started using BackgroundWorker
. After a bunch of reading, I came across multiple people saying that BackgroundWorker
is older and that async
and await
should be used. I have done a bunch of looking on this website as well as looking through the official MS documentation and examples but haven't found a good example that accomplishes what I need with the data structure I have.
My data structure is a list of custom class objects with multiple properties. Each object can hold more child objects of the same type. This forms a hierarchy. The way I'm using it is the top-level list actually only has one "root" element in it and then the rest of the elements are children under this. The hierarchy is displayed to the user (how that's displayed is a whole other complicated thing that I'm pretty sure doesn't factor in here).
BackgroundWorker
was halfway working. I set up the events and did the data acquisition in the DoWork
handler and did the UI updating in the RunWorkerCompleted
handler. The function I'm performing repopulates the data hierarchy.
What I did first was have a button click handler collects parameters and passes them into the DoWork
handler as arguments. In the DoWork
handler, primary data acquisition is performed and passed as an argument to the RunWorkerCompleted
handler. There, the top-level custom class object is created, placed in the list, and populated with children. Populating with children prepresents a secondary data acquisition operation (shorter than the first, but still a problem). This locks up the UI only for the secondary operation, but that is still a problem
The second thing I tried was to move the object creation, secondary data collection, and child creation into the DoWork
handler and just pass the top-level object out to the RunWorkerCompleted
handler as an argument. There, the only thing that would be done is to place the top-level object in the list. However I get the following error:
System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.'
Is this application outside the scope of BackgroundWorker
? What can be used instead? Is there a better way to structure my application that allows this hierarchy of objects to be created outside the UI thread at all?
EDIT (here's a simplified version of my code): View (or my approximation of it)
private void PopulateTreeFromAssembly(object sender, RoutedEventArgs e) //This is the button press code
{
//There is some UI stuff here that I stripped out
if (BackgroundPopulateOperation.IsBusy != true)
{
BackgroundPopulateOperation.RunWorkerAsync(new PopulateOperationArgs(ViewMod.AssemblyNumber));
}
}
private void BackgroundPopulateOperation_DoWork(object sender, DoWorkEventArgs e)
{
FileResult fileres = //Call file collection code that returns this custom object
//Create a new Item object to act as the new root
Item root = new Item(ViewMod, ViewMod.AssemblyNumber, 0, false);
root.AttachedFile = fileres;
root.SetIsChecked(true, true);
root.IsAvailable = false;
root.IsExpanded = true;
root.UpdateFileSourceText();
root.SetDescription(true, false); //this is the secondary data acquisition
root.PopulateChildren(false, true); //this is also the secondary data acquisition (and creates child objects)
//add the new root object to the argument object
((PopulateOperationArgs)e.Argument).NewRoot = root;
//Send along the event argument object to the result so that it gets picked up by the RunWorkerCompleted event method
e.Result = ((PopulateOperationArgs)e.Argument);
}
private void BackgroundPopulateOperation_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) //Runs on the UI thread
{
//Update the UI elements after the new top-level file has been found
ViewMod.TreeData.Clear();
ViewMod.TreeData = new TreeGridModel();
ViewMod.TreeData.Add(((PopulateOperationArgs)e.Result).NewRoot); //<-------------Error here
RootItem = ((PopulateOperationArgs)e.Result).NewRoot; //update the reference variable to the top-level (root) tree item
BOMTreeGrid.ItemsSource = ViewMod.TreeData.FlatModel;
//There is some UI stuff here that I stripped out
}
TreeData
is an instance of a custom object that inherits ObservableCollection
. The data is represented using a "TreeGrid" control from this tutorial that I modified for my purposes.
Upvotes: 0
Views: 1074
Reputation: 456457
I came across multiple people saying that BackgroundWorker is older and that async and await should be used.
Well, yes, but if you're new to C#, MVVM, and multithreading, feel free to take once concept at a time. There's "ideal" and there's also "good enough for a first solution". My blog posts and other writing usually take the "ideal" approach, but that's sometimes just not realistic.
The core principle is that UI elements may only be modified from their UI thread. This is called "thread affinity", and you can think of it as those elements belonging to a specific thread. So in your case, the tree view (and each tree view item) belong to the UI thread.
MVVM makes this a bit more complex because it defines objects (View Models) that are connected to UI elements. The rules here start to get a bit fuzzy - it's possible to do "simple" updates, and some "more complex" updates if you tweak some settings. But I prefer to treat View Model objects as also having thread affinity. In other words, all updates to View Models must be done on the UI thread. I find that making this (slightly over-strict) rule encourages cleaner code.
Now, to bring this down to multithreading, whether you use Task.Run
or BackgroundWorker
, this means your background work needs a way to send updates and/or results to the UI thread. I use the term "result" for the final resulting value(s) of the background work that are returned only at the end, and "update" for any intermediate value(s) that should update the UI immediately before the background work is done.
In summary, what you need to do is:
INotifyPropertyChanged
or INotifyCollectionChanged
if those types are bound to UI elements.Upvotes: 1