You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
abp/docs/en/Text-Templating.md

17 KiB

Text Templating

Introduction

ABP Framework provides a simple, yet efficient text template system. Text templating is used to dynamically render contents based on a template and a model (a data object):

TEMPLATE + MODEL => RENDERED CONTENT

It is very similar to an ASP.NET Core Razor View (or Page):

RAZOR VIEW (or PAGE) + MODEL => HTML CONTENT

Example

Here, a simple template:

Hello {{model.name}} :)

You can define a class with a Name property to render this template:

public class HelloModel
{
    public string Name { get; set; }
}

If you render the template with a HelloModel with the Name is John, the rendered output is will be:

Hello John :)

Template rendering engine is very powerful;

  • It is based on the Scriban library, so it supports conditional logics, loops and much more.
  • Template content can be localized.
  • You can define layout templates to be used as the layout while rendering other templates.
  • You can pass arbitrary objects to the template context (beside the model) for advanced scenarios.

Installation

It is suggested to use the ABP CLI to install this package.

Using the ABP CLI

Open a command line window in the folder of the project (.csproj file) and type the following command:

abp add-package Volo.Abp.TextTemplating

Manual Installation

If you want to manually install;

  1. Add the Volo.Abp.TextTemplating NuGet package to your project:
Install-Package Volo.Abp.TextTemplating
  1. Add the AbpTextTemplatingModule to the dependency list of your module:
[DependsOn(
    //...other dependencies
    typeof(AbpTextTemplatingModule) //Add the new module dependency
    )]
public class YourModule : AbpModule
{
}

Defining Templates

Before rendering a template, you should define it. Create a class inheriting from the TemplateDefinitionProvider base class:

public class DemoTemplateDefinitionProvider : TemplateDefinitionProvider
{
    public override void Define(ITemplateDefinitionContext context)
    {
        context.Add(
            new TemplateDefinition("Hello") //template name: "Hello"
                .WithVirtualFilePath(
                    "/Demos/Hello/Hello.tpl", //template content path
                    isInlineLocalized: true
                )
        );
    }
}
  • context object is used to add new templates or get the templates defined by depended modules. Used context.Add(...) to define a new template.
  • TemplateDefinition is the class represents a template. Each template must have a unique name (that will be used while you are rendering the template).
  • /Demos/Hello/Hello.tpl is the path of the template file.
  • isInlineLocalized is used to declare if you are using a single template for all languages (true) or different templates for each language (false). See the Localization section below for more.

The Template Content

WithVirtualFilePath indicates that we are using the Virtual File System to store the template content. Create a Hello.tpl file inside your project and mark it as "embedded resource" on the properties window:

hello-template

Example Hello.tpl content is shown below:

Hello {{model.name}} :)

The Virtual File System requires to add your files in the ConfigureServices method of your module class:

Configure<AbpVirtualFileSystemOptions>(options =>
{
    options.FileSets.AddEmbedded<TextTemplateDemoModule>("TextTemplateDemo");
});
  • TextTemplateDemoModule is the module class that you define your template in.
  • TextTemplateDemo is the root namespace of your project.

Rendering the Template

ITemplateRenderer service is used to render a template content.

Example: Rendering a Simple Template

public class HelloDemo : ITransientDependency
{
    private readonly ITemplateRenderer _templateRenderer;

    public HelloDemo(ITemplateRenderer templateRenderer)
    {
        _templateRenderer = templateRenderer;
    }

    public async Task RunAsync()
    {
        var result = await _templateRenderer.RenderAsync(
            "Hello", //the template name
            new HelloModel
            {
                Name = "John"
            }
        );

        Console.WriteLine(result);
    }
}
  • HelloDemo is a simple class that injects the ITemplateRenderer in its constructor and uses it inside the RunAsync method.
  • RenderAsync gets two fundamental parameters:
    • templateName: The name of the template to be rendered (Hello in this example).
    • model: An object that is used as the model inside the template (a HelloModel object in this example).

The result shown below for this example:

Hello John :)

Anonymous Model

While it is suggested to create model classes for the templates, it would be practical (and possible) to use anonymous objects for simple cases:

var result = await _templateRenderer.RenderAsync(
    "Hello",
    new
    {
        Name = "John"
    }
);

In this case, we haven't created a model class, but created an anonymous object as the model.

PascalCase vs camelCase

PascalCase property names (like UserName) is used as camelCase (like userName) in the templates.

Localization

It is possible to localize a template content based on the current culture. There are two types of localization options described in the following sections.

Inline localization

Inline localization uses the localization system to localize texts inside templates.

