You really owe it to yourself watch Rob Eisenberg’s amazing MVVM talk and download the source code. There are so many neat things in this talk it’s hard to know where to start. Basically, he’s written a short, understandable, piece of code that shows how to develop Silverlight applications in a manner with which an ASP.NET MVC developer like myself can be comfortable. I really liked the “co-routine trick”, so I thought I would write something explaining it in greater depth.
The basic trick is to use yield return’s “co-routine like” execution form an IEnumerable sequence of what code to run on which thread. Each action, when it completes, sets up the execution of the next action. This is a bit disguised in the rest of the talk’s code, so I’m ripping it out to show it directly. It also more clear with consistent threading model, so I use Retlang fibers to follow the thread of execution. (You’d get other benefits as well, but this post is long enough as it is.)
First off, I’m going to outline the basic form of the trick, as close to Rob’s original as I can managed. Then there’s a short program that uses it. Finally, I’ll show you an alternative version that’s arguably even more elegant.
You’re Going To Like It
So, what does Rob’s IResult look like in Retlang?
public interface IResult {
void Execute();
IDisposingExecutor Fiber { get; }
}
So you got some code and a “thread” to run it on. The example code lower down has the obvious dumb implementation, rather than the Command/Query framework that’s in Rob’s code. Then the co-routine trick looks like this:
public static class CoroutineTrick { public static void Execute(this IEnumerable<IResult> results) { var enumerator = results.GetEnumerator(); Action nextAction = null; nextAction = () => { if (!enumerator.MoveNext()) { return; } var result = enumerator.Current; result.Fiber.Enqueue(() => { result.Execute(); nextAction(); }); }; nextAction(); } }
The assignment of nextAction to null is a compiler dodge which I recommend ignoring. Each call to nextAction advances the iterator* and then enqueues the action on the correct fiber. Then nextAction is called again on the same fiber as the previous action. (Rob achieves the this effect in the ResultEnumerator class.)
*It helps to remember that MoveNext needs to be called before accessing the first element.
The Demo
Here’s the rest of the code. It shows a form, moves the progress bar, does a couple of Thread.Sleeps and exits. As always, note that the UI is responsive the whole time.
class FormProgressReporter : Form {
private readonly ProgressBar progressBar;
public FormProgressReporter() {
progressBar = new ProgressBar();
SuspendLayout();
progressBar.Dock = DockStyle.Fill;
progressBar.Location = new System.Drawing.Point(0, 0);
progressBar.Name = "progressBar";
progressBar.Size = new System.Drawing.Size(292, 266);
progressBar.TabIndex = 0;
Controls.Add(progressBar);
Height = 75;
Width = 600;
ResumeLayout();
}
public void Report(int nodesProcessed, int nodesEncountered) {
Text = string.Format("{0}/{1}", nodesProcessed, nodesEncountered);
SuspendLayout();
progressBar.Maximum = nodesEncountered;
progressBar.Value = nodesProcessed;
ResumeLayout();
}
protected override void OnShown(EventArgs e) {
Actions().Execute();
}
IEnumerable<IResult> Actions() {
yield return Update(10, 100);
yield return Calculate(() => Thread.Sleep(2000));
var nodes = 20;
yield return Update(nodes, 100);
yield return Calculate(() => {
nodes = 75;
Thread.Sleep(2000);
});
yield return Update(nodes, 100);
yield return Calculate(() => {
nodes = 100;
Thread.Sleep(2000);
});
yield return Update(nodes, 100);
yield return Calculate(() => Thread.Sleep(1000));
yield return new Result(() => Hide(), Program.ui);
}
IResult Calculate(Action action) {
return new Result(action, Program.worker);
}
IResult Update(int nodesProcessed, int nodesEncountered) {
return new Result(() => this.Report(nodesProcessed, nodesEncountered), Program.ui);
}
}
public static class Program
{
internal static IFiber ui = null;
internal static IFiber worker = new PoolFiber();
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var form = new FormProgressReporter();
ui = new FormFiber(form, new BatchAndSingleExecutor());
ui.Start();
worker.Start();
form.ShowDialog();
}
}
class Result : IResult {
private readonly Action action;
private readonly IDisposingExecutor fiber;
public Result(Action action, IDisposingExecutor fiber) {
this.action = action;
this.fiber = fiber;
}
public void Execute() {
action();
}
public IDisposingExecutor Fiber {
get { return fiber; }
}
}
You’ll notice that I’m quite dependent upon static variables, which I hate. However, the original code also relies on this at the moment, and I wanted to produce something that looked as similar as possible, within the constraints of including the entire thing in a single blog post. The static dependency is fixable, although you’ll have to lose Rob’s implementation of AsResult via extension methods.
Using Sensible Defaults
The code above is a quick hack to demonstrate the idea, but once you understand it there’s even more that can be done. You could, for instance, modify the code to always execute the MoveNext on a specific fiber. This could actually be quite elegant: you could always assume code between “yield return”s was on the UI thread (or never). Here’s how it could look like that:
public static void Execute(this IEnumerable<Action> results,
IDisposingExecutor worker, IDisposingExecutor ui) { var enumerator = results.GetEnumerator(); Action nextAction = null; nextAction = () => { if (enumerator.MoveNext()) { var command = enumerator.Current; worker.Enqueue(() => { command(); ui.Enqueue(nextAction); }); } }; ui.Enqueue(nextAction); }
At the loss of some flexibility, we’ve now got something that runs yield return actions on worker threads, but everything else on the UI thread. (A single UI thread is a reasonable assumption most of the time.) The actions code would then read:
IEnumerable<Action> Actions2() { Report(10, 100); yield return () => Thread.Sleep(2000); var nodes = 20; Report(nodes, 100); yield return () => { nodes = 75; Thread.Sleep(2000); }; Report(nodes, 100); yield return () => { nodes = 100; Thread.Sleep(2000); }; Report(nodes, 100); yield return () => Thread.Sleep(1000); Hide(); }
This model looks even more like we’re using continuations, and we no longer need specialized types. We might still want them: Rob’s framework mixes this in with its command/query infrastructure. The beauty of this is: you don’t have to choose. There’s nothing stopping you supporting both solutions within a framework.
Happy Easter. 🙂