Reading Code, Spark’s Once Attribute
For those who don’t know what Spark is… Spark is an open source view engine for Castle’s MonoRail Project (version 2.0 just recently released!) and ASP.NET MVC. The creator of Spark, Louis DeJardin, came up with the project in a comment left on a Phil Haack blog post amidst people complaining about the “tag soup” in the default view engine.
There’s a handy feature of Spark that allows you to specify a block of code that is only output one time to the overall view. This is especially nice when you have small partial files (similar to user controls if you prefer that term) that add some functionality that includes a JavaScript file. The Spark ‘once’ attribute allows you to name an include that will only be rendered once per name.
1: <script type="text/javascript" src="jquery-lightbox.js" once="lightbox"></script>
2: <script type="text/javascript">
3: // ... do something
4: </script>
</div> </div>
Later on, possibly in another partial, you might need this same functionality again. The second time around, you’re only going to render that script tag if it hasn’t yet been written out.
This seemingly simple implementation in a view engine was interesting to me in that I was curious to find out how it was implemented. If it was easy, why wouldn’t something like this be in the default WebForms view engine?
I thought I’d take a look by browsing through the Spark code…
How Spark Works
In very simple terms, the Spark view engine is a collection of TextWriters under a string parser; strings are read by the parser and turned into compiled code. Anytime you use a named content section, you’re essentially naming a TextWriter to which all that content under that name will be written. (It really is a Dictionary of string keys for TextWriter values.)
When the view is parsed, Spark uses the Visitor Pattern to handle each node that is contained within the view. Below is the implementation of IsSpecialAttribute by the class OnceAttributeVisitor. The visitor pattern is implemented in Spark in that there are different kinds of nodes that inherit from “Node”. Some of these classes need to tell their consumers whether or not they are “special”. Spark visits all nodes with 11 different Visitors, we’ll be looking at the OnceAttributeVisitor.
1: protected override bool IsSpecialAttribute(ElementNode element, AttributeNode attr)
2: {
3: var eltName = NameUtility.GetName(element.Name);
4: if (eltName == "test" || eltName == "if" || eltName == "elseif" || eltName == "else")
5: return false;
6:
7: if (Context.Namespaces == NamespacesType.Unqualified)
8: return attr.Name == "once";
9:
10: if (attr.Namespace != Constants.Namespace)
11: return false;
12:
13: var nqName = NameUtility.GetName(attr.Name);
14: return nqName == "once";
15: }
</div> </div>
After the code is parsed successfully, Spark will generate code from this translated view to be compiled. Any element that contains the ‘once’ attribute, like the script tag in the first code block, will create a call to a “Once()” function, passing in the value of the once attribute. (“lightbox” in our example above)
1: case ConditionalType.Once:
2: {
3: CodeIndent(chunk)
4: .Write("if (Once(")
5: .WriteCode(chunk.Condition)
6: .WriteLine("))");
7: }
</div> </div>
When that code is compiled, it’s turned into a derived type of SparkViewBase, which, implements the Once method. The element that contains the ‘once’ attribute is only rendered if the Once method below returns true.
1: public bool Once(object flag)
2: {
3: var flagString = Convert.ToString(flag);
4: if (SparkViewContext.OnceTable.ContainsKey(flagString))
5: return false;
6:
7: SparkViewContext.OnceTable.Add(flagString, null);
8: return true;
9: }
</div> </div>
The underlying type of OnceTable is Dictionary<string, string>, basically, we’ll allow the output a string value if the string key hasn’t already been registered in the table. The first element that is added to the OnceTable “wins” and will be the only output for that given key of all the elements that contain the same value in their ‘once’ attribute. All subsequent occurrences with the same attribute value will be ignored.