允许在UI线程上访问在后台线程上创建的对象

时间:2019-07-12 20:31:17

标签: c# .net wpf multithreading asynchronous

我有一个程序,该程序具有WPF UI并从服务器收集并格式化一堆数据。我希望用户能够按一个按钮来开始数据收集,并且在运行时不会冻结UI。数据收集完成后,我希望将所有收集的数据转储到UI进行显示。

我对C#,多线程和MVVM还是很陌生,所以可以随时纠正我犯的任何错误。

最初,我开始使用BackgroundWorker。一堆阅读后,我遇到了很多人,说BackgroundWorker年龄较大,应该使用asyncawait。我已经在该网站上进行了很多浏览,还浏览了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”控件表示的。

1 个答案:

答案 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,则使用“更新”。

总而言之,您需要做的是:

  1. 仅在 界面上保留所有视图模型类型。如果这些类型绑定到UI元素,则包括实现INotifyPropertyChangedINotifyCollectionChanged的所有类型。
  2. 定义 new 类型,这些类型表示后台工作的进度更新/结果。这些是“ POCO”(普通的旧C#对象)。让后台工作报告进度/返回这些类型。
  3. 编写代码,将后台工作使用的POCO类型的进度更新/结果复制到“视图模型”类型中。此代码将在UI线程上运行。