Spis treści:

Kategoria:C#Edytor T4


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...

Ilustracja przedstawia sposób dodawania szablonów do projektu Visual Studio
Dodawanie szablonu T4 do projektu Visual Studio

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.

Kategoria:C#Edytor T4

, 2014-05-16

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?