Ww.Texts.TextTemplates

Kontynuując temat kompilatorów, chciałbym opisać technikę tworzenia szablonów tekstowych. Szablony tekstowe mogą zostać użyte w przypadku, gdy chcemy umożliwić użytkownikowi aplikacji szeroką możliwość definiowania zawartości niektórych dokumentów. Szablony pozwalają również na zmiany w aplikacji bez potrzeby jej przekompilowywania. Pod pojęciem szablonu tekstowego rozumiem wzorzec, szkic, z którego powstanie finalny dokument tekstowy. Z podobnych, istniejących rozwiązań mogę wymienić na przykład, Apache Velocity (NVelocity), CodeSmith, Smarty lub nowe rozwiązanie Microsoftu T4 z DSL Tools. Za przykład mogą również posłużyć wszelkie rozwiązania ery post-CGI, takie jak PHP, ASP czy JSP. Również w ASP.NET podstawą są szablony tekstowe, lecz tak bardzo rozbudowane, że sama funkcjonalność generowania tekstu z szablonu niknie w morzu funkcjonalności w rodzaju “code behind”, “user controls” czy “post back”. Lecz nadal na samym dnie, można doszukać się idei szablonów tekstowych. Celem artykułu jest zaprezentowanie idei dynamicznego generowania fragmentów tekstu przy użyciu pewnego języka szablonów.

Definicja języka szablonów

Dla potrzeb artykułu stworzony zostanie prosty język definiowania szablonów tekstowych. Pozwalał będzie na wplatanie kodu w języku C# i definiowanie nazwanych parametrów. Zacznijmy od przykładu prostego szablonu:

Code 1. - Przykładowy szablon tekstowy generujący listę wiadomości

<%@ @uses="System, System.Collections.Generic" @debug="true"
Messages="List<string>"
FirstName="string"
LastName="string"
%>
Welcome <%= LastName %>, <%= FirstName %>.
You have <%= Messages.Count %> new message(s):
<% foreach (string msg in Messages) { %>
- "<%= msg %>"<%
}
%>

Have a nice day.

Zadaniem szablonu jest stworzenie notki powitalnej z listą nowych wiadomości. Jeżeli przekazalibyśmy do szablonu następujące parametry:

Code 2. - Fragment kodu prezentującego ideę użycia szablonu w kodzie
ITemplate tpl = CreateTemplate(...);

tpl["Messages"] = new List<string>(new string[] {
    "Re: templates",
    "New better Java 1.82!",
    "Latest topic on dev.pl",
});
tpl["FirstName"] = "Jan";
tpl["LastName"] = "Kapusta";

To należy oczekiwać następującego wyniku:
Code 3. - Wynik przykładowego szablonu z listingu 1

Good Morning Kapusta, Jan.
You have 3 new message(s):

- "Re: templates"
- "New better Java 1.82!"
- "Latest topic on dev.pl"

Have a nice day.

Taki szablon możemy zapisać na dysku w katalogu aplikacji i wczytać przy starcie. Umożliwia to podmianę szablonu, bez potrzeby rekompilowania całej aplikacji oraz umożliwia dostosowanie do potrzeb konkretnego użytkownika.
Teraz zostaną dokładniej opisane oczekiwania względem tego rozwiązania, czyli składnię języka szablonów tekstowych. Pierwszym elementem jest opcjonalna definicja szablonu:

Code 4. - Definicja nagłówka szablonu

[
“<%@”
[“@class” “=” ‘"’ <template base class name> ‘"’ ]
[“@uses” “=” ‘"’ <namespaces coma separated> ‘"’ ]
[“@imports” “=” ‘"’ <assembly file names coma separated> ‘"’ ]
[<parameter name> “=” ‘"’ <parameter value> ‘"’ ] *
“%>”
]

