Alternative view engine for ASP.NET Core: Passing data from controller

elfalem

elfalem

Posted on January 29, 2021

Alternative view engine for ASP.NET Core: Passing data from controller

Part III in this series looked at how view templates are dynamically compiled including the processing of simple expressions (like 1 + 1) embedded in the templates. However, view templates are useful when we can pass values to them to be shown in the variable areas of the template. In this final post of the series, we'll demonstrate how values added to the ViewData in the controller can be passed to the view template and rendered in the final output.

In the Razor view engine, accessing a value from ViewData looks like this:

Hello @@ViewData["name"]
Enter fullscreen mode Exit fullscreen mode

For our mustache syntax, we want a more readable syntax like this:

Hello {{name}}
Enter fullscreen mode Exit fullscreen mode

For the above to work name must exist as some sort of variable in the compiled template and have an assigned value.

You can find the full contents of the source files discussed below on this GitHub gist.

Identify Replaceable Values

The first step is knowing all the replaceable values in the view template. Since we allow expressions in our mustache syntax, we need a way to parse C# expressions and identify the variables. As we are already compiling the view template, we can use diagnostic messages from the compilation process to indirectly obtain the list of replaceable values.

In StacheTemplateCompiler.cs, add the following method:

private static List<string> GetMissingProperties(CSharpCompilation compilation)
{
  var missingProperties = compilation.GetDiagnostics().Where(d => d.Id.Equals("CS0103"))
            .Select(d => d.GetMessage()).Distinct()
            .Select(m => m.Replace("The name '", string.Empty)
            .Replace("' does not exist in the current context", string.Empty));

  return missingProperties.ToList();
}
Enter fullscreen mode Exit fullscreen mode

We're using the Roslyn compiler for the heavy lifting. Any names it reports as not existing in the current context are replaceable values in the view template.

Adjust Compiled Template

Once we know this list, we can create the class document a second time and declare the replaceable values as properties. In StacheTemplateCompiler.cs, insert the following into the Compile() method right before loading the assembly:

var missingProperties = GetMissingProperties(compilation);
if(missingProperties.Any())
{
    var secondClassDocument = CreateClassDocument(parsedResult, templateClassName, missingProperties);
    var secondSyntaxTree = CSharpSyntaxTree.ParseText(secondClassDocument);
    compilation = compilation.ReplaceSyntaxTree(syntaxTree, secondSyntaxTree);
}
Enter fullscreen mode Exit fullscreen mode

Additionally modify CreateClassDocument() to accept a list of strings as the last paramater named properties (optional, with default value of null). Insert the following code right after the creation of the StringBuilder object.

if(properties != null)
{
    foreach(var property in properties){
          result.AppendLine($"public dynamic {property} {{get; set;}}");
    }
}
Enter fullscreen mode Exit fullscreen mode

We declare each replaceable value as a dynamic property. Although we have been using strings in the examples, values sent through ViewData from the controller could have other types. Therefore treating it as a dynamic property gives us the flexibility to accept all types.

To use the dynamic keyword, we need to include references to additional assemblies when creating the compilation. Update the beginning of the CreateCompilation() method as follows:

var assemblyLocations = new List<string>{
    // system runtime assembly
    typeof(object).Assembly.Location,
    // the following are needed for dynamic keyword support
    Assembly.Load(new AssemblyName("System.Linq.Expressions")).Location,
    Assembly.Load(new AssemblyName("Microsoft.CSharp")).Location,
    Assembly.Load(new AssemblyName("System.Runtime")).Location,
    Assembly.Load(new AssemblyName("netstandard")).Location
};

var references = assemblyLocations.Select(location => MetadataReference.CreateFromFile(location));
Enter fullscreen mode Exit fullscreen mode

Pass Values to View Template

At this point, we have code to successfully complile a view template while including C# properties for each replaceable value in the template. The last step is to set the properties with the the actual values provided from ViewData when rendering the view template.

In StacheView.cs, update the RenderAsync() method to set the properties using Reflection before invoking the Execute() method on the compiled instance:

var properties = instance.GetType().GetRuntimeProperties()
  .Where((property) =>
    {
      return property.GetIndexParameters().Length == 0 &&
          property.SetMethod != null &&
          !property.SetMethod.IsStatic;
});

foreach(var prop in properties){
    if(context.ViewData.ContainsKey(prop.Name)){
        prop.SetValue(instance, context.ViewData[prop.Name]);
    }
}
Enter fullscreen mode Exit fullscreen mode

To test the changes, we will modify the Bar action in HomeController.cs we have been using to the following:

public IActionResult Bar(){
    ViewData["name"] = "Jonathan";

    return View();
}
Enter fullscreen mode Exit fullscreen mode

And modify the view template Bar.stache as follows:

Hello {{name}}, your name has {{name.Length}} characters.
Enter fullscreen mode Exit fullscreen mode

If you now run the application (dotnet run) and navigate to https://localhost:5001/home/bar, you should see:

Hello Jonathan, your name has 8 characters.
Enter fullscreen mode Exit fullscreen mode

We have successfuly passed a value from the controller to the view template.

Summary

In this series, we created an alternative view engine with its own template syntax. We parsed the template, created a compilation, and used it to render values supplied by the controller. Hopefully this illustrated the modular nature of ASP.NET and its ability to accept third party view engines.

If you're interested in other alternatives, this Stack Overflow answer compares various view engines. Also check out NVue, my experimental attempt at a view engine based on the Vue.js template syntax.

💖 💪 🙅 🚩
elfalem
elfalem

Posted on January 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related