Hlavní navigace

Seaside (6)

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

Sdílet

Komponentní model je dvousečná zbraň. Na jednu stranu umožňuje skutečně intuitivní a jednoduchou tvorbu aplikací, na druhou stranu však s sebou přináší i celou řadu nových úskalí, se kterými se v jiných webových frameworcích nesetkáte. V následujících dílech se s nimi pokusíme vypořádat.

V minulém dílu jsme si vytvořili primitivní aplikaci na úpravu seznamu osob. Budeme z ní i nadále vycházet. Na jejím základě si popíšeme, jak jsou v Seaside řešeny vazby mezi komponentami.

Vnořování komponent

Komponenty jako základní kameny webových aplikací lze do sebe libovolně vnořovat, to znamená, že v rámci jedné komponenty lze vykreslovat další, které budou v hierarchii skladby komponent na výsledné stránce hrát roli potomků původní komponenty.

Pro demonstraci si upravíme příklad z minulého dílu tak, aby původní komponenta se seznamem osob tvořila pouze řadovou komponentu, která nemůže být kořenem aplikace (odstraníme z ní metodu canBeRoot), a přejmenujeme ji na SeznamOsob.

Od nové komponenty Root budeme tentokrát vyžadovat, aby obsahovala dvě dceřiné komponenty, které obě vykreslíme v tabulce vedle sebe. Levá podkomponenta bude zobrazovat původní seznam osob a pravá bude sloužit k editaci údajů. Díky tomu půjde seznam zároveň prohlížet i editovat. Dceřiné komponenty budou referencovány příslušnými instančními proměnnými.

WAComponent subclass: #Root
    instanceVariableNames: 'seznam editor'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Osoby'

V inicializační metodě je pomocí běžného konstruktoru vytvoříme

initialize
    super initialize.
    seznam := SeznamOsob new.
    editor := EditorOsoby new.

K vykreslování dceřiných komponent se intuitivně využívá metoda render: Častou chybou je snažit se posílat přímo zprávu renderContentOn: Pro lepší názornost se omezíme na renderování bez přidaných definic tříd kaskádových stylů, které by výsledný kód znepřehledňovaly.

renderContentOn: html

    html heading: 'Osoby'.
    html table: [
            html tableData: [ html render: seznam ].
            html tableData: [ html render: editor ] ].

K tomu, aby byla Seaside schopna s dceřinými komponentami pracovat, jí je musíme ještě zapojit do hierarchie. Pro tento účel je v každé komponentě vyhrazena instanční metoda children. Jejím jediným úkolem je vracet kolekci referencí na dceřiné komponenty. Děje se tak nejčastěji s využitím výrazového pole.

children

    ^ { seznam. editor }.

Základní hierarchie komponent

Seaside sama nenabízí k práci s hierarchií komponent mnoho prostředků. Komponenty totiž tvoří dopředný strom bez zpětných vazeb. V praxi to znamená, že žádná komponenta si s sebou implicitně nenese informaci o svém předkovi a dokonce ani o kořenu. Má přístup jen ke svým potomkům.

Protože prakticky každá komponenta, která je k tomu uzpůsobena, může tvořit kořen a zároveň může sloužit jako řadová dceřiná komponenta, nenabízí Seaside explicitní přístup ke kořenu.

Představme si situaci, kdy bychom měli jednoduchou aplikaci na správu osob. Nějaká listová komponenta by se pokoušela získávat informace z kořenové komponenty této aplikace a využila by k tomu (ve skutečnosti neexistující) metodu rendereru pojmenovanou například getRoot. Vše by pracovalo normálně až do okamžiku, kdy takovou aplikaci vložíme jen jako pouhou součást větší aplikace. Najednou by se mohlo stát, že reálná kořenová komponenta by byla zcela jiné třídy, než s kterou aplikace počítá.

Proto Seaside přesouvá takřka veškerou zodpovědnost za hierarchii komponent na bedra samotného programátora.

Pokud například vývojář potřebuje provádět volání (call:) pro rodičovskou komponentu, nezbude mu nic jiného, než si pro uchování reference na rodiče vyhradit jednu instanční proměnnou. Jako demonstrace tohoto přístupu může sloužit jednoduchá třída WAParentTest, která se nachází ve sbírce základních příkladů Seaside.