Element zaczyna się od “<%@”, potem musi istnieć przynajmniej jeden biały znak, a kończy się tagiem “%>” poprzedzonym, co najmniej jednym białym znakiem. Zawartość tej definicji szablonu składa się z par nazwa-wartość, gdzie wartość jest ujęta w cudzysłowy, podobnie jak atrybuty w XMLu. Wyjątkiem są 3 specjalne atrybuty służące do konfigurowania kompilacji szablonu. Tag definicji szablonu jest opcjonalny i szablon może wyglądać tak:

Code 5. - Szablon tekstowy bez zdefiniowanego nagłówka

Liczymy od 0 do 10!
0<% for (int i = 1; i <= 10; i++) { Output.Write(“, {0}”, i); } %>.
Koniec.

W treści szablonu możemy dowolnie zagnieżdżać zwykły tekst i tagi specjalne takie jak kod języka C# bądź literał do wypisania na wyjściu.
Do zagnieżdżania kodu języka C# służą dwa elementy: “<%” oraz “%>”.

Code 6. - Definicja tagów służących do zagnieżdżania kodu w języku C#

“<%” <escaped C# code> “%>”

Pierwszy tag rozpoczyna blok kodu języka C# i po nim musi wystąpić, co najmniej jeden biały znak oddzielający od tekstu kodu. Blok kodu kończy się podobnym elementem “%>” tym razem poprzedzonym, co najmniej jednym białym znakiem. Wewnątrz może znajdować się dowolny kod języka C#. Jeżeli chcemy użyć znaku procent “%”, musimy zapisać go jako podwójny znak procent “%%”. Wewnątrz bloku kodu możemy używać każdego nie prywatnego elementu tej klasy, między innymi właściwości “Output”, do której możemy zapisać dowolny tekst.
Aby uniknąć używania za każdym razem właściwości Output i metody Write lub WriteLine, zdefiniowany został na podobieństwo ASP, następujący skrót rozpoczynający się od “<%=” i kończący elementem “%>”.

Code 7. - Definicja tagów służących do wypisywania wartości wyrażeń

 “<%=” <escaped C# code expression> “%>”

Podobnie jak poprzednio pierwszy element musi być zakończony białym znakiem, a ostatni poprzedzony białym znakiem. Obie poniższe linie są równoważne:

Code 8. - Konstrukcje wypisywania wartości wyrażeń

<% Output.Write(i); %>
<%= i %>

Wszystkie 3 przypadki użycia tagów szablonu (“<%”, “<%@”, “<%=”) można uogólnić do przypadku gdzie tag otwierający zaczyna się od “<%” i po nim następuje opcjonalny znak decydujący, jakie rozszerzenie jest użyte. Po tym opcjonalnym trzecim znaku ponownie musi wystąpić biały znak oddzielający zawartość tagu od samego tagu. Każdy z takich tagów kończy się białym znakiem oraz elementem “%>”. Takie rozwiązanie umożliwia dalsze rozszerzanie o nowe ułatwienia. W ASP.NET w podobny sposób definiuje się wiązanie danych (bindowanie) przy użyciu znaku “#” jako “rozszerzenia”.

Projekt implementacji

Kolejnym krokiem jest zastanowienie się nad implementacją powyższego rozwiązania. Fragment przykładowego użycia można zobaczyć w listingu 2. Musi istnieć możliwość łatwego przekazywania parametrów do szablonu. Należy umożliwić również kompilację szablonów z różnych źródeł: plików tekstowych, zasobów bibliotek, zmiennych tekstowych, itp. Należy pomyśleć o możliwości definiowania dla szablonów nowych rozszerzeń.
W zaproponowanym rozwiązaniu jedna statyczna klasa zajmie się zarządzaniem szablonami, ukrywając cały proces kompilacji przed programistą. Klasa parsera tekstu szablonu zajmie się parsowaniem i tworzeniem obiektu struktury szablonu. Klasa kompilatora szablonów umożliwi kompilację struktury szablonu do typu .NET implementującego interfejs szablonu tekstowego. Wydzielone zostaną odpowiednie interfejsy dla szablonów i rozszerzeń szablonów.
Cały proces od definicji szablonu tekstowego do uzyskania wyniku zaczynał będzie się od sparsowania treści szablonu i wyprodukowania obiektowej jego struktury. Kolejnym etapem jest uzyskanie typu implementującego interfejs szablonu a reprezentującego sparsowany szablon. W tym celu kompilator przekształci model szablonu w kod klasy języka C#, a następnie skompiluje kod i zwróci wygenerowany typ. W kolejnym kroku uzyskany typ .NET zostanie zinstancjonowany, uzupełniony parametrami i wykonany - wyrenderowany.

