Pro náš demonstrační příklad si nejprve vytvoříme běžným způsobem velmi jednoduchou třídu modelu, která bude zapouzdřovat nejzákladnější údaje o nějaké osobě – jméno a příjmení. Nazveme ji Osoba
Object subclass: #Osoba instanceVariableNames: 'jmeno prijmeni' classVariableNames: '' poolDictionaries: '' category: 'MyWebApplication'
Pro instanční proměnné si vygenerujeme přistupující metody, tedy např.:
jmeno ^ jmeno jmeno: anObject jmeno := anObject
Nezapomeneme na inicializační metodu, aby standardní hodnoty instančních proměnných nebyly nedefinovány.
initialize super initialize. self jmeno: String new. self prijmeni: String new.
Dále si přetížíme standardní metodu printOn: sloužící k získání textové reprezentace objektů tak, aby pro osobu zobrazovala její jméno a příjmení oddělené mezerou.
printOn: aStream aStream nextPutAll: self jmeno; space; nextPutAll: self prijmeni.
Tím jsme pro začátek s modelem skončili. Nyní si budeme chtít vytvořit komponentu obsahující formulář pro editaci údajů o osobě. Ta přirozeně bude referencovat model zpřístupněný pomocí metod osoba a osoba: (nejsou uvedeny). Při vytvoření komponenty jí jednu novou osobu k editování vytvoříme.
WAComponent subclass: #EditorOsoby instanceVariableNames: 'osoba' classVariableNames: '' poolDictionaries: '' category: 'MyWebApplication' initialize super initialize. osoba := Osoba new.
V renderovací metodě této komponenty zobrazíme formulář (metoda form:) tvořený tabulkou, jejíž řádky budou obsahovat vždy textový popis a editační pole.
Vstupní formulářové textové prvky vytvoříme pomocí metody textInputOn:of: Jedná se o velmi jednoduchou konstrukci, kdy metodě jako první parametr dosadíme symbol se jménem datové položky (např. #prijmeni) a jako druhý parametr objekt, který tuto datovou položku obsahuje. Seaside si ze vstupního symbolu odvodí jména přistupujících metod (prijmeni a prijmeni:) a použije je v okamžiku, kdy bude chtít editační pole naplnit daty nebo naopak předat získané údaje. Tyto zprávy zasílá uvedenému objektu. Nejčastěji se jedná přímo o samotnou komponentu, aby mohla vstupní i výstupní údaje předzpracovávat, např. konvertovat získané řetězce na čísla apod. V našem jednoduchém případě ovšem můžeme využít přímo model.
renderContentOn: html html form: [ html table: [ html tableRowWithLabel: 'Jmeno:' column: [ html textInputOn: #jmeno of: self osoba ]. html tableRowWithLabel: 'Prijmeni:' column: [ html textInputOn: #prijmeni of: self osoba ] ]. html submitButton. ].
Naším cílem bude vytvořit si komponentu, která bude zobrazovat seznam osob a poskytovat nejzákladnější operace nad ním. Protože se bude jednat o kořenovou komponentu (přetížíme u ní třídní metodu canBeRoot tak, aby vracela true), nazveme si ji Root. Bude obsahovat jednu instanční proměnnou osoby, kterou inicializujeme jako uspořádanou kolekci.
WAComponent subclass: #Root instanceVariableNames: 'osoby' classVariableNames: '' poolDictionaries: '' category: 'MyWebApplication' initialize super initialize. osoby := OrderedCollection new.
Dále si vytvoříme její renderovací metodu, tak aby kolekci osob zobrazovala jako běžný HTML seznam
renderContentOn: html html heading: 'Osoby'. html list: osoby.
U webových aplikací dává uživatel příkaz k vykonání nějaké akce nejčastěji pomocí kliknutí na odkaz nebo na formulářové tlačítko. Pokud chceme do komponenty přidat odkaz, k němuž je přiřazena nějaká akce prováděná na straně serveru, poslouží nám k tomu nejlépe metoda anchorWithAction:text:. Na základě této zprávy si Seaside definovanou akci uvedenou ve formě bloku zaregistruje a vytvoří si pro ni specifickou URL, kterou vloží jako referenci do vykresleného odkazu. Když na něj uživatel klikne, akce se provede, vygeneruje se nová stránka.
Přidání osoby s definovaným jménem do seznamu pak může vypadat např. takto:
renderContentOn: html html heading: 'Osoby'. html list: osoby. html anchorWithAction: [ osoby add: (Osoba new jmeno: 'Avi'; prijmeni: 'Bryant') ] text: 'Pridat'.
Volání komponent
My samozřejmě chceme, aby uživatel měl možnost zadat údaje o osobě sám. K tomuto účelu jsme si již výše předpřipravili komponentu EditorOsoby To, jak se taková komponenta využije, jsme si naznačili již v prvním dílu.
Pokud bychom programovali běžnou desktopovou aplikaci, pravděpodobně bychom si vytvořili modální dialogové okno s formulářem pro editovanou osobu, které by se v principu používalo nějak takto:
| dlg osoba | dlg := EditorOsoby new. dlg showModal == #ID_OK ifTrue: [ osoba := dlg osoba ].
Seaside umožňuje pracovat s komponentami velmi podobně.
Neuděláme nic jiného, než že si vytvoříme novou instanci třídy EditorOsoby a pak ji zavoláme, aby nám vrátila výsledek práce uživatele s ní, tedy upravenou osobu.
K předávání řízení mezi jednotlivými komponentami slouží metody komponent s intuitivními názvy call: a answer: V renderovací metodě třídy EditorOsoby místo obyčejného odesílacího tlačítka (submitButton) vytvoříme tlačítko, které bude mít přiřazenu akci, při jejímž provedení komponenta vrátí výslednou editovanou osobu pomocí metody answer:
EditorOsoby >> renderContentOn: html html form: [ html table: [ html tableRowWithLabel: 'Jmeno:' column: [ html textInputOn: #jmeno of: self osoba ]. html tableRowWithLabel: 'Prijmeni:' column: [ html textInputOn: #prijmeni of: self osoba ] ]. html submitButtonWithAction: [ self answer: self osoba ] ].
V komponentě Root se informace o dalším živáčkovi, který má být přidán do seznamu osob, získá naprosto triviálním voláním jedné jediné metody call:
Root >> renderContentOn: html html heading: 'Osoby'. html list: osoby. html anchorWithAction: [ osoby add: (self call: EditorOsoby new) ] text: 'Pridat'.
Nově vytvořená editační komponenta osoby se uživateli objeví na místě komponenty, která byla příjemcem zprávy call: Obecně se nemusí jednat o komponentu, v níž byl vyrenderován odkaz s akcí.
Volaná komponenta (EditorOsoby) samozřejmě může dle libosti volat další a další komponenty. Z pohledu programátora je pak celý tento proces velice transparentní a umožňuje vytvářet specializované snadno znovupoužitelné komponenty.
Validace
Validace se na straně serveru v Seaside provádí nejčastěji pomocí dekorátorů. Podrobnějí o nich ještě bude řeč později.
Do modelu (do třídy Osoba) si doplníme metodu, která nám bude validovat obsah objektu. U osoby budeme chtít, aby ani jméno ani příjmení osoby nebylo prázdné. V opačném případě vygenerujeme výjimku s popisem chyby.
validate ^ (jmeno isEmpty or: [ prijmeni isEmpty ]) ifTrue: [ self error: 'Udaje nejsou uplne'].
V komponentě EditorOsoby si pak nadefinujeme jako třídní metodu speciální konstruktor, který tuto komponentu vytvoří s validačním dekorátorem.
withValidation ^ self new validateWith: [ :osoba | osoba validate].
Pro fungování validace již jen stačí editační komponentu vytvářet pomocí tohoto konstruktoru.
Root >> renderContentOn: html html heading: 'Osoby'. html list: osoby. html anchorWithAction: [ osoby add: (self call: EditorOsoby withValidation) ] text: 'Pridat'.
To, jak tato konstrukce pracuje, je v podstatě velice prosté. Metoda validateWith: vytvoří kolem komponenty dekorátor třídy WAValidationDecoration. Ten funguje tak, že pokud odekorované komponentě zašlete zprávu call:, komponenta jej běžným způsobem vyhodnotí a vrátí výsledek, v našem případě instanci třídyOsoba. Jenže před tím, než se výsledek vrátí zpět komponentě, která komponentu zavolala (Root), dekorátor provede blok, který obdržel jako parametr zprávy validateWith:, s tím, že odchytí vzniklé výjimky. Pokud nějaká výjimka nastane, vyrenderuje před samotnou validovanou komponentou popis této výjimky a řízení zpět nepošle. K návratu k původní komponentě dojde až v případě, že k žádné výjimce nedojde.
K fungování takovéto validace samozřejmě není nutné vytvářet zvláštní konstruktor. Například validace pouze na neprázdnost příjmení může i vypadat takto:
html anchorWithAction: [ osoby add: (self call: (EditorOsoby new validateWith: [:osoba | osoba prijmeni ifEmpty: [ self error: 'Prijmeni musi byt zadano']])) ] text: 'Pridat'.
Takto použitý kód je samozřejmě z hlediska návrhu aplikace problematický kvůli podstatně horšímu zapouzdření.
Oprava údajů
Doposud jsme dali uživateli možnost pouze vytvářet nové osoby. Nyní budeme chtít, aby údaje o osobách mohl zpětně modifikovat.
Každý prvek seznamu vytvoříme jako odkaz. Po kliknutí na něj bude řízení předáno editační komponentě osoby (EditorOsoby). Jediný rozdíl spočívá v tom, že místo práce se zcela novou osobou předáme editační komponentě starší instanci.
html list: osoby do: [ :osoba | html anchorWithAction: [ self call: (EditorOsoby withValidation osoba: osoba) ] text: osoba asString ].
Metoda list:do: slouží k vytvoření HTML seznamu postupným projitím určité kolekce a její výstup je víceméně ekvivalentní následujícímu kódu:
html unorderedList: [ osoby do: [:osoba | html listItem: [ html anchorWithAction: [ self call: (EditorOsoby withValidation osoba: osoba) ] text: osoba asString ] ] ].
Všimněte si, že jsme na původní komponentě, kterou jsme předtím použili pro vytvoření nové osoby, nemuseli změnit ani čárku. V praxi by však bylo přirozeně vhodnější, kdyby editační komponenta získávala svůj model ihned v konstruktoru, ať už by se jednalo o existující, nebo zcela novou instanci.
Dialogy
Seaside obsahuje několik komponent, které se snaží některé často používané obraty usnadnit. Alespoň pro tuto chvíli by bylo vhodné zmínit ty nejpoužívanější z nich.
Jedná se v první řadě o metody inform: a confirm:, které plní stejnou funkci jako jejich ekvivalenty u standardních smalltalkovských objektů. Pouze místo tříd Morphicu či MVC využívají formulářové webové komponenty. Například následující doplnění metodyrenderContentOn: třídy Root přidá do stránky odkaz, po kliknutí na nějž se uživateli zobrazí komponenta (dialog), která od něj bude vyžadovat potvrzení zvolené akce (Ano/Ne).
html anchorWithAction: [ (self confirm: 'Opravdu smazat vsechny osoby?') ifTrue: [ osoby := OrderedCollection new ] ] text: 'Smazat vse'.
K dispozici je i standardní dialog pro zadání jednoduchého řetězcového vstupu
html anchorWithAction: [ self jmeno: (self request: 'Zadejte nove jmeno:' label: 'Zmenit jmeno' default: 'Administrator') ] text: 'Zmena jmena' .
Seaside obsahuje i o něco sofistikovanější komponenty. Např. použití třídy WALabelledFormDialog, která se sama stará o generování popisů a formulářových prvků, dokáže naši komponentu EditorOsoby při ekvivalentním výstupu výrazně zjednodušit. Stačí již pouze definovat model a říct, ke kterým jeho položkám má mít formulář přístup.
WALabelledFormDialog subclass: #EditorOsoby instanceVariableNames: 'osoba' classVariableNames: '' poolDictionaries: '' category: 'MyWebApplication' model ^ osoba model: anObject osoba := anObject ok self answer: osoba. rows ^ #(jmeno prijmeni)