Jediným implicitním nástrojem, který nám dává Seaside k dispozici, je mechanismus aktualizace kořene. Ten využívá instanční metody updateRoot: všech zobrazovaných komponent. Touto zprávou Seaside dodává komponentám instanci třídy WAHtmlRoot, díky níž jsou schopny ovlivnit parametry výstupu. Nejčastěji se tento mechanismus používá k úpravě textu hlavičky výstupní stránky (metoda title:).

EditorOsoby >> updateRoot: anHtmlRoot

    super updateRoot: anHtmlRoot.
    anHtmlRoot title: 'Osoby (' , osoba asString, ')'.

Aktualizaci kořenu lze využít i k jiným věcem. Například pokud byste generovali výstupní stránku nějaké sestavy a chtěli dát uživateli prostor k tomu, aby si její obsah mohl před vytisknutím ručně upravit, můžete metodu updateRoot: využít také.

updateRoot: html

    super updateRoot: html.
    html bodyAttributes at: #onload put: 'document.designMode = ''On'''.

V neposlední řadě se pak aktualizace kořenu dá použít k tomu, aby komponenty mohly modifikovat svůj obsah při každém vykreslení v závislosti na obsahu svých dceřiných komponent či jiných informacích. To se využívá k renderování různých hlaviček, stavových řádků apod. K aktualizaci kořenu dochází ještě před vykreslováním stránky.

updateRoot:  anHtmlRoot

    super updateRoot: anHtmlRoot.
    nadpis := editor osoba asString

Rozšířená hierarchie

S dopřednou hierarchií, kterou používá implicitně Seaside, si u jednodušších projektů celkem hravě vystačíme. Pokud však vyvíjíte náročnější aplikace s velkým množstvím zanořených komponent, zcela jistě se dostanete do situace, kdy se rozumné uniformní správě hierarchie nevyhnete.

Poměrně elegantní řešení je zavést si speciální sadu hierarchických metod, které budou počítat s tím, že každá třída bude mít vždy definovánu rodičovskou komponentu. Pro tento účel je velmi vhodné vytvořit si vlastní bázovou komponentní třídu, od které budete všechny další odvozovat. U větších projektů je to skutečně rozumné řešení, protože dalších rozšíření stávajících možností třídy WAComponent jistě budete potřebovat podstatně více, často počínaje definicí vlastního doplněného rendereru. Pouhá modifikace stávající třídy WAComponent není právě čisté řešení.

WAComponent subclass: #UserComponent
    instanceVariableNames: 'parent'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Osoby'

parent
    ^parent

parent: anObject
    parent := anObject

Pro tuto třídu si definujeme v třídních metodách konstruktor přiřazující rodičovskou komponentu.

parent: aComponent

    ^ (super basicNew parent: aComponent) initialize.

Referenci na rodiče musí nějakým způsobem přijímat všechny námi vytvořené konstruktory. S výjimkou případů, kdy komponenty mohou tvořit kořen a Seaside je konstruuje tedy jen pomocí zprávy new, tento standardní konstruktor zakážeme.

new

    self canBeRoot
        ifTrue: [ ^ super new ]
        ifFalse: [ self error: 'use parent:'. ].

Potřebné zpětné hierarchické metody doplníme pomocí rekurzivních volání. Testem respondsTo: ošetříme mimo jiné situaci, kdy je komponenta kořenová a jejím rodičem je tedy nil