Na rysunkach 9, 10 i 11 przedstawione zostały schematy klas należących do projektu.

Picture 9. - Główne klasy generatora szablonów

Picture 10. - Struktura klas rozrzerzeń szablonów

Picture 11. - Struktura szablonu

Interfejs szablonu

Zacznijmy od zdefiniowania interfejsu dla szablonów:

Code 12. - Interfejs szablonu tekstowego

public interface ITemplate {
    void Render(TextWriter output);
    string RenderToString();
    object this[string name] { get; set; }
}

Zdefiniowany indekser pozwala na manipulowanie parametrami szablonu. Dwie metody komponentu renderującego pozwalają na optymalizację różnych scenariuszów użycia. Metoda RenderToString zwraca nam gotowy ciąg tekstowy, gdzie nie musimy zajmować się strumieniami tekstowymi. Druga z metod Render przyjmuje parametr typu TextWriter, do którego treść szablonu zostanie zapisana. W przypadku zapisu do pliku czy do konsoli, możemy w taki sposób uniknąć nie potrzebnej alokacji pamięci.
Klasa implementująca powyższy interfejs powinna pamiętać o udostępnieniu właściwości o nazwie Output i typie TextWriter. Powinna wskazywać na obiekt, który jest przekazywany do metody Render.
Model szablonu
Reprezentacją obiektową szablonu będzie klasa TemplateModel. Ta klasa będzie właściwą strukturą, na której w kolejnym kroku działał będzie kompilator. Model szablonu powinien zawierać wszystkie elementy niezbędne do wygenerowania finalnego kodu klasy języka C#. Na tym etapie wszelka interpretacja zapisów szablonu powinna zostać zakończona i gotowa do użycia przez kompilator szablonów.

Code 13. - Klasa modelu szablonu

public sealed class TemplateModel {
    public string BaseClass { get; set; }
    public bool Debug { get; set; }
    public List<string> Imports { get; set; }
    public List<string> Uses { get; set; }

    public Dictionary<string, string> Parameters { get; set; }
    public List<ITemplateRenderer> Renderers { get; set; }
}

Właściwości klasy modelu szablonu można podzielić na dwie grupy. Pierwsza to konfiguracja kompilacji kodu języka C#, a druga odpowiada za konfigurację treści szablonu.
Szablon ma zdefiniowane następujące elementy konfigurujące kompilację:

  • Nazwa bazowej klasy (właściwość BaseClass i atrybut @baseClass szablonu tekstowego). Jeżeli wartość nie jest zdefiniowana, użyta zostanie wtedy standardowa klasa TemplateBase.
  • Zmienną określającą czy szablon powinien zostać kompilowany w trybie Debug (właściwość Debug i atrybut @debug szablonu tekstowego). Kompilacja w trybie debug pozostawi tymczasowe pliki w katalogu tymczasowym.
  • Lista zaimportowanych dodatkowych bibliotek (właściwość Imports i atrybut @imports szablonu tekstowego). Jest to lista nazw plików bibliotek separowana średnikami, która zostanie dołączona podczas kompilacji klasy języka C#.
  • Lista zaimportowanych przestrzeni nazw (właściwość Uses i atrybut @uses szablonu tekstowego). Jest to lista plików bibliotek separowana średnikami, która zostanie dołączona podczas kompilacji klasy języka C#.
  • Zdefiniowane zostały dwie właściwości odpowiadające za treść szablonu:
  • Słownik z parametrami szablonu (właściwość Parameters i wszystkie pozostałe atrybuty szablonu tekstowego). Jest to kolekcja nazwa-wartość, gdzie kluczem jest nazwa parametru, a wartością jego typ.
  • Lista komponentów renderujących treści szablonu (właściwość Renderers) reprezentująca właściwą treść szablonu.
  • Każde z rozszerzeń szablonów powinno mieć zdefiniowany unikalny znak rozszerzenia oraz metodę wytwarzającą właściwy komponent renderujący, który musi mieć metodę generującą kod klasy proxy szablonu.

