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

Zanechat odpověď

Vyplňte detaily níže nebo klikněte na ikonu pro přihlášení:

Logo WordPress.com

Komentujete pomocí vašeho WordPress.com účtu. Odhlásit /  Změnit )

Facebook photo

Komentujete pomocí vašeho Facebook účtu. Odhlásit /  Změnit )

Připojování k %s