Jak na validní XHTML výstup v ASP.NET

ASP.NET se od verze 2.0 chlubí tím, že jimi generovaný kód je validní XHTML. Prvotní nadšení vás přejde v okamžiku, kdy napíšete aplikaci a necháte ji zvalidovat nástrojem od W3C. Správnější by bylo říct, že ASP.NET umí vygenerovat validní XHTML, když mu trochu pomůžete. Pojďme se podívat na to, jak by taková pomoc měla vypadat.

Nerad bych na tomto místě rozpoutával diskusi o tom, zda má či nemá smysl lpět na XHTML validitě. Dle mého názoru je prostředkem, nikoliv cílem, ale rozhodně se vyplatí vědět, jak takový kód generovat z ASP.NET. Tímto také veřejně přiznávám, že řada mých webů včetně tohoto formální validací neprojde, protože tam nějaké chyby jsou. Vím o nich, vím proč tam jsou a nijak mi nepřekážejí.

Tento článek byl inspirován dotazem v diskusní skupině microsoft.public.cs.developer a vychází z dokumentů publikovaných v MSDN Library. Dobrým startovním bodem jest článek ASP.NET and XHTML.

Adaptivní rendering

Adaptivní rendering se dnes může jevit jako zbytečný přežitek a komplikace, protože až na pár různých quirků renderují všechny běžně používané prohlížeče rozumně napsaný HTML kód stejně nebo podobně. Ideové základy ASP.NET však byly položeny v dobách, kdy byla válka browserů v plném proudu a rozdíly mezi jednotlivými browsery byly značné. S tím nepochybně bude souhlasit každý, kdo se tvorbou webů zabýval v době, kdy byly aktuální čtyřkové verze prohlížečů.

V té době se jako správné řešejí jevil adaptivní rendering. Serverová aplikace detekuje, z jakého klienta na ni uživatel přistupuje, a podle toho mu vyrenderuje kód, který předmětný browser pochopí. Nemusí to být dokonce ani HTML: v dobách, kdy pár optimistů ještě věřilo v úspěch technologie WAP se takto daly renderovat stránky třeba i ve WML.

Adaptivní rendering má smysl i dnes, a to při tvorbě webových aplikací pro mobilní zařízení. Zatímco schopnosti dnešních desktopových prohlížečů jsou vcelku vyrovnané, mobilní zařízení mají kromě omezenějších schopností i diametrálně odlišné možnosti a způsoby použití: různé velikosti displeje (od nějakých 160x160 do 800x600), různé způsoby ovládání (numerická klávesnice, rozpoznávání písma...) a je nutné se podle toho k nim chovat.

Problém ASP.NET spočívá v tom, že nezná W3C validátor a považuje ho za generic downlevel browser, tedy něco na úrovni zhruba trojkových verzí prohlížečů. Myslí si, že neumí JavaScript, CSS a v podstatě ani pořádně HTML, takže mu renderuje šílený tag soup v nejlepším duchu konce devadesátých let minulého století.

Základem veškerých snah zahrnujících použití W3C Validátoru tedy budiž sdělit ASP.NET engine, že něco takového vůbec existuje. Jest tak učiniti pomocí takzvaných Browser Definition Files, které ovlivňují adaptivní rendering. Vytvořte v rootu své aplikace adresář App_Browsers (jeden ze speciálních adresářů, které ASP.NET zná) a v něm vytvořte soubor W3C-Validator.browser (ne názvu nezáleží, důležitá je přípona .browser). Jeho obsah budiž následující:

<browsers>

    <browser id="W3C_Validator" parentID="default">

        <identification>

            <userAgent match="^W3C_Validator" />

        </identification>

        <capabilities>

            <capability name="browser" value="W3C Validator" />

            <capability name="ecmaScriptVersion" value="1.2" />

            <capability name="javascript" value="true" />

            <capability name="supportsCss" value="true" />

            <capability name="tables" value="true" />

            <capability name="tagWriter" value="System.Web.UI.HtmlTextWriter" />

            <capability name="w3cdomversion" value="1.0" />

        </capabilities>

    </browser>

