Zhruba jednou do roka se na mne obrátí jistá nešťastná duše z Microsoftu, hnána touhou naplniti výkazy a formuláře přehršlí čísel. Klade dotazy typu "kolik lidí chodí na ASPNET.CZ" a nedá mně pokoj, dokud nedostane odpověď. Několik let jsem k řešení tohoto problému úspěšně používal metodu Monte Carlo, pak jsem přešel na Google Analytics. Výsledek je zhruba stejný, ale líp to vypadá ;-)
Měřící kód vkládám do stránek dynamicky a nyní jsem celou záležitost vyřešil pomocí HTTP modulu. Na tomto postupu lze ukázat celou řadu užitečných technik, najmě pak:
- Tvorbu a použití HTTP modulů všeobecně.
- Použití události PreRequestHandlerExecute k manipulaci se stránkou.
- Generování (a registrování) klientských skriptů z ASP.NET.
- Vytváření vlastních kongifuračních sekcí
Jak funguje Google Analytics a proč použít HTTP modul
Pokud chcete používat pro měření návštěvnosti svého webu Google Analytics, stačí vám vložit do stránky dva kousky JavaScriptu, Google se postará o zbytek.
<script src="http://www.google-analytics.com/urchin.js" type="text/javascript"></script> <script type="text/javascript"> <!--_uacct = "UA-172431-1";urchinTracker();// --> </script>
Proč tedy takovou věc řešit složitě pomocí HTTP modulu programově, místo abychom napsali dané do stránky "natvrdo"? Důvodů je hned několik:
Kód je jiný pro běžné stránky (HTTP) a jiný pro šifrované stránky (HTTPS). Pokud vaše aplikace využívá obé a dynamicky se mezi těmito režimy přepíná podle potřeby (jako např. web akce.altairis.cz), je nutné kód generovat dynamicky, podle použité přístupové metody.
Pokud má vaše aplikace být univerzální a na Internetu žije v několika exemplářích, je dobré mít možnost určovat použité měřící ID (či samu skutečnost, zda se má měření provádět) pomocí konfiguračního souboru. No a konečně, pokud takových aplikací máte povícero, jest obvykle žádoucí zajistit, aby jejich chování bylo stejné.
Stejný úkol lze řešit např. pomocí MasterPages nebo pomocí vlastní base class pro stránky. Mnou navrhované řešení má nicméně tu výhodu, že je použitelné pro jakoukoliv aplikaci, a to aniž by bylo nutno do aplikace samé jakkoliv zasahovat. Modul lze přiřadit čistě konfiguračně.
Vytvoření vlastní konfigurační sekce
Konfigurace tohoto modulu je jednoduchá, vyžaduje toliko jediný parametr, kterým je identifikátor pro měření - v mém případě UA-172431-1. Nabízející se možností je vytvořit v sekci appSettings nějak vhodně pojmenovaný klíč a ten pak použít. Tento přístup ovšem nepokládám v případě univerzálních komponent za příliš šťastný.
Nenabízí totiž žádnou záruku jedinečnosti - appSettings je sekce určená pro nastavení vlastní aplikace, ne univerzálních komponent. Neobsahuje tedy žádné nástroje pro zajištění jedinečnosti názvu konfigurační proměnné. Pokud ji pojmenujeme třeba "StatId", nikde není řečeno, že podobný nápad nebude mít někdo jiný, s jinou komponentou.
Postup s appSettings se také hodí toliko pro menší počet hodnot, které nemají žádnou zásadnější strukturu. Pokud vaše aplikace vyžaduje složitější konfiguraci, jest záhodno napsat si vlastní konfigurační systém. Tento úkol byl v ASP.NET verze 1.x dosti nevděčný, leč verze 2.0 přinesla programátorům úlevu i v tomto směru.
Pojďme se tedy podívat na to, jak bude vypadat odpovídající část konfiguračního souboru web.config:
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="altairis.web">
<section name="management" type="Altairis.Web.Configuration.ManagementSection, Altairis.Web" />
</sectionGroup>
</configSections>
<altairis.web>
<management>
<googleAnalytics trackId="UA-172431-1" />
</management>
</altairis.web>
</configuration>
Celou záležitost by samozřejmě šlo vyřešit i s mnohem jednodušeji a mnou navržený postup se zdá býti možná až příliš krkolomný. Prvním důvodem jest, že tento příklad je toliko malým výsekem z obsáhlejší knihovny, oplývající celou řadou užitečných funkcí, kdežtě jest složitější konfigurace nezbytná. Druhým důvodem jsou jako obvykle mé pohnutky didaktické, neb chci laskavé čtenáře obeznámit se širokou paletou možností, jimiž .NET Framework v této věci oplývá.
Configuration Section Group
Na samém vrcholu konfigurační struktury dlí skupina konfiguračních sekcí. V našem případě se jedná o element altairis.web. Cílem section groups je sdružovati jednotlivé elementy do logických skupin. Obvykle se pokládá za vhodné, aby struktura konfiguračního souboru alespoň částečně souzněla s logickou strukturou tříd a namespaces konfigurované knihovny.
Section Group je tedy vyšší organizační jednotka, kterou ve své architektuře použít můžete, ale nemusíte. Sama neobsahuje žádné konfigurační údaje přímo, ale je obálkou pro konfigurační sekce, které podrobněji pojednáme dále.
Doporučuje se (a třídy v .NET Frameworku vestavěné tuto zásadu dodržují), aby název section group odpovídal názvu namespace, který je konfigurován, ovšem malými písmeny. V případě knihoven se pak doporučuje, aby namespace obsahoval název firmy nebo jiný jednoznačný identifikátor. Moje knihovna webových utilit se jmenuje Altairis.Web a proto se i section group jmenuje altairis.web.
Skupinu konfiguračních sekcí jest nutno zaregistrovati na začátku souboru, v elementu configSections. Jako jediná zde zmiňovaná entita nemusí mít (ač může) svůj protějšek v podobě třídy, pomocí které by bylo možno s obsahem pracovat. Prostá registrace postačí. V případě extrémně složitých konfiguračních schémat lze do sebe section groups dokonce i zanořovat, ale nedoporučuji tohoto postupu využívat příliš často.
Configuration Section
Konfigurační sekce je základem celého systému. V našem případě se jedná o element s názvem management. Je také jedinou povinnou součástí - jak elementy nadřazené (section groups) tak podřízené (elements) použít dle náročnosti můžete, ale nemusíte. Sekce sama je ale základem a musí být přítomna vždy.
I sekci je nutné nejprve zaregistrovat a tentokráte je třeba i vytvořit třídu, která bude v kódu sekci reprezentovat. Odkaz na ni pak uvedete jako hodnotu atributu type. V mém případě se jedná o třídu Altairis.Web.Configuration.ManagementSection, která se nachází v assembly Altairis.Web.dll. Je dobrým zvykem pojmenovávat tyto třídy tak, že jejich název odpovídá názvu elementu a je na konci doplněn slovem "Section".
Při psaní vlastních sekcí v .NET 1.x vám nezbylo, než ve své třídě implementovat rozhraní IConfigurationSection a v podstatě celou funkčnost si napsat sami. V případě .NET 2.0 je situace výrazně jednodušší: v drtivě většině případů se obejdete bez "skutečného" programování, stačí nadefinovat odpovídající vlastnosti a odekorovat je vhodnými atributy. Sám .NET se postará o to, aby byly hodnoty správně načteny atd.
Zdrojový kód třídy ManagementSection vypadá takto:
using System;
using System.Configuration;
namespace Altairis.Web.Configuration {
public class ManagementSection : ConfigurationSection {
[ConfigurationProperty("googleAnalytics")]
public GoogleAnalyticsElement GoogleAnalytics {
get { return (GoogleAnalyticsElement)this["googleAnalytics"]; }
set { this["googleAnalytics"] = value; }
}
}
}
Třída sama je poděděna od base class System.Configuration.ConfigurationSection. Lze v ní vytvářet vlastnosti, které odpovídají jednotlivým vnořeným elementům, případně přímo atributům. Já jsem zde vytvořil jedinou vlastnost, která se nazývá GoogleAnalytics a vrací třídu typu GoogleAnalyticsElement (bude definována dále). Tato vlastnost je odekorována parametrem ConfigurationProperty, jehož konstruktor obsahuje specifikaci názvu elementu, který bude použit k vytvoření odpovídající třídy.
Opět se držím zvyklostí přítomných v .NETu a pro pojmenovávání víceslovných elementů používám camelCase. Proto se tedy vnořený element jmenuje googleAnalytics, ačkoliv technicky možné by byly i varianty jako GoogleAnalytics nebo Google_Analytics.
Sekce může obsahovat přímo konfigurační atributy s hodnotami (zapisují se stejně jako v případě elementu, viz níže) a nebo může obsahovat ještě další elementy.
Configuration Element
Nejnižší organizační složkou je element. Ten se nemusí zvlášť registrovat v prologu web.configu, stačí aby na něj bylo odkázáno ze sekce. Element může obsahovat buďto atributy s vlastními hodnotami a nebo další elementy.
V programovém světě je protějškem elementu třída poděděná od base class pojmenované vcelku logicky System.Configuration.ConfigurationElement. Pojmenování třídy samé obvykle vychází z názvu elementu a na konec se přidává slovo "Element", podobně jako u sekcí. Třída GoogleAnalyticsElement vypadá takto:
using System;
using System.Configuration;
namespace Altairis.Web.Configuration {
public class GoogleAnalyticsElement : ConfigurationElement {
[ConfigurationProperty("trackId", IsRequired = false, DefaultValue = "")]
public string TrackId {
get { return (string)this["trackId"]; }
set { this["trackId"] = value; }
}
}
}
Vlastnosti v tomto případě odpovídají konkrétním atributům, přičemž lze definovat i celou řadu dalších pomocných argumentů, jako například zda je daná položka povinná, jakou má výchozí hodnotu a podobně. Vlastnosti mohou být (a měly by být, jedná se o hlavní výhodu) strongly typed, k přetypování dojde automaticky.
Využití konfigurační sekce
Využití konfigurační sekce je jednoduché. Stačí zavolat metodu System.Configuration.ConfigurationManager.GetSection a výsledek odpovídajícím způsobem přetypovat. Poté je možno pomocí strongly-typed tříd přistupovat ke konfiguračním hodnotám.
Námi vytvořenou hodnotu tedy načteme následovně:
// using System.Configuration
// using Altairis.Web.Configuration
ManagementSection mgmt = ConfigurationManager.GetSection("altairis.web/management") as ManagementSection;
string trackId = mgmt.GoogleAnalytics.TrackId;
Vytvoření HTTP modulu
Obecně jsem zde o HTTP modulech již psal. Pro osvěžení paměti mohu doporučit následující články:
- Pohled do hlubin webserverovy duše (aneb jak fungují HTTP moduly a handlery)
- Modul pro 'basic' autentizaci v ASP.NET
- HTTP moduly prakticky
V tomto konkrétním případě využívám události PreRequestHandlerExecute. Tato událost nastane těsně před tím, než je daný HTTP požadavek předán ke zpracování věcně a místně příslušnému HTTP handleru. Ale, a to je pro nás důležité, až poté, co je vytvořena instance třídy, která je oním HTTP handlerem a tuto třídu potom máme k dispozici ve vlastnosti Handler aktuálního HttpContextu.
V kódu tohoto event handleru tedy můžeme již jaksi v předstihu volat metody stránky, případně se programově vázat na její události. Patřičný zdrojový kód vypadá následovně:
using System;
using System.Web;
using System.Configuration;
using Altairis.Web.Configuration;
namespace Altairis.Web.Management {
public class GoogleAnalytics : IHttpModule {
private string trackId;
public void Dispose() {
// NOOP
}
public void Init(HttpApplication context) {
ManagementSection mgmt = ConfigurationManager.GetSection("altairis.web/management") as ManagementSection;
this.trackId = mgmt.GoogleAnalytics.TrackId;
if (!string.IsNullOrEmpty(this.trackId)) context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute);
}
void context_PreRequestHandlerExecute(object sender, EventArgs e) {
System.Web.UI.Page page = System.Web.HttpContext.Current.Handler as System.Web.UI.Page;
if (page == null) return; // handler is not web form
if (!page.ClientScript.IsClientScriptBlockRegistered("ga_common")) {
if (System.Web.HttpContext.Current.Request.IsSecureConnection) {
page.ClientScript.RegisterClientScriptInclude("ga_common", "https://ssl.google-analytics.com/urchin.js");
}
else {
page.ClientScript.RegisterClientScriptInclude("ga_common", "http://www.google-analytics.com/urchin.js");
}
}
if (!page.ClientScript.IsStartupScriptRegistered("ga_startup")) {
page.ClientScript.RegisterStartupScript(page.GetType(), "ga_startup", string.Format("_uacct = \"{0}\";\r\nurchinTracker();\r\n", this.trackId), true);
}
}
}
}
Kód lze rozdělit na dvě hlavní části. První je metoda Init. Ta se zavolá pouze jednou, a to při startu aplikace. V ní načteme konfiguraci a pokud je v ní nějaké trackId pro Google Analytics uvedeno, postupujeme dále (konfigurace se tedy načítá a zpracovává jenom jednou, ne při každém požadavku).
Poté přiřadíme ke zmiňované události PreRequestHandlerExecute nový event handler v podobě metody context_PreRequestHandlerExecute. V něm se bude odehrávat vlastní generování měřícího kódu.
HTTP handler nemusí nutně být pouze web form. Může se jednat o webovou službu, samostatný handler, nebo některý ze systémových požadavků, např. na WebResource.axd. Proto si v prvním kroku ověříme, zda lze použitý handler přetypovat na System.Web.UI.Page a teprve pokud ano, pokračujeme v činnosti.
Činnost sama spočívá v zaregistrování dvou JavaScriptů, k čemuž slouží System.Web.UI.Page.ClientScript. Ten rozeznává tři základní typy skriptů
- ClientScriptBlock - obecný blok JavaScriptového kódu, v tomto případě ho nikde nepoužíváme.
- ClientScriptInclude - odkaz na JavaScript, uložený někde jinde, vyrenderuje se jako <script src="..."></script>.
- StartupScript - JavaScriptový kód, který se vloží přímo do stránky a tedy se spustí v okamžiku natažení stránky.
Každý z těchto bloků lze pojmenovat a máte k dispozici dvě metody: IsXXXRegistered a RegisterXXX.
Pomocí metod IsClientScriptBlockRegistered resp. IsStartupScriptRegistered si ověříme, zda takto pojmenované bloky do stránky už někdo nepřidat před námi. To by se v tomto případě stát nemělo, ale jistota je jistota. Poté pomocí metod RegisterClientScriptInclude a RegisterStartupScript zaregistrujeme příslušné skripty. Tím máme vyděláno, o zbytek se postará ASP.NET.
Další možnosti využití
Událost PreRequestHandlerExecute otevírá možnosti širokého uplatnění HTTP modulů pro práci s web forms, která by jinak musela být vykonávána uvnitř stránek samých. Výhodou je, že tato událost nastává na samém počátku životního cyklu stránky a tedy je možné dělat i takové věci, které jsou v pozdějších fázích nemožné - příkladně programové nastavování témat a master pages.
Použití HTTP modulu znamená univerzální řešení, které lze použít na jakoukoliv aplikaci, čistě změnou konfigurace, bez nutnosti zásahu do aplikace samé. Znamená to tedy, že je možné tuto techniku využít i pro modifikaci aplikací, od kterých nemáme k dispozici zdrojové kódy, ale jenom binární podobu.