使用 WPF ui 进行多线程处理

Posted

技术标签:

【中文标题】使用 WPF ui 进行多线程处理【英文标题】:Multi-threading with WPF ui 【发布时间】:2018-12-17 13:18:18 【问题描述】:

我不确定这是否可能,但是当我搜索它时我找不到任何东西。

我在 WPF 中制作了一个可视化的日程安排,用于加载和显示约会。问题是加载所有视觉效果需要一段时间,并且在此期间程序变得无响应。

是否可以在单独的线程中加载约会视觉效果并修改计划网格,同时让主线程为其他事情打开?或者可能将调度网格永久保留在第二个 STA 线程中,以便它可以在不干扰窗口的情况下做自己的事情?

编辑:

目前我所拥有的:

    private static void FillWeek()
     
        BindingOperations.EnableCollectionSynchronization(ObservableAppointments, _lockobject);
        for (int i = 1; i < 6; i++)
        
            FillDay(Date.GetFirstDayOfWeek().AddDays(i).Date);
        
    
    private static ObservableCollection<AppointmentUIElement> ObservableAppointments = new ObservableCollection<AppointmentUIElement>();
    private static object _lockobject = new object();
    public static async Task FillDay(DateTime date)
    
        ClearDay(date);
        Appointment[] Appointments;
        var date2 = date.AddDays(1);
        using (var db = new DataBaseEntities())
        
            Appointments = (from Appointment a in db.GetDailyAppointments(2, date.Date) select a).ToArray();
        
        await Task.Run(()=>
        
            foreach (Appointment a in Appointments)
            
                var b = new AppointmentUIElement(a, Grid);
                ObservableAppointments.Add(b);
            
        );      
    
    private static void ObservableAppointments_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    
        if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
        
            var a = e.NewItems[0] as AppointmentUIElement;
            a.Display();
        
    
    private static void ClearDay(DateTime date)
    
        var Queue = new Queue<AppointmentUIElement>(Grid.Children.OfType<AppointmentUIElement>().Where(a => a.Appointment.Start.DayOfWeek == date.DayOfWeek));
        while (Queue.Count > 0)
        
            var x = Queue.Dequeue();
            Grid.Children.Remove(x);
            ObservableAppointments.Remove(x);
        
        var Queue2 = new Queue<GridCell>(Grid.Children.OfType<GridCell>().Where(g => g.Date.Date == date));
        while (Queue2.Count > 0)
        
            Queue2.Dequeue().AppointmentUIElements.RemoveAll(a => true);
        
    

AppointmentUIElement 派生自Border

【问题讨论】:

【参考方案1】:

是的

现在所有这一切的挑战在于,视觉元素和绑定的 ObservableCollections 只能由 UI 线程修改,而无需进行一些额外的工作。不是集合的绑定属性不需要这个。

假设您将 UI 中的“约会视觉效果”绑定到其中包含约会数据的 ObservableCollection。您可以做的是使您的“搜索约会”功能异步并注册您的集合以进行线程同步,如下所示。为简洁起见,我将省略与 INotifyPropertyChange 相关的任何内容。

  public ObservableCollection<Appointments> Appointments = new ObservableCollection<Appointments>();
  private static object _lockobject = new object();
  public async Task Load()
  
        await Task.Run(() =>  /*load stuff into the Appointments collection here */ );
        ///possibly more code to execute after the task is complete.
  

  //in constructor or similar, this is REQUIRED because the collection is bound and must be synchronized for mulththreading operations
  BindingOperations.EnableCollectionSynchronization(YourCollection, _lockobject);

还有一种更讨厌且不推荐的方式来修改 UI 线程创建的视觉元素。

 this.Dispatcher.Invoke(() => /* do stuff with ui elements or bound things*/);

发生的事情的要点是,您从 UI 线程调用 load,当它遇到“await task.run”时,它将在单独的线程中处理任务的内容,同时允许 UI 线程继续响应用户。一旦任务完成,它将在后台返回到 ui 线程以执行 load 方法中的任何其他内容。

如果您忘记了 EnableCollectionSynchronization 部分,那么在 task.run 中添加或删除项目的任何尝试都会引发错误,抱怨您无法在与创建集合相同的线程中更改集合的内容(几乎与尝试直接修改 ui 元素相同的错误)。

评论回复->你在做什么的问题就在这里

  AppointmentUIElement(a,Grid)

您真正应该在这里做的是将 Grid 放入一个自定义控件,该控件定义了一个绑定项目模板,该模板绑定到 ObservableAppointments 中的项目,这些项目实际上应该是约会数据,而不是 UI 元素。所有这些都应该通过上下文中的 ViewModel 来实现。你这样做的方式只有在只有一个线程管理一切的情况下才有效,一旦另一个线程参与进来,它就会全部崩溃。

【讨论】:

不要使用async void,除了事件处理程序。它应该是public async Task Load() 我的 VisualAppointment 项目中创建了 wpf 元素,它们导致 STA 错误。我应该在集合中只有非视觉的东西吗? 我使用当前代码对问题添加了编辑 哎呀,我不会那样做的。我会创建只知道绘制 UI 的自定义控件和只知道数据的模型,并使用 MVVM 绑定将两者放在一起。你做这一切的方式是完全耦合的,我不确定有没有一种好的方法可以让它像你做的那样始终如一地工作。有关一些想法,请参阅更新。【参考方案2】:

是否可以在单独的线程中加载约会视觉效果并修改计划网格,同时让主线程为其他事情打开?或者可能将调度网格永久保留在第二个 STA 线程中,以便它可以在不干扰窗口的情况下做自己的事情?

您可以在一个单独的窗口中加载和显示调度网格,该窗口在一个专用的调度程序线程上运行。有关如何在单独的线程中启动 WPF 窗口的示例,请参阅 this 博客文章。

请记住,在新线程上创建的元素将无法与在主线程上创建的元素进行交互。因此,您不能简单地将调度加载到另一个线程上,然后将其带回主线程。视觉元素只能从最初创建它的线程访问。

【讨论】:

以上是关于使用 WPF ui 进行多线程处理的主要内容,如果未能解决你的问题,请参考以下文章

WPF 线程 Dispatcher

WPF 之 调用线程必须为 STA,因为许多 UI 组件都需要

富客户端 wpf, Winform 多线程更新UI控件

WPF怎么跨线程访问UI控件

WPF多线程使用示例

WPF异常捕获三种处理 UI线程, 全局异常,Task异常