IResult and IEnumerable<IResult> (Coroutines)

IResult is one of the most powerful features of Actions. Any action can return an instance of IResult or IEnumerable<IResult>. Doing so allows you to hand custom code to the runtime which will be executed upon completion of the action. In fact, the entire OutputPath binding is implemented as a single IResult which the framework executes by default. You can find this implementation in the framework class named DefaultResult.

IResult enables handling many difficult programming scenarios with ease, mainly asynchronous programming. Here's an example of what you can do:

public IEnumerable<IResult> Login(string username, string password)
{
    var authenticator = new AuthenticateResult(username, password);
    yield return authenticator;

    if(!authenticator.Succeeded)
    {
        yield return Show.MessageBox("The username or password provided is incorrect.", "Access Denied");
        yield break;
    }

    var result = new Result();
    var request = new GetUserSettings(username);

    yield return new ProcessQuery(request, result, "Logging In...");

    if (result.HasErrors)
    {
        var error = result.Errors.First();

        yield return Show.MessageBox(error.Message, "Error");
        yield break;
    }

    var response = result.GetResponse(request);

    if(response.Permissions == null || response.Permissions.Count < 1)
    {
        yield return Show.MessageBox("You do not have permission to access the dashboard.", "Access Denied");
        yield break;
    }

    _context.Permissions = response.Permissions;

    yield return Show.Child<IDashboard>().In<IShell>();
}

Note: "Show" is a static factory for instances of IResult. It is not included as part of the Caliburn v1.0 or 1.1, but it will be part of the ShellFramework in v2.0.

Each yield statement returns some instance of IResult which Caliburn executes. These results can execute synchronously or asynchronously. For example, the AuthenticateResult calls a web service asynchronously. This causes execution of the action to 'pause' until the asynchronous action completes. When the web service has returned its result, execition then begins on the line immediately following the yield. In fact, every yield in this action executes asynchronous code. Using this programming paradigm we can have very intricate asynchronous workflows that are easy to read and understand because they are written sequentially. This technique is know as Coroutines.

To create your own IResult simply implement the interface on your own class. This involves implementing the Execute method and being sure to raise the Completed event when your work is done. Here is an example of how the ProcessQuery result was written:

public class ProcessQuery : IResult
{
    private readonly Query _query;
    private readonly Result _result;
    private readonly string _waitMessage;

    public ProcessQuery(Query query, Result result, string waitMessage)
    {
        _query = query;
        _result = result;
        _waitMessage = waitMessage;
    }

    public ProcessQuery(Query query, Result result)
    {
        _query = query;
        _result = result;
    }

    public string WaitMessage
    {
        get { return _waitMessage; }
    }

    public Query Query
    {
        get { return _query; }
    }

    public Result Result
    {
        get { return _result; }
    }

    public void Execute(IRoutedMessageWithOutcome message, IInteractionNode handlingNode)
    {
        var service = ServiceLocator.Current.GetInstance<IServer>();

        if(!string.IsNullOrEmpty(_waitMessage))
            ServiceLocator.Current.GetInstance<ILoadScreen>().StartLoading(_waitMessage);

        service.ProcessQueryAsync(_query, result =>{
            _result.Add(result);

            if (!string.IsNullOrEmpty(_waitMessage))
                ServiceLocator.Current.GetInstance<ILoadScreen>().StopLoading();

            Completed(this, null);
        });
    }

    public event Action<IResult, Exception> Completed = delegate { };
}

This is just one example of the endless possibilities and was developed for a specific application where this type of interaction was common. Notice the signature of the Execute method. Caliburn will provide you with contextual information as well. The IRoutedMessageWithOutcome is the original message sent from the UI which caused the action to be executed and the IInteractionNode is the node within the UI that caught and handled the message. You can drill through the properties of these interfaces to discover much about the current scenario. Again, see the framework's DefaultResult for an example of this.

Note: Raising Completed with it's second parameter being an instance of Exception will cause IResult enumeration to immediately stop. The exception will then be thrown. However, there is a special exception, CancelResult, which can be sent. It only causes the cancellation of enumeration and does not get thrown as a normal exception would.

Note: Using IResult also aids in unit testing because the extremely complex interaction testing that would normally take place can be replaced with simple state-based testing by enumerating the results and checking their properties. (You don't need to call the Execute method during Action testing because you are not testing the IResults themselves, rather what results are returned and how they are configured. You should have separate unit tests for each type of IResult.)

Last edited May 5, 2010 at 8:36 PM by EisenbergEffect, version 18