Author Archives: Robert Haken

avatar Neznámé

About Robert Haken

Software Architect, Founder at HAVIT, Microsoft MVP - ASP.NET/IIS

ViewState vs. fáze Init, aneb jak jsem se chytil

Na svém posledním ASP.NET kurzu jsem se báječně chytil na jednom primitivním demu na fungování ViewState. Krásně jsem předváděl, jak „ve fázi Init není ještě ViewState trackován, po fázi Init nastává LoadViewState a před tím se zapne jeho tracking – TrackViewState()“, nu což, vyrobil jsem si krásné demo a nestíhal jsem pak chvíli zírat.

(Základy fungování ViewState viz článek Co by měl každý vědět o ViewState.)

„Nefunkční“ demo

Vyrobil jsem primitivní stránku, s jedním Labelem a jedním tlačítkem.

<asp:Label ID="MyLabel" Text="Výchozí text" runat="server" />
<asp:Button ID="MyButton" Text="Postback" runat="server" />

a jak jsem se demonstrovat, jak přiřazení vlastnosti Text ve fázi Init nepřežije postback:

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        MyLabel.Text = "Nová hodnota z fáze Init!";
    }
}

Uff, jaké bylo mé překvapení, když Label i po kliknutí na tlačítko Postback přežíval s hodnotou „Nová hodnota z fáze Init!“. Mnohým je hned jasné, kde jsem udělal chybu, ostatním přiblížím:

ViewState v životním cyklu stránek/controlů

Mé překvapení naštěstí netrvalo dlouho, rychle jsem si ověřil, kde přesně se volá v životním cyklu controlů TrackViewState() a rozuzlení bylo na světě:

  1. TrackViewState() není první operace po fázi Init, nýbrž poslední operace fáze Init samotné,
  2. fáze Init (jako jediná ze základních fází životního cyklu) probíhá zdola nahoru, procházením stromu control-tree do hloubky – fáze Init nadřazeného controlu je završena až po dokončení fází Init všech jeho child-controls.

Výsledek je tedy nasnadě, v obsluze události Page_Init jsme úplně na vrcholu control-tree a fáze Init jednotlivých controlů již proběhla. Jejich TrackViewState() již proběhl a ViewState controlů tak již sleduje změny jednotlivých hodnot. Nastavení MyLabel.Text ve fází Page_Init tedy již je operací, kdy změny ViewState labelu jsou sledovány, narozdíl od ViewState stránky (Page) samotné, kde ještě TrackViewState() neproběhl.

„Funkční“ demo

Demo jsem rychle opravil a vše krásně fungovalo, jak má:

void MyLabel_Init(object sender, EventArgs e)
{
    if (!Page.IsPostback)
    {
        this.Text = "Text nastavený ve fázi Init labelu, nepřežije Postback.";
    }
}

void Page_Init(object sender, EventArgs e)
{
    if (!IsPostback)
    {
        this.Title = "Title nastavený ve fázi Init page, nepřežije Postback.";
    }
}

Ve fázi Init samotného Labelu ještě jeho TrackViewState() neproběhl, tedy se Text nezachová.

Ve fázi Init stránky už proběhl TrackViewState() controlů, ale stránky samotné ještě ne, tedy třeba Title se nezachová.

Makra pro ExpandAllRegions, CollapseAllRegions (#region)

Již delší dobu používám ve Visual Studiu dvě makra – ExpandAllRegions a CollapseAllRegions, ke kterým mám přidružené klávesové zkratky Ctrl+Alt+NumPlus a Ctrl+Alt+NumMinus. Makra jsou prostá, rozbalí všechny regiony v kódu, či zabalí všechny #regiony v kódu (což se od doby, kdy si VS2005+ pamatuje poslední sbalení/rozbalení u každého souboru, narozdíl od předchozích verzí, kde se vždy vše dalo otevírat sbalené, ukázalo jako neobyčejně potřebnná funkce).

Makra jsou prostá:

Sub ExpandAllRegions()
    DTE.ExecuteCommand(&quot;Edit.StopOutlining&quot;)
    DTE.ExecuteCommand(&quot;Edit.StartAutomaticOutlining&quot;)
End Sub

Sub CollapseAllRegions()
    ExpandAllRegions()
    Dim objSelection As TextSelection
    objSelection = DTE.ActiveDocument.Selection
    objSelection.StartOfDocument()
    While (objSelection.FindText(&quot;#region&quot;))
        objSelection.StartOfLine(vsStartOfLineOptions.vsStartOfLineOptionsFirstColumn)
        DTE.ExecuteCommand(&quot;Edit.ToggleOutliningExpansion&quot;)
        objSelection.StartOfDocument()
    End While
    DTE.ActiveDocument.Selection.StartOfDocument()
End Sub

…a přiřazení klávesových zkratek je v Tools ~ Options ~ Environment ~ Keyboard ~ …a dále dohledat název makra dle procedury, takže např. Macros.MyMacros.Havit.CollapseAllRegions, atd.

Klientské (ne)cachování skriptů z WebResource.axd

Skripty emitované do stránky pomocí odkazů do assembly, pomocí WebResource.axd, se v browseru cachují pouze pokud máme debug-kompilaci vypnutou:

<compilation debug="false" />

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

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

Office SharePoint Designer ničí odkazy v .master souborech

Dvě hodiny jsem právě strávil s obligátní chybou „File Not Found.“, problém se nakonec ukázal v tom, že Microsoft Office SharePoint Designer při otevření default.master souboru změnil v direktivách <%@ Register %> atribut src z

<%@ Register TagPrefix="wssuc" TagName="Welcome" src="~/_controltemplates/Welcome.ascx" %>

na

<%@ Register TagPrefix="wssuc" TagName="Welcome" src="_controltemplates/Welcome.ascx" %>

…zabít málo!!!

Zobrazování podrobností výjimek a chyb

Jestli Vás taky rozčilují chybové hlášky SharePointu ve stylu „Unexpected error has occured.“ nebo „File not found.“ a marně hledáte detaily těchto chyb/výjimek, pak vězte, že stačí ve web.configu nastavit v elementu SharePoint/SafeMode atribut CallStack na true:

<SharePoint>
    <SafeMode MaxControls="200" CallStack="true" ...

Samozřejmě k tomu ještě <customErrors mode=“Off“ /> nebo mode=“RemoteOnly“:

<customErrors mode="Off" />

…a je to.

Samozřejmě na produkční mašině bychom měli mít CallStack=“false“.