</browsers>

Tímto zápisem sdělíte ASP.NET, že začíná-li hlavička User-Agent řetězcem W3C_Validator, jedná se o prohlížeč, který umí skiptování, CSS a XHTML. V důsledku toho se bude pro validátor renderovat to samé, co třeba pro IE nebo Firefox.

Použití správného DTD

Následující téma nemá mnoho společného s validitou, ale může vám pomoci při praktickém vytváření fungujících stránek. Vytvoříte-li si ve Visual Studiu novou stránku nebo master page, bude její začátek obvykle vypadat nějak takto:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

 

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

Direktiva @Page se sice na klienta nepošle, ale zalomení řádků ano. Specifikaci DOCTYPE tedy budou předcházet dva prázdné řádky. Což je sice z formálního hlediska v pořádku, ale zmate to starší verze Internet Exploreru.

Trik spočívá v tom, že prohlížeče obvykle podporují dva režimy práce s dokumentem. Takzvaný "standards compliance mode" a "quirks mode". Pokud prohlížeč nazná, že dokument psal někdo, kdo umí HTML a ví co dělá, renderuje ho obvykle přesně tak, jak je v dokumentu napsáno. Mezi jednotlivými prohlížeči sice jsou odchylky, ale nikterak dramatické.

Render mode ve Firefoxu Pokud prohlížeč dokument identifikuje jako tag soup, nevalidní dokument, bude pracovat v quirks mode. V takovém případě zapojí do renderingu vlastní kreativitu, mnohdy nepřípadnou a v každém případě u různých prohlížečů různou. Drtivá většina stesků typu "prohlížeč X mi zobrazuje tohle jinak než Y" vyplývá z toho, že stránka se renderuje v quirk mode a prohlížeč si tudíž vcelku oprávněně dělá co chce.

Nutým předpokladem pro zapnutí strict mode u starších verzí IE je, aby dokument začínal správnou specifikací DOCTYPE. Aby před ní nebyla žádná prázdná řádka, mezera, cokoliv. Mnoho problémů si tedy ušetříte, pokud řádky na začátku dokumentu prohodíte a direktivu @Page resp. @Master napíšete až za DOCTYPE. V případě běžných aplikací bude stačit, když tuto opičárnu provedete jednou, v master page.

Bohužel se mi nepodařilo přijít na způsob, jak v IE zjistit, zda stránku renderuje v quirk mode nebo ne. Pravda , nijak zvlášť jsem po tom ani nepátral. Firefox vám tuto informaci ochotně sdělí v okně Page Info (klepněte pravým tlačítkem do stránky a z kontextového menu zvolte Page Info).

Nastavení XHTML conformance mode

Dokumentace tvrdí, že nastavení míry dodržování standardů jest činiti konfiguračně, pomocí elementu xhtmlConformance:

<?xml version="1.0"?>

<configuration>

    <system.web>

        <xhtmlConformance mode="Strict"/>

    </system.web>

</configuration>

Atribut mode může nabývat hodnot Legacy, Transitional a Strict. Popis předpokládaných účinků lze nalézti příkladně ve shora odkazovaném článku, ale já jsem žádný zřetelný efekt nezaznamenal, controly jako Image nebo HyperLink se (např. atribut alt) renderují pořád stejně blbě. Toto nastavení ve svých aplikacích udržuji spíše z fetišistických důvodů.

Atribut alt u obrázků

V duchu hesla "cesta do pekel je dlážděná dobrými úmysly" učinilo W3C ve své nekonečné moudrosti u obrázku (element img) atribut alt povinným. Onen dobrý úmysl spočívá v tom, že každý obrázek by měl mít textové vyjádření. To se hodí v případě, že uživatel obrázky nemůže nebo nechce zobrazit - aby k němu předmětná informace nějak dolehla i bez nich.

