IResult and Coroutines

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.

using System.Collections.Generic;
using System.ComponentModel.Composition;

[Export(typeof(ScreenOneViewModel))]
public class ScreenOneViewModel
{
public IEnumerable<IResult> GoForward()
{
yield return new ShowScreen("ExternalScreen");
}
}

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:

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:

using System;
using System.Windows;
using System.Windows.Controls;

{

{
this.message = message;
}

{
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)
{
}

public static IResult Hide()
{
}
}

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:

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.ReflectionModel;
using System.Linq;

{
static readonly Dictionary<string, DeploymentCatalog> Catalogs = new Dictionary<string, DeploymentCatalog>();

[Import]
public AggregateCatalog Catalog { get; set; }

{
uri = relativeUri;
}

public void Execute(CoroutineExecutionContext context)
{
DeploymentCatalog catalog;

if(Catalogs.TryGetValue(uri, out catalog))
Completed(this, new ResultCompletionEventArgs());
else
{
catalog = new DeploymentCatalog(uri);
if(e.Error == null)
{
Catalogs[uri] = catalog;
catalog.Parts
.Select(part => ReflectionModelServices.GetPartType(part).Value.Assembly)
.Where(assembly => !AssemblySource.Instance.Contains(assembly))
}

Completed(this, new ResultCompletionEventArgs {
Error = e.Error,
WasCancelled = false
});
};

}
}

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:

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.

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:

using System;
using System.ComponentModel.Composition;

public class ShowScreen : IResult
{

[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.

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.

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.

posted @ 2015-10-22 11:05  三台  阅读(876)  评论(0编辑  收藏  举报