Dnes dopoledne jsem zase řešil jednu šílenost. Nevím, jestli je to nejjednodušší možný postup k dosažení, má to mnoho různých much a děr, ale třeba se najde pár odborníků, kteří poradí, co ještě vylepšit.
Co je cílem?
V Redwoodu, což je náš budoucí publikační systém, na kterém pracujeme s Tomášem Jechou, máme několik projektů pro webové aplikace v ASP.NET, které na Redwoodu poběží. Vzhledem k tomu, že v těchto aplikacích bude mnoho komponent společných, je samozřejmě nutné mít je v DLL knihovně, která se nareferencuje do jednotlivých webových aplikací.
Vzhledem k tomu, že některé komponenty budou mít komplexnější uživatelské rozhraní, chtěl jsem za každou cenu mít možnost vzít ASCX soubor a zakompilovat jej do DLL knihovny. To jsem ještě netušil, jaké lapálie to přinese. Standardními prostředky ASCX komponenty do DLL knihovny nedostanete, Visual Studio to nepodporuje a musíte si pomoci sami.
Pokud by vás napadlo udělat normální projekt Class Library případně ASP.NET Server Control a propašujete tam soubor ascx, ztroskotá to na tom, že se zkompiluje pouze soubor code behindu a ascx zůstane nedotčen. Možné je ascx prostě zkopírovat do všech aplikací a mít v DLLce jen code-behind, ale to se mi nelíbilo, nechci mít stejné soubory na víc místech, špatně se to aktualizuje a i kdybych to do webových aplikací dal jako link, tak při úpravě si nemusím uvědomit, kde všude se něco změní. Takže takto ne, chci prostě mít vše v DLL knihovně.
MSDN nabízí tento postup, kteréhožto jsem se víceméně držel, akorát jej zautomatizoval na standardní buildovací mechanismus Visual Studia.
Krok 1: Nachystat projekt pro komponenty
Pro aplikaci s komponentami prostě použijeme klasický projekt ASP.NET Web Application. Nebudou tam žádné stránky, jen komponenty. Tento projekt přidáme jako referenci do všech aplikací, kde jej budeme používat.
Tím mimo jiné zařídíme, že před kompilací projektů webových aplikací se bude kompilovat projekt s komponentami, aby byly respektovány závislosti mezi projekty.
Když se provádí klasická kompilace našeho projektu s komponentami, vezmou se všechny jeho soubory s kódem (ne s deklarativní syntaxí) a zkompilují se do výsledné DLL knihovny, která se jmenuje stejně jako projekt, řekněme Komponenty.dll. Třídy reprezentující komponenty jsou ale neúplné, protože obsahují jen metody v code behindu případně vlastnosti, které jste jim přidali. Šablona vzhledu definovaná v ASCX souboru se nezkompilovala a v komponentách není. V souboru Komponenty.dll jsou tedy jen jakési “kostry” komponent, které nám nestačí, potřebovali bychom k nim ještě ony ASCX soubory.
Pro ilustraci - udělal jsem do projektu komponentu Login.ascx, v souboru code behindu je jen prázdná metoda Page_Load a v ASCX souboru je tlačítko. Když se ale v Reflectoru podíváme na soubor Komponenty.dll, uvidíme tam jen toto.
Je tam zkrátka prázdná metoda Page_Load, vnitřní proměnná Button1 reprezentující tlačítko, ale nikde se nám nezkompilovala deklarativní šablona v souboru ASCX, nikde tam nemáme kód, který by tlačítko vytvořil a umístil do stránky.
Krok 2: Po buildování projektu s komponentami vytvořit Precompiled Website
Abychom dostali plné třídy komponent včetně jejich vzhledu, musíme provést takový trik. Pomocí aplikace aspnet_compiler si předkompilujeme projekt s komponentami. Standardně to neděláme, protože když se aplikace nasadí na server, při prvním požadavku na stránku nebo na komponentu se webserver podívá, jestli má tuto komponentu či stránku zkompilovanou celou, a pokud ne, má k dispozici soubory ASCX a ASPX, díky kterým si zkompiluje vzhled stránky a pak s ní teprve pracuje. Má tu to výhodu, že když potřebujeme na běžící aplikaci udělat nějakou drobnou úpravu (např. opravit překlep v nadpisu), nemusíme překompilovávat celou DLLku. Samozřejmě pokud do ASPX souboru na serveru přidáme komponentu, tak už je nutné celý web překompilovat, aby se opravila definice třídy.
Pokud předkompilaci spustíme ručně, projdou se tím všechny stránky a komponenty a zkompilujemí se rovnou. Každá se umístí do své vlastní assembly, tyto assembly mají název ve tvaru App_Web_komponenta.ascx.hash.dll. Místo komponenta se dosadí název komponenty, na místě hash je hodnota, která rozlišuje mezi dvěma komponentami, které by měly stejný název, akorát ležely v jiných složkách. Vzhledem k tomu, že všechny soubory dll leží ve složce jedné, je nutné pro ně mít unikátní názvy.
Vedle složky s projektem komponent si v adresáři naší solution vytvoříme složku Temp a ve vlastnostech projektu s komponentami na záložce BuildEvents přidáme do textového pole Post Build tyto dva řádky.
if exist $(SolutionDir)Temp rmdir /S /Q $(SolutionDir)Temp
c:\Windows\Microsoft.NET\Framework\v2.0.50727\aspnet_compiler.exe -p $(ProjectDir) -v $(ProjectName) -fixednames $(SolutionDir)Temp
První se podívá, jestli vedle složky projektu existuje složka Temp a pokud ano, celou ji smaže (předkompilace ASP.NET website vyžaduje prázdnou složku, do které web zkompiluje). Druhý řádek zavolá aspnet_compiler a s příšlušnými argumenty vytvoří předkompilovanou website. Pro každou komponentu ascx vytvoří ve složce bin vlastní assembly.
Kromě toho aspnet_compiler do složky bin nakopíruje obsah složky bin z původního adresáře projektu, kde bude naše standardní cestou zkompilovaná knihovna Knihovna.dll. Pokud bychom do projektu s komponentami přidali nějaké další samostatné třídy, budou právě v této knihovně.
Krok 3: Sloučení všech assemblies do jedné velké
Do Post Build sekce přidejte ještě tento třetí řádek, který zařídí, že se všechny assemblies ve složce Temp\Bin sloučí do jedné. Tady je pár problémů, o kterých se hned zmíním. Jinak aby to fungovalo, musíte mít nainstalován nástroj ILMerge, který slouží pro slučování .NETích knihoven.
"c:\Program Files\Microsoft\ILMerge\ilmerge.exe" /t:library /allowDup /wildcards /out:$(TargetPath) $(SolutionDir)Temp\bin\App_Web*.* $(SolutionDir)Temp\bin\$(TargetFileName)
Tímto řádkem zavoláme ILMerge, prvním parametrem mu říkáme, že chceme jako výstup knihovnu.
Parametr allowDup říká, že máme povolit duplicitní datové typy. To je potřeba, protože v assemblies App_Web… jsou plné definice typů komponent, které již můžeme používat. Knihovna Knihovna.dll je obsahuje také, ale v těchto třídách, jak už víme, není vše. Datové typy se stejným názvem nelze samozřejmě sloučit, protože kromě názvu spolu nemusí mít vůbec nic společného. Tento parametr zařídí, že duplicitní datové typy se přejmenují tak, aby žádné názvové kolize nebyly. Vzhledem k tomu, že na konci tohoto řádku uvádíme seznam knihoven které se slučují a soubor Knihovna.dll je až poslední, duplicity se najdou až při zpracovávání tohoto posledního souboru a budou přejmenovány neúplné třídy komponent z tohoto souboru. Třídám z assemblies App_Web…, které jsou pro nás důležité, názvy zůstanou, což je to, co chceme.
Parametr wildcards říká, že při hledání názvů knihoven se mají použít wildcardy, tzn. znaky * a ?. To také potřebujeme, protože DLL knihoven App_Web… je víc (pro každou komponentu jedna) a dají se matchnout najednou.
Parametr out říká cestu k výstupní sloučené assembly, která ukazuje na výstupní cestu kompilace projektu. Takže Knihovna.dll, která se vygenerovala standardní kompilací ještě před tím, než se spustily PostBuild příkazy, bude přepsána výstupem z ILMerge.
Protože máme reference z projektů webových aplikací na tento projekt, před jejich kompilací se nově vytvořená Knihovna.dll zkopíruje do adresáře bin v příslušné webové aplikaci a bude tam tedy správný soubor.
Krok 4: Jedna rada k namespaces komponent
Když si do projektu s komponentami přidáme třeba komponentu Login, bude její direktiva Control vypadat příkladně takto:
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Login.ascx.cs" Inherits="Redwood.Plugin.WebControls.Login" %>
Všimněte si především posledního parametru Inherits. Ten je třeba změnit na ClassName (hodnotu ponechat), aby se komponenta zkompilovala jako třída s daným názvem. Pokud bychom tam nechali Inherits, naše komponenta by se zkompilovala jako třída, která z uvedené třídy dědí, ale samotná by měla název login_ascx a byla by v namespace ASP, což samozřejmě nechceme. Změnou vlastnosti Inherits na ClassName docílíme toho, že naše komponenty budou mít přesně daný název, který tato vlastnost specifikuje.
A tady je ukázka, jak pak vypadá správná třída s komponentou obsahující i svůj vzhled. vidíme tam metody, které si vygeneroval kompilátor ASP.NET a kdybychom se na ně podívali uvidíme metodu, která vytvoří novou komponentu Button a vrazí ji do stránky.
Co dodat?
To je vše. Kdybych věděl, že s tím bude taková práce a laborování s příkazovou řádkou, tak bych se na to asi vykašlal a těch pár komponent napsal bez ASCX části. Ale když už to mám a funguje to …
Osobně nechápu, proč tohle prostě nejde udělat jednoduše. Oficiálním argumentem je to, že komponenty, které jsou natolik obecné, že se je vyplatí sdílet mezi aplikacemi, možnosti ASCX deklarativní části nevyužijí a stačí jim prostě třída v C# nebo VB.NET. Je to pravda, drtivá většina ASCX komponent, které jsem napsal, se týkala konkrétní webové aplikace a obecně použitelné nebyly.
Obecně použitelné komponenty, které se opravdu vyplatí sdílet, jsou takové ty utilitky, které se hodí, ale v ASP.NET nejsou. Třeba HyperLink, který umí zobrazit jak text, tak obrázek, nebo ButtonField pro GridView, který se před smazáním záznamu umí zeptat hláškou pomocí klientského skriptu, případně Label, který zobrazí maximálně N znaků a pokud je text delší, tak zobrazí jen jeho začátek a tři tečičky. To jsou prkotiny, které ale v ASP.NET chybí a každý, kdo s touto technologií trochu dělá, si jistě pár takových kousků napsal. Na jejich implementaci ASCX potřeba není.
Na druhou stranu občas se to prostě hodí, máte nějakou komplexnější komponentu (třeba přihlašovátko, které umí klasické ověření pomocí jména a hesla, OpenID, LiveID a CardSpace). Tam se ASCX vyloženě hodí a dělat to bez něj by celou komponentu znepřehlednilo a zkomplikovalo.
Nechce se někomu z tohoto návodu udělat ProjectTemplate pro projekt s komponentami, aby se to dalo používat i obecně a nemusel se s tím člověk štvát?