IResult and Coroutines
翻译【三台】;网址【http://home.cnblogs.com/u/3Tai/】
Previously, I mentioned that there was one more compelling feature of the Actions concept called Coroutines. If you haven’t heard that term before, here’s what wikipedia has to say:
之前我有提到过一个更吸引人的特性,叫做协同处理。如果你还没听过这个词,这里摘录了一些维基百科的话:
In computer science, coroutines are program components that generalize subroutines to allow multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, iterators,infinite lists and pipes.
在计算机科学中,协同处理是容纳子程序允许用于在多个位置暂停和恢复执行多个入口点的程序组件。协同处理非常适合执行常见的程序组件,如合作任务、 迭代器、 无限列表和管道。
Here’s one way you can thing about it: Imagine being able to execute a method, then pause it’s execution on some statement, go do something else, then come back and resume execution where you left off. This technique is extremely powerful in task-based programming, especially when those tasks need to run asynchronously. For example, let’s say we have a ViewModel that needs to call a web service asynchronously, then it needs to take the results of that, do some work on it and call another web service asynchronously. Finally, it must then display the result in a modal dialog and respond to the user’s dialog selection with another asynchronous task. Accomplishing this with the standard event-driven async model is not a pleasant experience. However, this is a simple task to accomplish by using coroutines. The problem…C# doesn’t implement coroutines natively. Fortunately, we can (sort of) build them on top of iterators.
你可以试想一下:能够执行的方法,然后在某个位置暂停去做别的事,然后回来刚才停止的位置继续执行。这种技术在基于任务的编程中功能极其强大,尤其是当这些任务需要异步运行。例如,假设我们有的 ViewModel 需要异步调用 web 服务,之后它需要获取结果并执行一些操作,并以异步方式调用另一个 web 服务。最后,需要在模态对话框中显示结果,然后响应用户的对话框中的选择操作并执行另一个异步任务。标准事件驱动的异步模型完成这些,不会有什么愉快经历。然而,用协同程序可以很简单的实现这个任务。问题是......C# 本身并没有实现协同程序。幸运的是,我们可以 (在某种程度上) 用迭代器实现它。
There are two things necessary to take advantage of this feature in Caliburn.Micro: First, implement the IResult interface on some class, representing the task you wish to execute; Second, yield instances of IResult from an Action2. Let’s make this more concrete. Say we had a Silverlight application where we wanted to dynamically download and show screens not part of the main package. First we would probably want to show a “Loading” indicator, then asynchronously download the external package, next hide the “Loading” indicator and finally navigate to a particular screen inside the dynamic module. Here’s what the code would look like if your first screen wanted to use coroutines to navigate to a dynamically loaded second screen:
在 Caliburn.Micro 中实现此功能要做到两点: 第一,在一些类中实现 IResult 接口,实现你要做的任务。第二,用 yield 返回每个 IResult 任务2 。让我们更具体一点。话说我们有一个 Silverlight 应用程序,想要动态地下载和显示未包含在主包中的内容。首先我们先想显示一个"加载"的提示,然后异步下载这个扩展包,下一步隐藏"加载"提示,最后定位到扩展包中的一个指定的内容。这里是代码,从第一个屏幕想要使用协同定位到第二个动态加载屏幕:
using System.Collections.Generic; using System.ComponentModel.Composition; [Export(typeof(ScreenOneViewModel))] public class ScreenOneViewModel { public IEnumerable<IResult> GoForward() { yield return Loader.Show("Downloading..."); yield return new LoadCatalog("Caliburn.Micro.Coroutines.External.xap"); yield return Loader.Hide(); yield return new ShowScreen("ExternalScreen"); } }
First, notice that the Action “GoForward” has a return type of IEnumerable. This is critical for using coroutines. The body of the method has four yield statements. Each of these yields is returning an instance of IResult. The first is a result to show the “Downloading” indicator, the second to download the xap asynchronously, the third to hide the “Downloading” message and the fourth to show a new screen from the downloaded xap. After each yield statement, the compiler will “pause” the execution of this method until that particular task completes. The first, third and fourth tasks are synchronous, while the second is asynchronous. But the yield syntax allows you to write all the code in a sequential fashion, preserving the original workflow as a much more readable and declarative structure. To understand a bit more how this works, have a look at the IResult interface:
首先,请注意"GoForward"方法返回的是 IEnumerable 类型。这是使用协同程序的关键。方法体中的有四个 yield 语句。这些 yields 每次都返回 IResult 的一个实例。第一是显示"下载"提示,第二个是异步下载 xap ,第三个隐藏"下载"提示,第四个显示一个新的xap中的内容。每次 yield 语句执行后,编译器将"暂停"执行此方法,直到特定的任务完成。第一、 第三和第四个任务是同步的,而第二个是异步的。但 yield 语法允许您以顺序的方式编写代码,保持可读性和声明式的结构实现原有的工作流。要理解这一点是如何工作的,请看 IResult 接口:
public interface IResult { void Execute(CoroutineExecutionContext context); event EventHandler<ResultCompletionEventArgs> Completed; }
It’s a fairly simple interface to implement. Simply write your code in the “Execute” method and be sure to raise the “Completed” event when you are done, whether it be a synchronous or an asynchronous task. Because coroutines occur inside of an Action, we provide you with an ActionExecutionContext useful in building UI-related IResult implementations. This allows the ViewModel a way to declaratively state its intentions in controlling the view without having any reference to a View or the need for interaction-based unit testing. Here’s what the ActionExecutionContext looks like:
相当简单的接口实现。直接在"Execute"方法中编写代码,确保在任务完成后提交"Completed"的事件,无论是同步还是异步任务。因为协程是在方法内执行的,我们还提供了 ActionExecutionContext,在与 UI 相关的 IResult 实现时它很有用。它提供了 ViewModel 在控制 View 方面的状态,无需直接引用 View,也适用需要基于交互的单元测试。ActionExecutionContext 看起来像这样:
public class ActionExecutionContext { public ActionMessage Message; public FrameworkElement Source; public object EventArgs; public object Target; public DependencyObject View; public MethodInfo Method; public Func<bool> CanExecute; public object this[string key]; }
And here’s an explanation of what all these properties mean:
下方是这些属性的解释:
Message The original ActionMessage that caused the invocation of this IResult.
调用 IResult 的 ActionMessage 源。
Source The FrameworkElement that triggered the execution of the Action.
触发绑定 Action 的控件 FrameworkElement
EventArgs Any event arguments associated with the trigger of the Action.
触发绑定 Action 的相关联的事件参数。
Target The class instance on which the actual Action method exists.
绑定 Action 所在类的实例。
View The view associated with the Target.
与 Target 对象关联的视图。
Method The MethodInfo specifying which method to invoke on the Target instance.
要在 Target 对象上调用的 MethodInfo 方法信息。
CanExecute A function that returns true if the Action can be invoked, false otherwise.
一个 function 委托类型,返回值为 true 表明绑定方法可用,反之表示不可用。
Key Index A place to store/retrieve any additional metadata which may be used by extensions to the framework.
存储/检索任何额外的元数据,它可以扩展框架,增加可用性。
Bearing that in mind, I wrote a naive Loader IResult that searches the VisualTree looking for the first instance of a BusyIndicator to use to display a loading message. Here’s the implementation:
通过这些点,我创建了一个 Loader 雏形,它实现了 IResult 接口,提供了搜索 VisualTree 寻找第一个 BusyIndicator 实例,用于显示加载消息。
using System; using System.Windows; using System.Windows.Controls; public class Loader : IResult { readonly string message; readonly bool hide; public Loader(string message) { this.message = message; } public Loader(bool hide) { this.hide = hide; } public void Execute(CoroutineExecutionContext context) { var view = context.View as FrameworkElement; while(view != null) { var busyIndicator = view as BusyIndicator; if(busyIndicator != null) { if(!string.IsNullOrEmpty(message)) busyIndicator.BusyContent = message; busyIndicator.IsBusy = !hide; break; } view = view.Parent as FrameworkElement; } Completed(this, new ResultCompletionEventArgs()); } public event EventHandler<ResultCompletionEventArgs> Completed = delegate { }; public static IResult Show(string message = null) { return new Loader(message); } public static IResult Hide() { return new Loader(true); } }
See how I took advantage of context.View? This opens up a lot of possibilities while maintaining separation between the view and the view model. Just to list a few interesting things you could do with IResult implementations: show a message box, show a VM-based modal dialog, show a VM-based Popup at the user’s mouse position, play an animation, show File Save/Load dialogs, place focus on a particular UI element based on VM properties rather than controls, etc. Of course, one of the biggest opportunities is calling web services. Let’s look at how you might do that, but by using a slightly different scenario, dynamically downloading a xap:
看一下我使用了 context.View 后的优势。它在保持视图和视图模型分离时,开辟了很多可能性。列举几个你可以用 IResult 实现的有趣的东西: 显示一个消息框,显示基于 VM 的模态对话框,在用户鼠标位置显示基于 VM 的Popup弹出内容,播放动画,显示文件保存/打开对话框,取代控件操作使用基于VM的属性将焦点置于特定的 UI 元素。当然,最大的用法之一是调用 web 服务。让我们来看看您该如何实现,通过一个不常见的场景下,异步下载 xap:
using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition.ReflectionModel; using System.Linq; public class LoadCatalog : IResult { static readonly Dictionary<string, DeploymentCatalog> Catalogs = new Dictionary<string, DeploymentCatalog>(); readonly string uri; [Import] public AggregateCatalog Catalog { get; set; } public LoadCatalog(string relativeUri) { uri = relativeUri; } public void Execute(CoroutineExecutionContext context) { DeploymentCatalog catalog; if(Catalogs.TryGetValue(uri, out catalog)) Completed(this, new ResultCompletionEventArgs()); else { catalog = new DeploymentCatalog(uri); catalog.DownloadCompleted += (s, e) =>{ if(e.Error == null) { Catalogs[uri] = catalog; Catalog.Catalogs.Add(catalog); catalog.Parts .Select(part => ReflectionModelServices.GetPartType(part).Value.Assembly) .Where(assembly => !AssemblySource.Instance.Contains(assembly)) .Apply(x => AssemblySource.Instance.Add(x)); } else Loader.Hide().Execute(context); Completed(this, new ResultCompletionEventArgs { Error = e.Error, WasCancelled = false }); }; catalog.DownloadAsync(); } } public event EventHandler<ResultCompletionEventArgs> Completed = delegate { }; }
In case it wasn’t clear, this sample is using MEF. Furthermore, we are taking advantage of the DeploymentCatalog created for Silverlight 4. You don’t really need to know a lot about MEF or DeploymentCatalog to get the takeaway. Just take note of the fact that we wire for the DownloadCompleted event and make sure to fire the IResult.Completed event in its handler. This is what enables the async pattern to work. We also make sure to check the error and pass that along in the ResultCompletionEventArgs. Speaking of that, here’s what that class looks like:
你可能看不懂,此示例使用了 MEF。此外,为了 Silverlight 4 的创建,我们使用了 DeploymentCatalog 。你真的不为了彻底的 MEF 或 DeploymentCatalog 而去叫外卖。只需注意到一个事实,我们为 DownloadCompleted 事件添加处理,并确保在这个事件处理程序内触发 IResult.Completed 事件。这样便了支持异步模式。我们也会获取错误并将其传递在 ResultCompletionEventArgs。这个类看起来像这样:
public class ResultCompletionEventArgs : EventArgs { public Exception Error; public bool WasCancelled; }
Caliburn.Micro’s enumerator checks these properties after it get’s called back from each IResult. If there is either an error or WasCancelled is set to true, we stop execution. You can use this to your advantage. Let’s say you create an IResult for the OpenFileDialog. You could check the result of that dialog, and if the user canceled it, set WasCancelled on the event args. By doing this, you can write an action that assumes that if the code following the Dialog.Show executes, the user must have selected a file. This sort of technique can simplify the logic in such situations. Obviously, you could use the same technique for the SaveFileDialog or any confirmation style message box if you so desired. My favorite part of the LoadCatalog implementation shown above, is that the original implementation was written by a CM user! Thanks janoveh for this awesome submission! As a side note, one of the things we added to the CM project site is a “Recipes” section. We are going to be adding more common solutions such as this to that area in the coming months. So, it will be a great place to check for cool plugins and customizations to the framework.
每次从 IResult 的回调获取到这些值时,Caliburn.Micro 的枚举器检查这些属性。如果有错误或 WasCancelled 设置为 true,我们停止执行。您可以使用他们并从中获益。比方说,您为 OpenFileDialog 创建一个 IResult。你可以检查该对话框的结果,如果用户取消了它,请在事件参数设置 WasCancelled。通过这样做,您可以编写一个假定,想要让后面的 Dialog.Show 代码执行,用户必须选择一个文件的操作。这种技术可以简化类似情况下的逻辑。很明显,如果你需要的话可以在 SaveFileDialog 或其他确认风格的消息框中使用相同的技术。我最喜欢的部分是如上所示的 LoadCatalog 的执行部分,最原始的实现是由 CM 用户 janoveh 编写的,感谢这个令人敬畏的提交!说一点备注,我们在 CM 项目网站添加的内容中有个“Recipes”区域。我们打算通过几个月时间,在该区域添加更多可公用的解决方案。因此,它将会是获取很酷的插件和自定义框架的好地方。
Another thing you can do is create a series of IResult implementations built around your application’s shell. That is what the ShowScreen result used above does. Here is its implementation:
你可以做的另一件事是围绕你应用程序外壳,创建一系列的 IResult 实现。下面是一个 ShowScreen 的实现:
using System; using System.ComponentModel.Composition; public class ShowScreen : IResult { readonly Type screenType; readonly string name; [Import] public IShell Shell { get; set; } public ShowScreen(string name) { this.name = name; } public ShowScreen(Type screenType) { this.screenType = screenType; } public void Execute(CoroutineExecutionContext context) { var screen = !string.IsNullOrEmpty(name) ? IoC.Get<object>(name) : IoC.GetInstance(screenType, null); Shell.ActivateItem(screen); Completed(this, new ResultCompletionEventArgs()); } public event EventHandler<ResultCompletionEventArgs> Completed = delegate { }; public static ShowScreen Of<T>() { return new ShowScreen(typeof(T)); } }
This bring up another important feature of IResult. Before CM executes a result, it passes it through the IoC.BuildUp method allowing your container the opportunity to push dependencies in through the properties. This allows you to create them normally within your view models, while still allowing them to take dependencies on application services. In this case, we depend on IShell. You could also have your container injected, but in this case I chose to use the IoC static class internally. As a general rule, you should avoid pulling things from the container directly. However, I think it is acceptable when done inside of infrastructure code such as a ShowScreen IResult.
此时会出现 IResult 的另一个重要特点。在 CM 执行结果之前,内部的属性通过注入方式传入了通过 IoC.BuildUp 方法创建的容器实例(这里是Ioc和MEF思想)。这允许您在您的视图模型中用通常方式创建它们,同时仍然允许他们能依赖应用程序服务式注入创建。在此例中,我们依赖注入了 IShell。您也可以通过你自己的容器注入,不过我在在此例中选择了框架内含的 IoC 静态类。有个全局规范,你应该避免直接从容器获取东西。不过当完成内部基础代码如 ShowScreen IResult 时,我觉得还是可以接受的。
Other Usages
Out-of-the-box Caliburn.Micro can execute coroutines automatically for any action invoked via an ActionMessage. However, there are times where you may wish to take advantage of the coroutine feature directly. To execute a coroutine, you can use the static Coroutine.BeginExecute method.
开箱即用的 Caliburn.Micro 可以通过 ActionMessage 自动执行调用绑定方法的协同程序。然而,又是你也希望不妨直接使用协同功能。若要执行一个协同程序,可以使用静态的 Coroutine.BeginExecute 方法。
I hope this gives some explanation and creative ideas for what can be accomplished with IResult. Be sure to check out the sample application attached. There’s a few other interesting things in there as well.
我希望已经给出了一些解释,以及可以用 IResult 来完成的一些创意。一定要查看一下附加的示例程序。里面还有一些其他有趣的东西。