英文:
How to bind to Task<T> in ObservableObject from CommunityToolkit.Mvvm?
问题
ObservableObject来自CommunityToolkit.Mvvm具有允许异步绑定到Task<T>
的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>
。
英文:
ObservableObject from CommunityToolkit.Mvvm has API which allows to bind asynchronously to Task<T>
(https://github.com/MicrosoftDocs/CommunityToolkit/blob/main/docs/mvvm/ObservableObject.md#handling-taskt-properties)
Problem is the sample do not include xaml part and I don't know how the binding should looks like.
Can anybody show me on the example bellow:
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>
I expect after button is clicked it waits 3 seconds and then my value is changed to 5.
I've already checked their sample app, but there is binding to Task
only, not to Task<T
> (https://github.com/CommunityToolkit/MVVM-Samples/blob/master/samples/MvvmSampleUwp/Views/ObservableObjectPage.xaml)
答案1
得分: 1
你必须始终等待一个 Task
对象。在你的情况下,TaskNotifier<T>
正在等待 Task
为你执行。一旦 Task
完成运行,它将触发 INotifyPropertyChanged.PropertyChanged
事件。然后,你可以从 Task.Result
属性中检索值。这意味着你必须始终绑定到 Task.Result
属性。
由于异步代码可能会长时间运行,你还应该在特定的 Binding
上设置 Binding.IsAsync
为 true
,以防止界面冻结:
<Window>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<Button Command="{Binding RequestValueCommand}"
Content="获取我的值"/>
<StackPanel Orientation="Horizontal" >
<TextBlock Text="我的值是:"/>
<TextBlock Text="{Binding RequestTask.Result, IsAsync=True}"/>
</StackPanel>
</StackPanel>
</Window>
然而,异步属性(长时间运行的属性)是一个自相矛盾的说法。属性和字段(或通常的变量)不会“运行”。方法才会运行。
属性预期存储一个值。从属性中引用值并不等同于执行方法。从属性中获取值不应该被认为是耗时的操作。
另一方面,我们自然期望一个方法会执行某些操作,然后一旦完成就返回一个值或更改对象的状态。我们期望方法可能会长时间运行。
你应该始终避免使用异步属性,只有在确实没有其他选项时才使用它们。
通常,通过适当地重构应用程序流程,你可以避免这种情况。通常情况下,长时间运行的操作是显式触发的。正如“操作”一词所示,我们使用方法来执行这些操作。
在你的情况下,你可以完全使用 ICommand
来触发长时间运行的操作。因为长时间运行的操作通常会影响界面,所以应该允许用户显式启动此操作。例如,你可以始终为用户提供一个“下载”按钮。用户可以从下拉列表中选择项目,然后单击按钮以开始下载。这感觉很自然,因为用户期望在单击按钮时开始耗时的下载。
相比之下,你实现的模式允许用户从下拉列表中选择项目。一旦用户选择项目,下载(长时间运行的操作)立即开始(因为 SelectedItem 被设置为幕后的异步属性)。
允许用户显式启动长时间运行的操作在可用性和用户体验方面有几个优点。在这个示例中,用户可以在选择项目后撤销他的决定并选择另一个项目。因为下载尚未开始,所以一切都很顺利。当用户准备好时,他可以通过按钮显式启动下载(触发长时间运行的操作的命令处理程序)。
大多数情况下,异步属性应该被替换为由用户触发并执行长时间运行操作的 ICommand
。
MVVM 工具包支持异步命令。只需定义类型为 Task
的执行处理程序(请注意,框架本身不支持异步命令,即没有可等待的 ICommand.Execute
成员。这意味着,具有 async void
执行处理程序的正常同步 ICommand
是可以的)。
与异步属性相比,更优雅的解决方案可以如下所示:
// 定义异步命令
[RelayCommand]
private async Task RequestValueAsync()
{
// 显式执行长时间运行的操作。
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="获取我的值"/>
<StackPanel Orientation="Horizontal" >
<TextBlock Text="我的值是:"/>
<TextBlock Text="{Binding RequestTask}"/>
</StackPanel>
</StackPanel>
</Window>
英文:
You must always await a Task
object. In your case the TaskNotifier<T>
is awaiting the Task
for you. It will raise the INotifyPropertyChanged.PropertyChanged
event as soon as the Task
has run to completion. You can then retrieve the value from the Task.Result
property. This means you must always bind to the Task.Result
property.
Because asynchronous code implies to be potentially long-running, you should also set Binding.IsAsync
to true
on the particular Binding
to prevent the UI from freezing:
<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>
However, an asynchronous property (long-running property) is an oxymoron. Properties and fields (or variables in general) don't "run". Methods do.
A property is expected to store a value. Referencing a value from a property is not synonymous to executing a method.
Never would anybody expect that getting the value from a property takes significant time.
We can consider a long-running property a code smell.
On the other hand, we naturally expect a method to do something and then once completed return a value or change an object's state. We expect a method to be potentially long-running.
You should always avoid asynchronous properties and only use them when you really ran out of options.
You usually avoid this situation by refactoring the application flow properly. Usually a long-running operation is explicitly triggered. And as the word "operation" suggests we use methods for this.
In your scenario you can perfectly use the ICommand
to trigger the long-running operation. Because a long-running operation usually affects the UI, you should allow the user to explicitly start this operation. For example, you can always provide the user a "Download" button. He can select am item from a drop down list and click the button to start the download. This feels natural as the user expects that the time consuming download start when he clicks the button.
In contrast, your implemented pattern allows the user to select an item from the drop down list. The moment he selects the item the download (the long-running operation) immediately starts (because the SelectedItem was set to the async property behind the scene).
Allowing the user to explicitly start the long-running operation has several advantages in terms of usability and user experience.In this example the user can revert his decision after selecting an item and pick another one. Because the download has not yet started, everything is smooth. When the user is ready, he explicitly starts the download via the button (a command handler that triggers the long-running operation).
Most of the time, an asynchronous property should be replaced with a ICommand
that is triggered by the user and executes the long-running operation.
The MVVM Toolkit supports asynchronous commands. Simply define the execute handler of type Task
(note, the framework itself does not support async commands i.e. there is no awaitable ICommand.Execute
member. This means, a normal synchronous ICommand
with an execute handler async void
is fine).
A more graceful solution (in contrast to async properties) could look as follows:
// 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>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论