Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Razor

Razor 2.0 template engine, supporting layouts

4.86/5 (38 votes)
25 Jan 2013Ms-PL13 min read 171.7K   1.9K  
Razor 2.0 template engine that works outside MVC and that supports layouts (masterpages) and _ViewStart, like ASP.NET MVC.

Introduction 

I moved the code base to GitHub. For getting the latest updates please visit RazorMachine at GitHub.  

There is a RazorMachine NuGet installer package available as well. To install RazorMachine using NuGet, run the following command in the Package Manager Console:

PM> Install-Package RazorMachine  

I assume that you know about the ASP.NET Razor view engine and its ability to use it stand alone for producing any text content. Why else would you be here reading this article? So I will not tell (too much) about all nice Razor features and about how Razor works. Instead I will point out a way to use layouts (masterpages) with the Razor view engine outside MVC. At this time you will find several frameworks that offer Razor template execution outside the MVC environment like Razor TemplateEngine at CodePlex. I did not find any of these supporting layouts in a simple way. And none of these use Razor 2.0. 

Anywhere in this article where I mention "MVC", I refer at the ASP.NET MVC framework and not at MVC in general. Anywhere I mention "content" I mean template content together with its embedded script, markup, etc.

This small framework offers a Razor 2.0 (part of MVC 4) based template engine that supports layouts and other features more or less the same way as MVC does support these features. The framework helps you when you want to use Razor to render reports, e-mails, source code, HTML, or whatever, outside the MVC environment, especially when you need to be able to merge specific content with layouts. I have kept to the MVC conventions with regard to template locations, namings, and template lookup as much as possible making it as easy as possible to start using this template engine if you are an MVC developer. The implementation "feels" like MVC and the default configuration allows you to use intellisense inside Visual Studio while editing your Razor templates. Since this framework uses Razor 2.0 (part of MVC 4) it offers you Razor 2.0 features like root operator replacement ("~/") and conditional HTML attributes, i.e., attributes that are not being rendered if their corresponding values evaluate to null or empty.

In terms of HTML I implemented the basic HTML features that are provided by Razor 2.0 and I implemented optional HTML encoding. With "optional" I mean that HTML encoding can be turned off completely which is convenient for e.g. code generators.

I did not include additional HTML helper features. Still you can extend the default template base class and add such features by yourself.

Using the code

To get a general picture I think the best way to achieve that is to dive straight away into some examples. You will find these examples at the source code (project Xipton.Razor.Example) as well.

When you start to use this framework then the most important class is the RazorMachine. The following examples (included at the examples project as well) use a local instance of RazorMachine. Please note that normally for performance reasons at any application you would create a singleton instance of RazorMachine (because of the inner instance bound JIT created type caching) and use that particular instance for all template executions.

Example 1 - Executing a template

C#
RazorMachine rm = new RazorMachine();
ITemplate template = rm.ExecuteContent("Razor says: Hello @Model.FirstName @Model.LastName", 
  new {FirstName="John", LastName="Smith"});
Console.WriteLine(template.Result);

Example 2 - Executing a template using a layout

C#
RazorMachine rm = new RazorMachine();
rm.RegisterTemplate("~/shared/_layout.cshtml", 
   "BEGIN TEMPLATE \r\n @RenderBody() \r\nEND TEMPLATE");
ITemplate template = rm.ExecuteContent("@{Layout=\"_layout\";} Razor says: Hello @Model.FirstName @Model.LastName", 
   new {FirstName="John", LastName="Smith"});
Console.WriteLine(template); // template.ToString() evaluates to template.Result

Example 3 - Executing a template using a layout and _viewStart

C#
RazorMachine rm = new RazorMachine();
rm.RegisterTemplate("~/shared/_layout.cshtml","BEGIN TEMPLATE \r\n @RenderBody() \r\nEND TEMPLATE"); 
rm.RegisterTemplate("~/_viewstart.cshtml","@{Layout=\"_layout\";}");
ITemplate template = rm.ExecuteContent("Razor says: Hello @Model.FirstName @Model.LastName", 
   new {FirstName="John", LastName="Smith"});
Console.WriteLine(template); // same result as example 2

Example 4 - Executing a template by a virtual path using a layout and _viewStart

C#
RazorMachine rm = new RazorMachine();

