V předposledním dílu našeho seriálu o diskusním fóru si povíme o tom, jak funguje vykreslování komponent na ASP.NET stránce, ukážeme si jak se dá obsah stránky generovat dynamicky a podrobněji si vysvětlíme mechanismus celé aplikace.
Struktura stránky
Jednu .aspx stránku si můžete představit jako strom různých komponent. Některé z nich sami vytvářejí HTML kód pro stránku, další slouží pouze jako kontainery pro jiné komponenty. Ku příkladu – komponenta Label je reprezentována pomocí tagů <span> - sama vytváří HTML kód. Naproti tomu komponenta Datagrid jenom využívá již existujících komponent a pouze je podle šablony seřazuje do tabulky.
Každá komponenta pro web dědí ze základní třídy System.Web.UI.Control. Ta obsahuje mimo jiné i vlastnosti Parent a Controls.
Parent je další objekt typu System.Web.UI.Control a určuje nadřazenou komponentu.
Controls je kolekce objektů typu System.Web.UI.Control a obsahuje všechny dceřiné komponenty.
Ukažme si to celé na příkladu. Vytvořil jsem stránku, která obsahuje komponentu DataGrid:

Objektová struktura části takovéto stránky vypadá takto:

Tato část stromu pochopitelně zobrazuje jenom velice malou část celé struktury stránky – sledoval jsem pouze cestu k prvnímu políčku druhého řádku, který obsahuje text “Bett, Vercelloti,Lovergen”. Každá z komponent uvedených ve stromu má několik dalších komponent, které nebyli vykresleny. Podívejme se nyní konkrétněji na vztah dvou objektů mezi sebou, ku příkladu HTMLForm a DataGrid.

Tento UML diagram ukazuje, že HTMLForm může vlastnit několik DataGridů ve vlastnosti Controls. Naproti tomu každý z DataGridů bude mít ve vlastnosti Parent referenci na nadřazený HTMLForm. Jak je na obrázku naznačeno, platí toto obecně pro všechny objekty, které dědí ze třídy System.Web.UI.Control.
Při vykreslování HTML je výsledná stránka v podstatě složena z HTML jednotlivých komponent. HTML kód je sestaven po sloučení všech dílčích HTML výstupů v pořadí, v jakém jsou přítomny ve stromu.
Měníme obsah stránky dynamicky
Samotná aspx stránka dědí ze třídy System.Web.UI.Page. Tato třída je také potomkem třídy System.Web.UI.Control, takže pro ni stejná pravidla jako pro jiné komponenty. Kolekci obsažených komponent můžeme měnit v průběhu programu. Uveďme si jednoduchý příklad. Do ovladače události Page_Load přidáme následující kód:
private void Page_Load(object sender, System.EventArgs e)
{
Label napis = new Label();
napis.Text = "Hello";
FindControl(“Form1”).Controls.Add(napis);
}
Výsledek bude vypadat:

Jak jsme tedy dostali tento nápis do stránky ? Vytvořil jsem novou instanci třídy Label, a následně ji přidal do formuláře. Příkaz FindControl našel na stránce objekt formuláře reprezentující tagy <form></form> a vložil do kolekce Controls tohoto objektu nově vzniklou komponentu Label. Vkládání do kolekce se provádí příkazem Add.
Proč jsem musel vyhledat objekt formuláře pomocí metody FindControl ? Pokud bychom napsali pouze Controls.Add(napis) – vložil by se nápis nakonec celé stránky – což by znamenalo až za konec </html> tagu.
Další příklad ukazuje, jak vytvoříme dynamicky více komponent:
private void Page_Load(object sender, System.EventArgs e)
{
for(int c=1;c<10;c++)
{
Label napis = new Label();
napis.Text = "Nápis č."+c+"<br>";
FindControl("Form1").Controls.Add(napis);
}
}

