使用 Codon FX 实现异步命令





5.00/5 (3投票s)
了解如何利用 Codon FX 中的异步 ICommand 实现来支持启动长时间运行操作的命令。
目录
引言
您是否曾为您的应用程序创建过一个包含 ICommand
的视图模型,而该命令需要执行一些异步活动?例如调用 Web API 或将数据保存到文件?如果您有此经验,您会知道同步的 ICommand
接口并不容易适应异步操作。您最终不得不构建一个小型状态机,在命令完成时禁用和重新启用命令目标。如果命令可以异步工作,那不是很棒吗?在 Codon 中,它们就可以。
Codon FX 是一个用于构建可维护应用程序的跨平台框架。Codon 提供了丰富的命令基础结构。正如您所期望的,有一个基本的 ICommand
实现:ActionCommand
,它允许您提供在命令执行期间或在评估命令的 Enabled
属性时调用的委托。还有一个 UICommand
类,除了 ActionCommand
类提供的功能外,还支持文本、图标和可见性。
现在,您可能会认为这些 ICommand
实现位于 Codon 的核心 .NET Standard 库中,是 Codon 在命令方面所能提供的全部。但事实并非如此。在 Codon 的Extras包中,存在许多其他命令,它们类似于核心库中的命令,但提供了异步支持。AsyncActionCommand
引入了异步方法支持,同时无缝实现了 ICommand
,使其与 UWP、WPF、Xamarin Forms 和 Codon 的 Xamarin Android 绑定系统的内置命令基础结构兼容。
在这篇文章中,您将了解如何使用 AsyncActionCommand
。您将看到如何创建一个具有异步命令的视图模型,该命令会启动一个潜在的长时间运行的操作。您还将探讨如何全局处理异步操作执行期间发生的异常。
Codon FX 入门
Codon 基于 .NET Standard 构建。它具有特定于平台的包来支持其对话框服务和其他一些服务。但是,如果您不需要 IDialogService
实现、页面导航或任何其他特定于平台的 features,那么对Codon或Codon.Extras.Core的 NuGet 引用就足够了。
示例 UWP 应用程序使用了 Codon 的 IDialogService
。因此,我添加了对Codon.Extras.Uwp包的引用。
示例中的 MainViewModel
类包含一个名为 DoWorkCommand
的 ICommand
。请参见列表 1。
DoWorkCommand
使用以下两个参数创建:
- 一个名为
DoWorkAsync
的异步执行方法, - 以及一个名为
CanDoWorkAsync
的异步可执行方法。
DoWorkCommand
属性没有传统的 getter 或 setter。我使用了 C# 7.0 的表达式主体 getter 来惰性加载命令。您不必这样做,我只是喜欢这种语法的简洁性。
列表 1. 异步 DoWorkCommand
public class MainViewModel : ViewModelBase, IExceptionHandler
{
...
AsyncActionCommand doWorkCommand;
public ICommand DoWorkCommand => doWorkCommand
?? (doWorkCommand = new AsyncActionCommand(
DoWorkAsync, CanDoWorkAsync));
...
}
MainViewModel
继承自 Codon 的 ViewModelBase
类。Codon.UIModel.ViewModelBase
类继承自 ObservableBase
,而 ObservableBase
通过 PropertyChangeNotifier
对象实现了 INotifyPropertyChanged
(和 INotifyPropertyChanging
)。
MainViewModel
包含一个布尔类型的 Busy
属性,正如我们稍后将看到的,该属性用于在页面上显示忙碌的进度环。该属性在视图模型中的定义如下:
bool busy;
public bool Busy
{
get => busy;
private set => Set(ref busy, value);
}
在我们查看 DoWorkCommand
的委托之前,让我们简要检查一下 Codon 的属性设置器基础结构以及幕后如何发生属性更改通知。
理解 Codon 的属性设置器 API
ViewModelBase
类的 Set
方法仅在字段更改时更新字段,并确保更新在 UI 线程上进行,以避免抛出跨线程异常。
Set
方法返回以下 AssignmentResult
枚举值之一:
- 成功
- 已取消
- 已分配
- 所有者已释放
Success
表示字段不等于正在应用的值,并且字段现在已设置为指定值。
如果 INotifyPropertyChanging
方法的订阅者将 PropertyChangingEventArgs
标记为Cancelled,从而阻止字段值被更新,则可能返回 Cancelled
。
如果返回 AlreadyAssigned
,则表示字段等于正在应用的值。在这种情况下,不会引发 PropertyChanging
事件或 PropertyChanged
事件。
ViewModelBase
继承自 ObservableBase
,后者使用 PropertyChangeNotifier
对象。PropertyChangeNotifier
类允许您聚合 INPC (INotifyPropertyChanged) 行为,并免除了继承实现 INotifyPropertyChanged
的基类的需要。
有趣的事实:您可以使用
PropertyChangeNotifier
在任何类上启用 INPC。
如果您对 Codon 的 INPC 基础结构的内部工作原理感兴趣,请参阅 Codon.ComponentModel.ObservableBase
类。
理解异步命令操作
在本文的这一部分,我们将重点介绍传递给 AsyncActionCommand
构造函数的两个方法委托。第一个是命令的执行函数 DoWorkAsync
,第二个是 CanDoWorkAsync
,它是一个确定命令的 Enabled
状态以及是否可以执行它的函数。
CanDoWorkAsync
方法依赖于 busy
标志,如下面的摘录所示:
Task<bool> CanDoWorkAsync(object arg)
{
return Task.FromResult(!busy);
}
当视图模型的 Busy
属性设置为 true
时,CanDoWorkAsync
方法返回一个等于 false
的 Task<bool>
,这将命令的 Enabled
状态设置为 false
。幕后有一些神奇的操作使其能够异步完成所有这些。如果您感兴趣,请查看 AsyncActionCommand 的源代码。
您知道吗? Codon 命令还支持参数类型转换。Codon 的通用支持意味着,例如,如果一个命令需要一个
bool
参数,那么在 XAML 中指定为true
的参数会自动转换为bool
。此机制也是可扩展的;您可以创建自定义IImplicitTypeConverter
类来添加自己的类型转换功能,然后将其添加到 IoC 容器中,如下所示:
Dependency.Register<IImplicitTypeConverter, MyImplicitTypeConverter>();
让我们回到命令的 DoWorkAsync
方法。
当执行 DoWorkCommand
时会调用 DoWorkAsync
方法。请参见列表 2。此方法标记为 async,这意味着我们可以在其主体中await其他异步方法。
它首先将Busy标志设置为 true。然后它向 doWorkCommand
发出信号,使其重新评估其 Enabled
属性。由于此时 busy
为 true,命令的 Enabled
属性将设置为 false。
注意:与传统的同步
ICommand
实现不同,RaiseCanExecuteChanged
可能是异步发生的,因此在调用其RaiseCanExecuteChanged
方法后,Enabled
状态不一定已更改。要等待命令更新其Enabled
属性,请await其RefreshAsync
方法。
您会注意到方法中有一个 if (raiseException)
块。我们稍后将探讨它的用途。
我们使用 Task.Delay
调用使方法在几秒钟内不完成,之后我们使用 Codon 的 IDialogService
向用户显示“活动完成”消息。
finally 块将 Busy
标志设置为 false
,并再次调用命令的 RaiseCanExecuteChanged
方法,该方法将命令的 Enabled
属性更新为 true
。
列表 2. DoWorkAsync 方法
async Task DoWorkAsync(object arg)
{
try
{
Busy = true;
doWorkCommand.RaiseCanExecuteChanged();
if (raiseException)
{
throw new Exception(
"This exception is handled by the ShouldRethrowException method.");
}
/* Wait for a few seconds before completion. */
await Task.Delay(5000);
await Dependency.Resolve<IDialogService>().ShowMessageAsync(
"The command has finished processing asynchronously.", "Activity Complete");
}
finally
{
Busy = false;
doWorkCommand.RaiseCanExecuteChanged();
}
}
那么,if (raiseException)
块是做什么的?视图模型包含一个 RaiseException
属性,当设置为 true
时,会导致命令执行时抛出异常。此目的旨在演示命令基础结构中的全局异常处理。
从非 UI 线程抛出的异常很难正确处理。特别是当您的代码在不同平台上运行时。Codon 试图通过提供一个异常处理扩展点来缓解这一问题。这适用于命令基础结构、解耦消息传递系统和应用程序设置系统。
例如,要接收有关命令执行期间抛出的异常的通知并有机会处理它们,我们可以将自定义 IExceptionHandler
注册到 IoC 容器。我们可以全局完成此操作,使用独立于任何特定视图模型的服务(我更倾向于这种方法),或者我们可以采取简单的做法,在视图模型中实现 IExceptionHandler
并将该视图模型注册到 IoC 容器,就像我在本例中所做的那样。请参见列表 3。
列表 3. 注册 IExceptionHandler
public class MainViewModel : ViewModelBase, IExceptionHandler
{
public MainViewModel()
{
/* If an exception occurs during the execution of a command,
* the ShouldRethrowException method is called. */
Dependency.Register<IExceptionHandler>(this);
}
}
当在executeAsync或canExecuteAsync函数中抛出异常时,IExceptionHandler
实现有机会处理(忽略/记录等)该异常。请参见列表 4。
MainViewModel
的 ShouldRethrowException
方法使用 IDialogService
在对话框中显示异常。它也可以使用 Codon 的 ILog
记录异常,并评估一些规则来确定是否应重新抛出异常,如返回值所示。如果方法返回 true
,则命令基础结构将重新抛出异常。
现在,如果您和我一样年纪大,您可能会想:哦,这让我想起了 Patterns and Practices 的 Enterprise Library 中那个非常复杂的 Exception Handling Application Block。是的,它有点像那样。但是,它真正的目的不是应用错误策略,而是让您的应用程序有机会处理可能发生在不同线程上并导致应用程序崩溃的第一方或第三方组件抛出的异常。例如,在使用 Xamarin Android 时,没有全局处理异常的方法。
列表 4. MainViewModel ShouldRethrowException 方法
bool IExceptionHandler.ShouldRethrowException(Exception exception, object owner,
[CallerMemberName]string memberName = null,
[CallerFilePath]string filePath = null,
[CallerLineNumber]int lineNumber = 0)
{
Dependency.Resolve<IDialogService>().ShowMessageAsync(
"Exception thrown: " + exception.Message);
return false;
}
现在让我们探讨一下视图模型是如何连接到视图的。应用程序的 MainPage
类具有类型为 MainViewModel
的 ViewModel
属性。请参见列表 5。
我们将 MainViewModel
暴露为一个属性,以便在 XAML 中使用 x:Bind
绑定表达式。为了保险起见,页面的 DataContext
属性也设置为 MainViewModel
。我发现这样做对于需要旧样式 Binding 表达式灵活性的情况很有用。
列表 5. MainPage.xaml.cs
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
DataContext = Dependency.Resolve<MainViewModel>();
}
public MainViewModel ViewModel => (MainViewModel)DataContext;
}
MainPage.xaml 文件绑定到视图模型的 DoWorkCommand
。请参见列表 6。
ProgressRing
和 StackPanel
都共享父 Grid
的第 0 行。ProgressRing
位于其他元素的上方。
列表 6. MainPage.xaml 摘录
<Page x:Class="AsyncCommandsExample.MainPage" ...>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<Button Command="{x:Bind ViewModel.DoWorkCommand}"
Content="Show Dialog with Timer" />
<ToggleSwitch IsOn="{x:Bind ViewModel.RaiseException, Mode=TwoWay}"
Header="Raise Exception during Command Execution" />
</StackPanel>
<ProgressRing IsActive="{x:Bind ViewModel.Busy, Mode=OneWay}" />
</Grid>
</Page>
当视图模型的 Busy
属性设置为 true
时,将显示 ProgressRing
控件,该控件在单击按钮时持续 5 秒。请参见图 1。

当 DoWorkCommand
完成时,将显示一个对话框,并将忙碌状态恢复为 false
。请参见图 2。

ToggleSwitch
绑定到视图模型的 RaiseException
属性。当 IsOn
设置为 true
并且单击按钮时,会在视图模型的 DoWorkAsync
方法中引发异常。请参见图 3。

结论
在本文中,您已经看到 Codon 提供了丰富的命令基础结构。正如您所期望的,有一个基本的 ICommand
实现:ActionCommand
,它允许您提供在命令执行期间或在评估命令的 Enabled
属性时调用的委托。还有一个 UICommand
类,除了 ActionCommand
类提供的功能外,还支持文本、图标和可见性。然而,在 Codon 的Extras包中,存在许多异步命令,它们类似于核心库中的命令,并提供异步支持。AsyncActionCommand
引入了异步方法支持,同时无缝实现了 ICommand
接口,使其与 UWP、WPF、Xamarin Forms 和 Codon 的 Xamarin Android 绑定系统的内置命令基础结构兼容。
您已经看到如何创建一个具有异步命令的视图模型,该命令会启动一个潜在的长时间运行的操作。您还探讨了如何全局处理异步操作执行期间发生的异常。
我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。