Kdysi, v dřevních dobách ASP a jiných skriptovacích jazyků, se zobrazení stromové struktury z tabulky v databázi provádělo prostým rekurzivním voláním funkce, která pro každé podřízené položky provedla nový dotaz do databáze. Moderní ASP.NET nám dává k dispozici modernější postup, který nezatěžuje zdroj dat opakovanými dotazy – celou strukturu načteme najednou.

Nejprve by se patřilo připomenout, co že máme onou stromovou strukturou na mysli. Jde tedy o takový způsob uložení záznamů, kdy každý záznam má v sobě obsažen také identifikátor nadřízeného záznamu. Kořenové položky mohou mít identifikátor nadřízeného záznamu roven nule nebo roven vlastnímu identifikátoru. V našem příkladu uvažujeme, že kořenové položky mají jako rodiče uveden záznam „0“. Pokud někdo potřebuje důkladněji osvěžit teorii stromové struktury, odkazuji jej na článek Stromové menu na stránce.

V rychlosti si jen osvěžíme dřívější možné řešení pomocí rekurzivního volání VBScript funkce v ASP – vidíme, že se zde několikrát (v cyklu) provádí dotaz do datového zdroje:

Function BrowseTree(Id)
  Dim TreeQ
  Set TreeQ = myADODBConnection.Execute(„SELECT Id, ItemText FROM Tree WHERE ParentId = “ & Id)
  If Not TreeQ.EOF Then
    Response.Write(„<ul>“)
    Do While Not TreeQ.EOF
      Response.Write(„<li>“ & TreeQ(„ItemText“))
      BrowseTree TreeQ(„Id“) ‚rekurze
      Response.Write(„</li>“)
      TreeQ.MoveNext
    Loop
    Response.Write(„</ul>“)
  End If
  Set TreeQ = Nothing
End Function

Nehledě na archaické použití Response.Write pro výstup stránky, je vůbec postup několikanásobného přístupu do datového zdroje pro pár záznamů dost nevhodný. V našem příkladu využijeme SqlDataAdapter, pomocí něj si naplníme DataTable a tu si projdeme sami – nevyužijeme ovšem rekurzi, podřízené položky dohledáme při obsluze události ItemCreated prvku Repeater. Jakmile je v Repeateru vytvořena nějaká položka, zkusíme dohledat podřízené položky. Pokud nějaké existují, dynamicky vygenerujeme vnořený Repeater a podřízenými položkami jej naplníme. Tento postup pak probíhá, dokud nejsou všechny podřízené položky dohledány a vykresleny. Výslednou stránku nebo control cachujeme, abychom přístupy do datového zdroje omezili na minimum.

Základem pro výstup stromové struktury je Repeater:

<%@ Page Language=“C#“ EnableViewState=“False“ Debug=“False“ AutoEventWireUp=“False“ CodeBehind=“ServerMap.aspx.cs“ Inherits=“Interval.CZ.ServerMap“ %>
<%@ OutputCache Duration=“7200″ VaryByParam=“none“ %>
<html>
  <body>
    <asp:Repeater Id=“Repeater1″ EnableViewState=“False“ RunAt=“Server“ />
  </body>
</html>

Tento Repeater jakoby nemá šablonu, zavádíme ji totiž z externích souborů. Mohli bychom sice šablonu klidně vepsat přímo jako vnořené položky Repeateru v designu stránky, záměrně jsem se ale rozhodl využít tento článek k ukázce, jak dynamicky načíst šablonu. To se hodí v případě, že chceme uživateli dát k dispozici různé možnosti přizpůsobení nebo generování odlišných podob stránky pro různá zařízení. Vidíme také, že třídu naší stránky ServerMap, definovanou v CodeBehind ServerMap.aspx.cz jsme zařadili do prostoru názvů Interval.cz.

Šablony záhlaví, zápatí a položky máme definovány v souborech s příponou .ascx podobně, jako user control:

ServerMapHeaderTemplate.ascx:

<%@ Control Language=“C#“ %><ul>

ServerMapFooterTemplate.ascx

<%@ Control Language=“C#“ %></ul>

ServerMapItemTemplate.ascx

<%@ Control Language=“C#“ %>
<li>
  <asp:HyperLink Text='<%# ((System.Data.DataRow)((RepeaterItem) Container).DataItem)[2] %>‘ NavigateUrl='<%#  (String) ((System.Data.DataRow)((RepeaterItem) Container).DataItem)[3] %>‘ RunAt=“Server“ />