To by byl teoretický úvod do problematiky objektového modelu stránky v ASP.NET. Podívejme se nyní jak je podle tohoto modelu vytvořena komponenta pro diskusní forum.
Diskusní Forum jako strom komponent
Třída ForumControl představující komponentu forum obsahuje ve své kolekci Controls pro každý příspěvek na kořenové úrovni objekt ForumControlLinkedItem. Tato kolekce je vytvářena při načítání stránky v metodě CreateControlsHierarchy():
protected void CreateControlHierarchy() // ForumControl
{
this.Controls.Add(new LiteralControl("<table width=\"100%\" cellpadding=0 cellspacing=0>"));
...
foreach(ForumMessage m in _forum.Messages)
{
ForumControlLinkedItem i = new ForumControlLinkedItem(m);
...
this.Controls.Add(new LiteralControl("</td><td width=\"100%\">"));
this.Controls.Add(i);
this.Controls.Add(new LiteralControl("</td></tr>"));
}
this.Controls.Add(new LiteralControl("</table>"));
...
}
Můžeme vidět, že se jedná v podstatě akorát o trošičku komplikovanější příklad generování komponent dynamicky. Na začátku funkce je vložena tabulka, která ohraničí celou komponentu. Po ní pak následuje smyčka foreach která prohledává všechny příspěvky z datového zdroje. Pro každý příspěvek vytvoří novou instanci třídy ForumControlLinkedItem, předá ji jako parametr příspěvek, kterou má tato komponenta zobrazit a přidá jej do vlastní kolekce Controls. Všimněte si, že funkce se už dále nestará, jak je jedna konkrétní zpráva (příspěvek) vykreslena – to už je v kompetenci třídy ForumControlLinkedItem.
Ta představuje jeden konkrétní příspěvek a obsahuje také metodu CreateChildControls():
protected override void CreateChildControls() // ForumControlLinkedItem.CreateChildControls
{
this.Controls.Clear();
...
base.CreateChildControls();
...
if(this.Expanded) foreach(ForumMessage m in this.Message.Replies)
{
ForumControlLinkedItem i = new ForumControlLinkedItem(m);
...
this.Controls.Add(i);
}
...
}
Můžete si všimnou, že základní myšlenka je opět velice jednoduchá. Nejdříve vytvoří sama sebe (vykreslí údaje daného příspěvku pomocí metody rodičovské třídy base.CreateChildControls() – k této metodě se dostaneme za chvíli.), následně pak opakuje rekurzivně vytváření dalších komponent typu ForumControlLinkedItem, které zobrazí odpovědi na aktuální příspěvek.
Výsledný strom objektů (komponent) pak přesně kopíruje strom příspěvků v daném fóru. Ku příkladu:

Objektový UML model celého systému pak vypadá následovně:

Proč máme dvě třídy pro reprezentaci samotných příspěvků ? ForumControlItem reprezentuje základní funkčnost samotného jednoho příspěvku – vykresluje jenom daný příspěvek bez závislostí na dalších příspěvcích (reakcí). Metoda CreateChildControls() tohoto objektu, kterou jsme volali pomocí base.CreateChildControls(), vykreslí všechny potřebné políčka pro zobrazení příspěvku:
protected override void CreateChildControls() // ForumControlItem.CreateChildControls
{
this.Controls.Add(new LiteralControl("<table cellSpacing=\"0\" cellpadding=\"0\" width=\"100%\">"));
this.Controls.Add(new LiteralControl("<tr><td class=\"frametop\">"));
if(Mode==ControlMode.View)
{
Label title = new Label();
title.Text="<a href=\"mailto:"+this.Message.AuthorEmail+"\">"+this.Message.Author+"</a> > "+this.Message.Subject+" > "+this.Message.PostDate.ToShortDateString()+" "+this.Message.PostDate.ToShortTimeString();
title.Font.Bold=true;
this.Controls.Add(title);
this.Controls.Add(new LiteralControl(" "));
if(_replyAllowed)
{
ReplyButton.Text = "Reply";
this.Controls.Add(ReplyButton);
this.Controls.Add(new LiteralControl(" "));
}
if(_editAllowed)
{
EditButton.Text = "Edit";
this.Controls.Add(EditButton);
this.Controls.Add(new LiteralControl(" "));
}
if(_deleteAllowed)
{
DeleteButton.Text = "Delete";
this.Controls.Add(DeleteButton);
this.Controls.Add(new LiteralControl(" "));
}
this.Controls.Add(new LiteralControl("</td></tr>"));
if(this.Expanded)
{
this.Controls.Add(new LiteralControl("<tr><td class=\"frame\">"));
Label text = new Label();
text.Text=this.Message.Body;
this.Controls.Add(text);
}
}
else // Edit mode
{
NameBox.Text=_message.Author;
NameBox.Width=150;
this.Controls.Add(new LiteralControl(" Name:"));
this.Controls.Add(NameBox);
EmailBox.Text=_message.AuthorEmail;
EmailBox.Width=150;
this.Controls.Add(new LiteralControl(" Email:"));
this.Controls.Add(EmailBox);
DateLabel.Text= _message.PostDate.ToShortDateString()+" "+_message.PostDate.ToShortTimeString();
this.Controls.Add(DateLabel);
SubjectBox.Text=_message.Subject;
SubjectBox.Width=300;
this.Controls.Add(new LiteralControl(" Subject:"));
this.Controls.Add(SubjectBox);
this.Controls.Add(new LiteralControl("</td></tr>"));
this.Controls.Add(new LiteralControl("<tr><td class=\"frame\">"));
BodyBox.Text=_message.Body;
BodyBox.Width=Unit.Percentage(100);
BodyBox.TextMode=TextBoxMode.MultiLine;
BodyBox.Height=300;
this.Controls.Add(BodyBox);
this.Controls.Add(new LiteralControl("</td></tr>"));
this.Controls.Add(new LiteralControl("<tr><td class=\"frame\">"));
SaveButton.Text="Save";
this.Controls.Add(SaveButton);
CancelButton.Text="Cancel";
this.Controls.Add(CancelButton);
}
this.Controls.Add(new LiteralControl("</td></tr>"));
this.Controls.Add(new LiteralControl("</table>"));
}
Tato na první pohled složitá metoda má dvě základní části: Rozlišuje v jakém módu se aktuální příspěvek právě nachází. Pokud je mód prohlížecí (Mode=View), vykreslí příspěvek pomocí Labelů. Pokud je mód editovací (Mode=Edit), vykreslí jej pomocí TextBoxů, aby umožnil uživateli měnit údaje příspěvku.
Dále také rozlišuje, zda je povoleno přidávat nové příspěvky, měnit obsah stávajících, nebo mazat příspěvky. Pokud je daná vlastnost (ReplyAllowed, EditAllowed, DeleteAllowed) nastavena na true, přidá také odpovídající tlačítko umožňující danou akci.
Tímto přístupem jsme zabili dvě mouchy jednou ranou – pokud totiž nastavíme:
ReplyAllowed = true;
DeleteAllowed = false;
EditAllowed = false;
máme připravené forum pro normální uživatele systému. Můžou pouze přidávat nové příspěvky a reakce. Pokud se ale do systému přihlásí administrátor, nastavení
ReplyAllowed = true;
DeleteAllowed = true;
EditAllowed = true;
mu umožní moderovat danou diskuzi – může měnit, mazat nebo přidávat příspěvky.
Tímto bychom měli zvládnutou strukturu celé komponenty. Jak ale probíhá interakce s uživatelem ? Co se vlastně děje po zmáčknutí tlačítka Save, nebo Edit ? Na to se podíváme v následující kapitole.
Předávání událostí
Celé forum funguje na základě předávání událostí mezi jednotlivými objekty. ForumControlLinkedItem vystavuje následující události, které může kdokoliv použít pro reagování na kroky uživatele:
- Reply – je vyvolána, pokud uživatel zmáčknul tlačítko pro odpověď na daný příspěvek.
- Save –
poté, co uživatel ukončil zadávání (nebo změnu) dat a zmáčkl tlačítko Save.
- Cancel
– vyvolána při zrušení zadávaných dat.
- ExpandedChanged –
vyvolá se pokud uživatel rozbalil, nebo zabalil daný příspěvek.
- Edit
– při přepnutí do editovacího módu (Mode=Edit)
- Delete –
při zmáčknutí tlačítka pro smazání příspěvku.
Při vytváření stromu objektů v metodách CreateChildControls registruje rodičovský příspěvek pro všechny události každé z reakcí svůj ovladač pro danou událost. To má za následek probublávání události ve stromu příspěvků až na nejvyšší vrstvu, kde ji odchytí uživatel. Přehledně je to znázorněno na ukázkovém diagramu:

Tento obrázek ukazuje situaci, kdy uživatel zmáčknul tlačítko Save. V příslušném objektu ForumControlLinkedItem byla vyvolána událost Save, která upozorní rodiče, že je potřeba danou zprávu uložit. Rodičovský objekt toto volání přepošle dál svému rodiči, až zpráva dorazí do kořenového příspěvku, který už žádného rodiče nemá. Tam ji odchytí samotný ForumControl:

Poté, co zprávu obdrží ForumControl, uloží data do databáze a sám vyvolá svoji událost Save. Tu pak může zpracovat uživatel bez toho, aby znal celý mechanismus zpráv uvnitř komponenty. Takže ku příkladu můžeme napsat:
OnForumSave(object sender, EventArgs e)
{
ForumMessage zprava = ((ForumControlLinkedItem)sender).Message;
// udelej neco se správou, např. vypiš, ze byla uložena
}
Tato výsledná architektura je velice flexibilní, přehledná a umožňuje jednoduché rozšíření o další funkce. Mechanismus zpráv je také velice rychlý, jedno předání zprávy je vlastně zavolání jedné funkce, přičemž hloubka stromu se většinou pohybuje v jednotkách příspěvků.
Komponenty jsou také velice soběstačné, ForumControlLinkedItem může existovat bez jakýchkoliv návazností na ForumControl, který tvoří jenom jakousi obálku pro jednodušší přístup ze strany uživatele.
Závěr
Na závěr bych chtěl dodat, že vytvářet takovýto pokročilý systém v neobjektovém skriptovacím jazyku by bylo téměř nemožné, nemluvě o výsledném kódu, který by byl velice těžce udržitelný.
Co bude příště ? Dnes jsem se opět ještě nedostal k označování přečtených/nepřečtených zpráv. Za to se čtenářům omlouvám, ale cítil jsem potřebu důkladněji vysvětlit, jak ASP.NET pracuje s komponentami při zobrazování stránky a také ozřejmit celkovou architekturu celé komponenty.