Assuming you need to send an email to a user to reset her/his password. Here, the template content:

<a href="{{model.link}}">{{L "ResetMyPassword"}}</a>

L function is used to localize the given key based on the current user culture. You need to define the ResetMyPassword key inside your localization file:

"ResetMyPassword": "Click here to reset your password"

You also need to declare the localization resource to be used with this template, inside your template definition provider class:

context.Add(
    new TemplateDefinition(
            "PasswordReset", //Template name
            typeof(DemoResource) //LOCALIZATION RESOURCE
        ).WithVirtualFilePath(
            "/Demos/PasswordReset/PasswordReset.tpl", //template content path
            isInlineLocalized: true
        )
);

That's all. When you render this template like that:

var result = await _templateRenderer.RenderAsync(
    "PasswordReset", //the template name
    new PasswordResetModel
    {
        Link = "https://abp.io/example-link?userId=123&token=ABC"
    }
);

You will see the localized result:

<a href="https://abp.io/example-link?userId=123&token=ABC">Click here to reset your password</a>

If you define the default localization resource for your application, then no need to declare the resource type for the template definition.

Multiple Contents Localization

Instead of a single template that uses the localization system to localize the template, you may want to create different template files for each language. It can be needed if the template should be completely different for a specific culture rather than simple text localizations.

Example: Welcome Email

Assuming that you want to send a welcome email to your users, but want to define a completely different template based on the user culture.

First, create a folder and put your templates inside it, like en.tpl, tr.tpl... one for each culture you support:

multiple-file-template

Then add your template definition in the template definition provider class:

context.Add(
    new TemplateDefinition(
            name: "WelcomeEmail",
            defaultCultureName: "en"
        )
        .WithVirtualFilePath(
            "/Demos/WelcomeEmail/Templates", //template content folder
            isInlineLocalized: false
        )
);
  • Set default culture name, so it fallbacks to the default culture if there is no template for the desired culture.
  • Specify the template folder rather than a single template file.
  • Set isInlineLocalized to false for this case.

That's all, you can render the template for the current culture:

var result = await _templateRenderer.RenderAsync("WelcomeEmail");

Skipped the modal for this example to keep it simple, but you can use models as just explained before.

Specify the Culture

ITemplateRenderer service uses the current culture (CultureInfo.CurrentUICulture) if not specified. If you need, you can specify the culture as the cultureName parameter:

var result = await _templateRenderer.RenderAsync(
    "WelcomeEmail",
    cultureName: "en"
);

Logic

A Text Template is a combination of two parts: template definition and template content.

Template Definition

Template Definition is an object that contains some information about your text templates. Template Definition object contains the following properties.

  • Name (string): Unique name of the template. It is then used to render the template.
  • IsLayout (boolean):
  • Layout (string) contains the name of layout template
  • LocalizationResource (Type) The localization resource type that is used if this template is inline localized.
  • IsInlineLocalized (boolean) describes that the template is inline localized or not
  • DefaultCultureName (string) defines the default culture for the template

Template Content

This is a simple content for your templates. For default, template contents stored as Virtual File.

Example: ForgotPasswordEmail.tpl

<h3>{{L "PasswordReset"}}</h3>

<p>{{L "PasswordResetInfoInEmail"}}</p>

<div>
    <a href="{{model.link}}">{{L "ResetMyPassword"}}</a>
</div>

Localization

You can localize your Text Templates by choosing two different methods.

  • Inline Localization
  • Multiple Content Localization

Inline Localization

An inline localized text template is using only one content resource, and it is using the Abp.Localization to get content in different languages/cultures.

Example Inline Localized Text Template content:

<a href="{{model.link}}">{{L "ResetMyPassword"}}</a>

Multiple Content Localization

You can store your Text Templates for any culture in different content resource.

Example Multiple Content Localization

ForgotPasswordEmail / en.tpl

<h3>Reset Your Password</h3>

<p>Hello, this is a password changing email.</p>

<div>
    <a href="{{model.link}}">Click To Reset Your Password</a>
</div>

ForgotPasswordEmail / tr.tpl

<h3>Şifrenizi Değiştirin</h3>

<p>Merhaba, bu bir şifre yenileme e postasıdır.</p>

<div>
    <a href="{{model.link}}">Şifrenizi Yenilemek İçin Tıklayınız</a>
</div>

Layout System

It is typical to use the same layout for some different text templates. So, you can define a layout template.

A text template can be layout for different text templates and also a text template may use a layout.

A layout Text Template must have {{content}} area to render the child content. (just like the RenderBody() in the MVC)

