ASP.NET – Diskusní forum (3)

V minulém dílu jsme si připravili datový adaptér, který má za úkol zprostředkovávat komunikaci s databází. Dnes si vytvoříme první verzi zobrazovací komponenty, kterou v dalším dílu vylepšíme.

Cílem dnešního dílu je vytvořit komponentu, která nám přehledně zobrazí stromovou strukturu všech správ v daném diskusním fóru. Zdrojové kódy k dnešnímu dílu si můžete stáhnout. Výsledek pak bude vypadat například takto:

Klepněte pro větší obrázek

Omluvte prosím grafickou stránku celé záležitosti, příroda mi moc designérského talentu nenadělila.

Komponenta bude typu Web Custom Control - více o komponentách si můžete přečíst v jednom z předchozích dílů seriálu. Čím tedy začneme? Pokud se podíváme na předchozí ukázku vidíme určitý opakující se vzorek. Tím je políčko jedné zprávy:

Klepněte pro větší obrázek

Pokud si dále představíme vztahy mezi jednotlivými zprávami, vidíme stromovou strukturu - jedna zpráva může vlastnit více dalších zpráv (odpovědí). Z této malé analýzy můžeme vyvodit následující závěr. Budeme mít 2 třídy:

  • ForumControl - celé diskusní forum.
  • ForumControlItem - třída pro jednu zprávu.

Výsledná abstraktní struktura pak může vypadat například takto:

Klepněte pro větší obrázek

Ve většině komponent v ASP.NET můžeme najít implementaci seznamu obsažených struktur.Ku příkladu DataGrid má vlastnost Items, která obsahuje seznam všech řádků dané tabulky. Právě proto, že použití vlastnosti Items je téměř standardní, implementujeme ji také v naší třídě ForumControl. Items je pouze jednorozměrné pole, to znamená že ztratíme "hloubku" stromu - všechny zprávy budou na stejné úrovni v jednom seznamu. Na druhé straně informaci o vztazích máme již obsaženou ve stromové struktuře třídy Forum, kterou jsme sestavili v předchozích dílech, takže tímto zjednodušením nic neztratíme.

Podívejme se nyní na jednotlivé třídy:

ForumControlItem

Třída dědí z komponenty WebControl. To znamená, že v podstatě ForumControlItem je komponenta sama o sobě - má na starosti vykreslení daného příspěvku. Komponenta obsahuje následující vlastnosti:
  • Message - obsahuje příspěvek (objekt ForumMessage), který tato komponenta vykresluje. Z tohoto objektu dosadí do výsledného HTML jméno autora, subjekt, text příspěvku a datum.
  • Expanded - je true, pokud je objekt ve stavu "rozbalen". V tomto stavu je vidět celý příspěvek spolu se zabalenými odpověďmi. Pokud vlastnost nastavena na false, je vidět pouze subjekt, datum, jméno a email autora u daného příspěvku.
  • Read - opět vlastnost typu boolean. Je nastavena na true, pokud uživatel již daný příspěvek přečetl.
/// <summary>
/// One message Item representing the visualisation of ForumMessage in ForumControl.
/// Message to be shown is stored in property <see cref="Message">Message</see>.
/// </summary>
#region ForumControlItem
public class ForumControlItem : WebControl
{
private bool _read;
private bool _expanded;
private ForumMessage _message;

/// <summary>True if current user has read the message.</summary>
public bool Read{get{return _read;}set{_read=value;}}

/// <summary>
/// True if message is expanded. In expanded state, all information including collapsed replies is show. In collapsed
/// state, only subject, author and date fields are show, with a plus sign next to them to allow expansion.
/// </summary>
public bool Expanded{get{return _expanded;}set{_expanded=value;}}

/// <summary>Actual message from Forum object to be drawn data from.</summary>
public ForumMessage Message{get{return _message;}set{_message=value;}}
}
#endregion

V dnešní implementaci naší komponenty nebudeme ještě rozlišovat mezi přečtenými a nepřečtenými zprávami. Pro zjednodušení také budeme považovat všechny zprávy za rozbalené. V příštím díle pak přidáme funkce které budou zprávy zabalovat a rozbalovat, podle toho, jak na ně uživatel klikne. Nyní přichází na řadu třída samotné komponenty.

ForumControl

V ASP.NET můžeme vlastní komponenty (Web Custom Controls) vytvářet v podstatě dvěma způsoby :
  • Napsat třídu, která dědí ze třídy Control. Dále napíšeme do funkce Render HTML kód který chceme vložit do výsledné stránky. Následující příklad ukazuje část kódu komponenty reprezentující tlačítko.

    protected override void Render(HtmlTextWriter output)
    {
        output.Write("<input type=button value=ok>"); 
    }

     

  • Další možností je využít již existujících tříd a postavit novou komponentu skládáním těchto tříd s tím, že generování samotného HTML již můžeme přenechat použitým komponentám. Skládání probíhá v metodě CreateChildControls, která přidává dané dceřiné třídy do kolekce Controls. Předchozí příklad by v tomto případě vypadal:
     
    protected override void CreateChildControls() {
        Button b = new Button();
        b.Text  = "ok";
        Controls.Add(b);
    }

     
    Komponenta se v tomto případě podívá na seznam dceřiných komponent (uložených v kolekci Controls) a zavolá pro každou z nich jejich vlastní metodu Render. Výsledný HTML kód je pak složen s těchto jednotlivých komponent.

