Page má property Page.MasterPageFile. Hodnotu lze přiřadit výhradně ve fázi Page_PreInit, jinak se vyvolá výjimka. Hodnotou je cesta k .master souboru.
Implementace dynamických změn není úplně primitivní, protože ve fází PreInit nemáme ještě PostBackData, ani neproběhly žádné RaisedEventy. V podstatě musíme použít nějaké „nezávislé“ uložiště pro použitý Master file (např. Profile nebo Session) a řešit kolizi, že MasterPageFile je potřeba nastavit u content stránky, kdežto přepínač layoutů může být už v samotném master file. Jde to udělat nějak takto:
Response.Redirect je zde potřeba, protože ke změně došlo až po fázi Page_PreInit a my potřebujeme znovunačtením stránky projít přes Page_PreInit. MasterPage si můžeme ukládat i do cookie, každopádně mechanizmus přepínání je podobný jako u lokalizace.
Pokud bychom změnu chtěli udělat jediným roundtripem, museli bychom v PreInit sami parsovat data z Forms nebo QueryStringu. Nejjednodušší je přepínání nasměrovat na samostatný soubor s ReturnUrl, jako se to dělá u lokalizace nebo loginu.
Pokud do control-tree přidáváme dynamicky nějaké controly, které mají obsluhu postbacku (data nebo event), pak pokud těmto controlům explicitně nanastavíme nějaké ID, může se nám snadno stát, že postback nebude korektně vyhodnocen, např.:
Button, LinkButton, ImageButton nebudou emitovat události Click, Command, …,
inputové controly (TextBox, DropDownList, …) zapomenou přes roundtrip data,
…
…v zábavnějším případě se nám může stát, že postback není korektní jen u některých dynamický controlů a u některých se vyhodnotí správně (třeba pokud přidáváme řádky objednávky, tak nám data zapomíná jen poslední řádek).
Nevím, jestli je to bug, ale dělalo to už v .NET 1.1 a dělá to i v .NET 2.0.
Každopádně nastavením ID u dynamicky přidávaných controlů se těchto potíží zbavíme!
private void CreateControlsHiearchy()
{
LinkButton lb = new LinkButton();
lb.ID = "MyLB"; // <--- bez toho není jisté, že se nám bude Click korektně volat !!!
lb.Click += ...
...
Controls.Add(lb);
}
Ukažme si základní schema jednoduchého controlu, který umožní uživateli zadat data předem neurčeného počtu položek – umožní tedy uživateli dynamické přidávání dalších položek – např. řádky objednávky, několik telefonních čísel, několik e-mail e-mailů – v našem případě několik zásilek k přepravě.
Netvrdím, že níže uvedený postup je jediný možný a uvítám jakoukoliv diskuzi pod článkem, berte to spíše jako nasměrování pro další snažení…
Neřeším také, jak na control navázat již existující data (možná v příštím článku), a i vytahování dat z controlu není úplně user-friendly. Nicméně cílem je demonstrovat základní schéma dynamického počtu položek a vylepšení kolem už si každý jistě umí udělat.
Základní schema by se dalo shrnout:
Vytvoříme si control představující jednu položku – není to sice nezbytně nutné, ale usnadní to další manipulaci – control samozřejmě může už existovat, pokud se ptáme jen na jména milenek, může nám stačit už TextBox – nicméně většinou budeme potřebovat control, který bude obsahovat nekolik prvků, validátory, DropDownListy naplněné daty, atp. V našem konkrétním případě jsem si teda připravil UserControl Zasilka.ascx, který má za předka třídu Zasilka (code-behind).
Vytvoříme CompositeControl, který bude v CreateChildControls dynamicky přidávat tolik položek, kolik je právě potřeba. Aby nám fungoval postback a viewstate, musí být control-tree vystavěn při každém roundtripu znovu, a to právě ve fázi Init. V CompositeControl je připraveno schéma pomocí metody CreateChildControls(), která je volána právě už ve fázi Init. Zásadním problémem zde je, jak ve fázi Init zjistit, kolik položek máme zrovna vygenerovat, když ještě nemáme načtena post-back data, ani ViewState.
Aktuální počet položek si musíme ukládat tak, abychom ho znali už ve fázi Init, například do hidden-inputu. Lze využít i Session, či jiná uložiště, vlastní hidden-input se však zde nabízí jako ideální.
Přidání další položky realizujeme v event-handleru pomocí Controls.AddAt(), odebírání položky pomocí Controls.RemoveAt(). Klikne-li uživatel na tlačítko „přidat další položku“, dozvíme se o tom v event-handleru až v poměrně pokročilé fázi zpracování postbacku, když už je control-tree vytvořen (jinak by ani event nemohl být korektně obsloužen). V této fázi jsou existující položky i naplněny daty a nebylo by tedy vhodné znovu rebuildovat control-tree, protože bychom tato data ztratili, popř. museli znovu dotahovat. Protože však control-tree díky jeho pevné stavbě známe, nic nám nebrání přidat další položku do existujícího control-tree, popř. položku odebrat.
Data z controlu vytahujeme například přímým přístupem do Controls, jelikož strukturu položek v Controls známe.
Kód takového controlu s dynamickým počtem položky by tedy mohl vypadat nějak následovně:
public class Zasilky : CompositeControl
{
// Počet zásilek si mezi postbacky posíláme v input-hidden,
// z něj ho načítáme přímo pomocí Form, ukládáme až těsně
// před renderem OnPreRender(), abychom měli poslední stav
// po případném přidání/odebrání.
public int PocetZasilek
{
get
{
if (_pocetZasilek == null)
{
_pocetZasilek = 1;
object tmp = Page.Request.Form[this.ClientID + "_PocetZasilek"];
if (tmp != null)
{
_pocetZasilek = Convert.ToInt32(tmp);
}
}
return (int)_pocetZasilek;
}
set
{
_pocetZasilek = value;
}
}
private int? _pocetZasilek;
// abychom mohli buttony referencovat (chceme je skrývat),
// uložíme si odkaz na ně do private fieldu
private LinkButton pridejZasilkuLB;
private LinkButton odeberZasilkuLB;
// uloží nám počet položek do input-hidden
protected override void OnPreRender(EventArgs e)
{
Page.ClientScript.RegisterHiddenField(this.ClientID + "_PocetZasilek", _pocetZasilek.ToString());
base.OnPreRender(e);
}
// klasické schéma CompositeControlu
protected override void CreateChildControls()
{
Controls.Clear();
CreateControlHiearchy();
ClearChildViewState();
}
// budujeme control-tree
private void CreateControlHiearchy()
{
// položky - jsou naschvál na začátku, což nám usnadňuje jejich snadné referencování pomocí Controls[i],
// jinak bychom museli přístup k nim mít složitější
for (int i = 0; i < PocetZasilek; i++)
{
CreateZasilkaControl(Controls.Count);
}
Literal lit1 = new Literal();
lit1.Text = "<tr><td colspan=\"3\" class=\"zindent\">";
Controls.Add(lit1);
// tlačítko pro přidání položky
pridejZasilkuLB = new LinkButton();
pridejZasilkuLB.ID = "PridejZasilkuLB"; // <-- nutno nastavit ID, jinak nám můžou blbnout postbacky
pridejZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject("Zasilky", "PridejZasilku");
pridejZasilkuLB.CssClass = "arrow";
pridejZasilkuLB.Click += new EventHandler(pridejZasilkuLB_Click);
pridejZasilkuLB.CausesValidation = false;
Controls.Add(pridejZasilkuLB);
Literal lit3 = new Literal();
lit3.Text = "<br/>\n";
Controls.Add(lit3);
// tlačítko pro odebrání položky
odeberZasilkuLB = new LinkButton();
odeberZasilkuLB.ID = "OdeberZasilkuLB"; // <-- nutné nastavit ID, jinak nám můžou blbnout postbacky
odeberZasilkuLB.Text = (string)HttpContext.GetGlobalResourceObject("Zasilky", "OdeberPosledniZasilku");
odeberZasilkuLB.CssClass = "arrow";
odeberZasilkuLB.Click += new EventHandler(odeberZasilkuLB_Click);
odeberZasilkuLB.CausesValidation = false;
odeberZasilkuLB.Visible = (PocetZasilek > 1);
Controls.Add(odeberZasilkuLB);
Literal lit2 = new Literal();
lit2.Text = "</td></tr>\n";
Controls.Add(lit2);
}
// vytvoření jedné položky v control-tree
private void CreateZasilkaControl(int index)
{
Zasilka zasilkaControl = (Zasilka)Page.LoadControl("~/Controls/Zasilka.ascx");
zasilkaControl.ID = "ZasilkaUC_" + index.ToString(); // <-- nutné nastavit ID kvůli korektním postback
Controls.AddAt(index, zasilkaControl);
}
// obsluha události - přidání položky
private void pridejZasilkuLB_Click(object sender, EventArgs e)
{
PocetZasilek++;
CreateZasilkaControl(PocetZasilek - 1); // přidáme za poslední položku
odeberZasilkuLB.Visible = true;
}
private void odeberZasilkuLB_Click(object sender, EventArgs e)
{
if (PocetZasilek > 1)
{
PocetZasilek--;
Controls.RemoveAt(PocetZasilek); // odebereme poslední položku
if (PocetZasilek == 1)
{
odeberZasilkuLB.Visible = false;
}
}
}
}
Vytahování dat z controlu by pak v nejjednoduším případě mohlo vypadat nějak takto:
for (int i=0; i < Zasilky.PocetZasilek; i++)
{
Zasilka zasilka = (Zasilka)Zasilky.Controls[i];
uloziste[i] = zasilka.Hmotnost;
}
// kdyby byl položkou jen TextBox
for (int i=0; i < Zasilky.PocetZasilek; i++)
{
TextBox polozka = (TextBox)MyControl.Controls[i];
uloziste[i] = polozka.Text;
}
ASP.NET 2.0 má v sobě jednu novou málo známou pomůcku – možnost dočasného odstavení webové aplikace umístěním souboru App_offline.htm do rootu webové aplikace.
Pokud do rootu aplikace umístíme soubor App_offline.htm, webová aplikace se zastaví a dokonce se zruší její aplikační doména (AppDomain), takže se odpojí všechny otevřená spojení, databázové soubory, zruší user-instance SQL, atp.
Případný request dostane v odpovědi obsah App_offline.htm souboru. Můžeme si tak připravit nějaký rozumný soubor a pouhým přejmenováním na App_offline.htm web shazovat.
Funkčnost je zamýšlena pro krátkodobé odstavení webové aplikace za účelem jejího update a využívá jí například i Visual Studio při „Publish Web Site“.
Pokud na straně klienta odešleme formulář klávesou ENTER, pak máme-li na stránce jediný submit-prvek (tlačítko) a k tomu jediný input-prvek (TextBox), pak díky chování mnohých prohlížečů (včetně Internet Exploreru), nedojde k vyvolání serverové události Click submit-prvku, nýbrž proběhne jen hluchý postback.
Předesílám, že se v tomto článku budu zabývat výhradně explicitní lokalizací webových projektů, tedy přímým odkazováním na resources pomocí <%$ Resources: … %>, popř. metod GetLocalResourceObject() nebo GetGlobalResourceObject(). Osobně mám tuto metodu radši, protože mám přesně pod explicitní kontrolou každý bajt, který chci lokalizovat.
Co všechno je tedy pro úspěšnou implementaci lokalizace udělat?
Vytvořit resources – .resx soubory s lokalizovanými texty, popř. i jinými objekty.
Přidat do stránek/kódu odkazy na resources tam, kde chceme lokalizaci.
Vytvoření lokalizovaných verzí .resx souborů.
Zajistit nastavení a případně i přepínání CurrentUICulture, popř. i CurrentCulture.
Vytvoření primárních resources – .resx souborů
Předpokládám běžnou práci ve Visual Studiu nebo Web Developer Express, nebudu se tedy zabývat takovými věcmi jako je resgen. VS/WDE přímo podporují přehledné vytváření a editaci .resx souborů.
Důležité je, že rozlišujeme dva typy resources:
globální resources
data v nich jsou přístupná ze všech míst webové aplikace,
jsou umístěny ve složce ~/App_GlobalResources
pojmenovávají se MojeJmeno.resx, např. ~/App_GlobalResources/Glossary.resx
může jich být libovolné množství, nicméně je rozumné používat jen několik logických celků, např. Navigation.resx, Glossary.resx, atp.
lokální resources
data v nich jsou přímo přístupná jen ze stránky/controlu/…, které se týkají,
umísťují se do složky App_LocalResources, která je podsložkou sloužky, kde je stránka/control,
pojmenovávají se MojeStranka.aspx.resx, popř. MujControl.ascx.resx, atp.; například pro stránku ~/admin/stranka.aspx bude lokální resource file ~/admin/App_LocalResources/stranka.aspx.resx
Resource-file vytvoříme snadno, ve VS/WDE prostě vytvoříme příslušnou složku a do ní přes Add přidáme položku typu Resource. Resource soubor .resx je ve skutečnosti XML souborem, který je přibuildován do assembly, pozadí však nechme stranou.
Po otevření .resx souboru se nám standardně zobrazí grid pro editaci/přidávání objektů typu string. Resource nemusí být jen typu string, ale například i obrázky, ikony, zvuky, soubory nebo obecné objekty, nicméně pro běžnou lokalizaci si naprosto vystačíme s typem String, protože ve webových aplikací i u obrázků a ostatních objektů obvykle „lokalizujeme“ jen odkaz na příslušný soubor na disku (ImageUrl = „vlajka_cz.gif“) spíše než samotné soubory.
V praxi .resx záznamy v .resx souborech vznikají současně s prvním odkazem na ně, přistupme tedy k použití ve stránce/kódu.
Použití resources ve stránce/kódu
Explicitní použití resource ve stránce je primitivní, v tagu controlu, všude, kde lze přiřadit string, stačí zapsat odkaz
<%$ Resources: ResourceName %> pro lokální resources
<%$ Resources: FileNameBezPripony, ResourceName %> pro globální resources
Pozor, že nejde použít <%$ … %> mimo serverové tagy, pro takový účel slouží právě nový control Localize, který není ničím jiným než Literalem s lepší design-time podporou.
Pro použití resources v kódu je několik možností
Globální resources se zrcadlí do strong-typed properties ve třídách namespace Resources. Například Resources.Glossary.Telefon nebo Resources.Images.Vlajka. Tento wrapper generuje ASP.NET v rámci web-site a je tak přístupný pouze v rámci web-site a nelze na něj odkazovat například z jiných assembly.
Pro obecný přístup ke globálním a lokálním resources slouží metody třídy HttpContext
public static object GetGlobalResourceObject(string classKey, string resourceKey);
public static object GetGlobalResourceObject(string classKey, string resourceKey, CultureInfo culture);
public static object GetLocalResourceObject(string virtualPath, string resourceKey);
public static object GetLocalResourceObject(string virtualPath, string resourceKey, CultureInfo culture);
// typicky třeba maily
mail.Subject = (string)HttpContext.GetGlobalResourceObject("MailTemplates", "MyMailSubject");
// nebo ve stránce
MessageLb.Text = (string)GetLocalResourceObject("Neuspech");
Metody jsou mimo třídy HttpContext přístupné i v třídě TemplateControl a jejích potomcích, tedy např. i Page.
Pro local resources bohužel ASP.NET žádný wrapper negeneruje a z kódu na ně přistupujeme rovnou přes GetLocalResourceObject().
Pro přístup k resources můžeme použít samozřejmě i standardní techniky přes ResourceManager atp., ale ve webových aplikacích to není běžné.
Co jsme dosud udělali nám samo o sobě už bude fungovat a zobrazovat, nicméně stále jsme jen u jednoho primárního jazyka. Pro přidání dalších jazyků a případně i možnost jejich přepínání musíme provést ještě dva kroky.
Vytvoření resources (.resx souborů) pro další jazyky
Vytvoření resources pro další jazyky je velmi primitivní záležitostí. Stačí vzít .resx soubor primárního jazyka a vytvořit jeho kopii se jménem Soubor.jazyk.resx, například tedy ze souboru ~/App_GlobalResources/Glossary.resx vytvoříme kopii ~/App_GlobalResources/Glossary.en.resx (pro angličtinu), nebo ze souboru ~/App_LocalResources/Login.aspx.resx vytvoříme kopii ~/App_LocalResources/Login.aspx.de.resx (pro němčinu).
…a nyní nezbývá než hodnoty v novém souboru přeložit. Pokud navíc nainstalujeme překladateli WDE a naučíme ho editovat přímo .resx soubory, krása. Existují dokonce mnohé šikovné utilitky pro práci s .resx soubory, aby nebylo nutné instalovat něco tak velkého jako WDE:
Jakou jazykovou verzi odešle ASP.NET na klienta se řídí hodnotou Thread.CurrentThread.CurrentUICulture, zároveň se doporučuje volit i Thread.CurrentThread.CurrentCulture, protože tím se řídí formát čísel, času, dat, měny, atp.
Automatická volba dle požadavku prohlížeče
První, co můžeme chtít, je, aby ASP.NET použilo jazykový požadavek prohlížeče, který ho posílá v HTTP requestu. Česká prostředí tedy rovnou uvidí české stránky, anglická anglické, atp. a nic nemusíme ani přepínat. Prohlížeče mají jazyk přednastaven od instalace a například v Internet Exploreru je možné ho měnit přes Nástroje ~ Možnosti Internetu ~ Obecné ~ Jazyky.
Jediné, co pro implementaci tohoto postupu potřebujeme udělat, je ve web.config nastavit hodnoty elementu <globalization>, atributy uiCulture, popř. culture:
Volba auto říká, že se má použít požadavek prohlížeče, za dvojtečku lze volitelně umístit primární nastavení, které se má použít, pokud není požadavek prohlížeče realizovatelný, resp. pokud prohlížeč požadavek neudal. Pro úplnost uvádím i volby encoding, ty však nejsou předmětem tohoto článku.
Explicitní volba jazyka uživatelem
Dále můžeme chtít, aby si uživatel sám mohl přepnou jazyk, nezávisle na požadavku prohlížeče. Je jasné, že budeme muset přepnout Thread.CurrentThread.CurrentUICulture popř. i CurrentCulture. Zvolený jazyk si budeme ukládat třeba do cookie (použitelná je i session) a je potřeba ho přepnout pro každý request, a to co nejdříve. Buď můžeme udělat override prázdné virtuální metody Page.InitializeCulture(), pokud máme připravenou bázovou třídu pro všechny stránky, anebo změnu provádět rovnou v události Application_BeginRequest, v Global.asax.cs:
Když si v ASP.NET spustíme nový thread (vlákno), tak tento sice získá některé vlastnosti z původního threadu (např. culture), ale nemá přístup k HttpContext.Current, resp. je v něm null.
V každém případě musíme vědět, co děláme, protože druhé vlákno například nemůže moc manipulovat s Response, neboť není vůbec jisté, do jakého stavu Response uvedl thread první, např. už totiž mohl Response odeslat na klienta.
Společné uložiště threadů – statická property
První možností je připravit hodnoty, které bude nový thread potřebovat, do uložiště, které je přístupné oboum threadům, klasicky třeba statické property nějaké třídy.
public static string RootUrl
{
get
{
if (rootUrl == null)
{
if (HttpContext.Current != null)
{
rootUrl = "http://" + HttpContext.Current.Request.Url.Host + HttpContext.Current.Request.ApplicationPath;
}
else
{
rootUrl = "http://nejaky/default/url";
}
}
return rootUrl;
}
}
private static string rootUrl;
Výše uvedený příklad statické property zahrnuje i určitou logiku. Rozhodující je však uložení výsledku prvního volání do statické privátní proměnné rootUrl. Pokud si tedy před spuštěním nového threadu zaručíme alespoň jedno čtení dané property, ta si inicializuje svojí hodnotu v prvním threadu a v druhém threadu už dá rovnou výsledek připravený v rootUrl (nebude HttpContext.Current potřebovat).
Teoreticky lze do statické property typu HttpContext cachovat celý HttpContext.Current, nepovažuji to však za příliš vhodné.
Předání contextu v instanci třídy zapouzdřující thread
Druhou možností, která je vhodnější, pokud už potřebujeme předat do druhého threadu opravdu celý context, je zapouzdření druhého threadu do samostatné třídy:
public class MyClass
{
private HttpContext _context;
public MyClass(HttpContext context)
{
_context = context;
}
public void Start()
{
ThreadStart start = new ThreadStart(DoWork);
Thread.Start(start);
}
private void DoWork()
{
// tady můžem něco dělat z contextem,
// ale pozor, že request už může být odeslán na klienta
}
}
private void Page_Load(object sender, EventArgs e)
{
MyClass myClass = new MyClass(HttpContext.Current);
myClass.Start();
}
HttpContext.Current.Cache
Pokud potřebujeme jen HttpContext.Current.Cache, tak je vše výše uvedené nesmysl, protože na cache máme přístupovat přes HttpRuntime.Cache, a to funguje i v novým threadu…
Někdy se nám může hodit kompletní vyčištění webové application cache (datové cache přístupné přes HttpRuntime.Cache, Control.Cache, HttpContext.Current.Cache, …), například po změně cachovaných hodnot „read-only“ číselníků.
.NET Framework pro takovou operaci žádnou přímou pomůcku nemá, nebo o ní alespoň nevím. Jde to však obejít poměrně snadno. Třída Cache totíž implementuje rozhraní IEnumerable a metoda GetEnumerator() vrací IDictionaryEnumerator. Stačí tedy po jedné vyházet všechny položky:
foreach (DictionaryEntry de in Cache)
{
Cache.Remove(de.Key.ToString());
}
Implementováno v Havit.Web.HttpServerUtilityExt.ClearCache().