</li>

Výše uvedené jsou soubory definující šablony pro záhlaví HeaderTemplate, zápatí FooterTemplate a výstup jednotlivých položek ItemTemplate v Repeateru. Všimněte si také, že jsme nepoužili obvyklého DataBinder.Eval() v bindovacím výrazu – chceme zde vysoký výkon aplikace a proto použijeme přímo přetypování na položku očekávaného typu. Obsah položky Container přetypujeme na RepeaterItem. Tuto pak můžeme přetypovat na DataRow, protože zdrojem dat je DataTable, která se skládá z řádků typu DataRow. Na řádek pak uplatníme číselný indexer (Int32), čímž získáme daný sloupec. Tento postup pak poskytuje nejvyšší výkon.

Pokud bychom chtěli odlišit sudé a liché položky výstupu, bylo by potřeba nadefinovat a zavést ještě šablonu pro AlternatingItemTemplate. V příkladu jsem toto vynechal nejen pro přehlednost, ale také proto, že u číslovaného seznamu takové rozlišení nemá obvykle smysl.

V obslužném kódu stránky deklarujeme její prvky a přiřadíme obsluhy událostí:

 namespace Interval.CZ.WebCMS
{
  using System;
  using System.Data;
  using System.Data.SqlClient;
  using System.Web.UI;
  using System.Web.UI.WebControls;
  using System.Web.UI.HtmlControls;
  public abstract class ServerMap : System.Web.UI.Page
  {
    protected Repeater Repeater1;
    protected DataTable treeDataTable = new DataTable();
    override protected void OnInit(EventArgs e)
    {
      InitializeComponent();
      base.OnInit(e);
    }
    private void InitializeComponent()
    {
      this.Load += new System.EventHandler(this.Page_Load);
      Repeater1.ItemCreated += new RepeaterItemEventHandler(this.MenuItemCreated);
    }
  }
}

Deklarovány jako protected zde máme dvě hlavní součásti – použitý Repeater a již zmíněnou DataTable. Procedurou InitializeComponent() přiřadíme obsluhy událostí Page (stránky) i Repeateru.

Získání zdroje dat a naplnění definovaného Repeateru provedeme v obsluze události Page_Load:

private void Page_Load(object sender, System.EventArgs e)
{
  SqlCommand myCommand = new SqlCommand(„dbo.GetServerMap“,new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings[„ConnectionString“]));
  myCommand.CommandType = CommandType.StoredProcedure;
  try
  {
    SqlDataAdapter mySqlDataAdapter = new SqlDataAdapter(myCommand);
    mySqlDataAdapter.Fill(treeDataTable);
    mySqlDataAdapter.Dispose();
    treeDataTable.Columns[„ItemID“].Unique = true;
    treeDataTable.PrimaryKey = new DataColumn[] {treeDataTable.Columns[„ItemID“]};
  }
  catch { }
  if (treeDataTable.Rows.Count > 0)
  {
    Repeater1.ItemTemplate = Page.LoadTemplate(„~/Controls/ServerMapItemTemplate.ascx“);
    Repeater1.HeaderTemplate = Page.LoadTemplate(„~/Controls/ServerMapHeaderTemplate.ascx“);
    Repeater1.FooterTemplate = Page.LoadTemplate(„~/Controls/ServerMapFooterTemplate.ascx“);
    Repeater1.DataSource = treeDataTable.Select(„ParentItemID = 0“);
    Repeater1.DataBind();
  }
  treeDataTable.Dispose();
}

Narozdíl od článku Generujeme RSS pomocí Repeateru, kde byl použit jako zdroj dat SqlDataReader, zde použijeme SqlDataAdapter, pomocí kterého elegantně a jednoduše naplníme DataTable. Z této potom postupně metodou Select() budeme vybírat záznamy odpovídající dané úrovni stromu. Jako první úroveň vybereme záznamy, kde identifikátor rodiče má hodnotu „0“, a nastavíme je jako zdroj DataSource Repeateru umístěného ve stránce. Všimněte si, že údaje pro připojení k databázi získáváme jako konfigurační hodnoty ze souboru Web.config.

V příkladu také vidíme, že se nestaráme o otevření a zavření připojení k datovému zdroji – SqlDataAdapter si spojení otevře sám dle potřeby a po použití jej zase uzavře (SqlDataAdapter zanechá spojení ve stavu, v jakém bylo před jeho použitím – pokud by tedy bylo otevřeno předem, SqlDataAdapter ho nebude zavírat).

