NullReferenceException v DefaultWsdlHelpGenerator.aspx při přístupu k webovým službám

Vytváříme klasickou webovou službu publikovanou pomocí ASMX. Při pokusu o přístup k této webové službě přes internetový prohlížet obdržíme výjimku NullReferenceException – a to ještě žádnou metodu nevoláme, jen prohlížíme dostupné služby.
Debugger se nám zastavuje v souboru DefaultWsdlHelpGenerator.aspx na řádku 1335:

OperationBinding FindHttpBinding(string verb) {
        foreach (ServiceDescription description in serviceDescriptions) {
            foreach (Binding binding in description.Bindings) {
                HttpBinding httpBinding = (HttpBinding)binding.Extensions.Find(typeof(HttpBinding));

Vskutku pozoruhodné je pak řešení problému: Ve web.configu máme v sekci <pages> nastavení autoEventWireUp=“false“. Po odstranění tohoto nastavení přístup k dokumentaci webových služeb funguje.
DefaultWsdlHelpGenerator.aspx je generátor dokumentace pro zobrazení v prohlížeči. Ten se evidentně kompiluje s použitím nastavení aplikace a v kódu spoléhá na zavolání metody Page_Load pomocí automatického navázání vybraných událostí (AutoEventWireUp). K dispozici máme i zdrojový kód tohoto souboru, standardně se nachází v C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG.
(Runtime .NET Framework 2.0)

Workaround

Na vývojářské mašině se dá taky rovnou zeditovat soubor C:\Windows\Microsoft.NET\Framework\v2.0.50727\CONFIG\DefaultWsdlHelpGenerator.aspx a na první řádek přidat:

<%@ Page AutoEventWireup="true" %>

Money – kouzlíme s generiky II.

Generika jsou dobrá nejenom pro kolekce. Že se dají velmi dobře využít i v jiných situacích, dnes předvedu na třídě Money  (diskuze, kdy má být Money třída a kdy je lepší struct není předmětem tohoto článku). Uvidíte, že jdou dělat i takové věci jako generické operátory.

Zadání problému

Chceme řešit imlementaci třídy Money, třídy reprezentující peněžní datový typ skládající se z částky (Amount) a její měny (Currency). Naším cílem je dosáhnout následujího:

  • chceme zavést opakovaně využitelnou třídu Money (typicky do vlastní class-library) – ta má obsahovat vlastnost Amount (částku typu decimal) a vlastnost Currency (měnu), jejíž typ obecně naznáme a chceme ho ponechat na libovůli klientské aplikace (známe však pravidla pro obecné Money, které nám říkají, že bez kurzových převodů můžeme běžné operace provádět jen mezi operandy stejné měny – porovnávání, sčítání/odečítání, násobení/dělení číslem, dělení mezi sebou, atp.). Do class-library tedy chceme implementovat třídu Money<TCurrency>.
  • implementaci veškeré logiky chceme mít součástí této třídy Money<TCurrency> – v této třídě chceme definovat potřebné operace a centrálně je udržovat v class-library, chceme mít pokryté všechny základní operátory, atp. atp.
  • v jednoduchých svých projektech zadefinujeme třídu Currency (obvykle business-object odpovídající číselníku měn v DB s vlastnostmi Nazev, Zkratka, atp.) a budeme v nich třídu Money<TCurrency> používat přímo v kódu
Money<Currency> cena = new Money(150.20M, Currency.Czk);
cena = cena + new Money(20M, Currency.Czk);
cena = cena * 10;
MyLabel.Text = cena.Amount.ToString("n2") + " " + cena.Currency.Zkratka;
...
  • v rozsáhlejších našich projektech máme také třídu Currency, chceme však vytvořit odvozenou negenerickou třídu Money, která bude základní funkčnost obecnějšího Money<TCurrency> rozšiřovat o nějaké nové project-specific možnosti, např. ony kurzové přepočty, standardní výstup, atp. Použití zamýšlíme nějak takto:
Money cena = new Money(10M, Currency.Czk);
cena = cena.ToCurrency(Currency.Usd) + new Money(159.3M, Currency.Usd);
MyLabel.Text = cena.ToString("n2");
...
  • stále však trváme na tom, že implementace běžných operací (zejména operátorů) musí být použita z obecného Money<TCurrency>. Uvědomme si však základní problém – jak se vyhnout nové definici operátorů v negenerickém potomkovi Money : Money<Currency>, když operátory jsou statické, nejsou předmětem dědičnosti a v Money<TCurrency> budou vypadat nejspíš nějak takto:
public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
// implementace
}

…v potomkovi Money chceme přeci součet dvou Money a hlavně návratový typ Money, nikoliv Money<Currency>

public static Money operator +(Money money1, Money money2)
{
// znovu implementace??? Tomu jsme se chtěli vyhnout! Implementaci chceme mít v class-library.
}
Kouzlo první – generická implementace funkčnosti operátorů

Čeho chceme dosáhnout – mít centralizovanou implementaci funkčnosti operátorů tak, abychom ji mohli snadno udržovat pro všechny naše projekty. Kouzlo první spočívá v extrahování implementace operací do generických metod třídy Money<TCurrency>, kteréžto metody sama třída Money<TCurrency> ve svých operátorech využije, stejnětak jako je může využít libovolný potomek při definici svých typovaných operátorů.

Money<TCurrency>:

public static Money<TCurrency> operator +(Money<TCurrency> money1, Money<TCurrency> money2)
{
    return SectiMoney<Money<TCurrency>>(money1, money2);
}

public static TResult SectiMoney<TResult>(Money<TCurrency> money1, Money<TCurrency> money2)
    where TResult : Money<TCurrency>, new()
{
    if (money1.Currency != money2.Currency)
    {
        throw ...
    }

    TResult result = new TResult();
    result.Amount = money1.Amount + money2.Amount;
    result.Currency = money1.Currency;
    return result;
}

Obdobně jako u BusinessObjectCollection využíváme kouzla generika s „cílovým typem“. Daní za tento způsob je nám nutnost vyžadovat constructor bez parametrů, abychom v generiku byly schopni vytvořit instanci cílového typu (možnost generika s constructorem předepsaných parametrů se asi v dohledné době nedočkáme, v .NET 3.5 pokud vím, nic takového stále není a po diskuzi s částí CLR teamu na letošním MVP Summitu v Redmondu už i chápu proč).

Project-specific potomek Money už pak může využívat této centrální implementace snadno:

public static Money operator +(Money money1, Money money2)
{
    return SectiMoney<Money>(money1, money2);
}
Kouzlo druhé – projektový potomek Money bez nové implementace operátorů

Řekněme, že se chceme v projektech, v project-specific třídách Money, úplně zbavit nutnosti implementovat operátory, byť už máme cestu jak v těchto operátorech využít centrální implementace funkčnosti. Projektový kód chceme mít co nejčistší, prostý takovýchto infrastrukturních redefinic, tak, abychom se v něm mohli přehledně věnovat jen nové funkčnosti Money oproti předkovi Money<TCurrency> – např. doplnit ony omílané kurzové přepočty, atp.

Jak na to?

Do řetězce dědičnosti mezi Money a Money<TCurrency> vložíme kouzelnou třídu MoneyImplementationBase, která nás dalším fíglem s generiky a operátory posune ke kýženému výsledku…

public abstract class MoneyImplementationBase<TCurrency, TResult>: Money<TCurrency>
    where TCurrency: class
    where TResult : MoneyImplementationBase<TCurrency, TResult>, new()
{
    public static TResult operator +(MoneyImplementationBase<TCurrency, TResult> money1, MoneyImplementationBase<TCurrency, TResult> money2)
    {
        return SectiMoney<TResult>(money1, money2);
    }
}

Tato třída samozřejmě patří do naší centrální class-library odkud ji budou využívat jednotlivé projekty.

A výsledek? Plně funkční projektové Money pak dostaneme takto:

public class Money : MoneyImplementationBase<Currency, Money>
{
    // toť vše!
    // sem už můžeme doplnit jen project-specific rozšíření Money, veškerou základní funkčnost nám poskytují předci
}

Hezké, že? Stejně jako u kouzlení s BusinessObjectCollection předhazujeme generickému předkovi sami sebe (pokud to bude potřeba, můžeme předhodit i svého potomka).

BusinessObjectCollection – kouzlíme s generiky

Po opakovaných přáních poodkrýt zákoutí našeho frameworku a generované business-vrstvy jsem se rozhodl zveřejnit vybraná témata a perličky. Dnes přicházím s BusinessObjectCollection, resp. s možným způsobem implementace kolekcí v business-vrstvě pomocí generik tak, abychom dosáhli maximální kompatibility typů a nemuseli bojovat s neustálou typovou neshodou kolekcí.

Čeho chceme u kolekcí business-objektů dosáhnout:

  1. Chceme jednu společnou bázovou třídu BusinessObjectCollection pro všechny kolekce business-objektů a v ní centrálně implementovat operace, které jsou společné pro všechny business-objekty. Pro příklad tam dáme primitivní metodu SaveAll(), která všechny business-objekty v kolekci uloží a její implementace je stejná, ať už se jedná o kolekci Faktur, nebo kolekci Subjektů.
  2. V bázové třídě BusinessObjectCollection chceme definovat i operace, které pracují přímo s business-objekty, pokud možno strong-type, typem co nejpřesnějším. Např. běžné Add(Order order), Remove(Subject subject), atp.
  3. V bázové třídě BusinessObjectCollection chceme definovat i operace, které pracují s kolekcemi business-objektů, pokud možno strong-type, typem kolekce co nejpřesnějším. Např. FindAll(), který vrací kolekci všech nalezených prvků odpovídajících nějaké podmínce, atp.
  4. Od této společné bázové abstraktní třídy BusinessObjectCollection chceme děděním vytvářet konkrétní potomky – třídy kolekcí pro jednotlivé business-objekty, např. OrderCollection, SubjectCollection, UserCollection, atp.

Jak tedy na to? Nejprve si zavedeme základní třídy, které budu v dalších příkladech používat:

public abstract class BusinessObjectBase
{
    public abstract void Save();
}

public class Order : BusinessObjectBase
{
    public decimal Cena { get; set; }
    public override void Save()
    {
        // implementace
    }
}

BusinessObjectBase je bázová třída pro všechny business-objekty, Order je příklad konkrétní implementace.

Způsob 1. – základní

Nejprve si ukážeme, jak vypadá běžná implementace kolekcí:

public abstract class BusinessObjectCollection<T> : Collection<T>
    where T : BusinessObjectBase
{
    public BusinessObjectCollection()
        : base(new List<T>())
    {
    }

    public void SaveAll()
    {
        foreach (BusinessObjectBase item in this.Items)
        {
            item.Save();
        }
    }

    public virtual List<T> FindAll(Predicate<T> match)
    {
        List<T> innerList = (List<T>)Items;
        List<T> found = innerList.FindAll(match);
        return found;
    }
}

public class OrderCollection : BusinessObjectCollection<Order>
{
    public decimal GetCelkovaCena()
    {
        decimal result = 0;
        // implementace
        return result;
    }
}

Co tato implementace ukazuje:

  • Jako úplný základ používáme generickou kolekci Collection<T>, která samotná už nám přináší všechny základní strong-type operace, např. Add(), Contains(),IndexOf(), Insert(), Remove(), RemoveAt() a indexer přes pořadí this[int].
  • Třída Collection<T> je implementačně pouze wrapperem nějaké vnitřní kolekce, kterou skrývá pod property protected IList<T> Items. Vnitřní implementaceCollection<T> používá jako tento datový nosič List<T>, v kontraktu této třídy to však popsáno není. Chceme-li tedy zaručit, že naše prvky budou fyzicky uloženy v datové struktuře typu List<T>, můžeme využít protected constructor odCollection<T>, který umožňuje předat instanci požadované datové struktury, která musí implementovat IList<T>. Nám se hodí právě List<T>, který zahrnuje spoustu dalších užitečných metod, které budeme chtít naše business-kolekce také naučit. V příkladu využíváme List<T>.FindAll().
  • Metoda SaveAll() provádí určitou operaci s prvky kolekce, její interface je vyhovující, není co namítat.
  • Metoda FindAll() ale už není tak krásná, jejím návratovým typem je totiž List<T> a tím se nám klientský kód pěkně zamotá:
OrderCollection orders = Order.GetAll();

List<Order> zeroOrders = orders.FindAll(delegate(Order item)
{
    return (item.Cena == 0);
});

zeroOrders.SaveAll(); //nejde
decimal d = zeroOrder.GetCelkovaCena(); //nejde
OrderCollection zeroOrders2 = (OrderCollection)zeroOrders; // nejde

Návratovým typem FindAll() je List<Order>, s kterým přicházíme o veškerou logiku, kterou jsme business-kolekce naučili, jak v bázové třídě BusinessObjectCollection, tak v konkrétní tříděOrderCollection.

Způsob 2. – Drobné vylepšení základní metody

Zlepšení na poli typů dosáhneme, pokud implementaci naší BusinessObjectCollection<T> upravíme takto:

public abstract class BusinessObjectCollection<T> : Collection<T>
    where T : BusinessObjectBase
{
    public BusinessObjectCollection()
        : base(new List<T>())
    {
    }

    public void AddRange(IEnumerable<T> source)
    {
        List<T> innerList = (List<T>)Items;
        innerList.AddRange(source);
    }

    public virtual BusinessObjectCollection<T> FindAll(Predicate<T> match)
    {
        List<T> innerList = (List<T>)Items;
        List<T> found = innerList.FindAll(match);

        BusinessObjectCollection<T> result = new BusinessObjectCollection<T>();
        result.AddRange(found);
        return result;
    }

    public void SaveAll()
    {
        foreach (BusinessObjectBase item in this.Items)
        {
            item.Save();
        }
    }
}
  • Metodu FindAll() jsme trošku typově vylepšili, namísto obecného List<T> ji necháme vracet alespoň datový typ BusinessObjectCollection<T>. Protože však musíme instanci BusinessObjectCollection<T> nějak získat, nezbývá než si založit novou a pomocí další vypropagované metody List<T>.AddRange() do ní prvky zkopírovat. Je to nepochybně určitý výkonový overhead, nicméně vzhledem k tomu, že kopírujeme pouze reference na existující instance jednotlivých business-objektů, jedná se o overhead zanedbatelný a naopak převáží výhoda příjemného vylepšení interface kolekcí.
  • Stále však nemůžeme být spokojeni:
OrderCollection orders = Order.GetAll();

BusinessObjectCollection<Order> zeroOrders = orders.FindAll(delegate(Order item)
{
    return (item.Cena == 0);
});

zeroOrders.SaveAll(); //HURÁ! Funguje!
decimal d = zeroOrders.GetCelkovaCena(); // stále nefunguje
OrderCollection zeroOrders2 = (OrderCollection)zeroOrders; // nejde

Stále nedostáváme od metody FindAll() kolekci typu OrderCollection. Sice jsme získali možnost využít operací implementovaných v BusinessObjectCollection, jako např. SaveAll(), stále však nedosáhneme na operace implementované v OrderCollection, nemáme GetCelkovaCena().

Způsob 3. – Kouzlíme s generiky

Generika v .NET Frameworku jsou udělána dobře a snesou všechno. Opakovaně jsem sám u generik předpokládal, že „tohle už přeci nemůže jít“. Generika opravdu unesou hodně:

public abstract class BusinessObjectCollection<TItem, TCollection> : Collection<TItem>
    where TItem : BusinessObjectBase
    where TCollection : BusinessObjectCollection<TItem, TCollection>, new()
{
    public BusinessObjectCollection()
        : base(new List<TItem>())
    {
    }

    public void AddRange(IEnumerable<TItem> source)
    {
        List<TItem> innerList = (List<TItem>)Items;
        innerList.AddRange(source);
    }

    public virtual TCollection FindAll(Predicate<TItem> match)
    {
        List<TItem> innerList = (List<TItem>)Items;
        List<TItem> found = innerList.FindAll(match);

        TCollection result = new TCollection();
        result.AddRange(found);
        return result;
    }

    public void SaveAll()
    {
        foreach (BusinessObjectBase item in this.Items)
        {
            item.Save();
        }
    }
}

public class OrderCollection : BusinessObjectCollection<Order, OrderCollection>
{
    public decimal GetCelkovaCena()
    {
        decimal result = 0;
        // implementace
        return result;
    }
}

Co jsme to udělali?

  • Definici třídy BusinessObjectCollection jsme rozšířili o další generický typ naBusinessObjectCollection<TItem, TCollection>, kde TItem je typ prvků kolekce aTCollection je typ kolekce samotné!
  • Pomocí generického typu TCollection můžeme nyní postavit metodu FindAll tak, že jejím návratovým typem bude TCollection, tedy požadovaný typ kolekce.
  • Získáváme plně typově konzistentní kolekci OrderCollection, jejíž všechny metody používají typ Order jako typ prvku a typ OrderCollection jako typ kolekce!
  • Dostáváme jednoduchý snadno použitelný interface:
OrderCollection orders = Order.GetAll();

OrderCollection zeroOrders = orders.FindAll(delegate(Order item)
{
    return (item.Cena == 0);
});

zeroOrders.SaveAll(); //HURÁ! Funguje!
decimal d = zeroOrders.GetCelkovaCena(); //HURÁ! Funguje!
// přetypovávat už ani nepotřebujeme
Způsob 4. – A jak je to u nás?

Generika snesou hodně, opravdu hodně. U nás je to takto:

  • V našich class-libraries (HAVIT Framework Extensions) máme abstraktní předkyBusinessObjectBase a BusinessObjectCollection<TItem, TCollection>. Tyto třídy definují operace společné pro všechny business-objekty, na všech našich projektech, prostě úplný základ, jádro.
  • Na konkrétních projektech pak používáme generování základního kódu business-tříd, tedy business-objektů i business-kolekcí, na základě vstupního schematu (např. DB diagramu). Generovaný kód umísťujeme do bázových tříd OrderBase aOrderCollectionBase, zatímco vlastní kód zapisujeme až do potomků těchto třídOrder a OrderCollection.
  • Při prvním spuštění generátor vygeneruje třídy všechny (třídy Order aOrderCollection vygeneruje prázdné), zatímco při dalších spuštěních generátor přepisuje třídy OrderBase a OrderCollectionBase (náš „dopsaný“ kód tak zůstává nedotčen).
  • Výsledný kód tříd vypadá nějak takto:

Nejprve business-objekt Order:

public abstract class OrderBase : BusinessObjectBase
{
    // toto je generovaná třída, generátor ji vždy přepíše, nic se sem nesmí ručně dopisovat
    public decimal Cena { get; set; }
    public override void Save()
    {
        // implementace
    }
}

public class Order : OrderBase
{
    // toto je používaná třída, sem mohu zapsat vlastní kód

    public decimal GetCenaPoSleve()
    {
        // implementace
    }
}

Dále business-kolekce OrderCollection:

public abstract class OrderCollectionBase : BusinessObjectCollection<Order, OrderCollection>
{
    // toto je generovaná třída, generátor ji vždy přepíše, nic se sem nesmí ručně dopisovat
}

public class OrderCollection : OrderCollectionBase
{
    // toto je používaná třída, sem mohu zapsat vlastní kód
    public decimal GetCelkovaCena()
    {
        decimal result = 0;
        // implementace
        return result;
    }
}

Ano! Generika umožňují dokonce v předkovi OrderCollectionBase používat jako TCollection odkaz na vlastního potomka OrderCollection.

Pokračování

Start Command Prompt Here na složce

  1. Průzkumník
  2. Nástroje ~ Možnosti složky…
  3. Typy souborů
  4. Najít „Složka“, „Folder“, nebo něco takového, co odpovídá verzi OS
  5. Upřesnit
  6. Nová… (akce)
  7. Akce: „Start Command Prompt Here“, Aplikace: „cmd.exe“
  8. OK, OK, OK, OK …

Vista: Nelze nainstalovat síťovou tiskárnu z Windows XP, chyba 0x00000035

Windows Vista Vás při pokusu o instalaci síťové tiskárny hostované na stroji Windows XP mohou obdařit elegantní chybovou hláškou o nemožnosti instalace s chybovým kódem 0x00000035. Obecně se jedná o chybu nenalezení síťové cesty, a to je i většinou problém Windows Vista při instalaci tiskárny. Windows Vista se totiž snaží tiskárnu nainstalovat nikoliv pomocí UNC share názvu, např. \\HAVIT\Printer1, ale pomocí názvu tiskárny, něco jako \\HAVIT\HP LaserJet P2015 PCL6, což se nepodaří.

Dá se postupovat jednou z následujících cest:

  • Tiskárny, Přidat novou tiskárnu, Síťovou tiskárnu, Není zobrazena, dle názvu = \\HAVIT\Printer1
  • Tiskárnu, Přidat novou tiskárnu, Lokální, Nový port, Název portu = \\HAVIT\Printer1  (a nebo jiný způsob přidání portu, např. přes Vlastnosti serveru)

Avšak pozor, ani tento postup mně nepomohl, při pokusu o přidání portu jsem dostával hlášku Přístup odepřen, přestože jsem byl k hostitelskému počítači přihlášen s právy administrátora. Pomohlo na dobu instalace dát na tiskárně všechna práva skupině Everyone. Po úspěšné instalaci ve Vistách jsem práva zase odebral a nechal jen Tisk a vše v pohodě chodí.

Firefox velmi pomalý při použití Windows Vista a ASP.NET Development serveru

Načítání webových stránek je pod Firefoxem v kombinaci s Windows Vista a ASP.NET Development serverem velmi pomalé. První podezření padlo na ASP.NET AJAX pod Firefoxem, nicméně pravda se ukázala být jinde.

Za všechno může automatické ladění TCP stacku ve Windows Vista. Na internetu jsem nalezl dvě řešení:

Zákaz automatického ladění TCP stacku…

… lze provést spuštěním „netsh interface tcp set global autotuninglevel=disabled“ z příkazové řádky.

Toto řešení jsem nezkoušel.

Zákaz IPv6 ve Firefoxu

Dle návodu pomůže ve Firefoxu:

  1. přejít na stránku about:config,
  2. vyhledat položku network.dns.disableIPv6,
  3. nastavit hodnotu na true.

Toto řešení bylo jednoduché a účinné.

Jaká je Vaše zkušenost s kombinací Firefoxu, Windows Vista a ASP.NET Development serveru? Nebo jste na stejný problém narazili i při jiné kombinaci produktů?

Programátorská hádanka – catch & throw

Jaký je rozdíl v prapagaci výjimky z bloku catch:

try
{
...
}
catch (Exception e)
{
   throw;
}

versus

try
{
...
}
catch (Exception e)
{
   throw e;
}

Odpověď je tentokrát jednoduchá a jako obvykle ji najdete o řádku níže napsanou bílým písmem (pro odtajnění třeba označte):

>>> Při vyhození výjimkt přes „throw;“ se nezmění stack trace – výjimka se stále tváří, že vzešla z původní metody. Při vyhození přes „throw e;“ je změněn stack trace, výjimka se tváří, že vzešla z naší metody obsluhující výjimku. <<<

Css styly a obrázky nefungují v IIS7

Instalace IIS7 ve Windows Vista obsahuje více možností než tomu bylo dříve. V dobré víře jsem si zvolil nástroje pro správu a podporu ASP.NET v minimalistické podobě.

Díky podpoře nainstalovené podpoře ASP.NET fungovaly webové aplikace, ovšem statický obsah (jako jsou právě obrázky nebo soubory kaskádových stylů) prohlížeč neobdržel. Při zadání libovolné URL na statický obsah (například i adresa k obrázku, který ani na disku není) prohlížeč nezobrazí nic (i zdrojový kód stránky je prázdný).

Aby IIS7 poskytoval statický obsah, je nutné mezi instalovanými komponentami IIS zvolit i „Statický obsah“.

Programátorská hádanka – Výjimka ve výjimkách

Jaký je rozdíl v zachytávání výjimek při použití typu výjimky Exception

try
{
...
}
catch (Exception e)
{
...
}

a bez použití tohoto typu, tedy

try
{
...
}
catch
{
...
}

Zdůrazňuji, že tento rozdíl existuje jen v .NET Frameworku 1.x, ve verzi 2.0 jsou způsoby funkčně rovnocenné.

Odpověď najdete o řádku níže napsanou bílým písmem (pro odtajnění třeba označte):

>>> Konstrukce catch (Exception e) zachytává jen CLS-compliant výjimky, catch zachytává všechny chyby. Praktický rozdíl je při zachytávání chyb z COM objektů, jejichž chyby nejsou CLS-compliant výjimkami. .NET Framework 2.0 tyto chyby z COM objektů zabalí do RuntimeWrappedException, které jsou CLS-compliant, takže je chyba zachycena i při použití catch (Exception e).  <<<

…a jako posledně: teď se přiznejte, kdo jste to znal!

Lokalizace skinů (Themes)

Nevím jak vás, ale mně už delší dobu rozčiluje, že skiny nejsou lokalizovatelné. Pánové z Microsoftu na to buď zapomněli, nebo nevím, každopádně se tak skiny staly do značné míry nepoužitelnými pro lokalizované aplikace.

Štvalo mě to tak dlouho, až jsem napsal vlastní lokalizaci skinů. Není to nic překrásného, protože jen máloco je v ASP.NET tak zapouzdřeno jako témata a skiny a tak se kód hemží reflexí – nicméně to výborně funguje k tomu účelu, na který to sám potřebuji – do lokalizovaných webových aplikací s běžným provozem bez masové zátěže.

Optimalizace opět ponechám na každém z vás, stejnětak případný příhodnější způsob integrace do ASP.NET. Níže popisuji jednoduché řešení, jehož účelem je pochopení použitého způsobu lokalizace skinů. Taktéž předesílám, že se jedná o explicitní metodu lokalizace, protože implicitní lokalizaci nepovažuji za obecně vhodnou.

Jak lokalizace skinů funguje

První, co každý asi zkusí, je použít ve skinu běžnou expression <%$ Resources: MyResources, MyValueName %>. ASP.NET Vás odmění krásnou chybovou hláčkou parseru: „Expressions are not allowed in skin files.“ Tudy cesta nevede…

Inspiroval jsem se tedy v syntaxi lokalizace, kterou používá XmlSiteMapProvider pro web.sitemap soubory, tedy např.:

&lt;siteMapNode url=&quot;~/default.aspx&quot; title=&quot;$resources:MyResources,MyResourceKey,My default value&quot; /&gt;

a řekl jsem si, že stejnou syntaxi bych mohl použít i pro lokalizaci skinů:

&lt;asp:Button Text=&quot;$resources: MyResource, MyValue, Default Value&quot; runat=&quot;server&quot; /&gt;
&lt;asp:Button SkinID=&quot;SkinWithDefault&quot; Text=&quot;$resources: MyResource, InvalidResource, Default Value&quot; runat=&quot;server&quot; /&gt;

…samozřejmě se tím odepře možnost použití textů začínajících znaky „$resource:“, ale to mi nevadí.

Teď ještě vymyslet, kde a jak v životním cyklu stránky zajistit odchytávání a vyhodnocování těchto lokalizačních výrazů. Po chvilce bádání s Reflectorem lze dospět k následujícím poznatkům:

  1. téma stránky je uložena v private fieldu Page._theme typu PageTheme
  2. stylesheet-téma stránky je uloženo v private fieldu Page._styleSheet typu PageTheme
  3. témata se inicializují (InitializeThemes) po ve fází PreInit životního cyklu stránky, avšak až po události PreInit (v obsluze události PreInit tedy zřejmě není možné témata modifikovat),
  4. témata se aplikují ve fázi Init životního cyklu stránky, avšak před událostí Init (v obsluze události Init je tedy pozdě je modifikovat),
  5. třída PageTheme má metodu ApplyControlSkin(Control control), která najde ve slovníku skinů skin (typ ControlSkin) příslušející danému controlu a aplikuje ho pomocí skin.ApplySkin(Control control)
  6. metoda ControlSkin.ApplySkin(Control control) nedělá nic jiného, než že zavolá vygenerovaného delegáta.
public void ApplySkin(Control control)
{
    this._controlSkinDelegate(control);
}

Zdánlivě nikde není rozumný prostor pro modifikaci skinu, nikde není seznam hodnot skinu, žádná property-value kolekce, kde by se dal resource-odkaz najít a modifikovat. Vygenerovaný delegát prostě natvrdo nastavuje vlastnosti příslušného controlu a hotovo.

…zde však přichází to kouzlo! Můžeme totiž delegáta, který je uložen v private fieldu _controlSkinDelegate vyměnit za delegáta vlastního. A to nejenomže ho můžeme vyměnit, ale můžeme si dokonce odkaz na původního delegáta uchovat a tuto původní metodu z našeho delegáta zavolat. Celý můj fígl tedy spočívá ve výměně tohoto delegáta, za jinou metodu, která však v sobě nejprve volá metodu původní (aplikuje skin) a následně prochází skinovatelné property controlu (pro jednoduchost pouze textové) a hledá v nich lokalizující resource-odkaz, který případně vyhodnotí. Abychom si měli původního delegáta jak uchovat, je nová metoda wrapována do instance třídy SkinLocalizationDelegateWrapper.

Poslední, co zbývá vyřešit, je jak se s vlastní obsluhou dostat do správného místa životního cyklu stránky, tedy za InitializeTheme(), které je po události PreInit a před ApplySkin(), které je na začátku Init fáze, před událostí Init. Nehledal jsem dlouho, a možná najdete vhodnější místo, ale já jsem pro tyto účely znásilnil virtuální metodu Control.ResolveAdapter(), která se volá na začátku InitRecursive(), tedy přesně v místě, kde se nám to hodí.

SkinLocalizationPageBase

Vytvořil jsem tedy nakonec třídu SkinLocalizationPageBase, která je potomkem System.Web.UI.Page, a která by měla být předkem všech stránek webu, kde lokalizované skiny používáme (nejlépe tedy úplně všech, pomocí <pages pageBaseType=“SkinLocalizationPageBase“ /> ve web.configu.

Výsledný kód vypadá takto:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Web.UI.Adapters;
using System.Reflection;
using System.Collections;
using System.Globalization;
using System.Resources;

namespace Havit.Web.UI
{
    /// &lt;summary&gt;
    /// Bázová třída pro stránky, které obsahují controly s lokalizovatelnými skiny.
    /// &lt;/summary&gt;
    public class SkinLocalizationPageBase : Page
    {
        #region ResolveAdapter (override)
        /// &lt;remarks&gt;
        /// Metoda ResolveAdapter je jediné místo, které jsem našel, že se pomocí něj dá vykonávat nějaký kód
        /// mezi InitializeTheme() (po OnPreInit) a ApplyControlSkin (před OnInit).
        /// &lt;/remarks&gt;
        protected override ControlAdapter ResolveAdapter()
        {
// Theme
            FieldInfo themeFieldInfo = typeof(Page).GetField(&quot;_theme&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            PageTheme theme = themeFieldInfo.GetValue(this) as PageTheme;

            if (theme != null)
            {
                PropertyInfo controlSkinsPropertyInfo = typeof(PageTheme).GetProperty(&quot;ControlSkins&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                IDictionary controlSkins = controlSkinsPropertyInfo.GetValue(theme, null) as IDictionary;

                foreach (DictionaryEntry entry in controlSkins)
                {
                    ControlSkin controlSkin = entry.Value as ControlSkin;

                    if (controlSkin != null)
                    {
                        FieldInfo controlSkinDelegateFieldInfo = typeof(ControlSkin).GetField(&quot;_controlSkinDelegate&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                        ControlSkinDelegate controlSkinDelegate = controlSkinDelegateFieldInfo.GetValue(controlSkin) as ControlSkinDelegate;

                        SkinLocalizationDelegateWrapper sldw = new SkinLocalizationDelegateWrapper(controlSkinDelegate);

                        controlSkinDelegateFieldInfo.SetValue(controlSkin, new ControlSkinDelegate(sldw.DoWork));
                    }

                }
            }

            // StylesheetTheme
            FieldInfo styleSheetFieldInfo = typeof(Page).GetField(&quot;_styleSheet&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            PageTheme styleSheet = styleSheetFieldInfo.GetValue(this) as PageTheme;

            if (styleSheet != null)
            {
                PropertyInfo controlSkinsPropertyInfo = typeof(PageTheme).GetProperty(&quot;ControlSkins&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                IDictionary controlSkins = controlSkinsPropertyInfo.GetValue(styleSheet, null) as IDictionary;

                foreach (DictionaryEntry entry in controlSkins)
                {
                    ControlSkin controlSkin = entry.Value as ControlSkin;

                    if (controlSkin != null)
                    {
                        FieldInfo controlSkinDelegateFieldInfo = typeof(ControlSkin).GetField(&quot;_controlSkinDelegate&quot;, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
                        ControlSkinDelegate controlSkinDelegate = controlSkinDelegateFieldInfo.GetValue(controlSkin) as ControlSkinDelegate;

                        if (!(controlSkinDelegate.Target is SkinLocalizationDelegateWrapper))
                        {

                            SkinLocalizationDelegateWrapper sldw = new SkinLocalizationDelegateWrapper(controlSkinDelegate);

                            controlSkinDelegateFieldInfo.SetValue(controlSkin, new ControlSkinDelegate(sldw.DoWork));
                        }
                    }

                }
            }

            return base.ResolveAdapter();
        }
        #endregion

        #region SkinLocalizationDelegateWrapper
        /// &lt;summary&gt;
        /// SkinLocalizationDelegateWrapper obalí původní výkonný kód skinování a zajistí vyhodnocení resource-odkazů.
        /// &lt;/summary&gt;
        class SkinLocalizationDelegateWrapper
        {
            private ControlSkinDelegate _oldControlSkinDelegate;

            public SkinLocalizationDelegateWrapper(ControlSkinDelegate oldControlSkinDelegate)
            {
                this._oldControlSkinDelegate = oldControlSkinDelegate;
            }

            public Control DoWork(Control control)
            {
                Control result = null;

                if (this._oldControlSkinDelegate != null)
                {
                    result = this._oldControlSkinDelegate(control);
                }

                PropertyInfo[] properties = result.GetType().GetProperties();
                foreach (PropertyInfo property in properties)
                {
                    if (property.PropertyType == typeof(String))
                    {
                        // zajímají nás jen property typu string
                        ThemeableAttribute themableAttribute = Attribute.GetCustomAttribute(property, typeof(ThemeableAttribute)) as ThemeableAttribute;
                        if ((themableAttribute == null) || themableAttribute.Themeable)
                        {
                            // které nemají atribut themable vůbec, nebo ho nemají s volbou false (výchozí je true)
                            string propertyValue = property.GetValue(result, null) as string;
                            if (((propertyValue != null) &amp;&amp; (propertyValue.Length &gt; 10)) &amp;&amp; propertyValue.ToLower(CultureInfo.InvariantCulture).StartsWith(&quot;$resources:&quot;, StringComparison.Ordinal))
                            {
                                string resourceOdkaz = propertyValue.Substring(11);
                                if (resourceOdkaz.Length == 0)
                                {
                                    throw new ConfigurationErrorsException(&quot;Resource odkaz skinu nesmí být prázdný.&quot;);
                                }
                                string resourceClassKey = null;
                                string resourceKey = null;
                                int length = resourceOdkaz.IndexOf(',');
                                if (length == -1)
                                {
                                    throw new ConfigurationErrorsException(&quot;Resource odkaz skinu není platný&quot;);
                                }
                                resourceClassKey = resourceOdkaz.Substring(0, length);
                                resourceKey = resourceOdkaz.Substring(length + 1);
                                string defaultPropertyValue = null;
                                int index = resourceKey.IndexOf(',');
                                if (index != -1)
                                {
                                    defaultPropertyValue = resourceKey.Substring(index + 1); // default value
                                    resourceKey = resourceKey.Substring(0, index);
                                }
                                else
                                {
                                    propertyValue = null;
                                }

                                try
                                {
                                    propertyValue = (string)HttpContext.GetGlobalResourceObject(resourceClassKey.Trim(), resourceKey.Trim());
                                }
                                catch (MissingManifestResourceException)
                                {
                                    // NOOP
                                }

                                if (propertyValue == null)
                                {
                                    propertyValue = defaultPropertyValue;
                                }

                                property.SetValue(result, propertyValue, null);
                            }
                        }
                    }
                }

                return result;
            }
        }
        #endregion
    }
}

Úplné demo v podobě Web Site je v připojeném ZIP souboru.

Související články