我有一个程序,该程序具有WPF UI并从服务器收集并格式化一堆数据。我希望用户能够按一个按钮来开始数据收集,并且在运行时不会冻结UI。数据收集完成后,我希望将所有收集的数据转储到UI进行显示。
我对C#,多线程和MVVM还是很陌生,所以可以随时纠正我犯的任何错误。
最初,我开始使用BackgroundWorker
。一堆阅读后,我遇到了很多人,说BackgroundWorker
年龄较大,应该使用async
和await
。我已经在该网站上进行了很多浏览,还浏览了MS的官方文档和示例,但没有找到一个很好的示例来满足我所拥有的数据结构的需要。
我的数据结构是具有多个属性的自定义类对象的列表。 每个对象可以容纳更多相同类型的子对象。这形成了层次结构。我使用它的方式是顶层列表,实际上其中仅包含一个“ root”元素,然后其余元素是该元素下的子元素。层次结构将显示给用户(显示方式是另外一件非常复杂的事情,我敢肯定,这不会在此处考虑)。
BackgroundWorker
正在进行一半。我设置了事件,并在DoWork
处理程序中进行了数据获取,并在RunWorkerCompleted
处理程序中进行了UI更新。我正在执行的功能将重新填充数据层次结构。
我首先要做的是一个按钮单击处理程序,它收集参数并将它们作为参数传递给DoWork
处理程序。在DoWork
处理程序中,执行主要数据获取,并将其作为参数传递给RunWorkerCompleted
处理程序。在那里,创建了顶级定制类对象,将其放置在列表中,并填充了子代。子级填充表示第二个数据采集操作(比第一个要短,但仍然是一个问题)。这样只会锁定辅助操作的用户界面,但这仍然是一个问题
我尝试的第二件事是将对象创建,辅助数据收集和子创建移动到DoWork
处理程序中,然后将顶级对象作为参数传递给RunWorkerCompleted
处理程序。在那里,唯一要做的就是将顶级对象放在列表中。但是我收到以下错误:
System.InvalidOperationException:“调用线程无法访问此对象,因为其他线程拥有它。”
此应用程序是否超出BackgroundWorker
的范围?可以代替使用什么呢?有没有更好的方法来构建我的应用程序,从而完全允许在UI线程之外创建这种对象层次结构?
编辑(这是我的代码的简化版): 查看(或我的近似值)
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
是继承ObservableCollection
的自定义对象的实例。数据是使用this tutorial中为我的目的修改的“ TreeGrid”控件表示的。
答案 0 :(得分:1)
我遇到了很多人,说BackgroundWorker年龄较大,应该使用async和await。
嗯,yes,但是如果您是C#,MVVM,和多线程的新手,则可以一次接受一个概念。有“理想的”,也有“足够好的第一个解决方案”。我的博客文章和其他文章通常采用“理想”的方法,但这有时是不现实的。
核心原理是只能从UI线程中修改UI元素。这称为“线程亲和力”,您可以将其视为属于特定线程的那些元素 。因此,在您的情况下,树视图(和每个树视图项)都属于UI线程。
MVVM使此操作变得更加复杂,因为它定义了连接到UI元素的对象(视图模型)。这里的规则开始变得有些模糊-如果您调整某些设置,则可以进行“简单”更新,而可以进行“更复杂”的更新。但是我更喜欢将View Model对象也具有线程亲和力。换句话说,对视图模型的所有更新都必须在UI线程上完成。我发现制定此规则(略为严格)可以鼓励使用更干净的代码。
现在,无论您使用Task.Run
还是BackgroundWorker
,都将其归结为多线程,这意味着您的后台工作需要一种将更新和/或结果发送到UI线程的方法。对于final resulting value(s) of the background work that are returned only at the end,我使用术语“结果”,对于任何intermediate value(s) that should update the UI immediately before the background work is done,则使用“更新”。
总而言之,您需要做的是:
INotifyPropertyChanged
或INotifyCollectionChanged
的所有类型。