root
    ^ (parent respondsTo: #root)
            ifTrue: [ parent root ]
            ifFalse: [ self ].

realRoot
    ^ (parent respondsTo: #realRoot)
            ifTrue: [ parent realRoot ]
            ifFalse: [ self ].

target
    ^ (parent respondsTo: #target)
            ifTrue: [ parent target ]
            ifFalse: [ self ].

Pak už jen stačí si tyto metody přetížit ve významných komponentních třídách. Metodu root definujeme v možných kořenových komponentách.

Root >> root
    ^ self.

Metodu realRoot naopak nikde, čímž zaručíme, že vždy vrátí skutečnou kořenovou komponentu.

Zprávu target využijeme k definování té komponenty, kterou budeme chtít použít jako nejvhodnější cíl volání dceřiných komponent.

Root >> target
    ^ editor

Například pro seznam osob bude pro editaci údajů nejvhodnější editační komponenta ležící vedle něj. Metoda withValidation: je upravený konstruktor withValidation, který navíc přijímá referenci na rodičovskou komponentu. Opět vypíšeme seznam osob ve formě odkazů a posléze odkaz pro přidání další osoby. Na rozdíl od minulého dílu se ale editační komponenta nebude zobrazovat na původním místě seznamu osob, ale jako samostatná komponenta vedle něj.

SeznamOsob >> renderContentOn: html

    html list: osoby do: [ :osoba |
        html anchorWithAction: [
                | editor |
                editor := (EditorOsoby withValidation: self target) osoba: osoba.
                self target call: editor  ]
            text: osoba asString ].

    html anchorWithAction: [
            | editor osoba |
            editor := EditorOsoby withValidation: self target parent.
            osoby add: (self target call: editor) ]
        text: 'Pridat'.

Tím jsme dosáhli kýženého cíle. Bohužel tato aplikace se potýká s jedním poměrně vážným problémem. Vezměme si situaci, kdy při inicializaci komponenty Root na místě editoru při startu aplikace umístíme pouze prázdnou komponentu.

initialize

    super initialize.
    seznam := SeznamOsob parent: self.
    editor := UserComponent parent: self.

Pokud kliknete na přidání osoby, vedle seznamu se objeví editační komponenta, kterou vyplníte a odešlete. Do seznamu se přidá nová osoba a editor zmizí.

Problém nastane v okamžiku, kdy na přidání osoby kliknete dvakrát a až poté vyplníte formulář. Do seznamu se sice přidá nová osoba, ale editační komponenta nezmizí. Zadali jsme totiž přidat dvě osoby, a proto se jakoby přes sebe vytvořily dvě editační komponenty, z nichž jedna nám ještě zbyla na vyplnění.

Toto chování je na jednu stranu zcela pochopitelné a leccos vypovídá o skutečně obecném přístupu Seaside k řešení volání komponent, na druhou stranu ale není žádoucí.

Za pozornost stojí fakt, že tím, že jsme zavolali komponentu na místě editoru, jsme ještě nezměnili instanční proměnnou editor v kořenové komponentě. Kořen se o tom, že jedna z jeho dceřiných komponent byla podrobena volání, vůbec nedozvěděl. Pokud se to pokusíte napravit například takto:

Root >> child: child calls: target

    child == editor
        ifTrue: [ editor := target ].


UserComponent >> call: aComponent

    ^ Continuation currentDo: [:cc |
        self parent == self ifFalse: [ self parent child: self calls: aComponent. ].
        self show: aComponent onAnswer: cc].

cíle stejně nedosáhnete, protože vám tentokrát zůstane otevřený editor nad nově přidanou osobou.

Řešení

Řešení tohoto problému je vlastně velice snadné a vychází z analogie s běžnými desktopovými aplikacemi. To, co jsme totiž provedli, je obdoba nemodálního otevření dialogového okna, kdy můžeme pracovat s původním oknem a nezávisle vyplňovat nabídnutý dialog.

Z toho plyne obecnější závěr. Pokud používáme volání komponent (call:), měli bychom tak činit modálně, tzn. že novou komponentou nahradíme nejméně tu komponentu, která ji zavolala.

V případech, kdy chceme použít nemodální přístup, a tedy například zobrazit seznam i formulář přidávané osoby v jedné stránce, použijeme výměnu komponent nebo metodu show:, která na rozdíl od call: nedokáže vracet výsledek. Editační komponenty pak ale musí umět pracovat s modelem nezávisle.

Při dodržení tohoto postupu se sice problémům s modálností vyhneme, ale vyrojí se nám opět potíže nové. Jaké to jsou, si povíme příště, kdy se budeme věnovat návrhovému vzoru Observer a jeho použití v prostředí Seaside.

Soubory ke stažení

Osoby.st

Seriál: Seaside

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

Autor článku