DSL Support in Brail, Part II
As covered in part one, the initial expansion of the "dsl" macro yields us the following code in Boo:
<?brail
dsl Html:
html:
head:
title:
text "Untitled page"
end
end
body:
p:
text "Hello World"
end
end
end
end
?>
dsl = Castle.MonoRail.Views.Brail.DslProvider(self)
dsl.Register(Castle.MonoRail.Views.Brail.HtmlExtension(OutputStream))
or, in C#:
DslProvider dsl = new Castle.MonoRail.Views.Brail.DslProvider(this);
dsl.Register(new Castle.MonoRail.Views.Brail.HtmlExtension(OutputStream));
The "this" and "OutputStream" references are what we're going tackle now. The Brail view engine for MonoRail functions loosely similar to traditional ASP.NET applications in that all ASPX inherit from System.Web.UI.Page; in Brail, all views inherit Castle.MonoRail.Views.Brail.BrailBase. Similarly to ASP.NET, whenever a view is rendered by MonoRail, the Brail view engine parses the view and generates a new assembly that contains a new object that derives from BrailBase. So, if our view were named "default.brail", our runtime-generated assembly would contain a class called "default_brail" that inherits BrailBase. When an MonoRail request is made and once MonoRail has determined what view(s) to render, the Run() method on the BrailBase, which is an abstract/must-override method and that is where the code in your View files is placed for execution. This is where the "dsl" macro is expanded.
Back to the "this" and "OutputStream" references...DSL support was designed to work similarly to VB's with block, allowing short-hand to invoke many methods on an object but only reference object by name once (for a laugh, read the documentation on the with...end with statement. Performance my butt...). This turns out to be quite a trick to accomplish, from the perspective of the compiler/parser; take this for example:
What's the context of the name and names variables? Remember, this code gets stuffed into the view's Run() method. Here's the resulting code created by the compiler within the Run() method:
<?brail
dsl Html:
for name in names:
p:
text "Hello ${name}"
end
end
end
?>
Done double-taking? You probably said, "why the hell does it look like its trying to get the value of properties called "name" and "names" off the dsl object?! That's gotta be a bug..." And you'd be right.
dsl = Castle.MonoRail.Views.Brail.DslProvider(self)
dsl.Register(Castle.MonoRail.Views.Brail.HtmlExtension(OutputStream))
for dsl.name in dsl.names:
dsl.p( { dsl.text("Hello ${name}") } )
end
Partially. "dsl.name" is a bug (found by Ayende) and has since been fixed, as you're actually declaring and referencing a new variable there all at one time. But, as you can also see, the DSL macro went thru the code and replaced all the other tag macros (like "p" to indicate a paragraph tag) as being methods on the dsl instance. The reason for that is duck-typing.
Under the covers, DslProvider implements Boo's IQuackFu interface allowing other .NET languages to work with the duck-typing semantics of the Boo language. Here, when a method is invoked on the DslProvider instance, Boo sees that DslProvider implements IQuackFu and invokes QuackInvoke with the method name and parameters that was called; in the case of "dsl.p(...)", "p" and the block of code are the parameters. The DslProvider keeps an IDictionary of all the methods that the referenced language implementation (HtmlExtension, in our case). When QuackInvoke is called on DslProvider, it tries to find a match give then the method name and number of parameters on the HtmlExtension object and then forwards the call onto it.
So what about the "dsl.names" reference? Simple answer: more duck-typing. Let's say your controller is written as so:
Without going into crazy detail, the Brail view engine essentially makes the items in the PropertyBag, Request, and other common and useful System.Web objects and collections class-level instances on the dynamically-generated BrailBase instance (that is a much-simplified explanation, but it does the job here). Duck-typing comes into play here because these "variables" are being treated as properties on the DslProvider instance. This time, QuackGet is called on DslProvider and a similar lookup is done, except this time against the BrailBase instance instead of the the language implementation.
public class HomeController : SmartDispatcherController
{
public void Default()
{
PropertyBag["names"] = new string[] { "Ayende", "Harris" };
}
}
So what's "OutputStream"? OutputStream is a property on BrailBase. Go ahead. Ask. Why isn't the generated code:
dsl = Castle.MonoRail.Views.Brail.DslProvider(self)
dsl.Register(Castle.MonoRail.Views.Brail.HtmlExtension(dsl.OutputStream))
Because, at this point, the macro is generating code just before the "with...end with", so we don't need to resolve the "OutputStream" reference using duck-typing. The "OutputStream" referenced is passed to the HtmlExtension object because the HtmlExtension object is what's actually responsible for "Response.Write" calls to give you your precious markup.
The idea for using the "with...end with" style seemed appropriate here as it provides, IMHO, more readable code instead of a lot of nested dsl.somethings( { dsl.moresomethings( { dsl.even_more_somethings( {...} ) } ) } ) when needing to produce an extensive amount of markup. The implementation of the "with...end with" design was taken by an example on the Boo project of how to do just that and can be found in the project's repository.
All that for pretty syntax.
Next time, we'll cover current ideas, status, and any other features that may/not be on the horizon.