Drtivá většina obrázků na současném webu ale žádnou zásadní informaci nenese, jenom dotvářejí design stránky (s čímž HTML příliš nepočítá - řeší sémantickou strukturu webu, ne to, jak web dnes reálně vypadá). V takovém případě by měly mít příslušné obrázky atribut alt také, ale prázdný. Oč lepší je povinný prázdný alt proti nepovinnému jest otázka, na kterou patrně musejí odpovědět chytřejší hlavy, než ta moje.

Server control Image disponuje vlastností AlternateText, která - je-li vyplněna - se použije jako hodnota alt atributu. Jest-li tato prázdná, standardně se atribut alt nevyrenderuje v rozporu se specifikací vůbec. Existuje nicméně další vlastnost, GenerateEmptyAlternateText, která může být nastavena na true a jest-li tomu tak, alt se vygeneruje správně tak, jak má - tedy prázdný.

Proč na to potřebujeme speciální vlastnost a proč je defaultní nastavení v rozporu se standardem nicméně opět přesahuje omezené schopnosti koňského mozku.

Řešením je použít ASP.NET Themes a definovat obsah této vlastnosti v Theme.

Velké problémy s malým HyperLinkem

Je až neuvěřitelné, jak velké problémy může způsobit nemístný optimismus W3C v kombinaci s nezdravou kreativitou na straně Microsoftu. Prvek HyperLink má v tomto směru dva problémy: prvním je již zmíněný alt atribut, jest-li obrázkem a druhým je populární leč zavržený atribut target.

Problém s atributem alt je v zásadě stejný, jako u prvku Image až na to, že nemáme k dispozici žádnou vlastnost, kterou bychom si mohli vynutit zobrazení prázdného atributu. Na druhou stranu, u hyperlinku je obecně dobrý nápad Text vyplňovat, aby uživatel věděl, na co kliká. Prázdný alt text lze tedy podle mého zkoumání řešit jenom pomocí adaptéru, viz dále.

Druhý problém se týká atributu target. Ten umožňuje odkaz otevřít ve specifikovaném okně, včetně rozličných specialit jako _parent, _top a podobně. Web konsorcium nicméně naznalo, že v zájmu vyšších principů mravních a čistoty jazyka jest třeba atribut target u XHTML vykynožit a podobné skopičiny provádět výhradně prostřednictvím klientského skriptování. Tato idea se setkala s pozoruhodným nedostatkem pochopení u obecné veřejnosti a je patrně nejochotněji porušovanou částí specifikace. Nejsnazší řešení samozřejmě spočívá v tom, že atribut target nebudete používat a správu oken necháte na uživateli. Pokud trváte na tom, že se mu do toho chcete motat a trváte na tom, že musíte být formálně XHTML validní, nezbude, než to zase řešit adaptérem.

HyperLinkAdapter

V úvodu tohoto článku jsem adaptivní rendering spíš pomlouval, ale nyní se nám bude náramně hodit. ASP.NET totiž obsahuje technologii takzvaných control adaptérů. Ve zkratce, jedná se o technologii, která umožňuje delegovat renderování server control na samostatnou třídu, která může být pro každý prohlížeč jiná. A to bez zásahu do controlu samotného.

Jest tak činiti tím, že vytvoříte třídu poděděnou od System.Web.UI.Adapters.ControlAdapter a v ní přepíšete metodu Render. Můj adaptér pro control HyperLink vypadá takto:

using System.Web.UI;

using System.Web.UI.Adapters;

using System.Web.UI.WebControls;

 

namespace Altairis.Web.UI.WebControls.Adapters {

    public class HyperLinkAdapter : ControlAdapter {

 

