Možnost uchovávat data je velikou výhodou serverových aplikací a mnoho tvůrců stránek touží po těchto aplikacích právě kvůli tomu, aby si mohli naprogramovat vlastní knihu návštěv nebo anketní systém, což jim standardní HTML nenabízí.
Máme mnoho možností. Dnes se ve velké míře používají databáze, jako je MS SQL, My SQL nebo Access. Pro méně navštěvovaný web je však programování databázových aplikací celkem zbytečné - tvořit questbook na osobní stránce pomocí MySQL je jako jít s kanónem na vrabce.
Co bychom tedy mohli použít? Třeba obyčejné soubory. Práce s nimi je jednoduchá a snadno pochopitelná, mají však velký problém - může k nim přistupovat jen jeden program zároveň. Což může způsobit problémy, hlavně pokud je váš web hodně navštěvovaný. V té chvíli již ale budete stejně potřebovat výkonější prostředky v podobě databází.
V této lekci se poprvé setkáváme s BCL (Basic Class Library) .NET Frameworku a budeme využívat jeho objektů. Bylo by tedy více než vhodné osvětlit si základy objektově orientovaného programování (běžně používaná zkratka je OOP).
Základním prvkem OOP je objekt. Co to je? Objekt je shluk metod, vlastností a událostí. Pokud chceme v OOP vyjádřit nějakou strukturu, vytvoříme pro ni objekt. OOP je elegantní právě díky tomu, že lidská mysl přirozeně myslí a řeší problémy skrze objekty.
Předlohou pro objekt je třída. Třída je forma, z které bude objekt vytvořen. Třídou nazýváme vlastní programový kód, objekt vznikne, když zavoláme konstruktor dané třídy.
Co to je? Ukažme si to na příkladě:
Dim alena As osoba = new osoba("Alena", "Nováková", 38)
V tomto případě jsme vytvořili objekt alena z třídy osoba, které jsme hned do začátku předali několik údajů. Z pohledu programátora třídy je konstruktor blok kódu, který se stará o přijmutí těchto několika údajů, z pohledu programátora aplikace využívajícího objekt můžeme konstruktorem nazvat obsah závorky.
Nyní máme objekt alena... Co s ním? Objekt může mít své vlastnosti. To jsou hodnoty, které si daný objekt drží po dobu své existence.
alena.věk = alena.věk + 1
Objekty mají obvykle metody. To jsou kusy kódu, které vykonávají určitou činnost. Znáte je možná pod jmény procedura nebo funkce.
alena.najezSe()
Objekty mají svoje události. To se týká hlavně rozhraní mezi uživatelem a programem - uživatel vyvolává událost, kterou objekt ošetřuje.
Vraťme se k metodám a konstruktorům. Co když chceme vytvořit více možností, jak danou metodu zavolat? Třeba soubor můžeme načíst buď s pomocí jeho cesty nebo přímo získáním datového proudu. Proto tu existuje možnost přetěžování.
alena.najdiSiPráci(aleninPočítač1) nebo alena.najdiSiPráci(úřadPráce)
Přetěžovat jdou i konstruktory. To už je snad jasné. Další zajímavou vlastností, kterou OOP nabízí, je dědičnost. Jedna třída může tzv. dědit třídu jinou. To znamená, že převezme všechny metody, vlastnosti a události jiné třídy. Sama pak může přidávat vlastní třídy nebo upravovat existující. Třída, která dědí, je většinou speciálním případem třídy, která je děděna. Takže třeba StreamWriter (objekt pro zapisování do souborů skrze datové proudy) je speciálním případem Streamu (objekt pro práci s datovými proudy).
Tak, toto základní obeznámení by vám mohlo pro tentokrát stačit. Můžeme se pustit do...
Pusťme se do toho. Co je to vlastně soubor? Běžně se tímto termínem myslí kolekce dat, která je uložena na pevném disku. Programátor za tímto termínem vidí místo, kam si jeho aplikace může uložit data na delší časové období, nejen po dobu spuštění programu nebo zpracovávání stránky.
Dvěma nejpoužívanějšími třídami v souvislosti s textovými soubory bude StreamReader a StreamWriter. Oba jsou potomci třídy Stream a podle toho s nimi budeme pracovat. Stream lze do českého jazyka přeložit jako proud. A opravdu, s textovým souborem načteným do StreamReaderu budeme pracovat jako s kontinuálním proudem dat.
Začneme se čtením souborů. Jak taková práce se StreamReaderem vypadá? Nejdříve musíme vytvořit instanci daného objektu. Pamatovat při ní musíme na to, že tato třída má přetížený kontruktor, tudíž ji můžeme volat různými způsoby. Nejobvyklejší jsou tyto:
- StreamReader(Stream)
Můžeme tedy použít již existujícího proudu dat. Přes tuto deklaraci můžeme načíst již vytvořenou instanci objektu FileStream. Například:
Dim soubor As FileStream = New FileStream(cesta, FileMode.Open, FileAccess.Read) Dim SR As StreamReader = New StreamReader(soubor)
To je užitečné ve chvíli, kdy chceme přesně nastavit chování při otevírání souboru. Konstruktor objektu FileStream je ale také přetížený. Asi nejužitečnější jsou tyto kontruktory:
- FileStream(String, FileMode)
- FileStream(String, FileMode, FileAccess)
- FileStream(String, FileMode, FileAccess, FileShare)
Parametr FileMode definuje, jak se má soubor otevřít. Nabízí tyto možnosti:
- FileMode.Append - Pokud soubor existuje, otevře ho a skočí na jeho konec - můžete rovnou přidávat nová data ke stávajícímu obsahu. Pokud soubor neexistuje, bude vytvořen.
- FileMode.Create - Pokud soubor existuje, bude jeho obsah vymazán. Pokud neexistuje, bude vytvořen.
- FileMode.CreateNew - Pokud soubor neexistuje, bude vytvořen. Pokud existuje, bude vyvolána vyjímka.
- FileMode.Open - Otevře soubor, pokud existuje, pokud ne, bude vytvořena vyjímka.
- FileMode.OpenOrCreate - Pokud soubor existuje, bude pouze otevřen, pokud ne, bude také vytvořen.
Atribut FileAccess definuje, co může být se souborem prováděno. Přípustné hodnoty jsou Read (pouze pro čtení), Write (pouze pro zápis) a ReadWrite (pro zápis i pro čtení). Tato hodnota samozřejmě musí odpovídat objektu používanému pro procházení (StreamWriter, StreamReader).
Atribut FileShare určuje, co si mohou ostatní procesy s daným souborem dovolit, pokud s ním právě pracuje naše aplikace. Je to těžká volba (mezi None, Inheritable, Read, ReadWrite, Write) - já osobně bych zřejmě nepovolil zápis, protože to by mohlo způsobit nestabilitu mé aplikace. Nejlepší je asi nechat standardní hodnotu None.
- StreamReader(String)
Rychlejší však je přímo uvést cestu požadovaného souboru.
Dim SR As StreamReader = New StreamReader(cesta)
- StreamReader(Stream nebo String, Encoding)
.NET standardně zapisuje a čte soubory ve formátu UTF-8. Pokud chceme z nějakého důvodu toto standardní kódování změnit, je možné ho buď změnit pro celou aplikaci v souboru web.config nebo právě v druhém parametru StreamReaderu. Ten si žádá instanci třídy System.Text.Encoding. Příklad použití:
Dim SR As StreamReader = New StreamReader(cesta, System.Text.Encoding.ASCII)
Instanci StreamReaderu jsme vytvořili... Co dál? Nyní máme k dispozici metody Read a ReadLine, které použijeme pro vlastní čtení souboru.
Metoda Read je přetížená, ve verzi bez parametrů vrací jeden znak ve tvaru celého čísla (int32), druhá verze je o něco složitější. Jako vstupy se bere pole znaků (char), celé číslo, které oznamuje, na kterém indexu v poli znaků se má začít zapisovat a další celé číslo, které označuje maximální možný počet načtených znaků. Po provedení tohoto přetížení této funkce se zadané pole naplní znaky ze souboru.
Metoda ReadLine je myslím používanější a užitečnější. Nemá žádné parametry - prostě přečte jeden řádek souboru a vrátí jeho obsah. Jeden řádek je v souboru ukončen speciálním znakem, který můžeme v programu zapsat jako \n (to ještě využijeme, až budeme chtít nějak "zcivilizovat" HTML kód generovaný serverem).
Možná bych se ještě mohl zmínit o metodě ReadToEnd. Přečte všechna data od aktuální pozice kurzoru v souboru a vrátí je.
Často budeme potřebovat přečíst veškerý obsah souboru po řádcích. Jak ale zjistíme, že už jsme čtení ukončili (že už se kurzor nachází na konci souboru)? K tomu se skvěle hodí metoda Peek. Vrátí následující znak, ale nepohne s kurzorem v souboru. Pokud není žádný další znak dostupný (narazili jsme na konec souboru), vrátí -1. Celý soubor obsah souboru tedy po řádcích vypíšeme tímto způsobem:
Do While SR.Peek() <> -1 Response.Write(SR.ReadLine() + "<br>") Loop
Elegantní, není-liž pravda? V tomto případě ale existuje elegantnější metoda s použitím funkce ReadToEnd.
Response.Write(SR.ReadToEnd().Replace("\r", "").Replace("\n", "<br>"))
Kurzor je na začátku souboru - metoda ReadToEnd přečtě soubor celý. Jak ale zajistit převedení odřádkování v souboru do odřádkování v HTML kódu? Metoda Replace je přístupná pro každý String, kterým výstup funkce ReadToEnd nepochybně je. Ve stringu vyhledá a nahradí jeden řetězec jiným. Takže nejdřív zrušíme znak \r, který se občas pro odřádkování kombinuje s \n a potom samotný \n změníme na <br>. Metodu Replace si zapamatujte, je velmi užitečná.
Pozor! Vždy po skončení práce se souborem doporučuji zavolat jeho metodu Close(). Ta uzavře soubor, se kterým jsme pracovali. Pokud ji nezavoláme, soubor zůstane otevřený a příští pokus o otevření skončí chybovou hláškou, že daný soubor je používán jiným procesem. Je možné upravit vlastnost FileShare u objektu FileStream, ale přesto je vhodnější každý soubor vždy zavřít.
Dostáváme se k třídě StreamWriter. Je v mnohém podobná StreamReaderu, má i velmi podobné konstruktory, takže je snad ani nebude třeba vysvětlovat.
Podobně jako StreamReader má i tato třída funkci Write a WriteLine. Write je opět přetížená. Tentokrát čtyřikrát:
- StreamWriter.Write(Char)
Samozřejmě, zapsat můžeme jeden znak.
- StreamWriter.Write(Char[])
... nebo pole znaků.
- StreamWriter.Write(String)
... obyčejný řetězec.
- StreamWriter.Write(Char[], Integer, Integer)
Složitější možnost přibližně odpovídající metodě StreamReaderu Read(). Umožňuje zapsat část pole znaků definované počátkem a délkou části pole.
A pak je tu samozřejmě WriteLine. Může vás zmást, že na MSDN Library v referenci tříd BCL tato metoda uvedena není. Není to však způsobeno nedostatky v dokumentaci, spíš tím, že vám "zamlčuji" některá méně důležitá fakta. Třeba to, že metoda StreamWriter (StreamReader je na tom obdobně) nedědí přímo Stream, ale třídu TextWriter, která se stejně jako StreamWriter nachází ve jmenném prostoru System.IO. Tato třída obsahuje mnohé funkce, včetně WriteLine.
Nebudu zde vypisovat všechna přetížení - je jich hodně. Zapisovat totiž mimo typu String jdou i další, jako je boolean, integer, decimal atp. Nám bude ale zatím bohatě stačit znát přetížení StreamWriter.WriteLine(String).
I zde musím zdůraznit nutnost používat metodu Close() při ukončení práce se souborem.
Nyní se pustíme do malého opakování všeho, co jsme se zatím o tvorbě webových stránek pomocí technologie ASP.NET naučili. Pokusíme se sestavit primitivní knihu návštěv. Jistě jste už někde na Internetu takovou aplikaci viděli - webmaster ji většinou dává na svoje stránky s lichou nadějí, že mu návštěvníci napíší nějakou konstruktivní kritiku jeho webu. Nebudeme se zatím snažit o nějakou velkou funkcionalitu - například o WYSIWIG editaci příspěvků nebo o threadovou diskuzi se zatím pokoušet nebudeme. Až se naučíme databáze, znovu se k tomutu projektu vrátíme.
Všechny příspěvky budeme ukládat - jak jinak - do textového souboru. Co řádek souboru, to příspěvek do diskuze. Formát jednoho řádku (příspěvku) bude následující:
datum a čas přidání#uživatelovo jméno#mailová adresa#vlastní příspěvek
Je tu ovšem otázka, jak tyto příspěvky v souboru řadit. Totiž jestli má být nejnovější příspěvek zapisován na začátek nebo na konec souboru. Pokud budeme nové příspěvky přidávat na konec, bude zapsání nového uživatelova postřehu trvat malou chviličku - módem přístupu Append můžeme na konec souboru příspěvky pohodlně přidávat. Na druhou stranu pro zobrazní první stránky knihy návštěv budeme muset dojet až na konec tohoto souboru, protože v tomto případě je zvykem zobrazovat příspěvky od nejnovějšího po nejstarší. Pokud se rozhodneme přidávat nové příspěvky na začátek souboru, budeme pohodlně číst, ale špatně zapisovat (budeme muset zapsat celý obsah souboru do pole, soubor vymazat, přidat nový zápis a zapsat původní obsah souboru z pole).
Vzhledem k tomu, že budeme pravděpodobně mnohem častěji zobrazovat než číst, rozhodl jsem se k druhé variantě a komplikovanému zapisování souborů.
Další otázkou je, jestli v tomto případě použít pokročilou funkcionalitu serverových ovládacích prvků. Myslím si, že pro tuto úlohu se moc nehodí, protože tyto prvky jsou zaměřeny na získávání údajů z databáze. Tedy přesněji řečeno z objektu DataSet, který si můžeme bez problémů naplnit z databáze sami. Stále si ale myslím, že pro tak jednoduchou webovou aplikaci nebude takových prostředků třeba. Použijeme je pouze pro vytvoření formuláře pro získání nových příspěvků. Vylastní výpis knihy budeme provádět přes serverový ovládací prvek Label.
Pusťme se do toho! Nezalekněte se tohoto kódu, je to kompletní zápis celého programu a podrobně ho vysvětlím.
01 <%@ Page Language="VB" Debug="True" %> 02 <%@ Import Namespace="System.IO" %> 03 <%@ Import Namespace="System.Text" %> 04 05 <script runat="server"> 06 Private adresa As String = "d:/localhost/questbook.txt" 07 Sub Page_Load() 08 Dim SR As StreamReader = new StreamReader(adresa) 09 10 Dim kniha As String = "" 11 Dim parsRadek() As String 12 Do While SR.Peek <> -1 13 parsRadek = SR.ReadLine.Split("#") 14 kniha = kniha + zobrazPrispevek(parsRadek(0), parsRadek(1), parsRadek(2), parsRadek(3), parsRadek(4)) 15 Loop 16 17 SR.Close() 18 20 popisek.Text = kniha 21 End Sub 22 23 24 25 Sub novyPrispevek(obj As object, e As EventArgs) 26 Dim zalSoubor(100) As String 27 28 Dim SR As StreamReader = new StreamReader(adresa) 29 Dim i As Integer = 0 30 Do While (SR.Peek <> -1 AND i < 100) 31 zalSoubor(i) = SR.ReadLine() 32 i = i + 1 33 Loop 34 35 SR.Close() 36 37 Dim soubor As FileStream = File.Open(adresa, FileMode.Create) 38 Dim SW As StreamWriter = new StreamWriter(soubor) 39 40 41 Dim datum As String = Now.ToString("dd. mm. yyyy HH:MM") 42 SW.WriteLine(datum + "#" + uzjmen.Value.Replace("#","") + "#" + mailad.Value.Replace("#","") + "#" + predmet.Value.Replace("#","") + "#" + obsah.Value.Replace("#","")) 43 44 For q As Integer = 0 To i - 1 45 SW.WriteLine(zalSoubor(q)) 46 Next q 47 48 SW.Close() 49 50 popisek.Text = zobrazPrispevek(datum, uzjmen.Value.Replace("#",""), mailad.Value.Replace("#",""), predmet.Value.Replace("#", ""), obsah.Value.Replace("#","")) + popisek.Text 51 End Sub 52 53 54 55 Function zobrazPrispevek(datum As String,uzjmen as String, mailad As String, predmet As String, obsah As String) 56 Dim SB As StringBuilder = new StringBuilder() 57 SB.Append("<small>" + datum + "</small> ") 58 SB.Append("<b>" + uzjmen + "</b> (<a href=""mailto:" + mailad + """>" + mailad + "</a>)<br>") 59 SB.Append("<big><b>" + predmet + "</b></big><br>") 60 SB.Append(obsah + "<br><br>") 61 return SB.ToString() 62 End Function 63</script> 64 65<form runat="server" action="questbook.aspx" method="post"> 66 Vaše jméno: <input type="text" runat="server" id="uzjmen" /><br> 67 Mailová adresa: <input type="text" runat="server" id="mailad" /><br> 68 <b>Předmět</b>: <input type="text" runat="server" id="predmet" /><br> 69 <textarea id="obsah" runat="server"> </textarea><br> 70 <button id="cudlik" onServerClick="novyPrispevek" runat="server">Odešli!</button> 71</form> 72 73<asp:Label id="popisek" runat="server" /> 74 75<a href="questbook.aspx">Obnovit</a>
Po úvodních direktivách přichzí jako první metoda Page_Load. Na šestém řádku se deklaruje proměnná adresa, do které si uložíme (překvapivě) adresu našeho souboru s obsahem diskuze. Každá její změna pak bude jednodušší. Nastupuje metoda Page_Load, která si nejdříve otevře náš soubor (8) a začne postupně načítat jednotlivé řádky. Mohli bychom se pozastavit u funkce Split. To je velmi užitečná věc, která nám textový řetězec "rozseká" na kusy. "Místa řezu" jsou voleny na místech se zadaným znakem nebo řetězcem. Takže například příkaz:
dim vysledek() as String vysledek = "abaqobac".Split("b")
uloží do proměnné vysledek pole s třemi prvky {"a", "aqo", "ac"}. Pokud zavoláte tuto funkci s parametrem "" (prázdný řetězec), vyjde vám za výsledek pole jednotlivých znaků původního Stringu. Opravdu velmi užitečné.
Vraťme se k našemu programu. Funkce Split je tu určena k rozdělení řádku souboru na jednotlivé položky oddělené znakem "#". V dalším kroku cyklu si zavoláme funkci zobrazPrispevek. Proč je to funkce? Protože stejnou operaci potřebujeme provést i v metodě novyPrispevek a bylo by nepraktické psát stejný kód na dvě místa. Tato funkce využívá třídy System.Text.StringBuilder. Ta nám umožňuje elegantně "složit" řetězec bez ošklivých bloků "vysledek = vysledek + ...". V této funkci postupně vytvoříme HTML kód jednoho příspěvku a vrátíme ho. Původní cyklus ho připojí do proměnné kniha.
Ta je následně vypsána (20) a soubor uzavřen (17). To bylo docela jednoduché, ne?
Následuje funkce novyPrispevek. Ta na řádcích 26 - 35 otevře soubor a načte všechny příspěvky v něm do proměnné. Kdyby do naší diskuze náhodou přišlo více než sto ohlasů, program nehavaruje (díky podmínce v příkazu while), ale ořízne nejstarší příspěvek. Dále je otevřen soubor a díky parametru FileMode.Create bude předchozí obsah souboru vymazán. Jako první posléze zapíšeme nový příspěvek a pomocí funkcí Replace dáváme pozor na to, aby se nám do souboru nevloudil další znak "#" - to by nám rozhodilo strukturu jednoho řádku souboru.
Na řádku 41 si můžete všimnout zajímavé věci - program vezme aktuální datum a provede funkci ToString s jakýmsi textovým parametrem. To je další úžasná věc - tímto způsobem můžete datum převést do prakticky libovolného formátu. Parametry viz SDK a MSDN.
Na řádcích 44 - 46 zapisujeme do souboru jeho původní obsah. Na řádku 50 přidáváme k obsahu Labelu popisek ještě nejnovější příspěvek, který by se jinak neprojevil a uživatel by tak mohl být zmaten.
Tak, dnešní lekce vám doufám dodá dostatek materiálu k přemýšlení na celý měsíc. Náš GuestBook je samozřejmě nedokonalý. Minimálně nabídnout možnost stránkování by bylo užitečné.
V příští lekci se budeme věnovat konfiguračním souborům web.config a global.asax. Povíme si také něco o možnostech cachování. A zkusíme dále vylepšit Guestbook.
Veškeré náměty, dotazy a připomínky pište na adresu lansky@czech-ware.net.