Code Voyeur
RSS
Data Access Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net Rate My Snippet

An Unobtrusive IronPython ViewEngine for ASP.NET MVC

Using the WebForms view engine with ASP.NET MVC leads to a complex mix of markup and code.
<% for (var i = 0; i < 10; i++) {%>
<%= Html.RadioButton("Rating", i, new { id = "vote" + i })%>
<label for="<%= "vote" + i %>"><%= i%></label>
<%} %>
Other view engines such as Razor reduce the noise in the markup by removing some angle brackets and percents.
@for (var i = 0; i < 10; i++) {
@Html.RadioButton("Rating", i, new { id = "vote" + i })
<label for="vote" + @i>">@i</label>
}
While the Razor syntax (this sample has not been tested) is certainly cleaner, it is still cluttered when compared to the syntax used by the sample project in this article. The code below is all that will exist in the markup (HTML) file.
#{ratings}
The loop will not go away, but will instead be placed aside in a rule processing file. This approach is not unlike using XSLT. A template is applied to data, but the template processing and data are not stored together. Basically, what this template engine will do is find tokens in view files. If a rule is defined for those tokens in a “code beside” file, the rule is processed. Otherwise, the rule is assumed to be a key to one of MVC’s state bags. The code beside file is an IronPython script.

A simple IViewEngine implementation called PyViewEngine wires up the new view renderer. Partial view support is not yet implemented.

public class PyViewEngine : IViewEngine {
  ...
  public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) {
      return new ViewEngineResult(new PyView(), this);
  }
  ...
}
The PyView class implements IView and is responsible for the basic parsing of the template and for passing data to the templating engine (PyRunner).
public class PyView : IView {
    public void Render(ViewContext viewContext, System.IO.TextWriter writer) {
        ...        
    }
}
PyView’s Render method starts off by pulling the action and controller out of the RouteData. Those values are then used to find the name of the view file for the action.
string action = viewContext.RouteData.Values["action"] as string;
string controller = viewContext.RouteData.Values["controller"] as string;

string viewFile = string.Format(@"Views\{0}\{1}.html", controller, action);
The next lines in Render setup the full paths to the view file and a special init file that contains shared code for the templating engine.
string appPath = viewContext.HttpContext.Server.MapPath(viewContext.HttpContext.Request.ApplicationPath);
string pathToInitFile = Path.Combine(appPath, @"Views\Shared\lib.py");
string pathToViewFile = Path.Combine(appPath, viewFile);
With the file paths set, the private instance of a PyRunner is created.
PyRunner pyRunner = new PyRunner(viewContext, controller, action, pathToViewFile + ".py", pathToInitFile);
The rest of the Render method reads the contents of the view file. Each instance of a token of the form #{TokenName} is parsed and fed to PyRunner, where the real work is performed.
using (StreamReader reader = new StreamReader(pathToViewFile)) {
    string contents = reader.ReadToEnd();
    MatchCollection matches = Regex.Matches(contents, @"#{(\w+)}", RegexOptions.IgnoreCase | RegexOptions.Compiled);
    foreach (Match match in matches) {
        contents = Regex.Replace(contents, match.Value, pyRunner.RunRule(match.Groups[1].Value));
    }
    writer.Write(contents);
}
PyRunner has some boiler-plate DLR setup code. It also has a couple of private fields for keeping track of request details.
public class PyRunner {

    private static Dictionary<string, PyRules> _rules = new Dictionary<string, PyRules>();
    private static ScriptRuntimeSetup _setup = null;
    private static ScriptRuntime _runtime = null;

    private string _currentView = null;
    private string _currentController = null;
    private ViewContext _viewContext = null;

    static PyRunner() {
        _setup = ScriptRuntimeSetup.ReadConfiguration();
        _runtime = new ScriptRuntime(_setup);
    }
    ...
}
PyRunner’s constructor receives a ViewContext instance (from PyView in this case). It also takes the controller and action names along with paths to the Python files it will execute.
public PyRunner(ViewContext viewContext, string controllerName, string actionName, string codeBesideFile, string initFile = null) {

    _currentView = actionName;
    _currentController = controllerName;
    _viewContext = viewContext;

    ...
}
The static _rules dictionary is of type Dictionary<string, PyRules>. PyRules is a collection class for keeping track of the code blocks a view needs to know about. Each PyRules instance is keyed off of a set of token names with values that are code blocks. PyRunner will store a PyRules instance for each controller and action combination.
public class PyRules {