ForumControl je postaven právě na této kompozici tříd. Obsahuje totiž komponenty ForumControlItem, které zajistí správné vykreslení jednotlivých zpráv. Metoda DataBind() je klíčová - na základě příspěvků z předané třídy Forum vytvoří rekurzivně odpovídající seznam objektů ForumControlItem a zapíše je do pole Items. Zároveň vytváří HTML kód potřebný k oddělení jednotlivých zpráv - všechno to vnořené tabulkování kolem. Zde je výpis zdrojového kódu třídy ForumCotrol:

/// <summary>
/// Web Forms Custom Composite Control for displaying a discussion forum. Shows all messages in collapsable tree structure. Use
/// property Items to access collection of all <see cref="ForumControlItem">ForumControlItem</see> objects. The property Forum sets the current Forum object to render.
/// Before using control call DataBind() method to create the Items collection corresponding to messages in forum,
/// and to create the actual child controls for rendering whole component.
/// </summary>
/// <remarks>
/// Before calling DataBind() method, make sure source Forum object has been initialized and filled with data.
/// Use can use <see cref="NetAt.Components.OleDbForumDataAdapter">OleDbForumDataAdapter</see> for filling date from Access database into the Forum object.
/// </remarks>
/// <example>
/// This example creates a <see cref="NetAr.Components.Forum">Forum</see> object, loads records with ForumID = 1 from underlying Access database using OleDbForumDataAdapter and finally
/// creates ForumControl to display corresponding discussion forum.
/// <code>
/// ForumControl          control = new ForumControl()
/// OleDbForumDataAdapter adapter = new OleDbForumDataAdapter(1);
/// Forum                forum  = adapter.CurrentForum;
///
/// control.CurrentForum=forum;
/// adapter.Load();
/// control.DataBind();
/// </code>
/// </example>
#region ForumControl

public class ForumControl : System.Web.UI.WebControls.WebControl,INamingContainer
{
private Forum _forum;
private ForumControlItemCollection _items = new ForumControlItemCollection();

/// <summary>Contains the Forum object which acts as a source of data for this control.</summary>
public Forum CurrentForum{get{return _forum;}set{_forum=value;}}

/// <summary>Items is collection of all ForumControlItem objects.</summary>
public ForumControlItemCollection Items{get{return _items;}set{_items=value;}}

private ForumControlItem RecurseDataBind(ForumMessage msg)
{
ForumControlItem i = new ForumControlItem();
i.Read=false;
i.Expanded=true;
i.Message=msg;
Items.Add(i);

i.Controls.Add(new LiteralControl("<table cellSpacing=0 cellpadding=0 width=\"100%\">"));
i.Controls.Add(new LiteralControl("<tr><td colspan=\"2\" class=\"frametop\">"));

Label title = new Label();
title.Text="<a href=\"mailto:"+i.Message.AuthorEmail+"\">"+msg.Author+"</a>    >    "+i.Message.Subject+"  >  "+i.Message.PostDate.ToShortDateString()+" "+i.Message.PostDate.ToShortTimeString();
title.Font.Bold=true;
i.Controls.Add(title);

i.Controls.Add(new LiteralControl("</td></tr>"));

if(i.Expanded)
{
i.Controls.Add(new LiteralControl("<tr><td colspan=\"2\" class=\"frame\">"));
Label text = new Label();
text.Text=i.Message.Body;
i.Controls.Add(text);
}

ForumControlItem child;
foreach(ForumMessage m in msg.Replies)
{
string imgsrc;
string imgback;

child = RecurseDataBind(m);
this.Items.Add(child);

if(i.Expanded)
{
if(msg.Replies.IndexOf(m) == msg.Replies.Count-1)
if(m.Replies.Count>0) imgsrc = "img/lminus.gif";
else                  imgsrc = "img/l.gif";
else
if(m.Replies.Count>0) imgsrc = "img/tminus.gif";
else     imgsrc = "img/t.gif";

if(msg.Replies.IndexOf(m)!=msg.Replies.Count-1) imgback = "background=\"img/i.gif\"";
else     imgback="";

i.Controls.Add(new LiteralControl("</td></tr><tr><td valign=\"top\" width=\"19\" "+imgback+" ><img src=\""+imgsrc+"\" width=\"20\" height=\"19\"></td><td width=\"100%\">"));
i.Controls.Add(child);
}
}

if(i.Expanded) i.Controls.Add(new LiteralControl("</td></tr>"));

i.Controls.Add(new LiteralControl("</table>"));

return i;
}

/// <summary>
/// Calling DataBind creates the tree structure and fills Items collection with ForumControlItems obejcts
/// corresponding to messages contained in <see cref="_forum">_forum</see>.
/// </summary>
public override void DataBind()
{
base.DataBind();

Items.Clear();
this.Controls.Add(new LiteralControl("<table width=\"100%\" cellpadding=0 cellspacing=0>"));

foreach(ForumMessage m in _forum.Messages)
{
this.Controls.Add(new LiteralControl("<tr><td width=\"19\" valign=\"top\">"));
ImageButton ParentPlus = new ImageButton();
ParentPlus.ImageUrl="img/minus.gif";
this.Controls.Add(ParentPlus);
this.Controls.Add(new LiteralControl("</td><td width=\"100%\">"));
ForumControlItem child = RecurseDataBind(m);
this.Controls.Add(child);
this.Items.Add(child);
this.Controls.Add(new LiteralControl("</td></tr>"));
}

this.Controls.Add(new LiteralControl("</table>"));
}
}
#endregion

