UI绑定依赖属性的异步处理

Posted

技术标签:

【中文标题】UI绑定依赖属性的异步处理【英文标题】:Async processing on UI bound dependency properties 【发布时间】:2020-05-27 05:59:58 【问题描述】:

我正在开发一个 WPF 应用程序,它的业务逻辑由类库(没有 MVVM)处理。业务逻辑的大多数属性都是依赖属性,它允许将数据轻松绑定到 WPF UI。

我有一个显示项目集合(类的依赖属性)的数据网格:ObservableCollection<ItemEntry> EntryCollection

目标是为 EntryCollection 中的每个项目异步调用 ItemEntryUpdateAnalyzer.Analyze(ItemTemplate, Company, entry) 静态方法,因为处理需要几秒钟。

我开始做以下事情:

    private async void AnalyzeButton_OnClick(object sender, RoutedEventArgs e)
    
        List<Task> tasks = EntryCollection.Select(entry => Task.Run(() => AnalyzeItemEntries())).ToList();
        await Task.WhenAll(tasks);
    

    private void AnalyzeItemEntries()
    
        Log.Debug("Begin");
        Thread.Sleep(500);
        Log.Debug("End");
    

效果很好,但是添加处理方法会在 ItemTemplate 的依赖属性上引发 System.InvalidOperationException

    private void AnalyzeItemEntries(ItemEntry entry)
    
        Log.Debug("Begin");
        ItemEntryUpdateAnalyzer.Analyze(ItemTemplate, Company, entry); //InvalidOperationException
        Log.Debug("End");
    

这是因为 Analyze 方法的参数属于主 UI 线程。所以我尝试使用调度程序通过执行以下操作来提供正确的上下文:

    private void AnalyzeItemEntries(ItemEntry entry)
    
        Log.Debug("Begin");
        /*tried with InvokeAsync as well*/
        Dispatcher?.BeginInvoke((Action) (() =>
        
            ItemEntryUpdateAnalyzer.Analyze(ItemTemplate, Company, entry);
        ));
        Log.Debug("End");
    

但这并没有真正帮助,因为这会锁定主线程。问题是参数通过依赖属性绑定到 UI,普通属性似乎不会抛出异常。

编辑:

我尝试使用 DeepCloner NuGet (https://github.com/force-net/DeepCloner) 将 ItemTemplate 和 ItemEntry 深度复制到局部变量:

    private async void AnalyzeButton_OnClick(object sender, RoutedEventArgs e)
    
        Log.Debug($"==== Main thread ID Thread.CurrentThread ===");

        ItemTemplate localTemplate = ItemTemplate.DeepClone();
        ObservableCollection<ItemEntry> localEntryCollection = EntryCollection.DeepClone();
        foreach (ItemEntry entry in localEntryCollection)
        
            await Task.Run(() => AnalyzeItemEntries(localTemplate, entry));
        
    

    private void AnalyzeItemEntries(ItemTemplate template, ItemEntry entry)
    
        Log.Debug($"Begin entry.ItemCode");
        ItemEntryUpdateAnalyzer.Analyze(template, Company, entry);
        Log.Debug($"End entry.ItemCode");
    

我仍然遇到同样的错误。该问题似乎仅与依赖属性有关,因为访问 entry.ItemCode(标准属性)有效,而访问 entry.Action 无效。

【问题讨论】:

一个异常有一个堆栈跟踪,检查它以获取有关错误发生位置/原因的更多详细信息。 这不是答案,但是当您出于好奇将.ConfigureAwait( false ); 添加到WhenAll 时会发生同样的情况吗? 我也试过加.ConfigureAwait(false),可惜没用。 @XAMIMAX :UI 元素未在 Analyze 方法中传递。 UI 控件绑定到位于 ItemTemplateItemEntry 对象中的 Dependency 属性。这算不算糟糕的设计? 当时ItemTemplate的实现和item entry是怎样的?如果这些是您的模型/视图模型,那么您将不需要 DP?通常的 INPC 就足够了。您是否使用这些道具的先前值?这就是为什么你会在模型上使用 DP。或者您是否将 xaml 中的值绑定到您的模型? 【参考方案1】:

依赖属性是一个 UI 级别的对象。后台线程(包括Task.Run 使用的线程)不能直接访问UI 对象。

解决此问题的一种方法是将 UI 属性复制到局部变量中并将这些变量传递给后台线程,如下所示:

private async void AnalyzeButton_OnClick(object sender, RoutedEventArgs e)

  var itemTemplate = ItemTemplate;
  var company = Company;
  List<Task> tasks = EntryCollection.Select(entry => Task.Run(() => AnalyzeItemEntries(itemTemplate, company, entry))).ToList();
  await Task.WhenAll(tasks);

根据ItemTemplate / Company / Entry 的形状,您可能需要将它们制作成“深层”副本。此外,根据AnalyzeItemEntries 的行为,您可能需要更改该方法以返回值,而不是将对象更新为副作用。

【讨论】:

我想过将对象的副本传递给任务,然后返回它们,问题是我无法克隆对象,因为它们不可序列化。 ItemTemplate 有一个手动实现的ToJSON(),所以我想我可以将它与 JSON 进行转换以获取副本。我更关心来自 API(SAP 的 DIAPI)的 Company 对象 @matthiasboularot:仅在需要时克隆它们(即,如果对象具有线程关联性)。【参考方案2】:

感谢大家的帮助,理想的解决方案是使用常规属性而不是依赖属性,但我不能在我的应用程序中这样做。

我已将DispatcherPriority 设置为ApplicationIdle。这会恢复对 UI 的一些响应能力。

            await Dispatcher?.InvokeAsync(() =>
            
                ItemEntryUpdateAnalyzer.Analyze(ItemTemplate, Company, entry);

            ,
            DispatcherPriority.ApplicationIdle);

【讨论】:

以上是关于UI绑定依赖属性的异步处理的主要内容,如果未能解决你的问题,请参考以下文章

javascript中异步操作的异常怎么处理

WPF使用异步+绑定的方式处理大数据量

必须处理简单的 CPU 绑定操作的异步方法执行流程

Android学习笔记 十六 使用RxBinding响应控件的异步事件

如何在双向绑定组合框(WPF)上调用异步操作

异步数据处理Handler