WPF MVVM:如何从一个复杂的视图模型中异步加载数据,并带有子视图模型?

iibxawm4  于 2023-04-13  发布在  其他
关注(0)|答案(1)|浏览(199)

我一直在努力寻找一种在复杂的ViewModel中加载数据的好模式,我在网上做了大量的阅读,也做了大量的实验,但我对异步操作还很陌生。
我读过articles written by Stephen Cleary,我知道它是如何工作的。我喜欢设计视图来处理不同状态(忙碌,故障,加载)的模式。像NotifyTask这样的类看起来很适合用于绑定到异步加载的属性。
我在概念化如何扩展到更复杂的用例时遇到了一些麻烦。让我们假设一个ViewModel有多个属性。一些属性很简单,可以同步加载。一些属性需要访问数据存储来检索它们的值。一些属性是子ViewModel的集合,它们本身有各种属性,需要同步或异步加载。
起初,我觉得最干净的方法是将整个ViewModel图的所有数据加载打包到一个异步加载操作中。这将允许视图中的所有控件绑定到该操作的结果,并共享结果状态。
下面的示例有点长,但它是我能想到的最小的示例,可以用来演示第一种方法的优缺点。

internal class ParentViewModel : ViewModelBase
{
    private readonly IUnitOfWorkFactory _unitOfWorkFactory;
    private readonly int _personID;

    public ParentViewModel(IUnitOfWorkFactory unitOfWorkFactory, int personID)
    {  
        this._unitOfWorkFactory = unitOfWorkFactory;
        this._personID = personID;
  
        _data = new NotifyTask<ParentViewModel_Context?>(LoadDataAsync(),null);
    }

    private NotifyTask<ParentViewModel_Context?> _data;
    public NotifyTask<ParentViewModel_Context?> Data
    {
        get => _data;
        set => SetField(ref _data, value); //SetField is responsible for raising INotifyPropertyChanged
    }

    private async Task<ParentViewModel_Context?> LoadDataAsync()
    {
        using var unitOfWork = await _unitOfWorkFactory.CreateAsync();

        var person = await unitOfWork.People.GetByIdAsync(_personID);
        List<Person> children = await unitOfWork.People.GetChildrenOfParentByParentIDAsync(_personID);

        var data = new ParentViewModel_Context()
        {
            InChargeOfDepartment = person.Department,
            PersonName = person.Name
        };

        var childrenVMs = new ObservableCollection<ChildViewModel>(
                children.Select(c => new ChildViewModel()
                {
                    PersonName = c.Name,
                    HostVM = data
                }));

        data.Children = childrenVMs;

        return data;
    }
}

internal class ParentViewModel_Context : ViewModelBase
{

    private string? _inChargeOfDepartment;
    public string? InChargeOfDepartment
    {
        get => _inChargeOfDepartment;
        set => SetField(ref _inChargeOfDepartment, value);
    }

    private string? _personName;
    public string? PersonName
    {
        get => _personName;
        set => SetField(ref _personName, value);
    }

    public ObservableCollection<ChildViewModel>? _children;
    public ObservableCollection<ChildViewModel>? Children
    {
        get => _children;
        set => SetField(ref _children, value);
    }
}

internal class ChildViewModel : ViewModelBase
{
    private IViewModel? _hostVM;
    public IViewModel? HostVM
    {
        get => _hostVM;
        set => SetField(ref _hostVM, value);
    }

    private string? _personName;
    public string? PersonName
    {
        get => _personName;
        set => SetField(ref _personName, value);
    }

}

我能看到的缺点是:
1.正如您在示例中看到的,我认为这需要第二个“ViewModel”ParentViewModel_Context来包含所有可绑定的属性,以确保所有值都通过单个Task结果返回到UI线程。通常,我会让ParentViewModel_Context上的所有属性直接驻留在ParentViewModel上。
1.所有的子ViewModel都在LoadDataAsync中更新,并且它们的所有数据都被推送到LoadDataAsync中。这是一个相对较小的示例,但是具有深度嵌套的ViewModel的真实示例将具有大量的LoadDataAsync方法。我也觉得DI或ViewModelFactory可能是更好的方法。
1.线程安全对我来说仍然是一门晦涩《双城之战》的知识,我不知道这种方法是否引入了可能的线程问题。
这种方法有一些地方可以改进,我可以把ParentViewModel_Context当作一个简单的DTO,直接在ParentViewModel上拥有可绑定的属性,并在任务完成后从DTO加载可绑定的属性。
这感觉好一点,但是这看起来需要一种方法来在LoadDataAsync()返回后触发延续任务,以便将数据从DTO传输到属性。我能看到的唯一方法是.ContinueWith(),但是我的阅读提醒我不要使用.ContinueWith()(尽管我会说原因确实超出了我的理解)。我还认为.ContinueWith()运行在另一个线程上,所以我想我必须使用Dispatcher来确保我是从UI线程设置的。
我不知道这是否真的比通过单个Data.Result属性绑定更好。它也会改变绑定数据的方式,并且可能会涉及到更多关于如何表示状态的想法。
我看到的第二种方法是让每个涉及异步加载操作的属性都有自己的NotifyTask

