ASP.NET mimo jiných užitečných vlastností podporuje tzv. sitemapy. Podíváme-li se na libovolný web, najdeme na něm jisté navigační prvky – ať už jde o prostý seznam odkazů, hierarchické rozbalovací menu nebo cokoliv jiného. Při návrhu složitějších webů navíc musíme počítat s tím, že navigační menu nebude statické, ale že se bude měnit podle aktuální situace.
Některé části menu mohou být generovány dynamicky, u blogu například seznam kategorií, v nichž jsou řazeny články. ASP.NET podporuje taktéž tzv. security trimming, což umožňuje skrýt položky sitemapy odkazující na stránky, k nimž nemá aktuálně přihlášený uživatel oprávnění. Tato oprávnění se definují v konfiguraci aplikace pomocí elementu authorization. Security trimming je velice užitečná funkce a podle mě je to z jeden z pádných argumentů, proč sitemapy v podání ASP.NET používat.
V tomto díle se nebudeme zabývat tím, jak vytvářet sitemapy, ani tím, jak psát vlastní providery – odkážu na článek Píšeme webovou aplikaci v ASP.NET krok za krokem - 2. část, kde je popsané, jak vytvořit statickou sitemapu z XML souboru (použít standardního ASP.NET SiteMap providera) a jak do ní vložit kousek, který je generován dynamicky podle dat v SQL databázi.
Komponenty pro zobrazení sitemapy
Přestože z předchozích odstavců může člověka přepadnou nadšení začít sitemapy používat, je třeba upozornit taktéž na stinné stránky této oblasti v ASP.NET. Největším problémem jsou komponenty – v ASP.NET máme k dispozici tři.
První z nich je TreeView, která generuje na pohled pěkné rozbalovací menu. Když se ale podíváte na HTML, které to generuje, už to tak hezké není. Je tam spousta skriptů (ty jsou pro rozbalování nutné), položky se renderují čert ví proč jako vnořené tabulky, zkrátka při vší úctě nic moc.
Dalším výplodem fantazie vývojářů ASP.NET je komponenta Menu, která se opět pomocí hordy skriptů snaží vyrenderovat menu, které se chová jako klasické menu v okenních aplikacích. Tento přístup se také na webu vždy nehodí, i když může mít v jistých případech své opodstatnění.
Poslední komponentou je SiteMapPath, která zobrazí aktuální zanoření v sitemapě s možností vracet se na rodičovské stránky.
Co ale dělat v případě, že potřebuji obyčejné prosté jednoduché a nejavascriptové menu, které se renderuje jako vnořené netříděné seznamy (HTML značka ul)? Možných přístupů je samozřejmě více, ukážu zde dva – v tomto dílu první, který se mi moc nelíbí, a příště druhý, který je čistší a bude umět ještě víc, než po něm chceme.
Řešení 1: Control Adapters
Je s podivem, že při návrhu ASP.NET někdo přemýšlel hlavou. Obecně to totiž neplatí a v životě většina věcí moc promyšlená není. ASP.NET vnikalo spolu s .NET Frameworkem 1 v době, kdy ještě dohořívala hořela válka prohlížečů. Ona tedy dnes vládne daleko horší a špinavější válka prohlížečů, ale o tom zde psát nebudu, musel bych se uchylovat k výrazům, které se do článků řekněme nehodí. ASP.NET počítá s tím, že každému klientovi jest možno naservírovat výstupní jiné HTML - podle toho, co umí a co neumí.
Tato featura je realizována pomocí tzv. control adapters. Control adapter je speciální třída, která dokáže přepsat způsob, jakým se určitá komponenta bude renderovat. Možná si říkáte, k čemu by to bylo? Prohlížeče mají problém spíš s CSS styly, což se dá řešit podmíněnými includy anebo CSS hacky. Přesto se control adapters hodí na dvě věci:
- Pokud se nám nelíbí HTML, které určitá komponenta produkuje, stačí napsat adaptér a vyrenderovat ji přesně tak, jak chceme. Tento adaptér pak aplikovat pro všechny klienty.
- V mnoha případech na náš web chodí i uživatelé, kteří používají k prohlížení mobilní telefon. Nutit je stahovat spousty flashových reklam, obrázků a trápit mobilní prohlížečky stovkami kilobajtů skriptů a CSS stylů není to pravé ořechové, takže většina uživatelů ocení mobilní verzi webu. S pomocí control adapters, MasterPages a témat jste velmi jednoduše schopni zajistit, že mobil dostane naservírovaný úplně jinak vypadající web, a to s minimálním vývojářským úsilím – vytvoříte téma a MasterPage s jednoduššími styly a vzhledem, pár komponent přepíšete, aby renderovaly minimalističtější variantu, anebo je pomocí skinu schováte úplně (nastavíte Visible na false, tím pádem se nebudou renderovat).
My si ukážeme, jak pomocí vlastního control adapteru přepíšeme komponentu TreeView, aby sitemapu renderoval příčetně a bez cavyků – jako vnořené seznamy s odrážkami. Pomocí CSS stylů si pak už doladíme vzhled dle našich představ. Nebude umět rozbalování a zabalování položek, obrázky a podobné výmysly, bude umět jen to nejnutnější – hierarchii odkazů na stránky.
Vytváříme ControlAdapter
Ptáte se, jak vytvořit ControlAdapter? Velmi jednoduše – stačí podědít třídu. Jak překvapivé, že? V ASP.NET se poděděním nějaké třídy řeší téměř všechno. Vtip je v tom podědit tu správnou.
Základní kostra naší třídy bude vypadat třeba takto:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI.Adapters;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.Adapters;
using System.Web.UI;
namespace MyComponents
{
/// <summary>
/// Jednoduchý adaptér pro TreeView
/// </summary>
public class TreeViewSimpleAdapter : HierarchicalDataBoundControlAdapter
{
/// <summary>
/// Vrací komponentu TreeView, kterou aktuálně renderujeme
/// </summary>
private TreeView Tree
{
get { return (TreeView)this.Control; }
}
/// <summary>
/// Zařídit, aby komponenta do stránky neemitovala žádné klientské skripty
/// </summary>
protected override void OnPreRender(EventArgs e)
{
// zakázat skripty bezprostředně před tím, než je komponenta bude registrovat do stránky
Tree.EnableClientScript = false;
base.OnPreRender(e);
}
/// <summary>
/// Vyrenderovat začátek hlavního elementu komponenty
/// </summary>
protected override void BeginRender(HtmlTextWriter writer)
{
Tree.ControlStyle.AddAttributesToRender(writer);
if (!string.IsNullOrEmpty(Tree.ClientID))
writer.AddAttribute(HtmlTextWriterAttribute.Id, Tree.ClientID);
if (Tree.Style != null)
foreach (string key in Tree.Style.Keys)
writer.AddStyleAttribute(key, Tree.Style[key]);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
}
/// <summary>
/// Vyrenderovat konec hlavního elementu komponenty
/// </summary>
protected override void EndRender(HtmlTextWriter writer)
{
writer.RenderEndTag();
}
/// <summary>
/// Provést vlastní renderování položek
/// </summary>
protected override void Render(HtmlTextWriter writer)
{
}
}
}
Dědíme z třídy HierarchicalDataBoundControlAdapter (stačilo by dědit jen z obecné třídy ControlAdapter, ale většinou je lepší použít nejspecializovanější možnou třídy, která nám vyhovuje).
Naše třída od svého předka zdědila vlastnost Control, která vrací komponentu, k níž adaptér patří. Protože ale tuto vlastnost budeme potřebovat často a budeme ji používat na mnoha místech, udělal jsem vlastnost Tree, která je private a která vrátí komponentu již přetypovanou na typ TreeView. Je to zkrátka pohodlnější.
Dále je potřeba komponentě TreeView zakázat, aby do stránky přidávala svoje vlastní klientské skripty. Bývá zvykem, že klientské skripty do stránky komponenta přidává ve fázi PreRender (a přitom kontroluje, jestli už ve stránce nejsou; co kdyby byly v jedné stránce dvě stejné komponenty). Navěsíme se tedy na metodu OnPreRender a těsně před jejím zavoláním nastavíme vlastnost EnableClientScript našeho TreeView na hodnotu false, což injektování skriptů pro rozbalování a sbalování položek učiní přítrž.
Dále následují metody BeginRender a EndRender, v nichž máme vyrenderovat úvodní a koncovou značku naší komponenty. Kvůli stylování bude celý hierarchický seznam položek ohraničen ještě komponentou div. Pokud ji nebudeme potřebovat, ničemu tam nevadí; pokud ano, prostě ji použijeme a nastylujeme, jak nám libo.
Renderování výstupu
Pro snadné renderování výstupního HTML má ASP.NET třídu HtmlTextWriter disponující nepřeberným množstvím metod. Pro nás budou důležité metody AddAttribute, AddStyleAttribute, RenderBeginTag, RenderEndTag a WriteEncodedText. Co dělají?
AddAttribute |
Přidá do zásobníku atribut s danou hodnotou. |
AddStyleAttribute |
Přidá do zásobníku inline CSS vlastnost s danou hodnotou. |
RenderBeginTag |
Vyrenderuje daný počáteční HTML tag s atributy a styly ze zásobníku. |
RenderEndTag |
Vyrenderuje koncový HTML tag. |
WriteEncodedText |
Vypíše do stránky text, v němž provede nahrazení řídících znaků za entity. |
V metodě BeginRender tedy zavoláme na stylu komponenty (ControlStyle) metodu AddAttributesToRender, která přidá všechny inline styly nastavené přes vlastnosti komponenty – např. CssClass, ForeColor, BorderColor atd.
Dále pokud má komponenta specifikované ID (což nutně nemusí mít), přidáme k renderování i tento atribut. Následně projdeme všechny styly (položky kolekce Style) a přidáme je k renderovaným atributům. Tím se přidají všechny CSS vlastnosti, které ve stránce komponentě dáme atributem style.
Pak už jen stačí zavolat RenderBeginTag a je hotovo – komponenta do stránky nyní vyrenderuje div se všemi atributy a styly, které jsme jí nachystali. V metodě EndRender jen zavoláme RenderEndTag, abychom zařveli značku, kterou jsme ráčili otevřít.
Třída HtmlTextWriter obsahuje také metody WriteBeginTag a WriteEndTag, ty však doporučuji nepoužívat – jsou trochu na nižší úrovni a je s nimi víc práce.
Renderování položek stromu
Nyní napíšeme metodu RenderNodes, která jako parametr dostane instanci třídy HtmlTextWriter, pomocí níž bude generovat výstup, a dále kolekci TreeViewNodeCollection, kteroužto bude mít za úkol vyrenderovat.
/// <summary>
/// Provést vlastní renderování položek
/// </summary>
protected override void Render(HtmlTextWriter writer)
{
RenderNodes(writer, Tree.Nodes);
}
/// <summary>
/// Vyrenderuje předanou kolekci položek
/// </summary>
private void RenderNodes(HtmlTextWriter writer, TreeNodeCollection nodes)
{
// začátek seznamu
writer.RenderBeginTag(HtmlTextWriterTag.Ul);
// projít položky
foreach (TreeNode node in nodes)
{
// začátek položky
writer.RenderBeginTag(HtmlTextWriterTag.Li);
// odkaz a text
writer.AddAttribute(HtmlTextWriterAttribute.Href, Tree.ResolveClientUrl(node.NavigateUrl));
writer.RenderBeginTag(HtmlTextWriterTag.A);
writer.WriteEncodedText(node.Text);
writer.RenderEndTag();
// dceřinné položky
if (node.ChildNodes.Count > 0)
RenderNodes(writer, node.ChildNodes);
// konec položky
writer.RenderEndTag();
}
// konec seznamu
writer.RenderEndTag();
}
Renderování je celkem přímočaré, používáme rekurzi (pokud nevíte, co to je, měli byste si doplnit znalosti třeba na Wikipedii). V cyklu projdeme položky kolekce a pokud má nějaké dceřinné, zavoláme se rekurzivně na tu kolekci.
A to je vše. Pro otestování si vytvořte prázdnou WebSite a do stránky Default.aspx dejte tento kód.
<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" />
<asp:TreeView ID="TreeView1" runat="server" DataSourceID="SiteMapDataSource1">
</asp:TreeView>
</div>
</form>
</body>
</html>
Pro zkoušku do projektu přidejte soubor Web.sitemap. Může vypadat třeba takto:
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="~/" title="Home" description="">
<siteMapNode url="~/Objednavky" title="Objednávky" description="">
<siteMapNode url="~/Objednavky/Prijate" title="Přijaté" description="" />
<siteMapNode url="~/Objednavky/Zpracovane" title="Zpracované" description="" />
<siteMapNode url="~/Objednavky/Uzavrene" title="Uzavřené" description="">
<siteMapNode url="~/Objednavky/2008" title="2008" description="" />
<siteMapNode url="~/Objednavky/2007" title="2007" description="" />
</siteMapNode>
</siteMapNode>
<siteMapNode url="~/Dopravci" title="Dopravci" description="" />
</siteMapNode>
</siteMap>
A poslední nejdůležitější věc – musíme ASP.NET říct, že má TreeView renderovat naším adaptérem. To uděláme jednoduše – v aplikaci vytvoříme složku App_Browsers, kam přidáme soubor libovolného názvu s příponou browser. Dovnitř napíšeme toto:
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.WebControls.TreeView" adapterType="MyComponents.TreeViewSimpleAdapter" />
</controlAdapters>
</browser>
</browsers>
Deklarovali jsme pravidlo Default (funguje zde dědičnost oproti defaultním definicím prohlížečů v adresáři .NET Frameworku; Default se vztahuje na všechny klienty bez rozdílu) a přidali mu do sekce controlAdapters náš adaptér – controlType je typ komponenty, kterou měníme, adapterType je typ našeho adaptéru.
V souboru browser můžeme mimo adaptérů pro komponenty definovat adaptéry pro stránky, definovat různé typy prohlížečů a říkat, jestli umí klientské skripty, CSS a mnoho dalších věcí. Většinou to člověk moc nepoužije, ale je dobré vědět, co všechno s browser soubory můžeme dělat.
Pokud nyní náš projekt spustíme, sitemapa bude vypadat takto. Podívejte se i na výstupní HTML, je daleko lepší než vnořené tabulky a tuny skriptů, zvláště pokud nechceme rozbalování a sbalování, které má smysl jen u složitých a rozsáhlých nabídek a ne u sitemapy, kde je všeho všudy deset položek.
Nevýhody tohoto řešení
Největším problémem je naprosto minimalistická implementace – napsali jsme jen to nejnutnější, co jsme chtěli. Ale za jakou cenu? TreeView má desítky vlastností, které náš adaptér nepodporuje, nejsou vůbec relevantní. Navíc náš control adapter přepíše veškerá TreeView, která použijeme v aplikaci – i ta, která sitemapy vůbec nerenderují.
Jsou zkrátka situace, kdy místo úpravy stávající komponenty je lepší napsat svou vlastní a při té příležitosti i trochu obecnější. To bude druhý způsob řešení našeho problému. A protože toto řešení bude dost dlouhé, podrobně ho rozebereme v následujícím díle tohoto seriálu.