Do tabulky nastavujeme sloupec ItemID jako unikátní a jako primární klíč prostřednictvím vlastnosti tabulky DataTable PrimaryKey a nastavením vlastnosti sloupce DataColumn Unique v kolekci Columns na „true“. Toto velmi zrychlí vykreslení stromové struktury, protože metoda Select() pak může vybírat řádky v DataTable mnohem rychleji.

Nakonec ověříme, že v DataTable jsou nějaké řádky, a pokud ano, pak načteme metodou LoadTemplate šablony Repeateru z externích souborů (mají příponu .ascx), nastavíme zdroj Repeateru na řádky nulté úrovně stromu výběrem z DataTable prostřednictvím Select("ParentItemID = 0") a provedeme metodu DataBind(). Tím se začnou v Repeateru tvořit nové položky, což je, jak jsme již uvedli, spojeno s voláním obslužné metody této události – projdou se tak podřízené položky, aby bylo možno je také vykreslit.

Hlavní díl práce při zobrazení podřízených položek je svěřen proceduře MenuItemCreated(), která obsluhuje popisovanou událost vytvoření položky v Repeateru:

private void MenuItemCreated(object sender, RepeaterItemEventArgs e)
{
  if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
  {
    DataRow myRow = (DataRow) e.Item.DataItem;
    DataRow[] subItem = treeDataTable.Select(„ParentItemID = “ + myRow[0].ToString());
    if (subItem.Length > 0)
    {
      Repeater parentRepeater = (Repeater) sender;
      Repeater subitemRepeater = new Repeater();
      subitemRepeater.EnableViewState = false;
      subitemRepeater.ItemTemplate = parentRepeater.ItemTemplate;
      subitemRepeater.HeaderTemplate = parentRepeater.HeaderTemplate;
      subitemRepeater.FooterTemplate = parentRepeater.FooterTemplate;
      subitemRepeater.ItemCreated += new RepeaterItemEventHandler(this.MenuItemCreated);
      subitemRepeater.DataSource = subItem;
      subitemRepeater.DataBind();
      e.Item.Controls.Add(subitemRepeater);
    }
  }
}

V obsluze události otestujeme, že jde o položku (ListItemType.Item) nebo alternativní položku (ListItemType.AlternatingItem). Pokud ano, „vydlabeme“ si z předaného parametru typu RepeaterItemEvenArgs zdroj dané položky. Jelikož je v našem případě datovým zdrojem Repeateru tabulka DataTable, tak zdrojem položky je řádek tabulky, tedy DataRow – ten budeme potřebovat pro získání aktuálního id položky, tedy ItemID. Tuto hodnotu pak použijeme v metodě Select(), kterou uplatníme opět na naši již dříve naplněnou DataTable, čímž získáme podřízené položky. Pokud jsme nějaké nalezli (Length kolekce řádků je větší než „0“), pak provedeme posloupnost povelů, které zajistí vykreslení všech podřízených položek.

Vytvoříme si nový Repeater pomocí konstruktoru Repeater() a nastavíme mu všechny potřebné vlastnosti – většinu jich převezmeme z rodičovského Repeateru předaného v parametru „sender“. Abychom ušetřili prostředky serveru, vypínáme ukládání stavu zobrazení pomocí EnableViewState="False". Přiřadíme taktéž obsluhu události při vytvoření položky jako nový RepeaterItemEventHandler – a „rekurze“ je na světě, dokud budou existovat nějaké podřízené položky záznamu, budou vznikat další a další podřízené Repeatery.

Uvedený postup se může samozřejmě hodit i pro různé nevizuální aplikace postavené na XML, například VoiceXML. Nebude složité upravit aplikaci pro použití jiného typu databázového zdroje, například nahrazením SqlDataAdapteru za OleDbAdapter.

Popisované soubory si můžete stáhnout (zdrojový kód), pro praktické použití je třeba je přizpůsobit vlastnímu zdroj dat, nejlépe vlastní uloženou proceduru. Je vhodné se ubezpečit, že struktura dodávaných dat je korektní, protože stačí jediná položka, která má nevhodné ItemID a ParentItemID a můžeme si přivodit nepěkné zacyklení s vyčerpáním prostředků serveru, z kterého se pak aplikace může vzpamatovávat i několik desítek sekund.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Žádný příspěvek v diskuzi

Odpovědět