我一直在努力寻找一种在复杂的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
?
1条答案
按热度按时间gk7wooem1#
其中一些是相反的力量:
避免大量LoadDataAsync()方法
避免了对数据库的许多原子和潜在的重复访问
如果你可以在一个操作中组织数据检索(例如,一个复杂的数据库查询,或者一个后端对前端的调用),那么这通常会更有效,尽管听起来你的大多数查询都是独立的,除了一些完全重复的查询。
所以,我想你可能最终会得到一种延迟加载的解决方案,如果每个查询已经运行过,它将返回它的数据。请注意,这实际上与MVVM或ViewModels甚至
async
没有任何关系:问题实际上是你有一大堆工作要做(LoadDataAsync
),但你不能完全分解它,因为你不想重复重叠的工作。懒惰评估是这个问题的一个解决方案:将其分解,但记住结果。即,您建议的“缓存代理”。我在AsyncEx库中有一个AsyncLazy<T>
类型,可能对此有用。现在,谈谈VM/async的问题。
我更喜欢(从你的问题听起来你也是这样做的)让每个VM都有自己的异步数据加载-在这种情况下,点击代理,这样它就只做额外的必要工作。如果你有一个不错的Suspense/Spinner组件,有些人可以用一堆在不同时间完成的Spinner。
听起来你只是想要一个整体的“我下面的一切都加载”旋转器,虽然。这也是一个完全好的选择。
这种方法的关键是
NotifyTask<T>.Task
属性,它提供了一种使用实际的Task
而不是可观察的属性来检测完成(和错误)的方法。因此,您可以使用
Task.WhenAll
构建一个Task
,当标量属性完成加载时,该Task
完成;首先考虑子VM:然后,您可以将其 Package 到一个
NotifyTask
中,该NotifyTask
表示整个子VM的初始化:接下来,将相同的想法扩展到父VM;标量性质是直接的:
子VM有点奇怪;你需要先
await
它们才能存在于集合中,然后await
每个的初始化:然后,这会在父VM上为您提供一个
Initialization
属性,该属性表示其所有自身属性及其所有子VM(表示其所有属性的初始化)的初始化。异常会像往常一样在ParentViewModel.Initialization
通知任务中传播和可用。同时,异步-惰性代理确保不会重复工作。旁注:
NotifyTask<T>
(有时候你想这样做),但很多时候它们只能是get
的属性。ContinueWith
犹豫不决是正确的。ContinueWith
的现代替代品是await
。async
方法中创建/填充ObservableCollection
没有任何问题,只要代码在UI线程上运行。如果从UI线程调用async
方法,并且如果它在任何地方都没有使用ConfigureAwait(false)
,那么代码将在UI线程上运行,并且没有问题。Dispatcher
;我之所以提到这一点,是因为你在问题中提到了几点。几乎总是有比使用Dispatcher
更好的解决方案。