翻譯【三臺】;網址【http://home.cnblogs.com/u/3Tai/】web
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:
以前我有提到過一個更吸引人的特性,叫作協同處理。若是你還沒聽過這個詞,這裏摘錄了一些維基百科的話:
shell
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 應用程序,想要動態地下載和顯示未包含在主包中的內容。首先咱們先想顯示一個"加載"的提示,而後異步下載這個擴展包,下一步隱藏"加載"提示,最後定位到擴展包中的一個指定的內容。這裏是代碼,從第一個屏幕想要使用協同定位到第二個動態加載屏幕:
app
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 語句執行後,編譯器將"暫停"執行此方法,直到特定的任務完成。第1、 第三和第四個任務是同步的,而第二個是異步的。但 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:
下方是這些屬性的解釋:async
Message The original ActionMessage that caused the invocation of this IResult.
調用 IResult 的 ActionMessage 源。ide
Source The FrameworkElement that triggered the execution of the Action.
觸發綁定 Action 的控件 FrameworkElementoop
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 時,我以爲仍是能夠接受的。
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 來完成的一些創意。必定要查看一下附加的示例程序。裏面還有一些其餘有趣的東西。