ASP.NET pro začátečníky
5. Práce se soubory
MENU

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í.

Intermezzo: Základy objektově orientovaného programování

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...

Práce s textovými soubory

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.

Čtení souborů

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:

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.

Zápis do souborů

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:

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.

Kniha návštěv

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.

Závěrem

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.

Lukáš Lánský
Veškeré náměty, dotazy a připomínky pište na adresu lansky@czech-ware.net.