Špatné či přehnaně restriktivní řešení problémů zpětné, dopředné či násobné navigace patří k častým neduhům mnoha webových aplikací. Abychom se jim vyhnuli, nabízí nám Seaside několik jednoduše použitelných prostředků, s nimiž se dnes seznámíme.
Stav rozhraní
Pro demonstraci problému se zpětnou navigací si vytvoříme jednoduchou komponentu, která bude zobrazovat dlouhý seznam pomocí stránkování. Aktuální číslo stránky bude uloženo v instanční proměnné, kterou inicializujeme na jedničku.
WAComponent subclass: #StrankovanySeznam
instanceVariableNames: 'page'
classVariableNames: ''
poolDictionaries: ''
category: 'StateDemo'
initialize
super initialize.
page := 1.
Komponenta zobrazí nadpis, poté seznam položek v aktivní stránce a následně číslo stránky, celkový počet stránek a navigační šipky, které budou číslo aktuální stránky zvyšovat či snižovat. Jako data použijeme demonstrační sadu osob, která se využívá ve Squeaku v PDAMorphu.
renderContentOn: html
| data pageSize pageData pagesCount |
data := PDA basicNew samplePeopleList.
pageSize := 2.
pagesCount := (data size-1)//pageSize+1.
pageData := data
copyFrom: (((page-1)*pageSize+1) max: 1)
to: (((page)*pageSize) min: data size).
html heading: 'Seznam osob'.
html list: pageData do: [:person | html text: person name ].
html text: ('[{1}/{2}]' format: { page. pagesCount }).
html space.
(page <= 1) ifFalse: [
html anchorWithAction: [page := page -1] text: '<'.
html space ].
(page >= pagesCount) ifFalse: [
html anchorWithAction: [page := page + 1] text: '>' ].
Když se na tuto komponentu v praxi podíváme, měla by fungovat podle našich představ. Problém ale nastane v okamžiku, kdy se klikáním na odkazy dostaneme např. na třetí stránku [3/4] a pak se pomocí tlačítka Zpět v prohlížeči vrátíme na stranu jedna. Popis stránky bude ukazovat [1/4], ale když klikneme na odkaz zvyšující číslo stránky, překvapivě se nedostaneme na stránku [2/4], ale na stránku [4/4].
Do podobné situace se dostaneme, když se proklikáme na stánku [3/4] a klikneme na odkaz pro navigaci na předchozí stránku (<) s tím, že si výsledek necháme zobrazit v novém okně prohlížeče. Ta bude zobrazovat správně stránku [2/4]. Když se ale vrátíme do původního okna se stránkou [3/4] a klikneme na odkaz pro navigaci na následující stránku, zůstaneme na stránce [3/4].
Příčina tohoto chování je prostá. V sezení pro uživatele stále pracujeme s jednou jedinou instancí komponenty, ať máme zobrazeno, kolik samostatných náhledů chceme, nebo se v navigaci v rámci prohlížeče přesunujeme kamkoliv. To je na jednu stranu ve většině případů velmi výhodné, ale jak je názorně vidět, může to uživateli občas pěkně zamotat hlavu.
Jednoduché řešení tohoto problému je jedním z nejspecifičtějších rysů Seaside. Ve většině případů stačí naši komponentu v inicializační metodě zaregistrovat pro backtracking následujícím způsobem:
initialize
super initialize.
self session registerObjectForBacktracking: self.
page := 1.
Při zobrazení stránky si Seaside uchovává informace o jejím aktuálním stavu, aby z něj mohla při obdržení dalších požadavků od uživatele vycházet. Této stavové informaci se říká kontinuace. Perzistentní forma stavu vychází z mělké kopie aktivního kontextu, tedy přesněji jeho otisku. To znamená, že pokud má obsahovat např. informace o komponentě se stránkovaným seznamem, pouze ji referencuje.
Pokud ale nějaký objekt zaregistrujeme pro backtracking, znamená to, že se kontinuace uloží včetně mělké kopie registrovaného objektu. U komponenty stránkovaného seznamu bude tedy s kontinuací uložena i reference na číslo aktuální stránky.
V případě, že potřebujeme pro backtracking uzpůsobit pouze některé instanční proměnné, je vhodné použít služby třídy WAStateHolder, která usnadňuje registraci samostatných objektů v rámci aktuálního sezení. Vytvářejí se rovněž nejčastěji v inicializační metodě.
initialize
super initialize.
page := WAStateHolder new contents: 1.
Její instance obalují skutečné hodnoty a je třeba k nim přistupovat striktně pouze přes metody contents a contents:, tedy např.:
(page contents >= pagesCount) ifFalse: [
html anchorWithAction: [page contents: page contents + 1] text: '>' ].
Zde se evidentně vyplatí vytvořit si standardním způsobem přistupující metody page a page:.
V jakých případech vlastně registraci pro backtracking používat? Na tuto otázku neexistuje jednoznačná odpověď, ale obecně platí, že by se měla využívat pro měnící se informace spjaté s aktuálním stavem uživatelského rozhraní, jako je například stav jednotlivých uzlů ve stromu, právě vybraný záznam apod.
Naopak by nikdy neměl být registrován objekt, který je součástí modelu, aby zobrazované informace vždy odpovídaly jeho skutečnému aktuálnímu stavu.
Úkoly
Úkoly jsou speciální komponenty třídy WATask. Na rozdíl od běžných komponent mají za úkol kontrolovat procesy v aplikaci. Pracuje se s nimi podobně jako s běžnými komponentami. Jediný zásadní rozdíl spočívá v tom, že se u nich nepřetěžuje metoda renderContentOn:, ale metoda go, která specifikuje sekvenci volání jednotlivých komponent, jež postupně zpracovávají kroky procesu. Úkoly jsou implicitně cyklické. Příklad bude uveden níže.
Omezování zpětné navigace
Občas je na místě uživatele ve zpětné a dopředné navigaci trochu omezit, aby úmyslně či neúmyslně nemohl provádět nepovolené kroky. Často citovaným příkladem je případ webového obchodu, kde si uživatel vybere zboží do košíku a je mu stanovena cena. Následně zadá platební údaje a dojde k převodu peněz. Poté se ale uživatel pomocí zpětné navigace vrátí k naplňování košíku a přidá do něj další zboží. Podobné situace by sice měly být řešeny i na úrovni logiky aplikace, ale omezení zpětné navigace přinejmenším pomůže podobným problémům předcházet.
Seaside nabízí programátorovi možnost využívat transakční zpracování pro sekvenci procesních kroků. K tomuto účelu slouží metoda komponent jménem isolate:, která přijímá jako parametr blok. Jednotlivé transakce mohou být do sebe zanořovány.
Teď slibovaný příklad. Budeme mít třídu pro úkol s izolovanými kroky. Můžete ji vytvořit i jako kořenovou komponentu.
WATask subclass: #TaskDemo
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'TaskDemo'
V ní si specifikujeme v metodě go sekvenci kroků procesu
go
| vybrano vyber |
self isolate: [
vybrano := Set new.
self isolate: [
self inform: 'Nyni vyberete zbozi' ].
self isolate: [ [
vyber := #(zbozi1 zbozi2 zbozi3 zbozi4) copyWithoutAll: vybrano.
vyber ifNotEmpty: [
vybrano add: (self call: (WASelection new items: vyber))].
self confirm: 'Prejete si pridat dalsi zbozi?' ] whileTrue ].
self isolate: [
self inform: 'Vybrali jste: ', vybrano asString ].
]
První krok procesu je informace pro uživatele, co má dělat. V dalším kroku je mu nabídnut seznam dostupného zboží. Poté, co si nějaké zvolí, je dotázán, zda se má ve výběru pokračovat. Pokud již další zboží vybírat nechce, je mu zobrazen výsledek jeho nákupu.
Celý jeden cyklus úkolu je izolován. To znamená, že pokud si uživatel martýriem výběru jednou projde, dostane se na začátek dalšího cyklu a pomocí tlačítka prohlížeče Zpět se vrátí o několik kroků, je při pokusu o jakoukoliv činnost konfrontován s hlášením, které ho upozorní na vypršení platnosti stránky, se níž pracoval.
Rovněž výběr zboží a následné zobrazení výsledku je od sebe izolováno, takže uživatel nemá možnost přidávat zboží, které již jednou potvrdil.
Na druhou stranu, pokud si po výběru zboží usmyslí přidat ještě další a vrátí se tlačítkem Zpět, žádné problémy mít nebude, protože krok výběru zboží a potvrzení leží v jednom transakčním bloku. Zde je dobré upozornit, že pokud vybere například první zboží a hned poté se vrátí v prohlížeči o krok zpět, aby vybral místo něj třetí zboží, bude mít ve výsledku v košíku dvě položky, přestože pravděpodobně chtěl mít pouze jednu. Samotné přidání položky do výběru by proto mělo následovat až po provedení potvrzení nebo by kroky výběru a rozhodování o pokračování v nákupu měly být od sebe izolovány.
Izolace má charakter transakce, což znamená, že nezakazuje, aby se přes izolovanou sekvenci kroků uživatel proklikal zpět a začal ji od jejího začátku. Zakazuje pouze, aby se uživatel mohl vrátit do průběhu již jednou ukončené transakce, což je podstatný rozdíl.
Kontrola průběhu úkolu
Seaside dává možnost průběžně prověřovat průběh úkolu pomocí metody ensure:. Blok, který je této zprávě dodán jako parametr, je proveden při každém kroku úkolu tímto blokem ohraničeného. Nikoliv až na konci úkolu.
go
| a b c |
a := 1.
b := 2.
c:= 3.
[
a := (self request: 'Zadejte A' label: 'Vstup' default: a) asNumber.
b := (self request: 'Zadejte B' label: 'Vstup' default: b) asNumber.
c := (self request: 'Zadejte C' label: 'Vstup' default: c) asNumber.
] ensure: [ {a. b. c} isSorted ifFalse: [ self inform: 'Vysledek neni serazen']].
V tomto příkladu tedy bude uživatel o špatném seřazení informován už v okamžiku, kdy zadá první hodnotu, která posloupnost naruší (např. jako A zadá trojku).
Kontrola probíhá skutečně po jednotlivých krocích a blok dodaný zprávě ensure: se provádí až tehdy, kdy volaná komponenta vrátí pomocí metody answer: nějakou hodnotu. Na změny a opětovné načítání stránky ze serveru při provádění úkolů v rámci volaných komponent se nereaguje.