Code 14. - Interfejsy rozrzerzeń szablonów

public interface ITemplateExtension {
    char ExtensionChar { get; }
    ITemplateRenderer CreateRenderer(string content);
}

public interface ITemplateRenderer {
    void Render(TextWriter writer);
}

Parsowanie szablonu

Przekształcaniem tekstu szablonu do jego modelu zajmuje się klasa TemplateParser. Nie jest to pełnoprawny lekser i parser, lecz jedna, uproszczona klasa. Parsowanie odbywa się rekurencyjnie od obiektu szablonu do poszczególnych parametrów i komponentów renderujących. Przetwarzanie poszczególnych elementów odbywa się za pomocą wyrażeń regularnych, które rozbijają poszczególne „tagi” (<% i %>) na elementy składowe.

Code 15. - Klasa parsera treści szablonu

public sealed class TemplateParser {
    public static TemplateModel Parse(TextReader reader);
}

Klasa parsera posiada jedną publiczną, statyczną metodę Parse, która przyjmuje na wejściu treść szablonu i generuje obiekt modelu. Rysunek 16 prezentuje sekwencję działania parsera.

Picture 16. - Diagram sekwencji opisujący działanie parsera

Kompilacja

Kompilacja jest bardzo podobna do tej z poprzedniego artykułu. Warto wspomnieć o tym, że przetwarzanie komponentów renderujących odbywa się po przez metodę Render interfejsu ITemplateRenderer.

Code 17. - Klasa kompilatora szablonów

public sealed class TemplateCompiler {
    public static Type Compile(TemplateModel model);
    public static Dictionary<TemplateModel, Type> Compile(params TemplateModel[] models);
}

Upublicznione zostały dwie metody zajmujące się kompilacją. Pierwsza przyjmuje na wejściu model szablonu i zwraca skompilowany typ. Druga przyjmuje na wejściu tablicę modeli i zwraca kolekcję skompilowanych typów. W drugim przypadku kompilacja wszystkich modeli przebiega jednoetapowo – generowane jest jedno assembly, więc oszczędzane są zasoby systemowe.

Klasa TemplateManager

Poszczególne klocki mamy już zdefiniowane i teraz parę zdań o klasie zarządzającej szablonami. Klasa ta ukrywa każdy z etapów przetwarzania szablonu, udostępniając najczęściej używaną funkcjonalność w postaci łatwo dostępnych, statycznych metod.

Code 18. - Klasa zarządzająca operacjami na szablonach

public static class TemplateManager {
    public static void RegisterExtension(

        ITemplateExtension extension);
    public static void UnregisterExtension(

        ITemplateExtension extension);
    public static ITemplateExtension GetExtension(char extChar);

    public static Type GetTemplateType(TextReader reader);
    public static ITemplate GetTemplate(TextReader reader);
    public static string RenderTemplate(TextReader reader);
    public static string RenderTemplate(TextReader reader,

        IDictionary<string, object> parameters);
    public static string RenderTemplate(TextReader reader,

       
params KeyValuePair<string, object>[] parameterList);
}

