ObservableObject 具有允许异步绑定到
Task<T>
(https://github.com/MicrosoftDocs/CommunityToolkit/blob/main/docs/mvvm/ObservableObject.md#handling-taskt-properties)的 API
问题是示例不包含 xaml 部分,我不知道绑定应该是什么样子。
谁能告诉我下面的例子:
public partial class MainWindowViewModel : ObservableObject
{
[RelayCommand]
private void RequestValue()
{
RequestTask = LoadAsync();
}
private TaskNotifier<int>? requestTask;
public Task<int>? RequestTask
{
get => requestTask;
private set => SetPropertyAndNotifyOnCompletion(ref requestTask, value);
}
private static async Task<int> LoadAsync()
{
await Task.Delay(3000);
return 5;
}
<Window>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<Button Command="{Binding RequestValueCommand}" Content="Get my value"/>
<StackPanel Orientation="Horizontal" >
<TextBlock Text="My value is:"/>
<TextBlock Text="{Binding ?????????}"/>
</StackPanel>
</StackPanel>
</Window>
我希望单击按钮后会等待 3 秒,然后我的值更改为 5。
我已经检查了他们的示例应用程序,但仅绑定到
Task
,而不是 Task<T
> (https://github.com/CommunityToolkit/MVVM-Samples/blob/master/samples/MvvmSampleUwp/视图/ObservableObjectPage.xaml)
您必须始终等待
Task
对象。就您而言,TaskNotifier<T>
正在等待您的Task
。一旦 INotifyPropertyChanged.PropertyChanged
运行完成,它将引发 Task
事件。然后,您可以从 Task.Result
属性检索该值。这意味着您必须始终绑定到 Task.Result
属性。
因为异步代码意味着可能会长时间运行,所以您还应该在特定的
Binding.IsAsync
上将 true
设置为 Binding
以防止 UI 冻结:
<Window>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<Button Command="{Binding RequestValueCommand}"
Content="Get my value"/>
<StackPanel Orientation="Horizontal" >
<TextBlock Text="My value is:"/>
<TextBlock Text="{Binding RequestTask.Result, IsAsync=True}"/>
</StackPanel>
</StackPanel>
</Window>
但是,异步属性(长时间运行的属性)是一个矛盾的说法。属性和字段(或一般变量)不会“运行”。方法可以。
财产应该存储价值。从属性引用值并不等同于执行方法。
从来没有人想到从房产中获得价值需要花费大量时间。
我们可以将长期运行的属性视为代码味道。
另一方面,我们自然期望一个方法做某事,然后一旦完成就返回一个值或更改对象的状态。我们预计一个方法可能会长期运行。
您应该始终避免异步属性,并且仅在确实没有选项时才使用它们。
通常可以通过适当重构应用程序流程来避免这种情况。通常,长时间运行的操作是显式触发的。正如“操作”一词所暗示的那样,我们为此使用方法。
在您的场景中,您可以完美地使用
ICommand
来触发长时间运行的操作。由于长时间运行的操作通常会影响 UI,因此您应该允许用户显式启动此操作。例如,您始终可以向用户提供“下载”按钮。他可以从下拉列表中选择项目,然后单击按钮开始下载。这感觉很自然,因为用户期望在单击按钮时开始耗时的下载。
相反,您实现的模式允许用户从下拉列表中选择一个项目。当他选择项目时,下载(长时间运行的操作)立即开始(因为 SelectedItem 被设置为场景后面的异步属性)。
允许用户显式启动长时间运行的操作在可用性和用户体验方面具有多种优势。在此示例中,用户可以在选择一项后恢复其决定并选择另一项。因为下载还没有开始,所以一切都很顺利。当用户准备好时,他通过按钮(触发长时间运行的操作的命令处理程序)显式开始下载。
大多数时候,异步属性应该替换为由用户触发并执行长时间运行的操作的
ICommand
。
MVVM 工具包支持异步命令。只需定义
Task
类型的执行处理程序(注意,框架本身不支持异步命令,即没有可等待的 ICommand.Execute
成员。这意味着,带有执行处理程序 ICommand
的普通同步 async void
就可以了) .
更优雅的解决方案(与异步属性相比)可能如下所示:
// Define the async command
[RelayCommand]
private async Task RequestValueAsync()
{
// Explicitly execute the long-running operation.
RequestTask = await LoadAsync();
}
private int requestTask;
public int RequestTask
{
get => requestTask;
private set => SetProperty(ref requestTask, value);
}
private async Task<int> LoadAsync()
{
await Task.Delay(3000);
return 5;
}
<Window>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<Button Command="{Binding RequestValueCommand}"
Content="Get my value"/>
<StackPanel Orientation="Horizontal" >
<TextBlock Text="My value is:"/>
<TextBlock Text="{Binding RequestTask}"/>
</StackPanel>
</StackPanel>
</Window>
--- 视图模型 ---
public class ViewModel : ObservableObject
{
public ViewModel()
{
RequestCommand = new RelayCommand(RequestAsync);
}
private TaskNotifier<string> _requestTask;
public Task<string> RequestTask
{
get => _requestTask;
set => SetPropertyAndNotifyOnCompletion(ref _requestTask, value);
}
public IRelayCommand RequestCommand { get; }
private void RequestAsync()
{
RequestTask = Foo();
}
private async Task<string> Foo()
{
await Task.Delay(1000).ConfigureAwait(false);
return DateTime.Now.Second.ToString();
}
}
--- 查看---
<Grid>
<Button Content="{Binding RequestTask.Result}" Command="{Binding RequestCommand}" Background="Transparent"/>
</Grid>