    private Dictionary<string, object> _codeBlocks = new Dictionary<string, object>(0);

    public Object this[string ruleName] {
        get { return _codeBlocks[ruleName]; }
        set { _codeBlocks[ruleName] = value; }
    }

    public int Count {
        get { return _codeBlocks.Count; }
    }

    public bool Contains(string ruleName) {
        return _codeBlocks.ContainsKey(ruleName);
    }       
}
The init file is executed to allow for sharing of code across templating files. Next, the view’s rules file is executed.
ScriptScope scope = _runtime.GetEngine("IronPython").CreateScope();
if (initFile != null) {
    _runtime.GetEngine("IronPython").ExecuteFile(initFile, scope);
}
_runtime.GetEngine("IronPython").ExecuteFile(codeBesideFile, scope);
The rules file should contain only function definitions. After it is executed, those functions are pulled out of the ScriptScope instance (above) and stored in the _rules dictionary.
var variables = scope.GetVariableNames().Where(s => !s.StartsWith("__") && !s.EndsWith("__"));

PyRules pyRules = new PyRules();
foreach (var variable in variables) {
    var rule = scope.GetVariable(variable);
    pyRules[variable] = rule;
}
_rules[getKey()] = pyRules;
The RunRule method takes a rule name and invokes that rule, passing it the current ViewContext as an argument.
public string RunRule(string ruleName) {
    if (_rules[getKey()].Contains(ruleName)) {
        return _runtime.Operations.Invoke(_rules[getKey()][ruleName], _viewContext) as string;
    }
    return (getValueFromBag(ruleName) ?? "").ToString();
}
The getKey() method simply concatenates the controller and action name to create a unique key.
private string getKey() {
    return _currentController + "::" + _currentView;
}
The getValueFromBag() method searches the available bags (ViewData, TempData and Session) for a value. The first found value is used.
private object getValueFromBag(string key) {
    return (_viewContext.ViewData[key] ?? _viewContext.TempData[key]) ?? _viewContext.HttpContext.Session[key];
}
The sample project demonstrates both simple and complex rules. For example, a simple rule defined in the Home/Index.html view looks like the following:
<h3>#{message}</h3>
In the Home/Index.html.py rule file a function named message is defined as follows:
def message(context):
    return "IronPython is good!"
In Books/Index.html, a token is placed where a listing of books should appear.
#{books}
In Books/Index.html.py, the rule for processing this token is defined.
def books(context):

    headers = ["Title", "Author", "ISBN"]
    props = ["Title", "Author.FullName", "ISBN"]
    return table(headers, context.ViewData.Model, props)
The table function is defined in lib.py, which is executed along with each action’s rule file.
from System.Text import *

def table(headers, rows, props):

    sb = StringBuilder()
    sb.Append("<table>")
    
    for header in headers:
        sb.AppendFormat("<th>{0}</th>", header)
    
    for row in rows:
        sb.Append("<tr>")        
        for prop in props:
            sb.AppendFormat("<td>{0}</td>", eval("row." + prop))
        sb.Append("</tr>")

    sb.Append("</table>")
    return sb.ToString()
This project shows how to move templating code out of view files and into companion, code beside files. The sample does not have support partial or HTML helpers, though it should be simple enough to add at some point. It also does not implement much caching or any type of efficient pre-compilation. But borrowing from jQuery and the goal of unobtrusive markup, it demonstrates how to have minimal impact on markup while allowing the full functionality of a templating engine.

Download Sample Project

Article Posted: Wednesday, August 04, 2010

Comments

Posted by Barbara on 3/21/2012 2:06:49 PM
It's spooky how celevr some ppl are. Thanks!

Leave a Comment

Shout it

Contact Code Voyeur about this article.