Szablony tekstowe T4 - statyczny kod
Hello, World of T4!
Przyjęło się, że nauka każdego nowego języka programowania rozpoczyna się od programu Hello, World!
Na pamiątkę wydarzenia, które miało miejsce w 1974 roku. W spółce Bell Laboratories Brian Kernighan, znana postać w programowaniu, stworzył kurs języka C. Jako pierwszy przykład umieścił on tam słynny kod wypisujący w oknie konsoli tekst hello, world
. Małymi literami i bez wykrzyknika. Nikt by o tym pewnie nie usłyszał, gdyby nie wydana cztery lata później słynna książka The C Programming Language
, współautorstwa Briana Kernighana i Dennisa Ritchie. Książka jest przez niektórych uznawana za matkę całej literatury języka C, ale także za matkę późniejszego standardu tego języka.. Nie inaczej będzie i w tym przypadku. Postaram się krok po kroku pokazać proces tworzenia bardzo prostego szablonu wypisującego statyczny tekst. Trudno będzie znaleźć praktyczne zastosowanie przykładu, ale to samo można powiedzieć o słynnym Hello, World!
w C. Przejdźmy zatem do części właściwej. Ze względu na ograniczone wsparcie Visual Studio w zakresie kolorowanie składni i Intellisense do pracy z szablonami może się przydać Edytor szablonów tekstowych T4.
Dodanie szablonu T4 do projektu
Praca z szablonem T4 rozpoczyna się od tych samych czynności co praca z dowolnym plikiem projektu Visual Studio. Tworzymy nowy projekt, klikamy na nim prawym przyciskiem myszy i wybieramy Add/New Item...
Plik nie jest pusty. Nie wszystkie linijki są jednak potrzebne i można je spokojnie usunąć. Ich znaczenie zostanie wyjaśnione wtedy, gdy okaże się to konieczne. Popatrzmy na przykład Hello, World!
i przeanalizujmy go:
<#@ output extension=".txt" #> Hello, World!
Szablony T4 mają proste założenia: wszystkie bloki wymagające przetworzenia znajdują się w znacznikach <#= #>, <#+ #>, <#@ #> lub <# #>. Każdy z nich ma swoje zastosowanie i tak:
- <#= #> oznacza blok wyrażenia. To co zostanie wyliczone przez wyrażenie będzie wstawione do pliku wynikowego.
- <#+ #> oznacza blok dodatków. Tutaj definiuje się funkcje i klasy, które nie powinny brać udziału w przekształceniach. To dobre miejsce na funkcje i klasy pomocnicze.
- <#@ #> oznacza blok dyrektyw. To instrukcje dla całego silnika T4. Definiujemy tu typ pliku wyjściowego, referencje do zewnętrznych bibliotek, deklaracje używanych przestrzeni nazw.
- <# #> oznacza blok kontrolny. Tu sterujemy całym procesem generowania szablonu. Można tu umieścić instrukcje warunkowe, pętle, deklaracje zmiennych. Wszystko co nie mieści się w innych blokach, najprawdopodobniej jest blokiem kontrolnym. Ten blok zostanie wykonany podczas sekwencyjnego przetwarzania szablonu.
Wróćmy zatem do przykładu Hello, World!
. Pierwsza linijka to dyrektywa. Ta akurat wskazuje plik wynikowy, a dokładniej jego rozszerzenie. Nazwa pliku będzie taka sama jak nazwa szablonu. Jeżeli nasz plik ma nazwę ABC.tt, w wyniku przekształcenia otrzymamy plik ABC.txt. Druga linijka to tekst, który znajdzie się w pliku wynikowym. Każda bowiem wartość nie umieszczona w znacznikach T4 zostaje bezpośrednio przeniesiona do pliku wynikowego. Po zapisaniu pliku z rozszerzeniem *.tt (lub wywołaniu funkcji Run Custom Template) silnik T4 wygeneruje plik wynikowy a w nim - Hello, World!
Szablon T4 nie do tego jednak służy. Nie będziemy przecież generować statycznych tekstów! Popatrzmy na coś bardziej skomplikowanego.
Szablony T4 i pętle
Szablonów T4 używa się przede wszystkim tam, gdzie zmuszeni byliśmy do tworzenia powtarzalnych fragmentów kodu. Jeżeli coś ma być wykonane wiele razy, stosujemy pętle. Gdybyśmy zechcieli wypisać Hello, World!
5 razy, moglibyśmy zrobić coś takiego:
<#@ output extension=".txt" #> <# for (int i = 0; i < 5; i++) { #> Hello, World! <# } #>
To samo można zrobić w nieco inny sposób:
<#@ output extension=".txt" #> <# for (int i = 0; i < 5; i++) WriteLine("Hello, World!"); #>
Oba fragmenty wygenerują taki oto plik tekstowy:
Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!
Czas na coś bardziej praktycznego. Spróbujmy wygenerować typ wyliczeniowy zawierający kolejne flagi bitowe. Można to zrobić na kilka sposobów, między innymi tak:
<#@ output extension=".cs" #> public enum States { <# for (int i = 0, bitValue = 1; i < 8; i++, bitValue <<= 1) { #> Bit<#=i#> = <#=bitValue#>, <# } #> }
Warto zwrócić uwagę, że zmienił się typ pliku wynikowego, z .txt na .cs. Po drugie, pojawił się nowy blok, blok wyrażenia. To, co stanie się wynikiem wyrażenia jest przekazywane do pliku wynikowego w postaci tekstowej. Ze zmiennej i zrobi się zatem tekst (ToString()) tworząc nazwy kolejnych pól wyliczeniowych Bit0, Bit1 i tak dalej. To samo można zrobić w nieco inny sposób:
<#@ output extension=".cs" #> public enum States { <# PushIndent("\t"); for (int i = 0, bitValue = 1; i < 8; i++, bitValue <<= 1) WriteLine("Bit{0} = {1},", i, bitValue); PopIndent(); #> }
Oba fragmenty wygenerują taki sam kod:
public enum States { Bit0 = 1, Bit1 = 2, Bit2 = 4, Bit3 = 8, Bit4 = 16, Bit5 = 32, Bit6 = 64, Bit7 = 128, }
To zwykły plik .cs, który zostaje automatycznie włączony do projektu i może być kompilowany z całą resztą programu. Mało tego, domyślnie akcja na tym pliku będzie ustawiona na Compile.
Generowanie plików skryptowych
Najprostszym sposobem przekazywania danych do szablonów jest umieszczenie ich w samym szablonie. Popatrzmy na przykład generowania klasy na podstawie kilku parametrów zdefiniowanych w samym szablonie:
<#@ output extension=".cs" #> <# string[] config = {"FirstName", "LastName", "Age:int", "Wage:decimal"}; #> public class T4Template { public int ID { get; set; } <# PushIndent("\t"); foreach (var property in config) { string[] parts = property.Split(':'); if (parts.Length > 1 ) WriteLine("public {0} {1} {{ get; set; }}", parts[1], parts[0]); else WriteLine("public string {0} {{ get; set; }}", parts[0]); } ClearIndent(); #> }
Znów, pomijając praktyczne zastosowania, przyjrzyjmy się całej konstrukcji. Definiujemy sobie listę atrybutów klasy, a na jej podstawie tworzymy klasę właściwą. Typ właściwości może, ale nie musi, być podany po dwukropku. Domyślnie będzie to string. Po transformacji otrzymamy taką oto klasę wynikową:
public class T4Template { public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public decimal Wage { get; set; } }
Można pójść o krok dalej i wygenerować od razu skrypt dla języka SQL:
<#@ output extension=".cs" #> <# string[] config = {"FirstName", "LastName", "Age:int", "Wage:decimal"}; #> public class T4Template { public int ID { get; set; } <# PushIndent("\t"); foreach (var property in config) { string[] parts = property.Split(':'); if (parts.Length > 1 ) WriteLine("public {0} {1} {{ get; set; }}", parts[1], parts[0]); else WriteLine("public string {0} {{ get; set; }}", parts[0]); } ClearIndent(); #> } /* ---Skrypt SQL--- CREATE TABLE T4Template ( ID int CONSTRAINT PK_T4Template_ID PRIMARY KEY CLUSTERED, <# PushIndent("\t"); for (int i = 0; i < config.Length; i++) { string[] parts = config[i].Split(':'); if (parts.Length > 1) Write("{0} {1}", parts[0], parts[1]); else Write("{0} nvarchar(50)", parts[0]); if (i == config.Length - 1) WriteLine(""); else WriteLine(","); } ClearIndent(); #> ) */
Tym razem otrzymamy taki oto rezultat:
public class T4Template { public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public decimal Wage { get; set; } } /* ---Skrypt SQL--- CREATE TABLE T4Template ( ID int CONSTRAINT PK_T4Template_ID PRIMARY KEY CLUSTERED, FirstName nvarchar(50), LastName nvarchar(50), Age int, Wage decimal ) */
Powyższe rozwiązanie to tylko przykład możliwości szablonu T4. Pokazane rozwiązanie wymagałoby zastosowania odpowiednich mapowań typów, bo nie zawsze istnieje tak proste przełożenie jak int-int i decimal-decimal.
Podsumowanie
Celem artykułu było pokazanie szablonów T4 i ich sposobu działania. Trzeba wiedzieć, że to dopiero początek, a prawdziwa siła drzemie w ich prawie nieograniczonych możliwościach. Taki szablon może wykonywać wszystko to, na co pozwala .NET. To między innymi czytanie konfiguracji, wykonywanie zapytań do bazy danych, budowanie klas i skryptów na podstawie mechanizmu refleksji, tworzenie plików. O tym jednak napiszę innym razem. Mam nadzieję, że tym wpisem zachęciłem do poznania tego, moim zdaniem, bardzo przydatnego narzędzia.
Brak komentarzy - bądź pierwszy