Example Email Layout Text Template

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
</head>
<body>
    {{content}}
</body>
</html>

Definition of a Text Template

First of all, create a class that inherited from TemplateDefinitionProvider abstract class and create Define method that derived from the base class.

Define method requires a context that is ITemplateDefinitionContext. This context is a storage for template definitions and we will add our template definitions to the context.

NOTE! For default, ABP uses Virtual File System for text templates. Do not forget to register your files as an Embedded Resource. Please check the Virtual File System Documentation for more details.

All given examples are for Virtual File Text Template Definitions.

public class MyTemplateDefinitionProvider : TemplateDefinitionProvider
    {
        public override void Define(ITemplateDefinitionContext context)
        {
            // Layout Text Template
            context.Add(
                new TemplateDefinition(
                    name: "MySampleTemplateLayout", // Template Definition Name
                    isLayout: true 
                ).WithVirtualFilePath("/SampleTemplates/SampleTemplateLayout.tpl", true)
            );

            // Inline Localized Text Template
            context.Add(
                new TemplateDefinition(
                    name: "ForgotPasswordEmail",
                    localizationResource: typeof(MyLocalizationResource),
                    layout: TestTemplates.TestTemplateLayout1
                ).WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail.tpl", true)
            );

            // Multiple File Localized Text Template
            context.Add(
                new TemplateDefinition(
                    name: "ForgotPasswordEmail",
                    defaultCultureName: "en"
                ).WithVirtualFilePath("/SampleTemplates/ForgotPasswordEmail", false)
            );
        }
    }

As you see in the given example, all Text Templates are added with (ITemplateDefinitionContext)context.Add method. This method requires a TemplateDefinition object. Then we call WithVirtualFilePath method with chaining for the describe where is the virtual files.

WithVirtualFilePath is requires one tpl file path for the Inline Localized Text Templates. If your Text Tempalte is Multi Localized you should create a folder and store each different culture files under that. So you can send the folder path as a parameter to WithVirtualFilePath.

Inline Localized File

/ Folder / ForgotPasswordEmail.tpl

Multi Content Localization

/ Folder / ForgotPasswordEmail / en.tpl
/ Folder / ForgotPasswordEmail / tr.tpl

Rendering

When one template is registered, it is easy to render and get the result with ITemplateRenderer service.

ITemplateRenderer service has one method that named RenderAsync and to render your content and it is requires some parameters.

  • templateName (string)
  • model (object)
  • cultureName (string)
  • globalContext (dictionary)

templateName is exactly same with Template Definition Name.

model is a dynamic object. This is using to put dynamic data into template. For more information, please look at Scriban Documentation.

cultureName is your destination rendering culture. When it is not exist, it will use the default culture.

If cultureName has a language tag it will try to find exact culture with tag, if it is not exist it will use the language family.

Example: If you try to render content with "es-MX" it will search your template with "es-MX" culture, when it fails to find, it will try to render "es" culture content. If still can't find it will render the default culture content that you defined.

globalContext = TODO

Template Content Provider

When you want to get your stored template content you can use ITemplateContentProvider.

ITemplateContentProvider has one method that named GetContentOrNullAsync with two different overriding, and it returns you a string of template content or null. (without rendering)

  • templateName (string) or templateDefinition (TemplateDefinition)
  • cultureName (string)
  • tryDefaults (bool)
  • useCurrentCultureIfCultureNameIsNull (bool)

Usage

First parametres of GetContentOrNullAsync (templateName or templateDefinition) are required, the other three parametres can be null.

If you want to get exact culture content, set tryDefaults and useCurrentCultureIfCultureNameIsNull as a false. Because the GetContentOrNullAsync tries to return content of template.

Example Scenario

If you have a template content that culture "es", when you try to get template content with "es-MX" it will try to return first "es-MX", if it fails it will return "es" content. If you set tryDefaults and useCurrentCultureIfCultureNameIsNull as false it will return null.

Template Definition Manager

When you want to get your Template Definitions, you can use a singleton service that named Template Definition Manager in runtime.

To use it, inject ITemplateDefinitionManager service.

It has three method that you can get your Template Definitions.

  • Get
  • GetOrNull
  • GetAll

Get and GetOrNull requires a string parameter that name of template definition. Get will throw error when it is not exist but GetOrNull returns null.

GetAll returns you all registered template definitions.

Template Content Contributor

You can store your Template Contents in any resource. To make it, just create a class that implements ITemplateContentContributor interface.

ITemplateContentContributor has a one method that named GetOrNullAsync. This method must return content without rendering if that is exist in your resource or must return null.