从后台线程更新DataContext

3

我使用后台线程获取WPF窗口的数据,代码如下[使用4.0框架和async/await]:

async void refresh()
{
    // returns object of type Instances
    DataContext = await Task.Factory.StartNew(() => serviceagent.GetInstances());
    var instances = DataContext as Instances;
    await Task.Factory.StartNew(() => serviceagent.GetGroups(instances));
    // * problem here * instances.Groups is filled but UI not updated
}

当我在GetInstances中包含GetGroups的操作时,UI会显示出组信息。
当我在另一个操作中更新DataContext时,它正确地包括了组信息,但UI不会显示它们。
在GetGroups()方法中,我为ObservableCollection的groups包含了NotifyCollectionChangedAction.Reset,但这并没有起到帮助作用。更奇怪的是,我只调用了一次列表上的NotifyCollectionChangedAction.Reset,但它却执行了三次,而该列表有十个项目?!
我可以通过编写以下内容来解决此问题:
DataContext = await Task.Factory.StartNew(() => serviceagent.GetGroups(instances));

但这是更新DataContext和UI的常规方式吗?
实际上,我只想更新现有的DataContext而不重新设置它。

编辑:serviceagent.GetGroups(instances)的更多细节:

public void GetGroups(Instances instances)
{
    // web call
    instances.Admin = service.GetAdmin();

    // set groups for binding in UI
    instances.Groups = new ViewModelCollection<Groep>(instances.Admin.Groups);

    // this code has no effect
    instances.Groups.RaiseCollectionChanged();
}

这里的ViewModelCollection<T>继承了ObservableCollection<T>,并且我添加了这个方法:
public void RaiseCollectionChanged()
{
    var handler = CollectionChanged;
    if (handler != null)
    {
        Trace.WriteLine("collection changed");
        var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
        handler(this, e);
    }
}

如果XAML文件中的绑定正确完成,我认为问题在于GetGroups方法。你能展示一下这个方法吗? - Michael Mairegger
基本上,代码只是这样的:{ instances.Groups = new ViewModelCollection<Group>(webcall); RaiseCollectionChanged(); } - Gerard
请将代码放在问题中,而不是注释中。还要添加任何缺失的相关代码。您在哪里调用Reset函数,它与问题的其余部分有什么关系? - Panagiotis Kanavos
3个回答

4

似乎有些人对DataContext的作用存在一些疑惑。DataContext不是你必须更新的某个特殊对象,而是一个指向你想要绑定到UI界面的对象或多个对象的引用。只要你对这些对象进行了更改,UI界面就会得到通知(如果你实现了正确的接口)。

因此,除非你明确更改了DataContext,否则你的UI无法猜测你现在想显示不同的对象集。

事实上,在你的代码中,没有理由两次设置DataContext。只需使用最终要显示的对象集来设置它即可。事实上,由于你操作的是同一组数据,所以使用两个任务是没有意义的。

async Task refresh()
{
    // returns object of type Instances
    DataContext=await Task.Factory.StartNew(() => {
             var instances = serviceagent.GetInstances();
             return serviceagent.GetGroups(instances);
    });
}

注意:

您不应该使用async void签名。它仅用于“fire-and-forget”事件处理程序,即您不关心它们成功或失败的情况。原因是async void方法无法被等待,因此没有人知道它是否成功。


异步 void 已经可以工作了,为什么我还需要异步 Task?请注意,GetInstances() 不是一个 void 方法,只有 GetGroups() 是 void,我以为我可以通过引用传递 DataContext? - Gerard

4
在你的代码中,async 部分有几个值得注意的点:
  • 我在我的 MSDN 文章中解释了为什么我们应该避免使用 async void。总的来说,void 对于 async 方法来说是一种不自然的返回类型,因此会有一些怪异之处,特别是异常处理方面。
  • 我们应该优先使用 TaskEx.Run 而不是 StartNew 进行异步任务,正如我在我的博客上所解释的那样:
  • 虽然不是必须的,但最好遵循基于任务的异步模式中的指南;遵循这些命名约定(等等)将有助于其他开发人员维护代码。

基于这些,我还推荐您阅读我的异步入门博客文章。

接下来是实际的问题......

从后台线程更新数据绑定代码始终很棘手。我建议您将 ViewModel 数据视为 UI 的一部分(它是一种“逻辑 UI”,可以这么说)。因此,在后台线程检索数据是可以的,但更新实际的 VM 值应该在 UI 线程上完成。

这些更改会使您的代码看起来更像这样:

async Task RefreshAsync()
{
  var instances = await TaskEx.Run(() => serviceagent.GetInstances());
  DataContext = instances;
  var groupResults = await TaskEx.Run(() => serviceagent.GetGroups(instances));
  instances.Admin = groupResults.Admin;
  instances.Groups = new ObservableCollection<Group>(groupResults.Groups);
}

public GroupsResult GetGroups(Instances instances)
{
  return new GroupsResult
  {
    Admin = service.GetAdmin(),
    Groups = Admin.Groups.ToArray(),
  };
}

下一步需要检查的是Instances是否实现了INotifyPropertyChanged接口。当设置Groups时,不需要触发Reset集合更改事件;因为GroupsInstances上的属性,则由Instances负责触发INotifyPropertyChanged.PropertyChanged事件。
或者,您可以在最后设置DataContext
async Task RefreshAsync()
{
  var instances = await TaskEx.Run(() => serviceagent.GetInstances());
  var groupResults = await TaskEx.Run(() => serviceagent.GetGroups(instances));
  instances.Admin = groupResults.Admin;
  instances.Groups = new ObservableCollection<Group>(groupResults.Admin.Groups);
  DataContext = instances;
}

感谢你对“TaskEx.Run”的建议,我之前不知道。你也看到了我“PropertyChanged”中的缺陷。你的解决方法的一个缺点是需要在 UI 项目中引用所有数据契约。我只想在 ViewModel 项目中引用这些。 - Gerard
因此,我在 UI 项目中不是使用 instances.Groups = new ObservableCollection<Group>(groupResults.Groups); ,而是在 VM 项目中完全相同,然后跟着使用 RaisePropertyChanged("Groups");。这难道不更符合 MVVM 的思路,而不是在 View 中设置列表吗? - Gerard

1
我发现RaiseCollectionChanged对于绑定了DataContextGroups属性没有影响。我只需要通知:instances.RaisePropertyChanged("Groups");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接