Hlavní navigace

Seaside (5)

4. 4. 2005
Doba čtení: 7 minut

Sdílet

Nyní nadešel čas k tomu, aby nám Seaside konečně více poodhalila svoji sílu. Po předchozích úvodních dílech se dostáváme k práci s formuláři, jejich validací a k volání komponent.

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 anchorWithActi­on: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 WAValidationDe­coration. 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í metodyrenderCon­tentOn: 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).

root_podpora

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 WALabelledFor­mDialog, 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)

Soubory ke stažení

Byl pro vás článek přínosný?