internal class ParentViewModel : ViewModelBase
{
    private readonly IUnitOfWorkFactory _unitOfWorkFactory;
    private readonly int _personID;

    public ParentViewModel(IUnitOfWorkFactory unitOfWorkFactory, int personID)
    {
        this._unitOfWorkFactory = unitOfWorkFactory;
        this._personID = personID;

        _inChargeOfDepartment = new NotifyTask<string?>(LoadDepartmentAsync(), null);
        _personName = new NotifyTask<string?>(LoadNameAsync(), null);
        _children = new NotifyTask<ObservableCollection<ChildViewModel>?>(LoadChildrenAsync(), null);
        
    }

    private NotifyTask<string?> _inChargeOfDepartment;
    public NotifyTask<string?> InChargeOfDepartment
    {
        get => _inChargeOfDepartment;
        set => SetField(ref _inChargeOfDepartment, value);
    }

    private NotifyTask<string?> _personName;
    public NotifyTask<string?> PersonName
    {
        get => _personName;
        set => SetField(ref _personName, value);
    }

    public NotifyTask<ObservableCollection<ChildViewModel>?> _children;
    public NotifyTask<ObservableCollection<ChildViewModel>?> Children
    {
        get => _children;
        set => SetField(ref _children, value);
    }

    private async Task<string?> LoadNameAsync()
    {
        using var unitOfWork = await _unitOfWorkFactory.CreateAsync();

        var person = await unitOfWork.People.GetByIdAsync(_personID);

        return person.Name;
    }

    private async Task<string?> LoadDepartmentAsync()
    {
        using var unitOfWork = await _unitOfWorkFactory.CreateAsync();

        var person = await unitOfWork.People.GetByIdAsync(_personID);

        return person.Department;
    }

    private async Task<ObservableCollection<ChildViewModel>> LoadChildrenAsync()
    {
        using var unitOfWork = await _unitOfWorkFactory.CreateAsync();

        List<Person> children = await unitOfWork.People.GetChildrenOfParentByParentID(_personID);        

        var childrenVMs = new ObservableCollection<ChildViewModel>(
                children.Select(c => new ChildViewModel()
                {
                    PersonName = c.Name,
                    HostVM = this
                }));

        return childrenVMs;
    }
}

这感觉已经好多了,不再有一个巨大的Load方法,每个属性都通过自己专用的NotifyTask.Result直接接收自己的值。
我能看到的两个最大的缺点是:
1.每个NotifyTask都可以独立地失败或成功。我看不到一种简单的方法来将所有这些单独任务的结果合并到一个结果中,供UI选择其状态。
1.此方法可能会导致多个Load...()方法重复工作。LoadDepartmentAsync()LoadNameAsync()查询数据库以查找同一个人访问单个属性可以证明这一点。
另一个突出的问题是,这个解决方案仍然是新的ChildViewModel的示例,并将数据推到其中。我觉得这将变得难以管理的非常快与深嵌套的ViewModel层次结构。
为了解决这个问题,你可以使用上面第二个示例中的相同模式,将每个嵌套的ViewModel视为负责加载自己的数据。你甚至可以创建一个工厂模式来抽象这些子ViewModel的构造。然后ChildViewModel类看起来像这样:

internal class ChildViewModel : ViewModelBase
{
    private readonly IUnitOfWorkFactory _unitOfWorkFactory;
    private readonly int _personID;

    public ChildViewModel(IUnitOfWorkFactory unitOfWorkFactory, int personID, IViewModel host)
    {        
        this._unitOfWorkFactory = unitOfWorkFactory;
        this._personID = personID;
        this._hostVM = host;
        _personName = new NotifyTask<string?>(LoadNameAsync(), null);
    }

    private IViewModel? _hostVM;
    public IViewModel? HostVM
    {
        get => _hostVM;
        set => SetField(ref _hostVM, value);
    }

    private NotifyTask<string?> _personName;
    public NotifyTask<string?> PersonName
    {
        get => _personName;
        set => SetField(ref _personName, value);
    }

    private async Task<string?> LoadNameAsync()
    {
        using var unitOfWork = await _unitOfWorkFactory.CreateAsync();

        var person = await unitOfWork.People.GetByIdAsync(_personID);

        return person.Name;
    }
}

... LoadChildrenAsync()方法可能看起来像:

private async Task<ObservableCollection<ChildViewModel>> LoadChildrenAsync()
{
    using var unitOfWork = await _unitOfWorkFactory.CreateAsync();

    List<Person> children = await unitOfWork.People.GetChildrenOfParentByParentID(_personID);

    var childrenVMs = new ObservableCollection<ChildViewModel>(
            children.Select(c => _childViewModelFactory.Create(c.ID, this)));

    return childrenVMs;
}

