Sammi Sinno

Development - Off The Beaten Path

A development blog that explores everything and puts an emphasis on lesser known community driven projects.

Home

Nancy - Super Simple View Engine

Monday, May 29, 2017

So what is the Super Simple View Engine? Well it would be difficult to describe it better than what is said in the Nancy Wiki:

The Super Simple View Engine, also known as SSVE, is a regex (the implementation uses regular expressions to perform substitutions) based view engine that was designed to support simple templating scenarios, so a lot of the features you see in other engines may not be available.

So what got me down this journey of using a view engine like SSVE? This blog actually!!

For basic things SSVE is very nice, its lightweight and works out of the box for Nancy based projects. No need to configure anything in your Web.config file or include any external packages; although, I started to run into situations where the basic functionality simply wasn't enough. For situations like this there is a very usefull interface called ISuperSimpleViewEngineMatcher.

When the Nancyfx application startup tasks run (by default the base Nancy bootstrapper takes care of this) It will look for all types that implement the ISuperSimpleViewEngineMatcher interface and register them with your IoC container. In which case you can make use of dependency injection and do some pretty neat things.

The situation in which I needed a bit more out of the SSVE is when I needed to render a hierarchical list in the sidebar for the categories. Before I go more in depth on how the ISuperSimpleViewEngineMatcher works I'll show you what my implementation looks like:

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using FastTrack.Content.DataModels;
using FastTrack.Domain.Managers;
using Nancy.ViewEngines.SuperSimpleViewEngine;
using NHibernate.Linq;

namespace FastTrack.Web.ViewEngineMatchers
{
    public class CategoryViewEngineMatcher : ISuperSimpleViewEngineMatcher
    {
        private readonly ISessionFactoryManager _sessionFactoryManager;

        /// <summary>
        ///   Compiled Regex for translation substitutions.
        /// </summary>
        private readonly Regex _categorySubstitutionsRegEx = new Regex(
                    @"@Category\.(?<TranslationKey>[a-zA-Z0-9-_]+);?",
                    RegexOptions.Compiled);

        public CategoryViewEngineMatcher(ISessionFactoryManager sessionFactoryManager)
        {
            _sessionFactoryManager = sessionFactoryManager;
        }

        public string Invoke(string content, dynamic model, IViewEngineHost host)
        {
            return _categorySubstitutionsRegEx.Replace(content,
                m =>
                {
                    // A match was found!
                    string translationResult;

                    // Get the translation 'key'.
                    var translationKey = m.Groups["TranslationKey"].Value;

                    List<BlogCategory> categories;
                    var sessionFactory = _sessionFactoryManager.GetSessionFactoryFor("FastTrack");
                    using (var session = sessionFactory.OpenSession())
                    {
                        categories = session.Query<BlogCategory>().ToList();
                    }

                    // Load the appropriate translation.  This could farm off to
                    // a ResourceManager for example.  The below implementation
                    // obviously isn't very useful and is just illustrative. :)
                    if (translationKey == "Menu")
                    {
                        var topLevel = categories.Where(x => !x.ParentIds.Any()).ToList();
                        translationResult = CategoryMenu(topLevel, categories);
                    }
                    else
                    {
                        // We didn't find any translation key matches so we will
                        // use the key itself.
                        translationResult = translationKey;
                    }

                    return translationResult;
                });
        }

        private string CategoryMenu(List<BlogCategory> categories, List<BlogCategory> allCategories)
        {
            var sb = new StringBuilder();

            sb.Append("<ul>");
            foreach (var category in categories)
            {
                sb.Append(GetCategoryItem(category, allCategories));
            }
            sb.Append("</ul>");

            return sb.ToString();
        }

        private string GetCategoryItem(BlogCategory dto, List<BlogCategory> allCategories)
        {
            var sb = new StringBuilder();
            sb.Append("<li>");
            sb.AppendFormat("<a href=\"/categories/{0}/1/\">", dto.SafeName);
            sb.Append(dto.Name);
            sb.Append("</a>");

            var children = allCategories.Where(x => x.ParentIds.Contains(dto.Id)).ToList();
            if (children.Any())
            {
                sb.Append(CategoryMenu(children, allCategories));
            }

            sb.Append("</li>");

            return sb.ToString();
        }
    }
}

And the magic line of code that outputs this string in the page is: Category.Menu.

You will have to put an @ character preceding "Category", I just wasn't able to because it was rendering the menu hehe.

The first thing I wanted to note is that any service that you inject when configuring your application container will be available when Nancy goes to setup your SSVE matchers. In this case I setup the SessionFactoryManager as a Singleton in the Autofact container and was easily able to inject it into my SSVE matcher.

Moving on to how the matcher actually works... Most important to note is the piece of regex towards the top, what it does it look for anything in your template that matches @Category (in this case) and replaces that with the result of the delegate in your .Replace function.

Since this is plain regex, you can approach this situation however you want, but in this case we set up one grouping construct to capture the substring and use it as a key to decide what to output onto the page.

After finding out which key was passed in I used a simple StringBuilder to generate HTML; although, I believe that the HtmlAgilityPack Nuget package would be an awesome tool to utilize when generating HTML in a situation like this.

And thats about it! This post gave me a great headstart, the one thing I wanted to note is that it says the SSVE Matchers are not automatically hooked up during service registration, but in my case it was... so I'm not sure if that has to do with the fact that this is a newer version of Nancy or not, but it definitely gets wired up when the IApplicationStartup tasks run.