Chitika

April 23, 2012

Localizable text template engine using StringTemplate 4

In the previous post I shown how to create localizable text template engine using RazorEngine.
I suggest to use the RazorEngine in any case.
But there is an one case where RazorEngine can not help you. It will happened if you try to use RazorEngine in dynamically loaded assembly.
There is a simple example. Your system can load the plugins dynamically. One of your plugin have to use text template engine to parse the strings. In that case RazorEngine will throw an error with the message:

Precondition failed: templateType != null

It's known issue and if you interested in it you can find a detailed description of this problem here.

In that case, you have to use something else for a template engine and I suggest to use StringTemplate to avoid that kind of problems.

In this post I will show you how to use StringTemplate engine.


Using the StringTemplate engine


The following command in the Package Manager console will install StringTemplate 4 package into your ASP.NET MVC 4 WebAPI application:

PM > Install-Package Antlr4.StringTemplate

StringTemplate is a template engine library used for generating text from data structures. StringTemplate's distinguishing characteristic is that it strictly enforces model-view separation unlike other comparable template engines. It is particularly good at multi-targeted code generators, multiple site skins, and internationalization/localization.

So, it fully meets our needs.

In my previous post I shown how to write and use the localizable template service.

So, here I will talk only about another realization of the interface ITemplateEngine for StringTemplate, because everything else will stay the same.

I would like to remind you how the ITemplateEngine interface looks:

    public interface ITemplateEngine
    {
        string Parse(string template, dynamic model);
    }

And there is the realization of this interface for StringTemplate engine:

    public class StringTemplateEngine : ITemplateEngine
    {
        public string Parse(string template, dynamic model)
        {
            var group = new TemplateGroupString("group", "delimiters \"$\", \"$\"");

            var renderer = new AdvancedRenderer();
            group.RegisterRenderer(typeof(DateTime), renderer);
            group.RegisterRenderer(typeof(double), renderer);

            group.DefineTemplate("template", template, new[] { "Model" });

            var stringTemplate = group.GetInstanceOf("template");
            stringTemplate.Add("Model", model);

            return stringTemplate.Render();
        }
    }

It is a little bit complicated example and I will explain you why.
The first line just creates a template group with specified delimiters '$' (default delimiters are '<' and '>').
The next three lines help us to specify the renderer for the types DateTime and double. I have to use custom renderer to allow formatting my DateTime and double values, and that is only one reason why I use the template group instead of specify only one template.
The next line creates new template in the template group with the name 'template' and use the template text for a content of the template.
Then I take this template from a group and add my model to template.
At last, just call Render method of the template and return result back.

To make formatting inside the template possible I use custom renderer for StringTemplate called AdvancedRenderer:

    public class AdvancedRenderer : IAttributeRenderer
    {
        public string ToString(object obj, string formatString, System.Globalization.CultureInfo culture)
        {
            if (obj == null)
                return null;

            if (string.IsNullOrEmpty(formatString))
                return obj.ToString();

            return string.Format("{0:" + formatString + "}", obj);
        }
    }

It is really simple but powerful renderer. I will show you how to use the formatting directly in the template later.

So, I have done with the template engine. And we can start using it already.

In the last part of the post, I would like to share the example of template that would be easier to understand the syntax of StringTemplate.

Using the localizable template service


To demonstrate how to use this service let's create the template file first.
Create a directory 'Templates' in your project.
Then click the right mouse button on this directory in the 'Solution Explorer' and select 'Add' -> 'New Item...' (or just press Ctrl+Shift+A).
In the new window click on 'Visual C# Items' and select the 'Text File' in the list. Enter the file name - 'first.template' and click 'Add' button.

Then insert the next text into template file:

ID: $Model.Id$
CreatedDate: $Model.CreatedDate; format="dd.MM.yyyy HH:mm"$
Name: $Model.Name$
Price: $Model.Price; format="0.00"$
Items:
$Model.Items:{item |
    item $i$ - $item.Key$  $if(item.Value)$enabled$else$-$endif$
}$

Then write some simple code to prepare the data model and to parse the template:

            
            var model = new { 
                Id = 10,
                CreatedDate = DateTime.Now,
                Name = "Name1",
                Price = 123.45356,
                Items = new List<KeyValuePair<string, bool>> {
                    new KeyValuePair<string, bool>("Item1", false),
                    new KeyValuePair<string, bool>("Item2", true),
                    new KeyValuePair<string, bool>("Item3", false),
                    new KeyValuePair<string, bool>("Item4", false),
                    new KeyValuePair<string, bool>("Item5", true)
                }
            };

            var templateService = new TemplatesService(new FileSystemService(), new StringTemplateEngine());

            var result = templateService.Parse("first", model);

You can find detailed information about TemplateService and FileSystemService in my previous post how to create localizable text template engine using RazorEngine.

After that, put the break point after the last line (var result = ...) and run your application in Debug mode.
When the debugger stops on that break point just check the value of the result variable in the Text Visualizer.

If should contains the next text:

ID: 10
CreatedDate: 23.04.2012 15:54
Name: Name1
Price: 123.45
Items:
    item 1 - Item1  -
    item 2 - Item2  enabled
    item 3 - Item3  -
    item 4 - Item4  -
    item 5 - Item5  enabled

So, as you can see our renderer (AdvancedRenderer) allows us to specify string for formatting as we use usually for string.Format() method.

That's all.
Good luck.

2 comments:

  1. Which methons do you personally use to search for information for your future posts and which exact search websites or techniques do you generally rely on?

    ReplyDelete
    Replies
    1. I don't use any search methods. I'm just trying to cover the topics which I'm working on.

      Delete