A Using Extension for Spark View Engine

We’ve recently started using Spark View Engine for some ASP.NET MVC projects here at Logos. To me, the syntax feels cleaner than ASP.NET views, probably because it’s more markup and less embedded code. One thing that bugged me was that in order to use MVC form helpers I had to go back to embedding code.

    # using (Html.BeginForm()) {
        Email: <input type="text" name="email" />
    # }

Some searching of the Internet revealed I wasn’t the only one who found that unpalatable. Unfortunately, the only suggested solution, using Spark’s completely undocumented bindings feature, doesn’t actually work. While reading the Spark code (man, I love open source) I found that there is an extension mechanism. I read some more code and figured out how I could use it to make a “using” extension that would allow me to avoid embedding code when I want to use using.

What I want is an element like this:

    <logos:using form="Html.BeginForm()">
        Email: <input type="text" name="email" />
    </logos:using>

When the Spark compiler encounters a logos:using element node, it should replace that node with a statement node that generates the correct “using” C# code and begin a code block. When the compiler encounters the end element, it should close the block. The following extension accomplishes this.

UsingExtension.cs:

    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Spark;
    using Spark.Compiler;
    using Spark.Compiler.ChunkVisitors;
    using Spark.Compiler.NodeVisitors;
    using Spark.Parser.Markup;
    namespace Logos.Utility.Web
    {
        public class
    UsingExtension : ISparkExtension
        {
        public
    UsingExtension(ElementNode node)
        {
        m_node = node;
        }
        public void
    VisitNode(INodeVisitor visitor, IList<Node> body, IList<Chunk> chunks)
        {
        if (visitor is
    ChunkBuilderVisitor)
        {
        // build a list of

    expressions from the node attributes
        List<string>
    expressions = m_node.Attributes
        .Select(attribute =>
    string.Format("var {0} = {1}", attribute.Name, attribute.Value))
        .ToList();
        // transform this

    node and its body into a new list of nodes to visit
        List<Node> newNodes
    = new List<Node>();
    newNodes.AddRange(expressions
        .Select(exp =>
    (Node) new StatementNode(string.Format("using ({0}) {{", exp))
        {
        OriginalNode =
    m_node
        }));
    newNodes.AddRange(body);
    newNodes.AddRange(Enumerable.Repeat((Node) new StatementNode("}"),
    expressions.Count));
        // visit the new

    nodes normally
    visitor.Accept(newNodes);
        // keep the output

    chunks to render later
        m_chunks = chunks;
        }
        }
        public void
    VisitChunk(IChunkVisitor visitor, OutputLocation location, IList<Chunk> body,
    StringBuilder output)
        {
        if (location ==
    OutputLocation.RenderMethod)
    visitor.Accept(m_chunks);
        }
        private readonly
    ElementNode m_node;
        private IList<Chunk>
    m_chunks;
        }
    }

Now we just need to tell the SparkViewEngine how to use our extension. To do that we need a factory.

LogosSparkExtensionFactory.cs:

    using Spark;
    using Spark.Compiler.NodeVisitors;
    using Spark.Parser.Markup;
    namespace Logos.Utility.Web
    {
        public class
    LogosSparkExtensionFactory : ISparkExtensionFactory
        {
        public const string
    Prefix = "logos";
        public
    ISparkExtension CreateExtension(VisitorContext context, ElementNode node)
        {
        string name =
    node.Name;
        if
    (!name.StartsWith(Prefix + ":"))
        return null;
        name =
    name.Substring(Prefix.Length + 1);
        if (name == "using")
        return new
    UsingExtension(node);
        return null;
        }
        }
    }

This is what my Application_Start() looks like:

        protected void
    Application_Start()
        {
    AreaRegistration.RegisterAllAreas();
        SparkViewFactory
    sparkViewFactory = new SparkViewFactory
        {
        Engine =
        {
        ExtensionFactory =
    new LogosSparkExtensionFactory()
        }
        };
    ViewEngines.Engines.Add(sparkViewFactory);
    RegisterRoutes(RouteTable.Routes);
        }

What I’ve presented here is actually a simplified version of what I ended up with. I extended UsingExtension with FormExtension, which has attributes for setting up the form and results in a view file that looks even more like markup and less like code. I also created some other extensions for other helpers.

    <logos:form controller="User" action="Update" routeValues="new { id = 123 }">
        Email:
    <logos:textbox for="Email" />
    </logos:form>

Now we have a small library of commonly used custom controls, of a sort, to help keep our presentation layer looking presentable.

Posted by Dave Dunkin on May 04, 2010