Tier-split Refactoring WinForms Applications with Volta

The Volta quickstart as well as Wes' blog post Volta: Redefining Web Development tier-split an application that runs entirely in the browser. The refactoring yields a distributed application comprising a browser-resident client and a service.

This transformation showcases two aspects of the Volta recompiler at the same time: architectural reshaping and retargeting. However they are orthogonal. Let me focus on the former and set the latter aside. I'll do that through tier-splitting a WinForms application.

In Visual Studio 2008 create a new project of type Volta Windows Forms Application (highlighted below):

image

Then with the forms designer put together a very simple form.

image

With a button labeled Compute Fn it should be clear where I'm headed :) Clear the value of of label2 in the properties pane; this is where the application will display the results.

Next add a class Computation to the project and add a method that computes Fibonacci numbers. Yes there are smarter ways of implementing it :)

namespace VoltaWindowsFormsApplication1
{
public class Computation
{

        public int Fibonacci(int n)
{
if (n == 0)
return 0;
else if (n == 1)
return 1;
else
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
}
}

Then add an instance variable of type Computation to the Form1 class and initialize it in the ctor:

public partial class Form1 : Form
{
    Computation computation;

    public Form1()
    {
        InitializeComponent();
        computation = new Computation();
    }
}

Finally wire the button such that clicking on it parses the input, validates it, invokes the computation, and displays the result:

    private void button1_Click(object sender, EventArgs e)
    {
        int n;

        if (int.TryParse(textBox1.Text, out n) && n >= 0)
        {
            label2.Text = string.Format("F({0})={1}",
                                         n,
                                         computation.Fibonacci(n).ToString());
        }
        else
            label2.Text = "Invalid input";
    }
}

So far so good, no voltage here. Let's flip the switch and plug in the rewriter.

After making sure that Enable Volta tiersplitter is checked on the project's property page, navigate or open the Computation class, right-click on the class and select Tier-split To Run At Server from the Refactor menu. The only thing that this refactoring does it to add a custom attribute aimed at the class (as well as add a few references to the project but those aren't source level changes):

namespace VoltaWindowsFormsApplication1
{
    [RunAt("Server")]
    public class Computation

Just like the sample from the Getting Started document, this attribute marks the distribution boundary. Volta uses the marker to split the application into two parts, with one part running as a service. Hit F5 and all the needed pieces unfold under the hood. Because the rewriting operates on MSIL Volta doesn't add any new source files or components to your project or solution. This makes it easy to experiment and fine-tune what runs on either side of the fence.

At this point I'd like to bring up a key point. There is an important difference between where the tier-splitting happens, and how it happens.

  • Volta makes the where part explicit: the developer marks the distribution boundary with a custom attribute, as shown above. Partitioning the functionality and communication are critical decisions when designing distributed systems. Volta doesn't attempt to make these decisions for you; they represent essential complexity.
  • Volta takes the how part out of sight: the rewriter encapsulates the class(es) marked to run at server within a service. It also arranges that the client classes call the service, and injects a marshalling layer between the two. Though required, these implementation details are orthogonal to the problem to be solved; they represent accidental complexity.

People building distributed systems have known for many years about the fallacy of making remote calls look like local calls, and thus setting a trap for the developers. We didn't ignore or forget this insight. Rather, Volta takes over the accidental complexity so Volta developers could focus on putting the distribution boundary at the right place.

It's easy to see that the tier-split WinForms application uses a service. Once the Form comes up double-click on the Volta WebServer's electric eel icon that appears in the notification area. Select the Log requests box and then compute a Fibonacci number. The POST appears on the server's log.

image

But for someone who grew up with the Smalltalk browser the debugger provides the best way of looking under the hood. Visual Studio has a first class debugger so let's put it to work. In the Form1 class set a breakpoint in the client code, before the call to Fibonacci:

image

Then in the Computation class set another breakpoint in the Fibonacci method:

image

Ensure that Visual Studio is in the Debug configuration and then hit F5. Enter either 0 or 1 into the text box to avoid having the debugger pop up as the computation recurses. Then click the form's button and examine the call stack. On the first breakpoint (in the form's event handler) the call comes from the client:

image

Resume execution and the second breakpoint (in the Fibonacci method of the Computation class) pops up the debugger. This time the call stack shows a server:

image 

At this point the Fibonacci calculator is a distributed application. The network is not involved when the application interacts with the service running in the development server. However this not the case beyond development, where the latencies and delays associated with going over the network are a fact of life. Under those circumstances synchronous method invocation becomes unsuitable.

I could make asynchronous calls using delegates. But that's another bit of accidental complexity lurking in the code. So instead let me rely on Volta to generate delegates and inject the BeginInvoke and EndInvoke pair in the code.

As Volta's architecture refactoring and retargeting are orthogonal asynchronous invocation is no different than explained in Step 7 in the quickstart, except one small idiosyncrasy due to to the UI thread (more on that shortly). In the Computation class add:

[Async]
public extern void Fibonacci(int n, Action<int> continuation);

If you haven't heard of CPS then think of the last argument as a callback, to be invoked with the result of the asynchronous computation upon its completion. Similar to [RunAtServer], [Async] is just a marker for the rewriter. The extern modifier makes the method visible to the IDE and C# compiler (so IntelliSense and type checking work as with the other methods) without requiring an implementation. Volta will generate this implementation and make it available in the final, rewritten program.

Finally modify the event handler to use the asynchronous method. To visualize the asynchronous call the code updates label2 prior to invoking the Fibonacci method, and then updates it again when the method completes. An integer around 37 takes enough milliseconds on my machine such that the result shows up a little after the F(37)= appears.

image image

Note that the following code uses a lambda form for the continuation: (f) => { ... } so that's where the => is coming from. In addition, the update happens through the Invoke method to avoid interfering with the UI thread. The argument for the Action ctor is a second lambda form: () => label2.Text = label2.Text + f.ToString()

private void button1_Click(object sender, EventArgs e)
{
    int n;

    if (int.TryParse(textBox1.Text, out n) && n >= 0)
    {
        var result = string.Format("F({0})=", n);
        computation.Fibonacci(n,
            (f) =>
            {
                label2.Invoke(new Action(
                    () =>
                    label2.Text = result + f.ToString()
                    ));
            });
    }
    else
        label2.Text = "Invalid input";
}

Let me bring up another important point. Here Volta hides the accidental complexity associated with asynchronous method invocation, just like it does with tier-splitting. Rather than spending time with BeginInvoke and EndInvoke, you focus on deciding what calls should stay synchronous and what calls should be asynchronous. In effect, Volta provides asynchronous methods through synchronous implementations (such as the initial Fibonacci method) paired with asynchronous declarations (such as attributed method).