Doufám, že vás můj předchozí článek navnadil na psaní HTTP modulů a pohled do budoucnosti přesvědčil, že se jedná o užitečnou dovednost. Podívejme se tedy do útrob jednoho HTTP modulu, jak funguje.

Oním kouskem software bude můj modůlek SkinAnywhere. Princip jeho činnosti je jednoduchý: v jakékoliv aplikaci "pod ním" lze jeho prostřednictvím přepínat kaskádové styly. Funguje to tak, že můj modul prohlíží aplikací vygenerované HTML a hledá v něm odkaz na originální stylesheet. Pokud ho najde, modifikuje HTML tak, aby místo něj byl odkaz na nějaký jiný stylesheet, v závislosti na nastavení konfigurační cookie. Tuto cookie lze nastavit zavoláním speciální stránky s vhodnými parametry.

Z hlediska funkčnosti tedy kód obsahuje tři hlavní části:

  1. Načítání konfigurace (tato problematika byla již popsána dříve a nebudu se jí věnovat).
  2. Ve vstupní fázi (před generováním výstupu ASPX stránkami) kontrolovat, zda se nejedná o požadavek na speciální URL pro změnu stylu (nastavení cookies) a pokud ano, zpracovat ho.
  3. Ve výstupní fázi (po vygenerování výstupu ASPX stránkami) prohledávat vygenerovaný kód a nahradit v něm odkaz na CSS vlastním.

IHttpModule

Jádrem celé aplikace je vlastní HTTP modul. Jedná se o třídu která implementuje rozhraní System.Web.IHttpModule. To specifikuje dvě metody: Init a Dispose.

Význam metody Dispose je jasný - slouží k uvolnění všech používaných zdrojů při odstranění modulu. My žádné zdroje nealokujeme a tudíž není co uvolnit.

Klíčová je pro nás metoda Init, která se zavolá při inicializaci (zavedení) modulu. V ní je možno pověsit vlastní metody jako event handlery na obsluhu událostí, které se přihodí v průběhu zpracování požadavku na stránku. Těch je mnoho a každá má svůj význam, proto se na ně podíváme podrobněji.

Události při zpracování požadavku

Následující události nastanou při zpracování webového požadavku přes HTTP runtime, v uvedeném pořadí:

Pomocí vlastních HTTP modulů můžeme tedy rozšířit nebo přepsat prakticky všechni činnosti, které při běhu .NET aplikace nastávají. Z výše uvedeného seznamu je jasné, kdy budeme vyřizovat jaké úkoly:

Na událost BeginRequest pověsíme vyhodnocení požadované adresy a přijetí vhodných opatření v případě, že se jedná o adresu pro změnu stylu. Na událost PostRequestHandlerExecute pověsíme zpracování a pozměnění vygenerovaných dat.

Za tímto účelem si vytvoříme metody HandleBeginRequest a HandlePostRequestHandlerExecute (mohou se jmenovat fakticky jakkoliv). V Init je pak přiřadíme k patřičným událostem:

Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
    AddHandler context.PostRequestHandlerExecute, AddressOf HandlePostRequestHandlerExecute
    AddHandler context.BeginRequest, AddressOf HandleBeginRequest
End Sub

Přepisování výstupního HTML

K dalšímu zpracování vygenerovaných dat lze použít vlastní třídu, odvozenou (Inherits) od System.IO.Stream. Instanci této třídy jest přiřaditi vlastnosti Response.Filter. To náš HTTP modul provádí v rámci obsluhy události PostRequestHandlerExecute nějak takto:

Public Sub HandlePostRequestHandlerExecute(ByVal sender As Object, ByVal e As EventArgs)
    With System.Web.HttpContext.Current
        ' If output is not HTML, give up
        If .Response.ContentType.ToLower() <> "text/html" Then Return
    ' Append filter
    .Response.Filter = New Filter(.Response.Filter)
End With

End Sub

Naše vlastní třída Filter slouží jako skutečný filtr: z jedné strany (metodou Write) jsou do něj data cpána a z druhé strany (metodou Read) z ní zase vytékají:

Public Class Filter
    Inherits System.IO.Stream
Private Base As System.IO.Stream

Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
    Return Me.Base.Read(buffer, offset, count)
End Function

Public Sub New(ByVal ResponseStream As System.IO.Stream)
    If ResponseStream Is Nothing Then Throw New ArgumentNullException("ResponseStream")
    Me.Base = ResponseStream
End Sub

Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
    ' Get HTML code
    Dim HTML As String = System.Text.Encoding.UTF8.GetString(buffer, offset, count)

    ' Do something with output
    HTML = HTML.Replace("DEMO", "TEST")

    ' Send output
    buffer = System.Text.Encoding.UTF8.GetBytes(HTML)
    Me.Base.Write(buffer, 0, buffer.Length)
End Sub

End Class

Skutečnou logiku nahrazování jsem pro přehlednost vypustil, tento kód jenom nahradí jakýkoliv výskyt řetězce "DEMO" řetězcem "TEST". Stejným způsobem jako s metodou Read je nutno naložit se všemi dalšími metodami a vlastnostmi, jimiž Stream oplývá - pokud nás jejich osud nezajímá, prostě zavoláme tutéž metodu se stejnými parametry, ovšem u "underlying" (vnitřního) streamu, který jsme mimo jiné za tímto účelem v konstruktoru zřídili.

Odchycení speciálního požadavku

V rámci HTTP handleru můžeme provést i odchycení speciálního požadavku, který například necheme předat níže ležící aplikaci. V našem případě se jedná o volání stránky pro přepnutí stylu. Odchycení provedeme v event handleru události BeginRequest. Skutečný kód je opět složitější, níže určený příklad jenom při požadavku na stránku /moduletest.aspx vrátí pevně definovaný text. Stránka moduletest.aspx přitom vůbec nemusí existovat (a i pokud existuje, vůbec na ní nezáleží, nikdy se nevykoná).

 Public Sub HandleBeginRequest(ByVal sender As Object, ByVal e As EventArgs) 
Dim Context As System.Web.HttpContext = System.Web.HttpContext.Current If Not Context.Request.Url.AbsolutePath.ToLower() = "/moduletest.aspx" Then Return

Context.Response.Clear() Context.Response.Write("<html><head><title>HTTP module test</title></head>")
Context.Response.Write("<body><h1>HTTP module test</h1></body></html>") Context.Response.End() End Sub

Registrace HTTP modulu

Aby byl HTTP modul aktivní (vykonán) je nutno ho nejenom napsat, ale též zaregistrovat v souboru web.config. Děje se tak v sekci /configuration/system.web/httpModules. Ukázkový konfigurační soubor může vypadat takto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
    <httpModules>
      <add name="SkinAnywhere" 
      type="AltairCommunications.SkinAnywhere.HttpModule, SkinAnywhere" />
    </httpModules>
  </system.web>
</configuration>

Přiřazení se děje pomocí elementu add a jeho dvou atributů:

Závěr

Kompletní komentovaný zdrojový kód jednoduchého ale užitečného HTTP modulu si můžete stáhnout na webu Altair Communications.