65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Codon FX 实现异步命令

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2018年4月1日

CPOL

9分钟阅读

viewsIcon

9064

downloadIcon

96

了解如何利用 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,那么对CodonCodon.Extras.Core的 NuGet 引用就足够了。

示例 UWP 应用程序使用了 Codon 的 IDialogService。因此,我添加了对Codon.Extras.Uwp包的引用。

示例中的 MainViewModel 类包含一个名为 DoWorkCommandICommand。请参见列表 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 方法返回一个等于 falseTask<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 属性,请awaitRefreshAsync 方法。

您会注意到方法中有一个 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);
	}
}

当在executeAsynccanExecuteAsync函数中抛出异常时,IExceptionHandler 实现有机会处理(忽略/记录等)该异常。请参见列表 4。

MainViewModelShouldRethrowException 方法使用 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 类具有类型为 MainViewModelViewModel 属性。请参见列表 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。

ProgressRingStackPanel 都共享父 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。

图 1. 视图模型处于忙碌状态,命令正在执行。

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

图 2. 命令执行完成,忙碌状态恢复为 false。

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

图 3. 命令执行期间引发异常。

结论

在本文中,您已经看到 Codon 提供了丰富的命令基础结构。正如您所期望的,有一个基本的 ICommand 实现:ActionCommand,它允许您提供在命令执行期间或在评估命令的 Enabled 属性时调用的委托。还有一个 UICommand 类,除了 ActionCommand 类提供的功能外,还支持文本、图标和可见性。然而,在 Codon 的Extras包中,存在许多异步命令,它们类似于核心库中的命令,并提供异步支持。AsyncActionCommand 引入了异步方法支持,同时无缝实现了 ICommand 接口,使其与 UWP、WPF、Xamarin Forms 和 Codon 的 Xamarin Android 绑定系统的内置命令基础结构兼容。

您已经看到如何创建一个具有异步命令的视图模型,该命令会启动一个潜在的长时间运行的操作。您还探讨了如何全局处理异步操作执行期间发生的异常。

我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。

© . All rights reserved.