V minulém dílu jsme začali vyvíjet naši aplikaci pro evidenci toho, co jsme kdy komu půjčili a rádi bychom ještě někdy viděli. Dnes navrhneme a vytvoříme strukturu celé aplikace, naučíme se řídit přístup k jednotlivým stránkám, abychom zabezpečili, že se uživatelé dostanou jen k funkcím, ke kterým mají oprávnění.
Otevřete si tedy projekt z minula a budeme pokračovat dál. Vytvoříme několik ContentPages pro naši MasterPage.Master. Už máme hotovou stránku Default.aspx, na které se bude zobrazovat seznam vypůjčených věcí.
Struktura projektu a jednotlivé stránky
- Admin - složka, ve které budou stránky pro administrátora
- Users.aspx - správa uživatelů a přidělování rolí
- Categories.aspx - správa kategorií výpůjček
- Default.aspx - seznam vypůjčených věcí, filtrování podle kategorií, uživatele, popisu a stavu vrácení, mazání položek
- Lend.aspx - stránka pro zadání/editaci informací o výpůjčce
- Login.aspx - přihlašovací dialog
Vytvořte tedy do kořenového adresáře aplikace novou Content Page a pojmenujte ji Lend.aspx. Dále vytvořte novou stránku (už ne Content Page) pomocí Add New Item, a pojmenujte ji Login.aspx. Tato stránka nebude začleněna do MasterPage.master, přidělali bychom si tím spoustu práce, museli bychom například pracně schovávat menu atd.
Při vytváření nových stránek si samozřejmě zvolte programovací jazyk, který v nich chcete používat. V tomto seriálu zveřejňuji kód jak v jazyce VB.NET, tak i v jazyce C#.
Dále vytvořte složku Admin a do ní přidejte dvě Content Pages. Vytvořte je tak, jako obvykle, a pak je do složky Admin přetáhněte. Vaše webová aplikace by nyní měla vypadat takto:
Vytvoření rolí, uživatelů a nastavení přístupových práv pro složky
Pro tyto účely je spolu s Visual Studiem dodávána webová aplikace, ve které lze vše přehledně nastavit. Občas je s touto aplikací hodně vztekání, protože poměrně často padá, ale zkusíme ji použít, snad budeme mít štěstí. Klikněte nahoře v okně Solution Explorer na ikonku .
V novém okně prohlížeče se spustí úžasná webová aplikace ASP.NET Configuration Tool.
Přepněte se na záložku Zabezpečení (Security) a klikněte na odkaz Vytvořit nebo spravovat role (Create or manage roles). Na objevivší se stránce přidejte roli admin a tlačítkem Zpět se vraťte na stránku Zabezpečení.
Klikněte na odkaz Vytvořit uživatele (Create user) a vytvořte uživatele s uživatelským jménem admin a přiřaďte jej do role admin. Vymyslete si nějaké heslo a zadejte e-mailovou adresu. Přidejte ještě alespoň dva další uživatele neadministrátory, ať máme na čem testovat. Pak se opět vraťte na stránku Zabezpečení.
Poslední věc, kterou provedeme, je nastavení přístupových pravidel. Klikněte na tlačítko Spravovat pravidla přístupu (Manage access rules).
Klikněte na složku Vypujcky a na tlačítko Přidat pravidlo. Pravidlo nastavte podle obrázku (odepřít přístup nepřihlášeným uživatelům).
Dále klikněte na složku Admin a vytvořte pravidlo podle následujícího obrázku:
Do této složky jsme povolili přístup všem, co mají roli admin. Přidejte této složce ještě jedno pravidlo podle tohoto obrázku:
Tím do této složky povolíme přístup jen uživatelům, kteří mají roli admin. Všichni ostatní budou mít přístup zakázaný. Jen ještě v celkovém přehledu zkontrolujeme pořadí pravidel, první musí být povolení přístupu pro admina, až druhé musí být zákaz přístupu všem. Při ověřování přístupu se toti použije první pravidlo, které se vztahuje na daného uživatele.
Webovou aplikaci pro správu přístupových práv můžeme nyní zavřít, už ji nebudeme potřebovat. Mimochodem pokud chcete vidět, jak tenhle krám spadne, klikněte na tlačítko Hotovo :-). To, co jsme nyní udělali pomocí tohoto nástroje, je možné udělat i bez něj. Do databáze bychom poslali 3 INSERT příkazy - první pro vytvoření role, druhý pro vytvoření uživatele, a třetí do tabulky UsersInRoles, která obsahuje každé přiřazení uživatele do role. Pokud chcete, podívejte se na strukturu tabulky, ať chápete, jak to funguje. Pro každého uživatele bude v této tabulce tolik záznamů, v kolika je rolích, každý záznam obsahuje položky UserName a RoleName. Pro každou dvojici uživatel - role tam bude jeden záznam. Oba sloupce jsou pomocí klíčů navázány na tabulku Users resp. Roles.
Dále webová aplikace nastavila přístupová práva, což bychom mohli udělat sami v souboru web.config. Když se do něj podíváte, uvidíte v něm novou sekci, kterou tam Configuration Tool vygeneroval.
<authorization>
<deny users="?" />
</authorization>
Tím se řekne, že nepřihlášení uživatelé nemají v aplikaci co dělat. Dále se nám vygeneroval i soubor web.config ve složce Admin. Je v něm pouze konfigurační sekce authorization:
<authorization>
<allow roles="admin" />
<deny users="*" />
</authorization>
Tady je povolená role admin a zakázán přístup pro úplně všechny uživatele (otazník jsou nepřihlášení, hvězdička jsou všichni).
Je dobré si uvědomit, že soubor web.config může změnit většinu nastavení ze souborů web.config a machine.config v kořenovém adresáři aplikace, upravená nastavení se pak vztahují pouze na danou složku. To, která nastavení se dají přepsat, a která ne, se samozřejmě dá nastavit, takže konfigurační model ASP.NET je velice silný a modulární a umožňuje provozovatelům webhostingů efektivně povolit či zakázat změny některých nastavení.
Ještě drobnost
Toto nastavení přístupových práv ještě není úplně ideální, uživatelé se totiž někde potřebují přihlásit, a to na stránce Login.aspx, na kterou se ale nyní nedostanou, protože přihlášení nejsou. Pro tuto stránku musíme přístup nepřihlášeným explicitně povolit. Povolíme jim navíc ještě složku App_Themes, aby se na stránku mohly stáhnout styly. Nastavení pro určité cesty můžeme přepsat v souboru web.config, a to pomocí elementu location, který umístěte mimo sekci system.web. Vše, co je uvnitř location, platí jen pro danou cestu a může měnit obecné nastavení pro celou aplikaci.
<location path="login.aspx">
<system.web>
<authorization>
<allow users="?" />
</authorization>
</system.web>
</location>
<location path="App_Themes">
<system.web>
<authorization>
<allow users="?" />
</authorization>
</system.web>
</location>
Tím jsme nepřihlášeným povolili přístup do složky App_Themes a na stránku login.aspx.
Menu a navigace v aplikaci
V této aplikaci bychom si menu klidně mohli udělat sami, budou v něm 4 položky pro administrátora a 2 pro běžné uživatele, ale ukážeme si, jak fungují ASP.NET SiteMaps, které ušetří spoustu práce hlavně u rozsáhlejších menu, kde podle oprávnění uživatelů potřebujeme zobrazovat různé položky, některé navíc třeba ještě vybíráme z databáze a ostatní jsou statické.
Jak Sitemapy fungují?
Každá ASP.NET aplikace může ve svém konfiguračním souboru definovat několik Sitemap providerů. Tito provideři se konfigurují v souboru web.config v sekci siteMap, princip je velmi podobný jako u Membership. SiteMap Provider nějakým způsobem vygeneruje mapu webu (hierarchickou stromovou strukturu názvů stránek a jejich URL), kterou může pak poskytnout například komponentám Menu nebo TreeView. Tyto komponenty pak mapu webu nějakým způsobem zobrazí.
To, jak provider strukturu webu získá, je čistě věcí tohoto konkrétního providera. ASP.NET má vestavěnou třídu XmlSiteMapProvider, což je provider, který si strukturu webu vygeneruje podle XML souboru (dává se mu přípona .sitemap). Vy sami si můžete napsat svého vlastního providera (jednoduše odvodíte novou třídu z třídy StaticSiteMapProvider), a můžete si strukturu webu vytáhnout například z databáze, textového souboru nebo čehokoliv jiného. Providery do sebe můžeme i zanořovat, takže například zde na VbNetu máme základní strukturu udělanou pomocí XmlSiteMapProvidera, zatímco seznam blogů v menu nebo seznam kategorií článků je vygenerován providery, které jsme si pro tento účel sami napsali. Napsat vlastního sitemap providera není nic těžkého, ukážeme si to v tomto článku, kdy do menu z databáze vytáhneme seznam kategorií našich výpůjček.
Výhodou použití sitemap je například to, že při vykreslování menu se automaticky zjišťuje, na které stránky má aktuální uživatel přístup (podle přístupových práv, která jsme nastavovali na začátku tohoto dílu), a zobrazí se jen stránky, kam se aktuální uživatel dostane.
Vytváříme základní strukturu menu
Pravým tlačítkem si klikněte na název website v podokně Solution Explorer a vybrte Add New Item.... Do projektu přidejte novou položku Site Map.
Visual Studio nám vygeneruje základní strukturu tohoto XML souboru. Upravte obsah takto:
<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
<siteMapNode url="~/" title="Root" description="">
<siteMapNode url="~/Default.aspx" title="Výpůjčky" description="" />
<siteMapNode url="~/Lend.aspx" title="Nová výpůjčka" description="" />
<siteMapNode url="~/Admin/Default.aspx" title="Administrace" description="">
<siteMapNode url="~/Admin/Categories.aspx" title="Nová výpůjčka" description="" />
<siteMapNode url="~/Admin/Users.aspx" title="Uživatelé" description="" />
</siteMapNode>
</siteMapNode>
</siteMap>
Struktura tohoto souboru je poměrně zřejmá, důležité je upozornění, že každá sitemapa musí mít právě jednu kořenovou položku siteMapNode. Já jsem jí dal název Root, v aplikaci ji zobrazovat vůbec nebudeme, ale být tam musí. Každý element siteMapNode v sobě může obsahovat libovolný počet elementů siteMapNode, díky čemuž můžeme vytvořit libovolnou stromovou strukturu. Celé menu ale musí mít právě jeden kořen.
Minule jsme si povídali o souboru machine.config, je na místě upozornit, že jeho konfiguraci dědí a může měnit nejprve soubor web.config umístěný ve stejném adresáři. Tyto dva soubory dohromady dávají výchozí konfiguraci každé ASP.NET aplikace. Co se nám na ní nelíbí, můžeme (pokud na to máme práva) změnit právě v našem souboru web.config v aplikaci. Proč to znovu připomínám? V souboru web.config v adresáři .NET frameworku (tam, kde je machine.config) je jeden výchozí SiteMap provider už nadeklarován, je to XmlSiteMapProvider nasměrovaný na soubor Web.sitemap. My si jej v naší konfiguraci předeklarujeme, abychom si předvedli, jak to celé funguje.
Do souboru web.config v kořenovém adresáři naší webové aplikace přidejte dovnitř elementu system.web následující kód, který obsahuje konfiguraci našeho sitemap providera. Všimněte si elementu clear/, ten smaže všechny providery, které jsme zdědili, tzn. ty definované v machine.config atd. Pokud bychom ho tam nedali, naše aplikace by měla providery dva (což v principu vůbec ničemu nevadí, použili bychom jen toho našeho, ale je dobré si to uvědomit).
<siteMap enabled="true" defaultProvider="MySitemapProvider">
<providers>
<clear/>
<add name="MySitemapProvider" type="System.Web.XmlSiteMapProvider"
siteMapFile="Web.sitemap" securityTrimmingEnabled="true" />
</providers>
</siteMap>
Zajímavá je vlastnost securityTrimmingEnabled. Pokud ji nastavíme na true, všechny stránky ze složky ~/Admin/ se zobrazí pouze uživatelům, kteří mají do této sekce přístup, tedy těm, kteří mají roli admin. To je obrovská výhoda, nemusíme vůbec řešit, jak schovat běžným uživatelům odkazy do administrační sekce, pokud zapneme security trimming, máme po starostech, sitemapy si to zařídí samy. U každé položky v sitemapě můžeme navíc specifikovat, kterým rolím přesně se zobrazit má, a kterým ne, to se hodí v případě složitějších sitemap a přístupových práv.
Vlastní Sitemap Provider
Aby toho nebylo málo a něco jsme se naučili, napíšeme si nyní vlastního sitemap providera, který se připojí do databáze, zjistí si seznam kategorií výpůjček, a zanoří je pod stránku Výpůjčky. Přidejte si tedy do projektu novou třídu a vyberte si programovací jazyk, který chcete použít. Třídu pojmenujte SqlSiteMapProvider.
ASP.NET nás upozorní, že soubory s kódem patří do složky App_Code, potvrdíme mu, aby tam třídu umístil.
Do souboru třídy zkopírujte tento kód. Naši třídu jsme zdědili od StaticSiteMapProvider. Visual Studio nám nyní řeklo, že tato třída vyžaduje, abychom naimplementovali její dvě metody - GetRootNodeCore a BuildSiteMap.
Imports Microsoft.VisualBasic
Imports System.Web
Imports System.Data.SqlClient
Public Class SqlSiteMapProvider
Inherits StaticSiteMapProvider
End Class
Visual Studio nám ve VB.NET podtrhne název třídy SqlSiteMapProvider, protože ve třídě StaticSiteMapProvider jsou dvě abstraktní metody, které musíme naimplementovat. Položte kurzor za slovo StaticSiteMapProvider a stiskněte klávesu Enter. Visual Studio definice obou požadovaných metod doplní.
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.UI;
using System.Data.SqlClient;
public class SqlSiteMapProvider : StaticSiteMapProvider
{
}
V C# se to dělá trochu jinak. Když budete chvíli myší jezdit po slově StaticSiteMapProvider, objeví se takový malý titěrný obdélníček, který se po přejetí změní v o něco větší rozbalovací nabídku. Nejjednodušší je kliknout na toto slovo StaticSiteMapProvider a stisknout Shift-Alt-F10, nabídka se rozbalí sama. Klikněte na Implement abstract class StaticSiteMapProvider a Visual Studio požadované deklarace metod do třídy doplní.
Nyní musíme tyto dvě metody naimplementovat, ve skutečnosti toho bude o něco více. Mohli bychom napsat providera přesně na míru naší aplikaci, ale to by nebylo ono, nedal by se použít v aplikaci jiné. Když už něco píšeme, můžeme zvážit, jestli nedá o moc víc práce napsat to obecně, abychom věc mohli využívat i jinde.
Co tedy náš provider bude dělat? Připojí se k databázi, provede nějaký dotaz nad nějakou tabulkou, hodnoty z jednoho sloupce použije jako název stránky a hodnoty z jiného sloupce dosadí někam do URL. Do souboru web.config tedy přidejte konfiguraci našeho providera (hned pod prvního providera, kterého jsme již přidali):
<add name="CategoriesSitemapProvider" type="SqlSiteMapProvider"
title="Výpůjčky" url="~/Default.aspx"
connectionStringName="VypujckyConnectionString"
selectCommand="SELECT [CategoryId], [Title] FROM [Categories] ORDER BY [Title]"
titleColumn="Title"
urlColumn="CategoryId" urlFormatString="~/Default.aspx?category={0}" />
Tím jsme vytvořili novou konfiguraci siteMapProvidera. Vlastnost type říká, která třída se má použít pro vygenerování sitemapy, pokud byste si providera chtěli zařadit do nějakého vlastního namespace, musíte jej do vlastnosti type uvést také, aby jej ASP.NET našlo.
Našemu providerovi jsme nastavili poměrně hodně atributů. Vlastnosti title a url patří kořenové položce, do které se kategorie z databáze vypíší, provider tuto kořenovou položku musí vygenerovat, takže mu je nutné sdělit, jak ji má pojmenovat a kam má odkazovat. Atribut connectionStringName určuje název připojovacího řetězce k databázi, kterou chceme použít, selectCommand je příkaz, který se nad databází spustí, titleColumn je název sloupce, který bude obsahovat názvy stránek, urlColumn jsou hodnoty, které se dosadí do hodnoty urlFormatString pomocí standardních formátovacích funkcí, tedy na místo řetězce {0}.
V našem případě děláme select do tabulky kategorií, jako názvy položek menu použijeme názvy kategorií a do URL dosadíme hodnotu ze sloupce CategoryId, tedy jednoznačného identifikátoru dané kategorie.
Inicializace providera
Abychom mohli providera napsat a používat tuto konfiguraci v souboru web.config, musíme vědět, jak provider funguje. Zjistit se to dá například z dokumentace. Už máme nadeklarovanou metodu BuildSiteMap, uvnitř ní se připojíme k databázi a sestavíme sitemapu, resp. její čast. Metoda GetRootNodeCore jen zavolá BuildSiteMap a vrátí její výsledek.
Abychom se dostali k položkám v konfiguraci souboru web.config a věděli, kam se máme připojovat, musíme přepsat metodu Initialize. Ta jako parametr dostane kolekci všech vlastností, které jsme ve web.config specifikovali. Přidejte tedy dovnitř třídy SqlSiteMapProvider tento kód:
#Region "Properties and configuration"
Private _title As String
''' <summary>
''' Název kořenové položky
''' </summary>
Public Property Title() As String
Get
Return _title
End Get
Set(ByVal value As String)
_title = value
End Set
End Property
Private _url As String
''' <summary>
''' URL kořenové položky
''' </summary>
Public Property Url() As String
Get
Return _url
End Get
Set(ByVal value As String)
_url = value
End Set
End Property
Private _connectionStringName As String
''' <summary>
''' Název connectionStringu, který použijeme k připojení do databáze
''' </summary>
Public Property ConnectionStringName() As String
Get
Return _connectionStringName
End Get
Set(ByVal value As String)
_connectionStringName = value
End Set
End Property
Private _selectCommand As String
''' <summary>
''' SQL příkaz, který vybere seznam položek
''' </summary>
Public Property SelectCommand() As String
Get
Return _selectCommand
End Get
Set(ByVal value As String)
_selectCommand = value
End Set
End Property
Private _titleColumn As String
''' <summary>
''' Název sloupce s názvy stránek
''' </summary>
Public Property TitleColumn() As String
Get
Return _titleColumn
End Get
Set(ByVal value As String)
_titleColumn = value
End Set
End Property
Private _urlColumn As String
''' <summary>
''' Název sloupce s hodnotami, které se budou dosazovat do URL
''' </summary>
Public Property UrlColumn() As String
Get
Return _urlColumn
End Get
Set(ByVal value As String)
_urlColumn = value
End Set
End Property
Private _urlFormatString As String
''' <summary>
''' URL adresa, do které se budou dosazovat hodnoty z datbaáze
''' </summary>
Public Property UrlFormatString() As String
Get
Return _urlFormatString
End Get
Set(ByVal value As String)
_urlFormatString = value
End Set
End Property
Public Overrides Sub Initialize(ByVal name As String, ByVal attributes As System.Collections.Specialized.NameValueCollection)
MyBase.Initialize(name, attributes)
'načíst hodnoty parametrů z konfigurace
If attributes("title") IsNot Nothing Then
Me.Title = attributes("title")
Else : Throw New ArgumentException("title")
End If
If attributes("url") IsNot Nothing Then
Me.Url = attributes("url")
Else : Throw New ArgumentException("url")
End If
If attributes("connectionStringName") IsNot Nothing Then
Me.ConnectionStringName = attributes("connectionStringName")
Else : Throw New ArgumentException("connectionStringName")
End If
If attributes("selectCommand") IsNot Nothing Then
Me.SelectCommand = attributes("selectCommand")
Else : Throw New ArgumentException("selectCommand")
End If
If attributes("titleColumn") IsNot Nothing Then
Me.TitleColumn = attributes("titleColumn")
Else : Throw New ArgumentException("titleColumn")
End If
If attributes("urlColumn") IsNot Nothing Then
Me.UrlColumn = attributes("urlColumn")
Else : Throw New ArgumentException("urlColumn")
End If
If attributes("urlFormatString") IsNot Nothing Then
Me.UrlFormatString = attributes("urlFormatString")
Else : Throw New ArgumentException("urlFormatString")
End If
End Sub
#End Region
#region "Properties and configuration"
private string _title;
/// <summary>
/// Název kořenové položky
/// </summary>
public string Title
{
get { return _title; }
set { _title = value; }
}
private string _url;
/// <summary>
/// URL kořenové položky
/// </summary>
public string Url
{
get { return _url; }
set { _url = value; }
}
private string _connectionStringName;
/// <summary>
/// Název connectionStringu, který použijeme k připojení do databáze
/// </summary>
public string ConnectionStringName
{
get { return _connectionStringName; }
set { _connectionStringName = value; }
}
private string _selectCommand;
/// <summary>
/// SQL příkaz, který vybere seznam položek
/// </summary>
public string SelectCommand
{
get { return _selectCommand; }
set { _selectCommand = value; }
}
private string _titleColumn;
/// <summary>
/// Název sloupce s názvy stránek
/// </summary>
public string TitleColumn
{
get { return _titleColumn; }
set { _titleColumn = value; }
}
private string _urlColumn;
/// <summary>
/// Název sloupce s hodnotami, které se budou dosazovat do URL
/// </summary>
public string UrlColumn
{
get { return _urlColumn; }
set { _urlColumn = value; }
}
private string _urlFormatString;
/// <summary>
/// URL adresa, do které se budou dosazovat hodnoty z datbaáze
/// </summary>
public string UrlFormatString
{
get { return _urlFormatString; }
set { _urlFormatString = value; }
}
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection attributes)
{
base.Initialize(name, attributes);
//načíst hodnoty parametrů z konfigurace
if (attributes["title"] != null)
{
this.Title = attributes["title"];
}
else throw new ArgumentException("title");
if (attributes["url"] != null)
{
this.Url = attributes["url"];
}
else throw new ArgumentException("url");
if (attributes["connectionStringName"] != null)
{
this.ConnectionStringName = attributes["connectionStringName"];
}
else throw new ArgumentException("connectionStringName");
if (attributes["selectCommand"] != null)
{
this.SelectCommand = attributes["selectCommand"];
}
else throw new ArgumentException("selectCommand");
if (attributes["titleColumn"] != null)
{
this.TitleColumn = attributes["titleColumn"];
}
else throw new ArgumentException("titleColumn");
if (attributes["urlColumn"] != null)
{
this.UrlColumn = attributes["urlColumn"];
}
else throw new ArgumentException("urlColumn");
if (attributes["urlFormatString"] != null)
{
this.UrlFormatString = attributes["urlFormatString"];
}
else throw new ArgumentException("urlFormatString");
}
#endregion
Tento strašně dlouhý kód nedělá nic závratného. Nadeklaroval jsem si pro každý přidaný atribut v souboru web.config vlastnost a přepsal metodu Initialize, ve které jsem hodnoty vlastností vytáhnul z kolekce Attributes. Vzhledem k tomu, že všechny jsou povinné, pokud nebyly specifikovány, vyhazuji ArgumentException s názvem parametru, který chybí.
Možná si říkáte, že aby to bylo napsané správně se vším všudy, potřebujeme desetkrát víc kódu, než kdybychom to napsali jednoduše a snadno. Toto však ale už není seriál pro začátečníky, takže se snažím ukázat, jak se to dělá doopravdy. Je velmi vhodné používat vlastnosti, nikdy totiž nevíte, kdy v budoucnu budete chtít hlídat, jaké hodnoty se přiřazují. S proměnnými se to pak upravuje velice špatně, s vlastnostmi je to hračka, prostě do nich přidáte nějakou validaci.
Odbočka - pár tipů pro Visual Studio
Když se podíváte na každou deklaraci vlastnosti, je to poměrně dost kódu. Vypisovat ho ručně není nic příjemného, takže využijeme toho, co Visual Studio umí. Pro každý jazyk je to trochu jinak, ale v obou je to velmi jednoduché.
Deklarace vlastností
Ve VB.NET stačí napsat dovnitř třídy slovo Property a zmáčknout tabulátor. Visual Studio automaticky vygeneruje kód pro deklaraci vlastnosti. Napíšete _titleColumn a klávesou tabulátor se přepnete do dalšího pole, napíšete TitleColumn a tabulátorem se přepnete opět na další pole. Je to velmi rychlé a elegantní.
V C# použijeme Refactor - nadeklarujeme privátní proměnnou dovnitř třídy, klikneme na ni pravým tlačítkem a v menu Refactor vybereme položku Encapsulate Field. Potvrdíme dvě dialogová okna a vlastnost se nám vytvoří.
XML komentáře
XML komentáře jsou věcí velice dobrou, pokud pečlivě okomentujete každou vlastnost a metodu, při jejím používání vám Visual Studio zobrazuje tento komentář spolu s IntelliSense. To je velmi užitečné zvlášť když používáte třídy, které neznáte, nebo které jste nepsali vy. Navíc se z těchto komentářů dá vygenerovat pěkná dokumentace.
Ve VB.NET stačí napsat tři apostrofy, v C# tři lomítka, a Visual Studio si už samo vygeneruje potřebnou strukturu. Pokud nechcete zadávat rozšířené parametry a stačí vám jen summary (souhrn), můžete ostatní elementy smazat, například Visual Basic je tam přidává a zabírá to hrozně místa.
Obě tyto techniky jsou vidět na kratičkém instruktážním videu, které jsem pro tento článek vytvořil:
Regiony
Kód, který tvoří nějakou logickou část, je vhodné uzavřít do regionů. Jsou to ty řádky začínající dvojkřížem - #Region a #End Region, resp. #region a #endregion. Celý region můžete na boku rozbalit nebo sbalit malým rozbalovátkem, aby nezabíral místo.
Vygenerování sitemapy
Nyní naimplementujeme metodu BuildSiteMap, která se připojí k databázi a vygeneruje sitemapu. To, jak se připojit k databázi, byste měli znát, kdyžtak se podívejte na článek Komunikace s MS SQL databází od Tomáše Jechy. Pak už je to velmi jednoduché:
Private root As SiteMapNode
Public Overrides Function BuildSiteMap() As System.Web.SiteMapNode
SyncLock Me
If root Is Nothing Then 'pokud je třeba generovat, generujeme
'vytvořit kořenovou položku
root = New SiteMapNode(Me, Guid.NewGuid().ToString(), Me.Url, Me.Title)
'podpoložky natáhnout z databáze
Dim con As New SqlConnection(ConfigurationManager.ConnectionStrings(Me.ConnectionStringName).ConnectionString)
Dim com As New SqlCommand(Me.SelectCommand, con)
con.Open()
Dim reader As SqlDataReader = com.ExecuteReader() 'provést dotaz
While reader.Read() 'projít vrácené záznamy
Dim node As New SiteMapNode(Me, Guid.NewGuid().ToString(), String.Format(Me.UrlFormatString, reader(Me.UrlColumn)), reader(Me.TitleColumn))
AddNode(node, root)
End While
reader.Close() 'zavřít reader a spojení
con.Close()
End If
Return root 'vrátíme kořenovou položku
End SyncLock
End Function
Protected Overrides Function GetRootNodeCore() As System.Web.SiteMapNode
Return BuildSiteMap()
End Function
private SiteMapNode root;
public override System.Web.SiteMapNode BuildSiteMap()
{
lock (this)
{
if (root == null) //pokud je třeba generovat, generujeme
{
//vytvořit kořenovou položku
root = new SiteMapNode(this, Guid.NewGuid().ToString(), this.Url, this.Title);
//podpoložky natáhnout z databáze
SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings[this.ConnectionStringName].ConnectionString);
SqlCommand com = new SqlCommand(this.SelectCommand, con);
con.Open();
SqlDataReader reader = com.ExecuteReader(); //provést dotaz
while (reader.Read())
{
//projít vrácené záznamy
SiteMapNode node = new SiteMapNode(this, Guid.NewGuid().ToString(), string.Format(this.UrlFormatString, reader[this.UrlColumn]), reader[this.TitleColumn].ToString());
AddNode(node, root);
}
reader.Close(); //zavřít reader a spojení
con.Close();
}
return root; //vrátíme kořenovou položku
}
}
protected override System.Web.SiteMapNode GetRootNodeCore()
{
return BuildSiteMap();
}
Jak to funguje? Protože metoda BuildSiteMap se může spustit v několika vláknech víckrát zároveň, použijeme zámek (Synclock ve VB.NET resp. lock v C#), čímž zabezpečíme, že se kód uvnitř zamčené sekce nespustí dvakrát zároveň. Druhé vlákno jednoduše počká, až první ze zamčené sekce vyleze.
Pokud je proměnná root prázdná, musíme sitemapu vygenerovat, jinak pouze kořenovou položku sitemapy vrátíme, protože je sitemapa už vygenerovaná a drží se v paměti, aby se nenatahovala z databáze při každém požadavku, to by hrozně zpomalovalo a zatěžovalo chudinku databázi.
Když máme sitemapu generovat, tak vytvoříme nejprve kořenovou položku - přiřadíme do ní novou instanci třídy SiteMapNode, jako argumenty konstruktoru předáme aktuálního providera, nějaký unikátní klíč (v tomto případě generujeme nový GUID - unikátní identifikátor), dále jí předáme URL a název. Tím jsme vytvořili kořenovou položku Výpůjčky odkazující na adresu Default.aspx, která bude obsahovat všechny kategorie.
Nyní se připojíme k databázi a vytvoříme nový SQL dotaz. Po spuštění projdeme všechny výsledky, které nám databáze na dotaz vrátila, a pro každý výsledek vytvoříme opět novou SiteMapNode, do níž nastavíme vytažené hodnoty z databáze. Pro náhradu {0} za hodnotu sloupce CategoryId použijeme standardní funkci String.Format. Aby bylo jasné, do jaké kategorie se mají položky zařadit, musíme je metodou AddNode začlenit pod položku root. Metoda AddNode je deklarována ve třídě, ze které je náš SqlSiteMapProvider odvozen, to jen aby bylo jasné, kde se tam ta metoda vzala. Po přečtení všech záznamů akorát stačí zavřít spojení a je hotovo.
Metoda GetRootNodeCore pouze zavolá naši metodu BuildSiteMap. Sitemapa se vygeneruje, pokud ještě vygenerovaná není, a pokud je, tak už se jen vrátí. Tím zajistíme, že se nám sitemapa nebude generovat stále znovu a znovu, prostě se sestaví jednou a hotovo.
Začlenění sitemap do sebe
Nyní musíme říci, kam se má struktura vygenerovaná naším providerem začlenit. Otevřete soubor Web.sitemap a nahraďte element siteMapNode ukazující na stránku ~/Default.aspx tímto elementem:
<siteMapNode provider="CategoriesSitemapProvider" />
Tím jsme zajistili, že místo položky Výpůjčky natvrdo nadefinované v tomto souboru se uloží kořenová položka providera, kterého jsme uvedli. CategoriesSitemapProvider je jméno (atribut name) našeho druhého providera ze souboru web.config, kde jsou také jeho patřičná nastavení. Jako výchozího providera jsme nastavili MySiteMapProvider, který používá soubor web.sitemap. Do tohoto souboru se na příslušné místo tedy doplní seznam kategorií z databáze.
Je samozřejmě úplně jedno, jaké typy providerů do sebe začleníme, do našeho databázového providera jsme mohli začlenit ještě další providery, nebo naopak našeho databázového použít jako výchozího a začlenit do něj web.sitemap. Anebo celé menu vygenerovat pouze z databáze, opravdu nejsme ničím omezeni, v konfiguraci si jednoduše nastavíme providery a je to.
Přidáváme menu do stránky
Nyní máme sitemapu nachystanou, jen ji budeme chtít někde zobrazit. Otevřete si stránku MasterPage.master a oddíl header upravte takto:
<div id="header">
<div id="loggedUser">
Přihlášený uživatel: <strong><asp:LoginName ID="LoginName1" runat="server" /></strong>.
<asp:LoginStatus ID="LoginStatus1" runat="server" />
</div>
<div id="menu">
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server" ShowStartingNode="false" SiteMapProvider="MySiteMapProvider" />
<asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1" Orientation="Horizontal">
</asp:Menu>
</div>
</div>
Za textem Přihlášený uživatel vypíšeme jeho uživatelské jméno (komponenta LoginName, nedělá nic jiného, že vypíše jméno aktuálně přihlášeného uživatele) a pak tlačítko Odhlásit (komponenta LoginStatus). Komponenta LoginStatus zobrazuje tlačítko Odhlásit či Přihlásit podle toho, jestli je někdo aktuálně přihlášen, nebo není.
Pro vykreslení menu jsme použili komponentu Menu. Musíme jí nastavit nějaký datový zdroj, který jí sitemapu poskytne, předáme jí tedy SiteMapDataSource. Ten jsme nastavili, aby nezobrazoval hlavní kořenovou položku (tu položku Root ze souboru web.sitemap, která tam musela být, i když jsme ji vlastně nechtěli), a aby používal providera MySiteMapProvider, tedy sitemapu z XML souboru. Samotnému Menu jsme ještě nastavili orientaci na vodorovnou, naše menu je totiž řádkové a nikoliv sloupcové.
Skiny a témata
Obecně se doporučuje co nejvíce se snažit oddělit vzhled od obsahu stránek, k tomu také byly zavedeny CSS styly. Vzhledem k tomu, že v ASP.NET používáme komponenty, často u určitého typu nastavujeme stále ty samé vlastnosti na stále ty samé hodnoty. Abychom se tomuto nepříjemnému faktu vyvarovali, přináší nám ASP.NET skiny. O tématech samotných jsme už mluvili, pokud vše týkající se vzhledu dáme do příslušné složky App_Themes, můžeme si témata přepínat ať už v konfiguraci, nebo programově, a pomocí nich měnit vzhled stránek. Se skiny je to podobné, jsou to vlastně takové CSS styly na ASP.NET komponenty. Přidáme si tedy do našeho tématu (do složky App_Themes/Default) nový skin:
Do tohoto souboru přidejte tento kód:
<asp:Menu runat="server"
StaticMenuItemStyle-CssClass="menuItem" StaticHoverStyle-CssClass="menuItemSelected"
DynamicMenuItemStyle-CssClass="menuItemDynamic" DynamicHoverStyle-CssClass="menuItemSelected" />
Tím jsme řekli, že všem komponentám Menu, které se objeví na stránce, se nastaví tyto čtyři vlastnosti na zde uvedené hodnoty, pokud v deklaraci komponenty ve stránce nenastavíme tyto vlastnosti jinak. Do skinů rozhodně nepatří vlastnost ID (naopak runat="server" tam být musí). Skiny se nejčastěji používají právě pro vlastnosti specifikující vzhled.
Všimněte si také, že ze style vlastností používám výhradně vlastnost CssClass a konkrétní vzhled dospecifikuji v CSS souboru. Je to proto, že pokud budeme styly nastavovat natvrdo zde, budou se do stránky renderovat jako inline styly. S rozsáhlejšími komponentami pak docela rychle narůstá velikost vygenerovaného HTML, protože se definice stylu opakuje pro každou položku. Je dobrým zvykem vše dělat přes CSS styly, které stejně většinou už máme nachystané od designéra.
Každá komponenta má navíc ještě vlastnost SkinID, což je ekvivalent třídy v CSS. Pokud ve skinu uvedu v deklaraci komponenty vlastnost SkinID, dané vlastnosti se nastaví pouze těm komponentám ve stránce, které mají SkinID nastaveno na stejnou hodnotu. Takto můžu v aplikaci přebarvit například všechna tlačítka na modro (což se mimochodem v CSS dělá dost blbě, protože se tím přebarví i textová pole a vše, co má značku input), akorát všem tlačítkům s textem Zrušit nastavím ve stránce SkinID na Cancel a pak je ve skinu všechny obarvím na červeno, protože uvedu Button znovu s nastaveným SkinID na Cancel a červenou barvou. Pro každou komponentu tedy můžete mít jeden výchozí vzhled a libovolně mnoho dalších pomocí SkinID.
Otestování sitemapy
Nyní budeme chtít naše menu otestovat. Otevřete si podokno Database Explorer (pokud ho nevidíte, zapněte v jej menu View), rozbalte databázi Vypujcky a klikněte pravým tlačítkem na tabulku Categories. Vyberte položku Show Table Data, čímž zobrazíte, co v tabulce je.
Do této tabulky přidejte kategorie podle obrázku (první sloupec nevyplňujte, měl by se vyplnit sám; pokud se nevyplní, tak jste v minulém dílu zapomněli nastavit Identity Specification pro tuto tabulku, tak to musíte napravit).
To je celé. Když nyní stisknete Ctrl+F5, měla by se zobrazit prázdná červená stránka s adresou Login.aspx?returnUrl=%2fVypujcky%2fdefault.aspx. Zatím jsme ještě na stránku nepřidali přihlašovací dialog. Musíme to tedy napravit.
Přihlašovací dialog
Vraťte se do Visual Studia a otevřete stránku Login.aspx. Záhlaví nechte, její HTML změňte takto:
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Výpůjčky - přihlášení</title>
</head>
<body>
<form id="form1" runat="server">
<div id="window">
<asp:Login ID="Login1" runat="server">
</asp:Login>
</div>
</form>
</body>
</html>
Do souboru SkinFile.skin přidejte tento řádek, aby se na nadpis Přihlášení v komponentě Login použila třídy title:
<asp:Login runat="server" TitleTextStyle-CssClass="title" />
Aby stránka hezky vypadala, přidejte ještě tyto definice do souboru App_Themes/Default/StyleSheet.css.
#window
{
width: 400px;
margin: 200px auto;
padding: 30px;
background-color: White;
}
#window table
{
width: 100%;
}
#window .title
{
color: #921f1f;
font-size: 150%;
font-weight: bold;
}
.menuItem
{
width: 120px;
padding: 5px 0px;
text-align: center;
background-color: transparent;
}
.menuItemDynamic
{
width: 120px;
padding: 5px 0px;
text-align: center;
background-color: #efd6d6;
}
.menuItemSelected
{
width: 120px;
padding: 5px 0px;
text-align: center;
background-color: #cd7f7f;
}
Tím jsme nastylovali div s id window tak, aby byl 400 pixelů široký, vevnitř měl okraj 30px na všech stranách, a vycentroval se na střed 200 pixelů od horního okraje. Všechny tabulky, které jsou uvnitř, budou mít šířku 100%, to je kvůli komponentě Login, ta se vyrenderuje jako tabulka. Nadpis bude větším písmem, tučně a tmavě červenou barvou. To je celé. Přihlašovací stránka bude nyní vypadat takto. Není to nic moc, ale nám to musí stačit.
Přihlaste se jako admin. Uvidíte celé menu s našimi položkami, když najedete na položku Výpůjčky, rozbalí se podkategorie, které jsme naším providerem vytáhli z databáze. Vidíme, že máme přístup i do administrační sekce.
Pokud kliknete na tlačítko Odhlásit, dostanete se opět na přihlašovací stránku. Jakmile se přihlásíte jako normální uživatel, v menu už administrátorskou kategorii neuvidíte. Vše tedy funguje tak, jak má.
Pokračování příště
To je pro tento díl všechno, doufám, že se vám seriál líbí a že vám je užitečný. Jakékoliv dotazy, náměty a připomínky pište do diskuse.