rm.RegisterTemplate("~/shared/_layout.cshtml","BEGIN TEMPLATE \r\n @RenderBody() \r\nEND TEMPLATE"); 
rm.RegisterTemplate("~/_viewstart.cshtml","@{Layout=\"_layout\";}");
rm.RegisterTemplate("~/simpleTemplate.cshtml","Razor says: Hello @Model.FirstName @Model.LastName"); 

ITemplate template = rm.ExecuteUrl("~/simpleTemplate.cshtml", 
   new {FirstName="John", LastName="Smith"});
Console.WriteLine(template); // same result as example 2

Example 5 - Returning information from anywhere (even from a layout) to the caller using the ViewBag

C#
rm.RegisterTemplate("~/shared/_layout.cshtml", "@{ViewBag.PiValue=3.1415927;}");
rm.RegisterTemplate("~/_viewstart.cshtml", "@{Layout=\"_layout\";}");
ITemplate template = rm.ExecuteContent("Anything");
Console.WriteLine(template.ViewBag.PiValue); // => writes 3.1415927

Example 6 - Adding information from anywhere to a predefined ViewBag and returning it to the caller

C#
RazorMachine rm = new RazorMachine();
rm.RegisterTemplate("~/shared/_layout.cshtml", "@{ViewBag.Values.Add(3.1415927);}");
rm.RegisterTemplate("~/_viewstart.cshtml", "@{Layout=\"_layout\";}");
ITemplate template = rm.ExecuteContent("Anything",
   viewbag:new {Values = new List<double>{0,1,2});
Console.WriteLine(template.ViewBag.Values[3]); // => writes 3.1415927

Example 7 - Show generated code

C#
RazorMachine rm = new RazorMachine(includeGeneratedSourceCode:true);
rm.RegisterTemplate("~/shared/_layout.cshtml", 
   "BEGIN TEMPLATE \r\n @RenderBody() \r\nEND TEMPLATE");
rm.RegisterTemplate("~/_viewstart.cshtml", 
   "@{Layout=\"_layout\";}");
ITemplate template = rm.ExecuteContent("Razor says: Hello @Model.FirstName @Model.LastName", 
   new { FirstName = "John", LastName = "Smith" });
Console.WriteLine(template); // writes output result
Console.WriteLine(template.GeneratedSourceCode); // writes generated source for template
Console.WriteLine(template.Childs[0].GeneratedSourceCode); // writes generated source for layout

Example 8 - HTML encoding

C#
RazorMachine rm = new RazorMachine();
// not encoded since all output is literal content. Literal content is never encoded.
Console.WriteLine(rm.ExecuteContent("Tom & Jerry").Result);

// encoded since the content is written as a string value
// and by default HtmlEncode is on
Console.WriteLine(rm.ExecuteContent("@Model.Text", 
   new {Text="Tom & Jerry"}).Result);

// not encoded since content is a written as a raw string
Console.WriteLine(rm.ExecuteContent("@Raw(Model.Text)", 
   new { Text = "Tom & Jerry" }).Result);

// not encoded since HtmlEncoding is turend off in code
Console.WriteLine(rm.ExecuteContent("@{HtmlEncode=false;} @Model.Text", 
   new { Text = "Tom & Jerry" }).Result);

rm = new RazorMachine(htmlEncode: false);
// not encoded since now html encoding if off by default, still you can set it on in code
Console.WriteLine(rm.ExecuteContent("@Model.Text", 
   new { Text = "Tom & Jerry" }).Result);

Example 9 - Root operator is resolved directly

C#
RazorMachine rm = new RazorMachine(rootOperatorPath:"/MyAppName");
rm.RegisterTemplate("~/MyTemplate",
   "<a href='~/SomeLink'>Some Link</a>");
var template = rm.ExecuteUrl("/MyAppName/MyTemplate");
// same result as:
template = rm.ExecuteUrl("~/MyTemplate");
Console.WriteLine(template); // writes: <a href=/MyAppName/SomeLink>Some Link</a>

Example 10 - Razor 2: Attributes with null values are not rendered

C#
RazorMachine rm = new RazorMachine();
var template = rm.ExecuteContent(@"<a href='~/SomeLink' 
    data-brand='@Model.Brand' 
    data-not-rendered='@Model.NullValue'>Some Link</a>", new {Brand="Toyota",NullValue=(string)null});
Console.WriteLine(template); // writes: <a href=/SomeLink data-brand='Toyota'>Some Link</a>

Assuming you know MVC I think the examples are rather obvious. As you see most examples "feel" like MVC.

Diving deeper

Basically the RazorMachine takes a virtual path as a template locator. Still you can execute template content directly, as you saw at the examples. The template execute methods at RazorMachine look like:

C#
public virtual ITemplate ExecuteUrl(
    string templateVirtualPath, // any virtual path as the resource locator, 
                                // absolute or with virtual root operator
    object model = null, // though optional its presence is obvious
    object viewbag = null, // you may pass an initialized ViewBag
    bool skipLayout = false, // you may force to skip any possibly layout
    bool throwExceptionOnVirtualPathNotFound = true) // if false then null is returned 
                                                     // if the virtual path was not found
public virtual ITemplate ExecuteContent(
    string templateContent, // template script itself, internally managed by a generated url
    object model = null, 
    object viewbag = null, 
    bool skipLayout = false)

There is an additional convenience execute method at RazorMachine, named Execute, that forwards the execution to either ExecuteUrl or ExecuteContent. The signature looks like:

C#
public virtual ITemplate Execute(
string templateVirtualPathOrContent, // if it starts with a '/' or a '~' => it is a path
object model = null, 
object viewbag = null, 
bool skipLayout = false, 
bool throwExceptionOnVirtualPathNotFound = true) // only holds with a path argument

The execute methods return an ITemplate instance. The interface looks like:

C#
public interface ITemplate {

    #region MVC alike
    string Layout { get; set; }
    dynamic Model { get; }
    dynamic ViewBag { get; }

    // Returns the Parents's result as a LiteralString which should not be encoded again.
    LiteralString RenderBody();

    // Returns the given virtual path result as a LiteralString which should not be encoded again.
    // You may skip the child's layout using the parameter skipLayout which can be handy 
    // when rendering template partials (controls)
    LiteralString RenderPage(string virtualPath, object model = null, bool skipLayout = false);

    // Returns any Parents's section as a LiteralString which should not be encoded again.
    LiteralString RenderSection(string sectionName, bool required = false);

    // returns true if any parent defines a section by the given sectionName
    bool IsSectionDefined(string sectionName);
    #endregion

    // If set true then the Write method HTML encodes the output. 
    // If set false never any HTML encoding is performed (at this template)
    // The default setting can be configured.
    bool HtmlEncode { get; set; }

    // If generated source code must be included (needs to be configured)
    // then that source code can be accessed by this property
    // for debug purposes
    string GeneratedSourceCode { get; }

    // If this template is rendered by another template 
    // (this instance is a layout or control) then that other template is 
    // registered as the Parent and can be accessed during execution time 
    ITemplate Parent { get; }

    // Any layout and all controls are registered as childs 
    // The child list should not be accessed during
    // template execution, but only after complete execution, because the child list
    // is built during excution  
    IList<ITemplate> Childs { get; }
   
    // Returns the Root parent, or this instance if no Parent exists.
    ITemplate RootOrSelf { get; }
    
    // VirtualLocation is the virtual path without the template name
    // and can be used as a starting point for building virtual pathes
    VirtualPathBuilder VirtualLocation { get; }

    // The actual virtual path that resolved this template
    string VirtualPath { get; }

    // Context gives access to the template factory and to the razor configuration
    RazorContext RazorContext { get; }

    // The rendered result
    String Result { get; }

    // Writes output to the output StringBuilder. It encodes value.ToString() 
    // if HtmlEncode is true, unless value is a LiteralString instance.
    void Write(object value);

    // Writes output to the output StringBuilder, never encoded
    void WriteLiteral(object value);

    // Ensures the value to be written as a raw string. 
    // Since it returns a (literal) string type you can invoke it 
    // as @Raw("a & b"), so that you do not need a code block
    LiteralString Raw(string value);

}

Note that the Xipton.Razor.LiteralString represents a string that must not be encoded (again), like a MvcHtmlString.

Internally the framework relies on content providers that are able to resolve single template content (i.e. template scripts) by a virtual path. You initialize content providers for each separate template resource type. This framework contains content providers for files, embedded resources and in memory content. You can add any additional custom content provider as long as it implements IContentProvider.

On top of the content providers is the ContentManager. The content manager decides which content provider will respond to any virtual request. You could see the ContentManager as a router. But because its main task is to assemble content and to provide it to the TemplateFactory I decided to call it the ContentManager

The ContentManager delivers the assembled template script to the TemplateFactory. The template factory's task is to create template instances. It embeds both the Razor and the CodeDom compiler and it internally manages the full process of compiling template scripts into .Net types. It applies JIT compilation. So any first template request results into a compilation of script into a template type. Any following same request reuses the compiled template type. Any request always returns a fresh new template instance.

The following class diagram shows the most important classes and methods.

Image 1

Working with template files

for the sake of simplicity the examples 1-10 at the top of this article register templates in memory. The convenience method RegisterTemplate at RazorMachine forwards the template registration to the MemoryContentProvider.

Most probably you want to create template files, at your application folder (or application subfolder ./Views which is the MVC standard). Within your Visual Studio's project that could be look like:

Image 2

Now you can execute Products.cshtml by the virtual path "~/Reports/Products.cshtml" (or "~/Reports/Products" since you always can omit the default extension).

C#
RazorMachine rm = new RazorMachine();
ITemplate template = rm.ExecuteUrl("~/Reports/Products", new MyProductList());
The FileContentProvider will find the files by the given virtual path and eventually will provide the corresponding content to the template factory. Note that you must set all your template property fields named "Copy to output directory" to the values "Copy always" or "Copy if newer".

Configuration

How can the framework be initialized and configured? To get straight to that point the default configuration looks like:

XML
<configuration>
  
  <configSections>
      <section name="xipton.razor.config" 
          type="Xipton.Razor.Config.XmlConfigurationSection, Xipton.Razor" />
  </configSections>

  <xipton.razor.config>
      <xipton.razor>

        <rootOperator path="/" /> <!-- same as application name at Asp.Net -->

      <!-- if includeGeneratedSourceCode==true the GeneratedSourceCode can be found at ITemplate.GeneratedSourceCode  -->
      <!-- if htmlEncode==true all strings written by the template method Write(string) are HTML encoded by default. 
           Note: WriteLiteral(string) will never encode any output -->
      <!-- language is C#, VB, or a customized language type name  -->
        <templates
          baseType="Xipton.Razor.TemplateBase"
          language="C#" 
          defaultExtension=".cshtml"
          autoIncludeName="_ViewStart"
          sharedLocation="~/Shared"
          includeGeneratedSourceCode="false" 
          htmlEncode="true" 
        />

        <references>
          <clear/>
          <add reference="mscorlib.dll"/>
          <add reference="system.dll"/>
          <add reference="system.core.dll"/>
          <add reference="microsoft.csharp.dll"/>
        <!-- all *.dll assembly references at the execution context are referenced by the compiled template assemblies -->
          <add reference="*.dll"/>
        <!-- same for all *.exe assemblies -->
          <add reference="*.exe"/> 
        </references>
        
        <namespaces>
          <clear/>
          <add namespace="System"/>
          <add namespace="System.Collections"/>
          <add namespace="System.Collections.Generic"/>
          <add namespace="System.Dynamic"/>
          <add namespace="System.IO"/>
          <add namespace="System.Linq"/>
          <add namespace="Xipton.Razor.Extension"/>
        </namespaces>

        <contentProviders>
          <clear/>
        <!-- by default the file content provider is added  -->
          <add type="Xipton.Razor.Core.ContentProvider.FileContentProvider" rootFolder="./Views"/> 
        </contentProviders>
      </xipton.razor>
  </xipton.razor.config>

</configuration>

Some notes about the configuration:

  • If you do not configure anything at all then the above configuration represents the actual configuration
  • You do not need to configure all settings. Any omitted configuration setting is set to the corresponding default setting as shown above.
  • If you do not add a <clear/> element at references, namespaces, or contentProviders, then your corresponding configuration settings are added to the default configuration:
    XML
    <xipton.razor>
        <namespaces>
            <add namespace="MyApp.Utils"/>
        </namespaces>
    </xipton.razor>
    Now the namespace "MyApp.Utils" is added to the default namespaces instead of replacing the default namespaces.
  • You may include the configuration at your app.config as shown above. You also could load the configuration from a separate configuration file name, or from an xml string. Then the node <xipton.razor> must be the root node. Take a look at the RazorMachine initializers to see how this works.
  • You also can load the default configuration first (no explicit configuration at all or the configuration section from your app.config) and pass optional arguments that override the (configured) default settings like:
    C#
    var RazorMachine = new RazorMachine(includeGeneratedSourceCode:true);
  • I choose to decouple the framework's configuration not using all of the Microsoft configuration classes for getting more configuration flexibility. By implementing an XmlConfigurationSection the framework's configuration can be integrated with your app.config as shown on top of this section.

Template References

You can specify the patterns "*.dll" and "*.exe" at the references add element. This means that respectively all DLL assemblies and EXE assemblies, that are loaded in the current appdomain's execution context, are added to the compiled template's assembly references. Initially right before finding all loaded assemblies at the execution context all assemblies from the application's bin folder are ensured to be loaded in the execution context:

C#
public static AppDomain EnsureBinAssembliesLoaded(this AppDomain domain) {

    if (_binAssembliesLoadedBefore.ContainsKey(domain.FriendlyName))
       return domain;

    var binFolder = !string.IsNullOrEmpty(domain.RelativeSearchPath)
                   ? Path.Combine(domain.BaseDirectory, domain.RelativeSearchPath)
                   : domain.BaseDirectory;

    Directory.GetFiles(binFolder, "*.dll")
        .Union(Directory.GetFiles(binFolder, "*.exe"))
        .ToList()
        .ForEach(domain.EnsureAssemblyIsLoaded);

    _binAssembliesLoadedBefore[domain.FriendlyName] = true;

    return domain;
}

private static void EnsureAssemblyIsLoaded(this AppDomain domain, string assemblyFileName) {
    var assemblyName = AssemblyName.GetAssemblyName(assemblyFileName);
    if (!domain.GetAssemblies().Any(a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.GetName()))) {
        domain.Load(assemblyName);
    }
}

I added this pre load behaviour in version 2.2. As a result you do not need to set "Copy Local" to "True" anymore for GAC assemblies (although still it is no problem to do that). Else make sure that the corresponding GAC assembly is loaded in the execution context before executing the first template. Referring at any type from such an assembly is enough to load that assembly in the execution context.

Content providers

You may configure one or more content providers. Such content providers can be added at your code base or these can be configured. You may add any custom content provider as long as it implements IContentProvider

The content provider on request delivers template content (i.e. template script) to the content manager for any specific resource. The content provider must be able to translate a virtual path into a specific resource name, file name, or whatever. And it must provide content by that specific resource name. Further the content provider must publish any template change by raising an event.

The content provider is initialized by passing the corresponding configuration element to the initializer InitFromConfig(XElement element)

The IContentProvider interface looks like:

C#
public interface IContentProvider
{
    event EventHandler<ContentModifiedArgs> ContentModified;
    string TryGetResourceName(string virtualPath);
    string TryGetContent(string resourceName);
    IContentProvider InitFromConfig(XElement configuredAddElement);
}
You could configure the included content providers as follows:
XML
<add type="Xipton.Razor.Core.ContentProvider.MemoryContentProvider"/>
<add type="Xipton.Razor.Core.ContentProvider.FileContentProvider" 
    rootFolder="./Views"/>
<add type="Xipton.Razor.Core.ContentProvider.EmbeddedResourceContentProvider" 
    resourceAssembly="Xipton.Razor.UnitTest.dll" 
    rootNameSpace="Xipton.Razor.UnitTest.Views"/>

As you probably expect you can define any additional attribute for your specific provider configuration. The whole XElement node <add type="..." [otherAttributes] /> is passed to the provider's initializer.

The framework uses a composite pattern to forward any IContentProvider invocation to the content provider list. Any first forwarded invocation that has a valid result (non null result) determines the final result. At that time further forwarding stops and the obtained result is returned. So the order of content providers matters because the first content provider that has a valid result determines the final result, thus overriding any following content provider's result.

Take a look at the configured content providers again. Now if, using that configuration, you create a _ViewStart.cshtml file then that corresponding content is provided by the FileContentProvider. Now suppose you would register a new _ViewStart like:
rm.RegisterTemplate("~/_ViewStart.cshtml","... [any content] ...");    
// will automatically clear the type cache since it is an autoInclude script

Because the call RegisterTemplate("~/_ViewStart.cshtml","...[any content]...") registers the template at the MemoryContentProvider and because that provider is on top of the FileContentProvider any execute request will load the in memory _ViewStart, instead of the file _ViewStart, thus overriding the corresponding file content. This behavior brings the opportunity to prioritize content, and thus allowing additional (manual) configuration like changing layout settings (at _ViewStart). Or you could create default templates as embedded resources still allowing to "override" templates by creating files that are resolved by the same corresponding virtual pathes.

The @model directive

The framework is capable of handling the @model directive (ModelType at VB). Therefore I had to extend the Razor parser a bit for both C# and VB. You will find the corresponding codebase at Xipton.Razor.Core.Generator. Because the framework supports the @model directive you can use it together with Visual Studio's intellisense. Of course you may use the @inherits directive as well since that directive is part of Razor itself.

Intellisense

Image 3

To get the Razor intellisense to work within a non MVC project first make sure that you meet the MVC conventions with regard to file extensions, i.e., .cshtml for C# and .vbhtml for VB. Next copy the configuration file named web.config (included at the Xipton.Razor project) to your non MVC template project. You must keep it named "web.config" else it won't work. Place the web.config file besides your default app.config file. Now your intellisense should work inside Visual Studio. Please read the comments in the included web.config on how to configure intellisense for different scenarios with regard to either referencing the Xipton.Razor.csproj project or referencing the signed Xipton.Razor.dll.

If you use Xipton.Razor within an MVC project then you need to include an Xipton.Razor.dll assembly reference in your MVC app.config. Inside your templates you must use the @inherits directive, instead of the @model directive. Please read the comments in the web.config for details.

Creating your own custom template base class

You can extend the included TemplateBase and configure your custom sub class as the new default template base class. In order to get it work you need to create two classes. Now lets call your new base class MyTemplateBase.

First you must inherit MyTemplateBase directly from TemplateBase. Add your non-typed-model implementation as much as possible to this class.

Secondly you must create another subclass called MyTemplateBase<TModel> (same name as base class!) that inherits MyTemplateBase and implements ITemplateBase<TModel>. So your code will look like:

C#
public class MyTemplateBase : TemplateBase
{
    // your base implementation goed here
}

public class MyTemplateBase<TModel> : MyTemplateBase, ITemplate<TModel> {
    #region Implementation of ITemplate<out TModel>

    public new TModel Model{
        get { return (TModel)base.Model; }
    }

    #endregion

    // your typed implementation goed here
}

Now you can configure typeof(MyTemplateBase) as your default template base class.

Error reporting

I paid some extra attention to error reporting with regard to parse errors, compile errors and runtime binding errors, trying to make those error reports as clear as possible. The error messages include the resource name and the corresponding code line where ever that is possible. You will receive separate exception types for parsing, compiling and runtime binding.

Performance

There is a rather great performance difference between running a template the first time, and running that same template again. I tested 200 templates writing a GUID string. Running these templates the first time took about 10 seconds. Running the same templates again took less than 1 ms, 10000 times faster!

So template compilation at the first execute request is rather slow due to the several needed compilation steps. Once your templates have been compiled the performance is really good.

Finally

With this article I present a global overview of my RazorMachine giving you the opportunity to decide whether this RazorMachine has added value for you. I skipped a lot of details though. To learn about the details you should take a look at the source code which completely is included. I added some unit tests as well.

Enjoy!

History

  • 2012/07/17 - Initial release
  • 2012/08/21 - v2.0
    • Upgrade to released Razor 2 (part of released MVC 4)
    • Renamed TemplateProvider to ContentManager
    • Added method named Execute at RazorMachine executing both content and URL.
    • Fixed URL problem with using anchors
    • Fixed intellisense problem on systems with both MVC 3 and MVC 4 installed
    • Other minor improvements
  • 2012/09/27 - v2.1: Fixed bug on resolving references for different scenarios
  • 2012/10/01 - v2.1a: Updates in content and updates in comments at code base
  • 2012/10/24 - v2.2
    • Improved reference resolving. No need to set "Copy Local" True anymore for GAC references
    • Refactored the RazorConfig class
    • Removed the br-field at TemplateBase (could be declared at _viewStart instead)
    • Other minor changes in source code like warning suppressions 
  • 2012/10/31 - v2.3
    • Improved assembly loading at EmbeddedResourceContentProvider
    • Moved code base to GitHub
  • 2012/11/17 - v2.4 
    • bugfix: spaces in attributes are preserved now 
    • bugfix: nested anonymous types are handled now
  • 2013/01/25 - v2.5 
    • added support for @helper
    • any unmanged dll is ignored and does not throw an exception anymore
    • configuration improvements

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)