Klasa zarządzająca szablonami posiada dwie główne funkcjonalności: zarządzanie rozszerzeniami oraz kompilację szablonów.
Manipulacja rozszerzeniami szablonów tekstowych – dodawanie, usuwanie oraz pobieranie rozszerzeń – odbywa się za pomocą metod RegisterExtension, UnregisterExtension i GetExtension. Metoda GetTemplateType kompiluje szablon tekstowy i zwraca w wyniku swojego działania typ klasy proxy dla szablonu tekstowego. Metoda GetTemplate generuje instancję klasy szablonu tekstowego, gotową do użycia.
Zbiór przeładowanych metod RenderTemplate ma za zadanie kompilację i wykonanie szablonu tekstowego. Zwracaną wartością jest wynik wygenerowanego szablonu tekstowego.

Generator klas - TextTemplates.ClassGen

Za praktyczny przykład zastosowania tego rozwiązania posłuży proste narzędzie do generowania treści klas na podstawie definicji z pliku XML. Podobnych narzędzi dostępnych jest wiele (na przykład MyGeneration, Sooda) i posiadają znacznie większe możliwości. Zadaniem projektu jest zaprezentowanie jednego ze sposobów użycia szablonów tekstowych.
Narzędzie przyjmowało będzie na wejściu plik XML z definicją encji i generowało będzie plik z klasa w języku C#. Definicja wyglądu generowanej klasy jest umieszczona w szablonie tekstowym – Class.tpl.txt. Podczas działania programu plik XML jest przekształcany do obiektu Entity za pomocą standardowej serializacji XML, który to obiekt jest potem przekazywany do szablonu w celu dalszego przetwarzania.
Przykładowa definicja encji:

Code 19. - Przykładowy plik konfiguracyjny dla generatora klas

<?xml version="1.0" encoding="utf-8" ?>
<entity
 name="ExampleEntity"
 namespace="Ww.Texts.TextTemplates.ExampleNS"
 xmlns="http://ww/texts/textTemplates/classGen/"
>
 <a name="FirstName">string</a>
 <a name="LastName">string</a>
 <a name="Age">string</a>
 <a name="Contacts">List&lt;string&gt;</a>
</entity>

A to wynik działania programu:

Code 20. - Przykładowy wynik działania generatora klas.

using System.Collections.Generic;

namespace Ww.Texts.TextTemplates.ExampleNS {
  public class ExampleEntity {

    private string _firstName;
    private string _lastName;
    private string _age;
    private List<string> _contacts;

    public ExampleEntity() { }
    public ExampleEntity(string firstName, string lastName,

       
string age, List<string> contacts)
    {
      _firstName = firstName;
      _lastName = lastName;
      _age = age;
      _contacts = contacts;
    }

    public string FirstName
    { get { return _firstName; } set { _firstName = value; } }
    public string LastName
    { get { return _lastName; } set { _lastName = value; } }
    public string Age
    { get { return _age; } set { _age = value; } }
    public List<string> Contacts
    { get { return _contacts; } set { _contacts = value; } }
  }
}

Podsumowanie

W tym artykule pokazano w jaki sposób działają procesory szablonów tekstowych. Separacja samego procesu generowania tekstu od sterowania procesem generacji, pozwala zwiększyć przejrzystość kodu. Skorzystanie z kompilatora języka C# pozwala na etapie kompilacji projektu (kod w klasie bazowej szablonów) lub działania programu wyeliminować sporą część błędów programistycznych. Wydzielenie funkcjonalności rozszerzeń szablonów umożliwia dalszą optymalizację pracy programisty, po przez stosowanie wygodniejszych rozszerzeń podczas pracy z szablonami.

Opublikowane 28 stycznia 07 04:23 przez Wojciech Gebczyk

Komentarze:

Brak komentarzy
Komentarze anonimowe wyłączone

About Wojciech Gebczyk

Code Sculptor.