        protected override void Render(HtmlTextWriter writer) {

            HyperLink link = (HyperLink)(this.Control);

 

            string href = link.NavigateUrl;

            if (!href.StartsWith("#")) href = this.Page.ResolveUrl(href);

 

            // Prepare script for onclick event

            string onClick = link.Attributes["onclick"] ?? string.Empty;

            if (onClick != string.Empty && !onClick.EndsWith(";")) onClick += ";";

            if (!string.IsNullOrEmpty(link.Target)) onClick += "window.open(this.href, \"" + link.Target + "\");";

 

            // Render link begin tag

            if (!string.IsNullOrEmpty(link.NavigateUrl)) writer.AddAttribute(HtmlTextWriterAttribute.Href, href);

            if (!string.IsNullOrEmpty(link.CssClass)) writer.AddAttribute(HtmlTextWriterAttribute.Class, link.CssClass);

            if (!string.IsNullOrEmpty(link.ToolTip)) writer.AddAttribute(HtmlTextWriterAttribute.Title, link.ToolTip);

            if (!string.IsNullOrEmpty(onClick)) writer.AddAttribute(HtmlTextWriterAttribute.Onclick, onClick);

            if (link.Style != null && link.Style.Count > 0) {

                foreach (string key in link.Style.Keys) writer.AddStyleAttribute(key, link.Style[key]);

            }

            if (!string.IsNullOrEmpty(link.NavigateUrl)) writer.RenderBeginTag(HtmlTextWriterTag.A);

            else writer.RenderBeginTag(HtmlTextWriterTag.Span);

 

            if (string.IsNullOrEmpty(link.ImageUrl)) {

                // Text link

                writer.Write(link.Text);

            }

            else {

                // Image link

                writer.WriteBeginTag("img");

                writer.WriteAttribute("src", Page.ResolveUrl(link.ImageUrl));

                writer.WriteAttribute("alt", link.Text);

                if (!link.Width.IsEmpty) writer.WriteAttribute("width", link.Width.ToString());

                if (!link.Height.IsEmpty) writer.WriteAttribute("height", link.Height.ToString());

                if (!string.IsNullOrEmpty(link.ToolTip)) writer.WriteAttribute("title", link.ToolTip);

                writer.Write(HtmlTextWriter.SelfClosingTagEnd);

            }

 

            // Render link end tag

            writer.RenderEndTag();

        }

 

    }

}

Použití adaptéru zařídíme opět v souboru s příponou .browser ve složce App_Browsers. Adaptéry lze vyčlenit do samostatného souboru a nebo příslušné elementy zapsat do již existujícího souboru, který naučí ASP.NET rozpoznat validátor:

<browsers>

    <browser refID="Default">

        <controlAdapters>

            <adapter controlType="System.Web.UI.WebControls.HyperLink"

                     adapterType="Altairis.Web.UI.WebControls.Adapters.HyperLinkAdapter" />

        </controlAdapters>

    </browser>

</browsers>

Tento adaptér řeší oba dva v úvodu zmiňované problémy, tedy atributy alt i target. Kromě toho celkově zjednodušuje generované HTML, jak jest vidět na následujícím příkladu:

Mějme control zapsaný v ASPX souboru takto:

<asp:HyperLink ID="HyperLink1" runat="server"

               NavigateUrl="none.htm"

               ImageUrl="none.jpg"

               Width="100px"

               Height="100px"

               Target="_blank" />

Bez použití adaptérů bude výsledné HTML následující:

<a id="HyperLink1" href="none.htm" target="_blank" style="display:inline-block;height:100px;width:100px;"><img src="none.jpg" style="border-width:0px;" /></a>

S použitím adaptéru bude výsledné HTML vypadat takto:

<a href="/TestSite/none.htm" onclick="window.open(this.href, &quot;_blank&quot;);"><img src="/TestSite/none.jpg" alt="" width="100px" height="100px" /></a>

Adaptér řeší všechny zde zmiňované problémy s validitou a kromě toho renderuje poněkud konzervativnější markup (kde je specifikována velikost obrázku a ne odkazu). Oproti vestavěnému řešení explicitně nevypíná okraj okolo obrázku, dávám přednost tomu, řešit to v CSS nastavením img { border: none; }. To lze samozřejmě do adaptéru dopsat.

  • Altairis
  • Nemesis
  • Microsoft MVP
  • IIS
  • ASP.NET