这应该解决了必须加载整个ViewModel树并将数据推送到每个后代的问题。然而,这也加剧了在视图中没有单个状态可使用的问题,因为这现在分散在许多ViewModel上的许多属性中。
这也可能使重复工作的问题更加明显。想象一下,如果ChildViewModel有一个List<City> Cities属性用作ComboBox的DataSource。如果每个ChildViewModel负责获取自己的数据,每个孩子将自己前往数据库检索相同的城市列表。

如果您创建了一个代理来缓存不可变的下拉数据源,这可以最小化,但这并不能解决根本的问题。
有没有一种我没有看到的金发女孩方法可以处理我遇到的所有问题:
1.异步加载ViewModel的深度嵌套层次结构中的属性
1.具有报告整个加载功能的聚合结果的同步状态
1.避免大量的LoadDataAsync()方法
1.避免了对数据库的许多原子和潜在的重复访问
1.与IoC模式配合良好
1.考虑螺纹安全性
最后(虽然这是一个题外话),初始化和填充任务内部的ObservableCollection感觉不对。我应该返回一个基本的项目列表,以便它可以传递给UI线程上的ObservableCollection构造函数吗?如果是这样,我该怎么做?.ContinueWith() + Dispatcher

gk7wooem

gk7wooem1#

其中一些是相反的力量:
避免大量LoadDataAsync()方法
避免了对数据库的许多原子和潜在的重复访问
如果你可以在一个操作中组织数据检索(例如,一个复杂的数据库查询,或者一个后端对前端的调用),那么这通常会更有效,尽管听起来你的大多数查询都是独立的,除了一些完全重复的查询。
所以,我想你可能最终会得到一种延迟加载的解决方案,如果每个查询已经运行过,它将返回它的数据。请注意,这实际上与MVVM或ViewModels甚至async没有任何关系:问题实际上是你有一大堆工作要做(LoadDataAsync),但你不能完全分解它,因为你不想重复重叠的工作。懒惰评估是这个问题的一个解决方案:将其分解,但记住结果。即,您建议的“缓存代理”。我在AsyncEx库中有一个AsyncLazy<T>类型,可能对此有用。
现在,谈谈VM/async的问题。
我更喜欢(从你的问题听起来你也是这样做的)让每个VM都有自己的异步数据加载-在这种情况下,点击代理,这样它就只做额外的必要工作。如果你有一个不错的Suspense/Spinner组件,有些人可以用一堆在不同时间完成的Spinner。
听起来你只是想要一个整体的“我下面的一切都加载”旋转器,虽然。这也是一个完全好的选择。
这种方法的关键是NotifyTask<T>.Task属性,它提供了一种使用实际的Task而不是可观察的属性来检测完成(和错误)的方法。
因此,您可以使用Task.WhenAll构建一个Task,当标量属性完成加载时,该Task完成;首先考虑子VM:

var initializationTask = await Task.WhenAll(
    _personName.Task,
    ... /* any other properties */ );

然后,您可以将其 Package 到一个NotifyTask中,该NotifyTask表示整个子VM的初始化:

NotifyTask Initialization { get; }
...
Initialization = NotifyTask.Create(() => Task.WhenAll(
    _personName.Task,
    ... /* any other properties */ ));

接下来,将相同的想法扩展到父VM;标量性质是直接的:

NotifyTask Initialization { get; }
...
Initialization = NotifyTask.Create(() => Task.WhenAll(
    _inChargeOfDepartment.Task,
    _personName.Task));

子VM有点奇怪;你需要先await它们才能存在于集合中,然后await每个的初始化:

NotifyTask Initialization { get; }
...
Initialization = NotifyTask.Create(async () =>
{
  await Task.WhenAll(
      _inChargeOfDepartment.Task,
      _personName.Task,
      _children.Task);
  var children = await _children.Task;
  await Task.WhenAll(children.Select(x => x.Initialization.Task));
});

然后,这会在父VM上为您提供一个Initialization属性,该属性表示其所有自身属性及其所有子VM(表示其所有属性的初始化)的初始化。异常会像往常一样在ParentViewModel.Initialization通知任务中传播和可用。同时,异步-惰性代理确保不会重复工作。
旁注:

  • 虽然可以设置一个NotifyTask<T>(有时候你想这样做),但很多时候它们只能是get的属性。
  • 您对使用ContinueWith犹豫不决是正确的。ContinueWith的现代替代品是await
  • async方法中创建/填充ObservableCollection没有任何问题,只要代码在UI线程上运行。如果从UI线程调用async方法,并且如果它在任何地方都没有使用ConfigureAwait(false),那么代码将在UI线程上运行,并且没有问题。
  • 避免直接使用Dispatcher;我之所以提到这一点,是因为你在问题中提到了几点。几乎总是有比使用Dispatcher更好的解决方案。

相关问题