Nedávno jsem někoho slyšel nadávat, že “v .NETu není pořádný šablonovací systém a jak má sakra člověk v aplikaci vygenerovat mail nebo kus HTML”. Pravdou je, že .NET Framework, či spíše Visual Studio šablonovací systém má, a to docela pěkný. Jmenuje se T4, což je zkratka z Text Template Transformation Toolkit.
I přes to, že šablonovací systém existuje již od verze Visual Studia 2005 a samotné VS jej používá pro generování ledačeho, ne zrovna každý o něm věděl, jelikož použití z Visual Studia nebylo úplně přímočaré a většina lidí na tento šablonovací systém narazila náhodou při brouzdání v dokumentaci. To se ale změnilo s příchodem nového Visual Studia 2010, kde jsou v okně Add New Item přidány T4 šablony, stačí je tedy jen použít. V tomto článku si ukážeme, jak na to.
K čemu šablony?
Pokud jste nikdy žádný šablonovací systém nepoužívali, možná se ptáte, k čemu je to vůbec dobré. Použití je celá řada, šablonovací systém se obecně hodí na programové generování složitějších výstupů, do nichž občas potřebujeme dosazovat nějaké hodnoty. Typicky se to používá pro generování HTML, ale lze generovat například i kusy kódu.
Obecně všude, kde byste normálně napsali například následující hrůzu, byste místo toho měli použít šablonovací systém, například právě T4.
/// <summary>
/// Vygeneruje tělo e-mailu s fakturou
/// </summary>
public static string GenerateInvoiceMailBody(Invoice invoice)
{
var sb = new StringBuilder();
sb.AppendLine("<html><head><title>Faktura č. " + invoice.Number + "</title></head><body>");
sb.AppendLine("<table><tr>");
sb.AppendLine("<td><h3>Zákazník</h3><p>" + invoice.CustomerAddress.Name + "<br />" + invoice.CustomerAddress.Street + "<br />");
sb.AppendLine(invoice.CustomerAddress.City + "<br />" + invoice.CustomerAddress.ZIP + "</p></td>");
sb.AppendLine("<td><h3>Dodavatel</h3><p>" + invoice.SupplierAddress.Name + "<br />" + invoice.SupplierAddress.Street + "<br />");
sb.AppendLine(invoice.SupplierAddress.City + "<br />" + invoice.SupplierAddress.ZIP + "<p></td>");
sb.AppendLine("</tr></table><table><tr><th>Položka</th><th>Kvantita</th><th>Cena bez DPH</th><th>DPH</th><th>Cena s DPH</th></tr>");
foreach (var line in invoice.Lines)
{
sb.AppendLine("<tr><td>" + line.Name + "</td><td>" + line.Quantity + " " + line.QuantityUnit + "</td><td>");
sb.AppendLine(Math.Round(line.PriceWithTax / (1 + line.TaxRate), 2) + " Kč</td><td>" + (line.TaxRate * 100) + "%</td><td>");
sb.AppendLine(line.PriceWithTax + " Kč</td></tr>");
}
sb.AppendLine("</table><p>Vygenerováno " + invoice.CreationDate + "</p></body></html>");
return sb.ToString();
}
Výše uvedená ukázka kódu je totiž velmi velmi nepřehledná a v případě, že bude někdo někdy muset tuto šablonu upravovat nebo rozšiřovat, asi bude autora tohoto kódu proklínat či titulovat velmi neslušnými vžrazy. Smutné je, že takovéto bloky kódu se objevují v mnoha aplikacích, a navíc generují mnohem složitější výstupy. Opravujte něco v takovéto změti kódu, zvláště když je takový blok dlouhý několik obrazovek.
S použitím šablonovacího systému můžeme toto generování provést velmi elegantně a především tak, aby bylo možné časem výstup snadno upravovat, doplňovat a jinak s ním manipulovat.
Šablonovacích systémů, které tyto problémy řeší, existuje celá řada, zdaleka nejpoužívanějším “něco jako šablonovací systém” je PHP. V .NETu se jich dá použít několik, my si ukážeme T4.
T4 šablony lze použít dvojím způsobem:
- Generování kódu šablonou během kompilace projektu
- Generování za běhu programu
Přestože první možnost je poměrně zajímavá a uplatníte ji například, používáte-li Entity Framework pro přístup k datům. Díky možnosti generování kódu a jeho následného zkompilování můžete napsat vlastní šablonu, která z Entity modelu vygeneruje datové třídy.
V tomto článku se budeme zaobírat druhou možností, která se celá odehrává za běhu programu. Chceme, aby se šablona zkompilovala, abychom ji my mohli za běhu aplikace naplnit daty a nechat z ní vygenerovat vásledný text.
Začínáme
Ještě než začneme, doplním kód k předchozí ukázce, aby bylo možné příklad odzkoušet. Používáme klasickou konzolovou aplikaci, výše uvedená ošklivá metoda je uvnitř hlavní třídy Program, kde je i následující metoda:
static void Main(string[] args)
{
var invoice = new Invoice()
{
CreationDate = new DateTime(2010, 5, 3),
CustomerAddress = new Address()
{
Name = "Josef Vopršálek",
Street = "Pekelná 666",
City = "Ludrov",
ZIP = "543 21"
},
Number = "15GS639-325",
Lines = new List<InvoiceLine>()
{
new InvoiceLine() { Name = "Grafický návrh webu", Quantity = 34, QuantityUnit = "hod", PriceWithTax = 16000m, TaxRate = 0.2m },
new InvoiceLine() { Name = "Analýza řešení", Quantity = 15, QuantityUnit = "hod", PriceWithTax = 23500m, TaxRate = 0.2m },
new InvoiceLine() { Name = "Vývojářské práce", Quantity = 54, QuantityUnit = "hod", PriceWithTax = 68000m, TaxRate = 0.2m },
new InvoiceLine() { Name = "Testování a nasazení", Quantity = 12, QuantityUnit = "hod", PriceWithTax = 10000m, TaxRate = 0.2m }
},
SupplierAddress = new Address()
{
Name = "Herceg Enterprises, s.r.o.",
Street = "Náměstí G. Verdiho 16/1824",
City = "Praha",
ZIP = "123 45"
}
};
// uložit šablonu do souboru
System.IO.File.WriteAllText("d:\\test.html", GenerateInvoiceMailBody(invoice));
}
Tato metoda vytvoří testovací instanci třídy invoice a naplní ji fiktivními daty a fiktivními položkami zakázky s uvedenou cenou a počtem odpracovaných hodin.
Dále je ještě do projektu potřeba přidat deklarace třídy Invoice, InvoiceLine a Address, které jsou zde.
public class Invoice
{
public string Number { get; set; }
public Address CustomerAddress { get; set; }
public Address SupplierAddress { get; set; }
public DateTime CreationDate { get; set; }
public List<InvoiceLine> Lines { get; set; }
}
public class InvoiceLine
{
public string Name { get; set; }
public decimal PriceWithTax { get; set; }
public decimal TaxRate { get; set; }
public int Quantity { get; set; }
public string QuantityUnit { get; set; }
}
public class Address
{
public string Name { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string ZIP { get; set; }
}
Nyní bychom chtěli metodu GenerateInvoiceMailBody trochu zkultivovat a ukázat si, jak generování faktury do HTML udělat pomocí T4 šablon.
Přidání šablony do projektu
První věcí, kterou je nutno k dosažení výše uvedeného cíle učinit, je přidání T4 šablony do projektu. Pravým tlačítkem klikněte na název projektu v okně Solution Explorer a vyberte položku Add New Item, která vyvolá následující dialog. V něm najděte položku Preprocessed Text Template (stačí do hledátka napsat Text Template).
Jaký je rozdíl mezi obyčejnou Text Template a Preprocessed Text Template? Obyčejná Text Template se pro naše použití nehodí, jelikož ta generuje svůj výstup již při kompilaci aplikace. Ta se hodí například pro generování tříd z Entity Data modelu apod., ne ale pro použití za běhu. Pro práci se šablonou za běhu aplikaci se používá právě Preprocessed Text Template. Šablonu pojmenujte InvoiceTemplate.tt.
Visual Studio 2010 sice v okně Add New Item přidalo položky pro T4 šablony, nicméně v okamžiku, kdy chceme šablonu editovat a vytvářet, zjistíme, že s jejich podporou to není až tak slavné. Samotné Visual Studio totiž nad šablonami neumí IntelliSense ani zvýrazňování syntaxe, díky čemuž je práce s šablonami poměrně nepříjemná. Naštěstí tento problém řeší Tangible T4 Editor, jehož očesaná verze je k dispozici zdarma. S tímto doplňkem je práce se šablonami již celkem snesitelná. Rozšíření tedy nainstalujte a po otevření souboru šablony se zobrazí její kód, což by mělo vypadat zhruba takto. I když IntelliSense nefunguje vždy, máme alespoň zvýrazňování syntaxe.
Na prvním řádku vidíme direktivu template a její parametry – jediný určený parametr language specifikuje, že šablona bude v jazyce C#.
Syntaxe šablon
Šablona je obyčejný textový soubor. Pokud přímo do těla (pod direktivu template) napíšeme nějaký text, beze změny se při generování okopíruje na výstup. To by patrně nebylo příliš užitečné, proto uvnitř šablony mohu používat bloky kódu, existují tři druhy.
- Výraz – ohraničuje se značkami <#= a #>, může obsahovat libovolný výraz v jazyce šablony (v našem případě C#), například 5 + 5, nebo string.Format(“{0:d}”, DateTime.Now).
- Blok kódu – ohraničuje se značkami <# a #> a může obsahovat libovolný fragment kódu v jazyce šablony, například začátek podmínky či cyklu, volání metody atd.
- Deklarace – ohraničuje se značkami <#+ a #> a může obsahovat libovolnou deklaraci, například metody, vlastnosti či proměnné.
Jednotlivé bloky kódu si nyní představíme podrobněji. Naše šablona musí mít nějaký vstup, aby věděla, co bude generovat. V našem případě je vstupem šablony objekt s detaily faktury, takže nadeklarujeme vlastnost Invoice do naší šablony.
<#@ template language="C#" #>
<#+
public Invoice Invoice { get; set; }
#>
Ve chvíli, kdy projekt zkompilujeme, šablona InvoiceTemplate.tt se zkompiluje a vytvoří se třída InvoiceTemplate. Ta bude obsahovat všechny členy, které nadeklarujeme uvnitř bloků <#+ #>. V našem případě tedy tato třída bude mít vlastnost Invoice typu Invoice, do níž budeme moci nastavit fakturu, pro níž chceme generovat výstup. Je nutno podotknout, že sekce s deklaracemi musí být až na konci šablony.
Nyní doplníme základní kostru našeho generovaného souboru, kterou postupně upravíme tak, aby se do ní na příslušná místa dosadily hodnoty z vlastností objektu Invoice obsahujícího naši fakturu.
<#@ template language="C#" #>
<html>
<head>
<title>Faktura č. 15GS639-325</title>
</head>
<body>
<table>
<tr>
<td>
<h3>Zákazník</h3>
<p>
Josef Vopršálek<br />
Pekelná 666<br />
Ludrov<br />
543 21
</p>
</td>
<td>
<h3>Dodavatel</h3>
<p>
Herceg Enterprises, s.r.o.<br />
Náměstí G. Verdiho 16/1824<br />
Praha<br />
123 45
<p>
</td>
</tr>
</table>
<table>
<tr>
<th>Položka</th>
<th>Kvantita</th>
<th>Cena bez DPH</th>
<th>DPH</th>
<th>Cena s DPH</th>
</tr>
<tr>
<td>Grafický návrh webu</td>
<td>34 hod</td>
<td>13333,33 Kč</td>
<td>20,0%</td>
<td>16000 Kč</td>
</tr>
<tr>
<td>Analýza řešení</td>
<td>15 hod</td>
<td>19583,33 Kč</td>
<td>20,0%</td>
<td>23500 Kč</td>
</tr>
<tr>
<td>Vývojářské práce</td>
<td>54 hod</td>
<td>56666,67 Kč</td>
<td>20,0%</td>
<td>68000 Kč</td>
</tr>
<tr>
<td>Testování a nasazení</td>
<td>12 hod</td>
<td>8333,33 Kč</td>
<td>20,0%</td>
<td>10000 Kč</td>
</tr>
</table>
<p>Vygenerováno </p>
</body>
</html>
<#+
public Invoice Invoice { get; set; }
#>
Na určitá místa v této šabloně budeme chtít vyplnit hodnoty ze šablony, stačí tedy tato místa nahradit blokem pro výraz <#= výraz #>. Nejprve zpracujeme tabulku s adresou zákazníka a dodavatele.
<#@ template language="C#" #>
<html>
<head>
<title>Faktura č. <#= Invoice.Number #></title>
</head>
<body>
<table>
<tr>
<td>
<h3>Zákazník</h3>
<p>
<#= Invoice.CustomerAddress.Name #><br />
<#= Invoice.CustomerAddress.Street #><br />
<#= Invoice.CustomerAddress.City #><br />
<#= Invoice.CustomerAddress.ZIP #>
</p>
</td>
<td>
<h3>Dodavatel</h3>
<p>
<#= Invoice.SupplierAddress.Name #><br />
<#= Invoice.SupplierAddress.Street #><br />
<#= Invoice.SupplierAddress.City #><br />
<#= Invoice.SupplierAddress.ZIP #>
<p>
</td>
</tr>
</table>
Tím se nám do šablony dosadí hodnoty z vlastností CustomerAddress a SupplierAddress. Nyní chceme vygenerovat druhou tabulku, která vytvoří pro každý záznam z kolekce Lines v naší faktuře jeden řádek tabulky.
Na to již potřebujeme klasický blok kódu <# kód #>. Dovnitř tohoto bloku dáme jednoduchý foreach cyklus.
<table>
<tr>
<th>Položka</th>
<th>Kvantita</th>
<th>Cena bez DPH</th>
<th>DPH</th>
<th>Cena s DPH</th>
</tr>
<# foreach (var line in Invoice.Lines) { #>
<tr>
<td><#= line.Name #></td>
<td><#= line.Quantity #> <#= line.QuantityUnit #></td>
<td><#= (line.PriceWithTax / (1 + line.TaxRate)).ToString("c") #></td>
<td><#= line.TaxRate.ToString("p") #></td>
<td><#= line.PriceWithTax.ToString("c") #></td>
</tr>
<# } #>
</table>
<p>Vygenerováno <#= Invoice.CreationDate #></p>
</body>
</html>
Dále jsme oproti původnímu kódu použili místo hloupého skládání čísla a řetězce Kč formátovací funkce .NETu. Pokud metodě ToString číselného typu předáme jako parametr c, vyformátuje nám číslo jako měnu, např. 123,45 Kč. Pokud jí předáme jako parametr p, zformátuje nám hodnotu jako procenta.
Poslední věc, kterou bychom měli udělat, aby naše šablona správně fungovala, je převedení nebezpečných znaků v dosazovaných datech na HTML entity. Pokud by nám někdo dal do adresy například znak <, budeme mít poměrně vážný problém. Aby to bylo správně, je třeba nahradit znak < entitou <, znak > entitou >, a ještě pár dalších. Jednoduše to lze vyřešit například extension metodou, která HTML enkódování provede.
Extension metoda představuje způsob, jak rozšířit stávající funkcionalitu tříd, které nemůžeme upravit jinak (například protože od nich nemáme zdrojové kódy). My ji použijeme k tomu, abychom třídě String přidali metodu HtmlEncode, která jej upraví tak, aby v HTML nepůsobil potíže. Proč pomocí extension metody? Aby výrazy lépe vypadaly.
Místo <#= HttpUtility.HtmlEncode(line.Name) #> budeme moci psát <#= line.Name.HtmlEncode() #>. To je o něco přehlednější a kratší.
Vzhledem k tomu, že třída HttpUtility v .NETu, která převádění znaků na entity umí, je definována v assembly System.Web, musíme na ni do projektu přidat referenci přes volbu Add Reference v kontextovém menu. Pokud ji tam nevidíte, ujistěte se, že ve vlastnostech projektu nemáte nastavenu verzi frameworku .NET Framework 4 Client Profile, ale plný .NET Framework 4. Verze Client Profile obsahuje jen věci pro desktopové aplikace a assembly System.Web tam není.
Rozhodně si nepište funkcionalitu metody HttpUtility.HtmlEncode sami! Takových implementací jsem viděl několik, přičemž jen zlomek z nich byl kompletní a správný.
Do projektu přidáme třídu ExtensionMethods a do ní vložíme tento kód:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
namespace T4Demo
{
public static class ExtensionMethods
{
public static string HtmlEncode(this string str)
{
return HttpUtility.HtmlEncode(str);
}
}
}
Po použití této extension metody (dejte ji do stejného namespace jako je tt šablona) bude šablona vapadat takto:
<#@ template language="C#" #>
<html>
<head>
<title>Faktura č. <#= Invoice.Number.HtmlEncode() #></title>
</head>
<body>
<table>
<tr>
<td>
<h3>Zákazník</h3>
<p>
<#= Invoice.CustomerAddress.Name.HtmlEncode() #><br />
<#= Invoice.CustomerAddress.Street.HtmlEncode() #><br />
<#= Invoice.CustomerAddress.City.HtmlEncode() #><br />
<#= Invoice.CustomerAddress.ZIP.HtmlEncode() #>
</p>
</td>
<td>
<h3>Dodavatel</h3>
<p>
<#= Invoice.SupplierAddress.Name.HtmlEncode() #><br />
<#= Invoice.SupplierAddress.Street.HtmlEncode() #><br />
<#= Invoice.SupplierAddress.City.HtmlEncode() #><br />
<#= Invoice.SupplierAddress.ZIP.HtmlEncode() #>
<p>
</td>
</tr>
</table>
<table>
<tr>
<th>Položka</th>
<th>Kvantita</th>
<th>Cena bez DPH</th>
<th>DPH</th>
<th>Cena s DPH</th>
</tr>
<# foreach (var line in Invoice.Lines) { #>
<tr>
<td><#= line.Name.HtmlEncode() #></td>
<td><#= line.Quantity #> <#= line.QuantityUnit.HtmlEncode() #></td>
<td><#= (line.PriceWithTax / (1 + line.TaxRate)).ToString("c") #></td>
<td><#= line.TaxRate.ToString("p") #></td>
<td><#= line.PriceWithTax.ToString("c") #></td>
</tr>
<# } #>
</table>
<p>Vygenerováno <#= Invoice.CreationDate #></p>
</body>
</html>
<#+
public Invoice Invoice { get; set; }
#>
Čísla a datum není třeba enkódovat, jelikož nebezpečné znaky z hlediska HTML obsahovat určitě nebude. Stačí metodou HtmlEncode prohnat jen řetězce.
Spojíme to dohromady
Úspěšně jsme napsali šablonu, zbývá poslední důležitá věc, a to její naplnění a vygenerování výstupu. Obsah metody GenerateInvoiceMailBody nahradíme tímto kódem a máme hotovo.
var template = new InvoiceTemplate();
template.Invoice = invoice;
return template.TransformText();
Na prvním řádku vytvoříme instanci třídy, kterou nám šablona vygenerovala (jmenuje se stejně jako název souboru šablony). Na dalším řádku do vlastnosti Invoice nastavíme fakturu, kterou budeme generovat. Metodou TransformText pak šablonu spustíme a získáme vygenerovaný výstup.
Jak třída šablony vypadá? Není to nic extra pěkného, ale je dobré mít představu. Stačí v okně Solution Explorer otevřít soubor InvoiceTemplate.cs. Je třeba ale mít na paměti, že není radno v souboru InvoiceTemplace.cs nic měnit, jelikož při úpravě šablony bude obsah tohoto souboru přegenerován!
Vnitřek metody TransformText vypadá velmi ošklivě, podobně jako úplně první ukázka kódu v tomto článku. Rozdíl je ale v tom, že první ukázku v článku psal člověk (i když to vypadá, jako by jí psalo nějaké prase), ktežto metodu TransformText vygenerovalo Visual Studio z naší šablony. Upravit šablonu je daleko snazší a praktičtější, než upravovat nepřehledný kód z první ukázky.
Závěrem
T4 šablony umožňují snadno a rychle generovat složitější výstupy z aplikace. Kód šablony je přehledný a dá se velmi snadno upravovat. Šablona se při kompilaci převede na obyčejnou .NET třídu, jejíž vlastnosti lze naplnit a následně pro vygenerování výstupu stačí zavolat metodu TransformText.
Jedinou výjimkou, kde šablony nepoužívejte, je generování XML či RSS feedů. Na to máme v .NETu specializované třídy, například XElement či XmlWriter pro XML, anebo SyndicationFeed pro RSS, použít tyto třídy je vhodnější.
T4 najde použití při generování e-mailů, jednoduchých reportů či různých strukturovaných textových výstupů. Šablony se dají též velmi dobře využít ke generování kódu.