Samotná metoda DataBind() vytvoří ohraničující tabulku a pro každý kořenový příspěvek (ten, který je na nejvyšší úrovni) zavolá metodu RecurseDataBind(). Ta se postará o vytvoření HTML pro daný příspěvek a vytvoří seznam odpovědí. Pro každou z nich rekurzivně zavolá RecurseDataBind().

Nakonec si ukážeme ukázkovou stránku, která využívá naší komponenty pro zobrazení diskusního fora. Jako databáze je použit Access, pro přístup do něj využívám OleDbForumDataAdapter vytvořený v předchozím dílu. Pro stručnost nevypisuji celý zdrojový kód stránky (ten si můžete stáhnout), ukážu pouze metodu Page_Load, ve které se to všechno odehrává:

public class WebForm1 : System.Web.UI.Page
{
protected NetAr.WebControls.ForumControl control;

private void Page_Load(object sender, System.EventArgs e)
{     
OleDbForumDataAdapter adapter = new OleDbForumDataAdapter(1); // Forum s ID=1

adapter.Load();
control.CurrentForum=adapter.CurrentForum;
control.DataBind();
}
.
.
.
}

Závěr

Dnešní komponenta moc funkcí nepobrala, vytváří ale "nosnou konstrukci" pro další vývoj. Příště nás čekají 2 úkoly:
  • umožnit rozbalování a zabalování příspěvků
  • vytvořit funkce pro přidávání nových příspěvků (odpovědí)
Budeme také muset popřemýšlet nad cachováním a realizací označení nepřečtených/přečtených příspěvků. To bude tématem posledního dílu o diskusním foru.
Diskuze (2) Další článek: Nové chladiče Titan i u nás

Témata článku: Software, Programování, Následující závěr, Forum, Child, Adapter, Jednotlivý díl, Private, Public, Message, Jednotlivé komponenty, Read, Příspěvek, Make sure, Label, Výsledné dílo, Button, Celý příspěvek, ASP, Elsa, Custom


Určitě si přečtěte

Micro:bit V2: Tuto destičku plnou čipů dokáže naprogramovat i vaše babička

Micro:bit V2: Tuto destičku plnou čipů dokáže naprogramovat i vaše babička

** Chcete se teď hned naučit programovat čipy? ** Nechcete nic instalovat a číst zdlouhavé manuály? ** Naprogramujeme si Micro:bit, který zahraje Tichou noc

Jakub Čížek | 35

Jakub Čížek
Pojďme programovat elektronikuProgramování pro děti
Messenger a Instagram přicházejí v Evropě o funkce. Kvůli nové směrnici o soukromí
Vladislav Kluska
EvropaInstagramFacebook Messenger
Můžete mít dvakrát rychlejší VDSL? Mapa Cetinu ukazuje, kde je dostupný bonding
Lukáš Václavík
CETINPřipojení k internetu
Technici nestíhají. Cetin dočasně přerušil zavádění VDSL bondingu
Lukáš Václavík
CETINPřipojení k internetu
Šmírování kamerami Googlu: Koukněte, co se objevilo na Street View

Šmírování kamerami Googlu: Koukněte, co se objevilo na Street View

Google stále fotí celý svět do své služby Street View. A novodobou zábavou je hledat v mapách Googlu vtipné záběry. Podívejte se na výběr nejlepších!

redakce | 4

redakce
Mapy GoogleStreet View
Archivovat data do cloudu, na HDD, SSD, DVD, nebo Blu-ray? Co je nejvýhodnější?

Archivovat data do cloudu, na HDD, SSD, DVD, nebo Blu-ray? Co je nejvýhodnější?

** Kam doma natrvalo uložit data? Vyplatí se ještě optická média? ** Jaké kapacity disků a médií má smysl koupit? ** Cenovou výhodnost si ukážeme na příkladech s 2TB úložištěm

Lukáš Václavík | 125

Lukáš Václavík
ZálohováníÚložištěPevné disky
26 užitečných rozšíření pro Chrome: Naučte prohlížeč nové věci

26 užitečných rozšíření pro Chrome: Naučte prohlížeč nové věci

** Prohlížeč Chrome obsahuje širokou škálu funkcí, neumí ale všechno ** Jeho schopnosti můžete rozšířit pomocí rozšíření ** Vybrali jsme pro vás zajímavé a užitečné doplňky

Karel Kilián | 47

Karel Kilián
Doplňky do prohlížečeChromeProhlížeče