Breadcrumb a modifikovanie SiteMapy
Modifikovanie SiteMapy je bežnou potrebou. Predstavme si, že máme nejaké článočky pozaraďované do kategórií. V MasterPage máme užitočný breadcrumb(SiteMapPath control), prostredníctvom ktorého spoločne pre všetky podstránky v rámci nášho webu zobrazujeme aktuálnu cestu. Pri browsnutí stánky, ktorá zobrazuje článok chceme zahrnúť do cesty aj názov kategórie(alebo kategórií ak sú hierarchické) a rovnako aj nadpis aktuálneho článku. Toto len pomocou pridania záznamu do mapy stránky nedosiahneme. Jediné čo môžeme dosiahnuť je spoločná cesta pre všetky články.
<siteMapNode url="Default.aspx" title="Domov" description="Domov">
<siteMapNode url="~/Clanok.aspx" title="Článok" description="Články"/>
</siteMapNode>
Domov – Článok
To má ale veľmi ďaleko ku požadovaného správaniu. SiteMapProvider nás notifikuje o vyhodnocovaní uzla v kontexte každej stránky, kde je to potrebné prostredníctvom udalosti SiteMapResolve. V ňom môžeme vykonať potrebné mofikácie, ak nám nevyhovuje to, čo sa vyhodnotilo na základe mapy stránky.Keď som kedysi hľadal spôsob, ako ľudia riešia problém mofikácií, našiel som v zásade dva prístupy. Prvý, pomerne často sa vyskytujúci, odporúčal registrovať sa na udalosť SiteMapResolve v rámci metód životného cyklu stránky(OnLoad,OnInit). Výsledkom samozrejme je, že zavesený event handler sa potom volá aj v kontexte iných stránok, kedže existuje je len jedna inštancia providera per aplikácia. Tomu sa snažil cowboy programátor predísť záplatou v podobe rôznych podmienok vyhodnocujúcim, či sa udalosť vyvolala alebo nevyvolal v kontexte správnej stránky. Druhý prístup, kvalitatívne lepší, správne pridával jeden spoločný handler pre SiteMapResolve vrámci application start. Samozrejme, problémom takto je, ako získať kontext požadovanej stránky.Jeden prístup staval na url parametroch a čítal nezávisle dáta z databázy, aby mohol vyskladať cestu.(V praxi to znamená zvačša duplicitné dotazovanie sa na databázu, lebo všetky potrebné dáta načítal v stránke) Ďalší prístup aspoň staval na Handler atribúte z HttpContextu, čo je v tomto prípade inštancia našej stránky a ťahal údaje cez inteface priamo z inštancie stánky.Objektový design a prehľadnosť dostali dovolenku. Stop. To chce štýl ;-) Vytvorme si najprv interface pre stránky, ktoré potrebujú mofikovať SiteMapu pri vyhodnocovaní s jedinou metódou, v ktorej zmeny vykonáme.
public interface ISiteMapHandler
{
SiteMapNode ProcessSiteMap(SiteMapProvider oProvider);
}
V zásade sa nám stačí zavesiť na SiteMapResolve event pri štarte aplikácie(Application_Start handler v global.asax), zistiť si, či aktuálna inštancia HttpHandlera je stránka, ktorá implementuje tento interface a ak áno, pretypovať ju na náš interface, zavolať z neho metódu a vrátiť modifikovaný uzol mapy stránky(SiteMapNode) alebo null v prípade, že stránka tento interface neimplementuje, čím oznámime že sme spokojný s pôvodne resolvnutým uzlom z danej mapy stránky. My si však chceme uľahčiť život a preto si vyrobíme kompoment, do ktorého stačí zaregistrovať správneho providera, ktorého uzly niektorá zo stránok modifikuje a o všetko ostatné sa postará za nás. Ja som urobil thread safe implementáciu, ale pokiaľ si vystačíme iba s registáciou v rámci štartu aplikácie, môžeme zostať pri jedinej statickej metóde, ktorá nám zabezpečí registráciu event handlera s volaním ProcessSiteMap metódy.(SiteMapResolver je implementovaný ako singleton s lazy inicializáciou kôly thread safety a zoznam registrovaným providerov je implementovaný ako synchronizovaný arrayList z rovnakých dôvodov)
public class SiteMapResolver
{
private static readonly SiteMapResolver m_oInstance = new SiteMapResolver();
private ArrayList m_cProviders;
private SiteMapResolver()
{
m_cProviders = ArrayList.Synchronized(new ArrayList());
}
public static SiteMapResolver GetInstance()
{
return m_oInstance;
}
public bool IsRegistered(string sProviderName)
{
return IsRegistered(GetProviderByName(sProviderName));
}
public bool IsRegistered(SiteMapProvider oProvider)
{
lock (m_cProviders.SyncRoot)
{
return m_cProviders.Contains(oProvider);
}
}
public void Register(string[] arrProviderNames)
{
foreach (string sProviderName in arrProviderNames)
Register(GetProviderByName(sProviderName));
}
public void Register(string sProviderName)
{
Register(GetProviderByName(sProviderName));
}
public void Register(SiteMapProvider oProvider)
{
lock (m_cProviders.SyncRoot)
{
if (m_cProviders.Contains(oProvider))
throw new Exception("Provider is already registered.");
oProvider.SiteMapResolve += new SiteMapResolveEventHandler(m_SiteMapResolve);
m_cProviders.Add(oProvider);
}
}
public void Unegister(string sProviderName)
{
Unregister(GetProviderByName(sProviderName));
}
public void Unregister(SiteMapProvider oProvider)
{
if (oProvider == null)
throw new ArgumentNullException("oProvider");
lock (m_cProviders.SyncRoot)
{
if (!m_cProviders.Contains(oProvider))
throw new Exception("Provider is not registered.");
oProvider.SiteMapResolve -= new SiteMapResolveEventHandler(m_SiteMapResolve);
m_cProviders.Remove(oProvider);
}
}
private SiteMapProvider GetProviderByName(string sProviderName)
{
SiteMapProvider oProvider = SiteMap.Providers[sProviderName];
if (oProvider == null)
new Exception(String.Format("Site map provider {0} doesn't exist.", sProviderName));
return oProvider;
}
private SiteMapNode m_SiteMapResolve(object oSender, SiteMapResolveEventArgs oEArgs)
{
return OnSiteMapResolve((SiteMapProvider)oSender, oEArgs.Context);
}
protected virtual SiteMapNode OnSiteMapResolve(SiteMapProvider oProvider, HttpContext oHttpCtx)
{
IHttpHandler oHttpHandler = oHttpCtx.Handler;
if (oHttpHandler is ISiteMapHandler)
{
ISiteMapHandler oSiteMapHandler = (ISiteMapHandler)oHttpHandler;
return oSiteMapHandler.ProcessSiteMap(oProvider);
}
return null;
}
}
Použitie?
V global.asax zaregistrujeme zvolený provider. Defaultný sa dá jednoducho získať ako SiteMap.Provider. Ja ich v aplikáciách vždy používam viac, takže som tomu prispôsobil rozhranie.
void Application_Start(object sender, EventArgs e)
{
SiteMapResolver oSiteMapResolver=
SiteMapResolver.GetInstance();
oSiteMapResolver.Register("breadcrumb");
//pre default
//oSiteMapResolver.Register(SiteMap.Provider);
}
Ako by mohla vyzerať implementácia ProcessSiteMap metódy modifikujúca SiteMapu?
public SiteMapNode ProcessSiteMap(SiteMapProvider oProvider)
{
if (oProvider.Name == "breadcrumb")
{
SiteMapNode oCurrentNode = oProvider.CurrentNode.ParentNode.Clone(true);
SiteMapNode oCategNode = new SiteMapNode(
oProvider,
"cat" + m_oDAC.Category.Id.ToString(),
String.Format("~/Clanky.aspx?c={0}", m_oDAC.Category.Id),
m_oDAC.Category.Name,
m_oDAC.Category.Name);
SiteMapNode oArtNode = new SiteMapNode(
oProvider,
"art" + m_oDAC.Id.ToString(),
String.Format("~/Clanok.aspx?a={0}", m_oDAC.Id),
m_oDAC.Title,
m_oDAC.Title);
oCategNode.ParentNode = oCurrentNode;
oArtNode.ParentNode = oCategNode;
return oArtNode;
}
return null;
}
Výsledok?
Domov – Novinky – Mám inteligetný breadcrumb?
Vieme si pomocť samozrejme aj nejakými dalšími helper metódami, keďže naša implementácia v metóde ProcessSiteMap bude vždy veľmi "podobná", ale to už asi zvládne každý sám.