ObjectSquare [2008 年 11 月号]

[技術講座]


Seaside へ GO!!

-- 楽々サーバサイド Web プログラミング --

 
seaside logo
第3回:コンポーネントの連携
ブループレイン
梅澤 真史

1. はじめに

1.1 コンポーネントスタイルを極める

今回のテーマはコンポーネントの組み合わせです。 Seaside を使った開発の真骨頂は、ここにあるといっても過言ではありません。 コンポーネントの入れ子や呼び出しの方法をマスターすれば、素晴らしい速さでアプリケーションを作っていけるようになります。 前回までに比べ、サンプルコードを多めにして解説していきます。

2. コンポーネントを入れ子に

Seaside では任意のコンポーネントを入れ子にして使うことができます。 ここでは例として簡単な投票のアプリケーションを作ってみたいと思います。

2.1 MyStarRankComponent の作成

前回の復習もかねて、まずは入れ子の中身となるコンポーネントを作ることにします。 あるトピックについて☆のマークを+と−リンクで増減させることができるだけのものです。 画面のイメージは下記のようになります。

図 1: MyStarRankComponent の画面イメージ
図 1: MyStarRankComponent の画面イメージ

クラス定義は以下のようにします。 トピック(topic) と星マークの数(stars) をインスタンス変数として用意しました。

 
WAComponent subclass: #MyStarRankComponent
    instanceVariableNames: 'topic stars'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SeasideGo-Lesson'
 

変数の初期化用に initialize を定義しましょう。

MyStarRankComponent >> initialize ('initialization'カテゴリ)
initialize
    super initialize.
    stars := 1.
    topic := '名無し'
 

stars は Back ボタン対応にしたいので、states メソッド1も定義します。

MyStarRankComponent >> states ('accessing'カテゴリ)
states
    ^{self}
 

レンダリングはこんな感じで良いでしょう。

MyStarRankComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html 
    "トピックの表示"
    html span style: 'margin-right: 10px';with: [html text: topic]. 

    "☆マークの表示"
    stars timesRepeat: [html text: '☆'].

    "+と-のリンクの表示"
    html span style: 'margin-left: 10px'; with: [
        html anchor callback: [stars := stars + 1]; with: '+'.
        html space.
        html anchor callback: [stars := stars - 1]; with: '-'.
    ]
 

前回までに比べ、特に目新しいところはありません。 強いて挙げると span のタグブラシに style: を送り、CSS のスタイルを適用させている箇所があります2。 また☆マークの表示で使っている timesRepeat: は、繰り返しを実現するための Smalltalk ではおなじみのメソッドです。 数値オブジェクトに送ると、引数のブロックを自身の数だけ繰り返して実行します。 これにより☆マークを stars の数だけ表示することになります。

 

rank という名前でコンポーネントを登録しましょう。 ワークスペースで下記を実行します。

MyStarRankComponent registerAsApplication: 'rank'
 

これで Web ブラウザから動作を確認できます。

http://localhost:9090/seaside/rank
 
図 2: MyStarRankComponent の動作確認
図 2: MyStarRankComponent の動作確認

トピックが'名無し'固定だと面白くないので、外から設定できるように、アクセス用のメソッド(アクセッサ)を定義しておきましょう。 アクセッサは Smalltalk のシステムブラウザから簡単に作成できます。

 

MyStarRankComponent が選択されている状態からクラスペイン上でポップアップメニューを出し、「さらに...」->「アクセッサの作成」を選びます。

図 3: アクセッサの自動生成
図 3: アクセッサの自動生成

topic と stars 両方についてアクセッサが作成されました。

図 4: 生成されたアクセッサ
図 4: 生成されたアクセッサ
 

では、topic の値を変えてみましょう。 Web ブラウザ上からやってみることにします。

 

Web ブラウザ下部の "Toggle Halos" でハローを出し、インスペクタを開きます。

図 5: Web 版インスペクタの起動
図 5: Web 版インスペクタの起動

topic をラジオボタンで直接選んで値を変えることもできますが、ここではあえて topic: のメッセージを送ってみることにします。 下のペインに

self topic: 'バナナ'

と書いて "do it" しましょう。

 

これで値が変わりました。

図 6: インスペクタからメッセージを送る
図 6: インスペクタからメッセージを送る

右上の x アイコンをクリックしインスペクタを閉じると、バナナのランキングに変わっています。

 
図 7: topic が更新された MyStarRankComponent
図 7: topic が更新された MyStarRankComponent

*1 states メソッドについては前回に解説があります。継続による状態管理を行いたい変数を指定します。

*2 CSS を直接書かずにレンダリングコードから分離する方法については後に扱います。

2.2 MyVoteComponent の作成

では、いよいよ MyStarRankComponent を内部にいくつも持つコンポーネントを作っていくことにしましょう。

以下のような画面ができあがるもくろみです。

図 8: MyVoteComponent の画面イメージ
図 8: MyVoteComponent の画面イメージ

Seaside でコンポーネントの入れ子を作るのは、あっけないほど簡単です。 単に入れ子を含むコンポーネント側で、子供となるコンポーネント用のインスタンス変数を定義するだけです。 クラス定義は下記のようになります。 ranks という変数に、先ほどの MyStarRankComponent のインスタンスがいくつも入ることになります。

 
WAComponent subclass: #MyVoteComponent
    instanceVariableNames: 'ranks'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SeasideGo-Lesson'
 

では実際に ranks に値を入れましょう。 まずは initialize をオーバーライドして、デフォルトの値で初期化するようにしてみます。

 
MyVoteComponent >> initialize ('initialization'カテゴリ)
initialize
    super initialize.
    ranks := #('みかん' 'リンゴ' 'バナナ' 'レモン' )
            collect: [:each | MyStarRankComponent new topic: each]
 

collect: で配列をイテレートして、それぞれ異なるトピックの MyStarRankComponent のインスタンスを生成し、ranks に代入しています。

 

次にレンダリングを書きましょう。ここではテーブルを使うことにします。

MyVoteComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html 
    html table: [
        ranks do:[:each | 
            html tableRow: [html tableData: [html render: each]]
        ]
    ]
 

ここでポイントとなっているのは render: です。 入れ子となったコンポーネントを書き出すには、レンダラに対して丸ごと render: で渡せばよいのです。

2.3 children メソッドの定義

さらに children というメソッドを定義します。 このメソッドでは、子供となるコンポーネントの集合を返すようにします。

 
MyVoteComponent >> children ('accessing'カテゴリ)
children
    ^ranks
 

以上で完了です。

 

ワークスペースから vote という名前でコンポーネントを登録しましょう。

MyVoteComponent registerAsApplication: 'vote'
 

Web ブラウザからアクセスすると、イメージ通りの画面ができています。

http://localhost:9090/seaside/vote
図 9: 立ち上がった MyVoteComponent
図 9: 立ち上がった MyVoteComponent

もしかすると children メソッドの定義は余計なもののように思えるかもしれません。 しかしコンポーネントのインスタンス変数には単に状態を持つためのもの (MyStarRankComponent の topic など) もあるので、Seaside 側で子供のコンポーネントを持つ変数として区別できる必要があるのです。 ちょうど、継続で状態管理したい変数について states メソッドを定義するのと同じですね。

 

children を適切に定義しないと、Seaside はコールバックのハンドラをうまく見つけられず、ボタンやリンクのクリック時に下記のようなエラー (WAComponentsNotFoundError) が出ることになります。

図 10: children メソッドがないと WAComponentsNotFoundError が起こる
図 10: children メソッドがないと WAComponentsNotFoundError が起こる

これは Seaside 初心者がよく遭遇するエラーです。 このエラーが出た場合には、children メソッドを定義し忘れたり、children 内で返す変数を誤ったりしていないか、確認してみると良いでしょう。

3. コンポーネントを呼び出す

今までは基本的にページ遷移のないアプリケーションのみを扱ってきました。 Seaside ではコンポーネントを呼び出すという簡単なやり方で、ページ全体の遷移や、ページの特定部分の遷移を実現することができます。

3.1 call: による遷移

コンポーネントの呼び出しには call: というメソッドを使います。 引数には、呼び出したいコンポーネントのインスタンスを指定します。

 

例えば ComponentA が ComponentB を呼び出す場合、以下のように書くことができます。

 
ComponentA >> callB
    self call: ComponentB new
 

これで ComponentA は制御を ComponentB に渡すことになります。 ComponentA は表示とイベントハンドリングを中止して見えなくなり、代わりに ComponentB が前面に立ち上がってくるのです。 ちょうど GUI でモーダルなダイアログを開いたときの動作に似ています。

図 11: call: によるアクティブなコンポーネントの切り替え
図 11: call: によるアクティブなコンポーネントの切り替え

call: されたコンポーネントから、さらに別のコンポーネントを call: していくことも可能です。 Seaside は、この単純な仕組みで、Web における画面の切り替えをサポートするのです。

図 12: 多段階の call: によるコンポーネントの切り替え
図 12: 多段階の call: によるコンポーネントの切り替え

切り替えはページではなく、コンポーネントの単位になっていることにも注意してください。 つまりコンポーネントが入れ子のときには、call: は、ページの特定部分のみを切り替えるために使うことができるというわけです。

図 13: 入れ子になったコンポーネントの切り替え
図 13: 入れ子になったコンポーネントの切り替え

3.2 call: の例

では早速、簡単なサンプルを書いていきましょう。 別ページに遷移した上で何らかのメッセージを表示するコンポーネントにします。 Seaside に最初から用意されている、inform: メソッドの動作とほぼ同じものを作ることを目指します3

 

クラス定義は以下のようにします。

 
WAComponent subclass: #MyInformComponent
    instanceVariableNames: 'message'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SeasideGo-Lesson'
 

文字列を保持するため、message というインスタンス変数を定義しています。 アクセッサも「アクセッサの作成」で作っておきましょう。

 

message の初期値ですが、外部からセットすることを前提とするので、initialize は定義しないこととします4

 

レンダリングは、メッセージの文字列を表示するだけであり、極めて簡単なものになります。

MyInformComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html
    html heading: self message
 

呼び出す側のコンポーネントは何でも良いのですが、せっかくなので、ログイン画面でも作ってみましょう。 名前は MyLoginComponent にします。

 
WAComponent subclass: #MyLoginComponent
    instanceVariableNames: 'userName password'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SeasideGo-Lesson'
 

ログイン画面という想定なので、userName と password というインスタンス変数を用意しました。 これも同様にアクセッサも作っておきます。

 

レンダリングを書きましょう。

MyLoginComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html 
    html form: [
        html text: 'ユーザ名'.
        html textInput on: #userName of: self.
        html text: 'パスワード'.
        html passwordInput on: #password of: self.
        html submitButton value: 'ログイン'; callback: [self login]
    ]
 

on:of: というのが出てきました。 これは callback: の省略型とも言えるものです。

今までは userName の表示と設定のために以下のように書く必要がありました。

html textInput value: self userName; 
    callback: [:newValue | self userName: newValue].
 

on:of: を使うとよりすっきりと書けるというわけですね。

 

さて、本題は self login の部分です。 サブミットボタンが押されたときの処理を別メソッド login に分けて書いているということです。 login の中では call: を使い、ログイン状態に応じて先ほどの MyInformComponent を呼び出すことにします。 login はクラス内でのみ呼ばれる補助的なメソッドなので、private カテゴリにでも入れておきましょう5

 
MyLoginComponent >> login ('private'カテゴリ)
login
    (self userName notEmpty and: [self userName = self password])
        ifTrue: [self call: (MyInformComponent new message: 'OK!')]
        ifFalse: [self call: (MyInformComponent new message: 'パスワードが違います!')]
 

本当はもっときちんとパスワードのチェックを行うと良いのですが、サンプルなので、単にユーザ名とパスワードが等しいときに 'OK!'、そうでないときに、'パスワードが違います!' というメッセージを出すようにしています。

 

では MyLoginComponent の登録を行います。 ワークスペースで下記を実行してください。

MyLoginComponent registerAsApplication: 'login'
 

Web ブラウザからアクセスして動作を確認してみましょう。

http://localhost:9090/seaside/login
 

簡素なログイン画面が出ますので、適当にユーザ名、パスワードを入れサブミットします。

図 14: MyLoginComponent の表示
図 14: MyLoginComponent の表示

ユーザ名とパスワードの文字列が異なると、MyInformComponent が call: され、'パスワードが違います!' というメッセージが別画面に切り替わって表示されます。

 
図 15: call: により MyInformComponent へと切り替わる
図 15: call: により MyInformComponent へと切り替わる

同様に、ユーザ名とパスワードが等しい場合は、'OK!' がセットされた MyInformComponent が開くことになります。


*3 inform: が初めてという人は前回までの連載をご覧下さい。

*4 仮に nil のままレンダラに表示を行わせてもエラーにはなりません。単に何も表示されないだけです。

*5 Smalltalk の場合、private カテゴリに入れたからといって、外から呼び出しが出来なくなるわけではありません。あくまで呼ばないことを推奨するためのしるしです。

3.3 answer: による復帰

call: で切り替わったコンポーネントは、特定の処理(ユーザからの入力を受けつけるなど)を行います。 その後で、呼び出し元のコンポーネントに制御を戻したいというときが当然あるでしょう。

 

Seaside には answer: というメソッドが用意されています。 これにより、いつでも呼び出し元のコンポーネントに制御を戻せます。 answer: の引数には、呼び出し元に返す値(戻り値)も指定することができます。 呼び出し元はその値を受け取った上で、以後の処理を行っていくことが可能です。

GUI でダイアログを開いた後、OK ボタンを押すと、ダイアログは閉じて元のウィンドウに制御が戻ります。 これと同じような動きが Web の世界でも簡単に実現できるということです。

図 16: answer: によるコンポーネントの復帰
図 16: answer: によるコンポーネントの復帰

3.4 answer: の例

では、answer: を実際に使ってみましょう。 まずは MyInformComponent の改良です。 レンダリングを少し変えて、メッセージ表示のページを閉じられるようにします。

 
MyInformComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html
    html heading: self message.
    html form: [
        html submitButton callback: [self answer: nil]; with: '閉じる'
    ]

form: 以下が付け加わった部分です。 サブミットボタンのコールバック内で answer: を使っています。 この例では、nil を戻り値として書いています。 戻り値を指定したくないときは、単に answer と書くこともできます6

 

MyInformComponent を書き換えた上で、ログインのアプリに再び Web ブラウザからアクセスしてみましょう。

http://localhost:9090/seaside/login
 

誤ったパスワードでログインを試みると、「パスワードが違います!」と出ますが、今度は閉じるボタンが下のほうに付け加わっています。

図 17: 閉じるボタン付きとなった MyInformComponent
図 17: 閉じるボタン付きとなった MyInformComponent

ボタンを押すとコールバックが呼ばれ、self answer: nil が実行されます。 結果 MyInformComponent は閉じて、呼び出し元の MyLoginComponent に制御が戻ります。 これで Seaside 組み込みの inform: メソッドとほぼ同様のものができたことになりますね。

 
図 18: 呼び出し元への復帰
図 18: 呼び出し元への復帰

では answer: の戻り値を受け取ってみましょう。 今度は MyLoginComponent を改良し、ログイン成功時に true、失敗時に false を返すようにしてみます。

MyLoginComponent >> login ('actions'カテゴリ)
login
    (self userName notEmpty and: [self userName = self password])
        ifTrue: [self answer: true]
        ifFalse: [self answer: false]
 

ログイン成功・失敗のメッセージはあえて出さないようにしました。 answer: の値を受け取った側で、好きなメッセージを出せるようにします。 そのほうがログインダイアログとしては汎用的でしょう。

この新しくなった MyLoginComponent を試すため、入れ子の節で使った MyStarRankComponent を変更することにします。 +や−をクリックしたときにログインを促し、パスワードが正しいときだけ変更を許すようにするのです。

 

まずは MyStarRankComponent にログイン用のメソッドを追加します。

MyStarRankComponent >> login ('private'カテゴリ)
login
    | result |
    result := self call: MyLoginComponent new.
    result ifFalse: [self call: (MyInformComponent new message: 'パスワードが違います!')].
    ^result
 

MyLoginComponent を call: し、パスワード検査の結果を受け取ります。 結果が false の時のみ、「パスワードが違います!!」とメッセージを出します。

 

最後に renderContentOn: を、この login を呼ぶように書き換えます。

MyStarRankComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html 
    "トピックの表示"
    html span style: 'margin-right: 10px';with: [html text: topic]. 

    "☆マークの表示"
    stars timesRepeat: [html text: '☆'].

    "+と-のリンクの表示"
    html span style: 'margin-left: 10px'; with: [.
        html anchor callback: [self login ifTrue: [stars := stars + 1]]; with: '+'.
        html space.
        html anchor callback: [self login ifTrue: [stars := stars - 1]]; with: '-'.
    ]

self login の戻り値を利用して、true の時だけ stars の値を変更するようにしています。

 

実際に動きを Web ブラウザから確認してみましょう。

http://localhost:9090/seaside/rank
 

+や−のリンクをクリックすると、パスワードの入力を求められるようになりましたね。

 

MyStarRankComponent からの流れをシーケンス図で表すと以下のようになります。

図 19: login のシーケンス図
図 19: login のシーケンス図

もちろんコンポーネントが入れ子になっていた場合もきちんと動作します。 vote のアプリケーションのほうで試してみましょう。

http://localhost:9090/seaside/vote
 

画面としては奇妙ですが、入れ子の中で call: と answer: が問題なく動くことを確認できます。

図 20: 入れ子になったコンポーネントでの call:
図 20: 入れ子になったコンポーネントでの call:
 
図 21: answer: の結果、呼び出し元に戻る
図 21: answer: の結果、呼び出し元に戻る

*6 正確には answer を使った場合の戻り値はコンポーネント自身 (self) になります。

4. コントロールフロー

call: と answer: を使えば、ページ遷移も非常に簡単にできるということがわかりました。 ここではさらに、アプリケーション全体の流れを統括する、タスクという仕組みについて見ていくことにします。

4.1 WATask を使った流れの制御

Web アプリケーションの中には、アクセスしてからブラウザを閉じるまでの間に、一連の処理を行わせるというものが少なくありません。 例えばショッピングを行うサイトであれば、会員としてログインし、商品を選んでカートに入れ、最後に購入を行います。 アンケートサイトであれば、やはりログインを行い、いくつかのアンケートに答え、最後にポイントを獲得するという流れがあるでしょう。

Seaside では、こうした一連の処理を、タスクによって管理できます7

 

タスクを表現するには、WATask という抽象クラスを継承します。 WATask 自身は WAComponent のサブクラスであり、コンポーネントとしての機能を持ちます。 ただし、複数のコンポーネントを call: して全体の流れを決めることに専念し、自分自身ではレンダリングを行わないようになっています。

 

そのため renderComponentOn: を定義することはありません。 代わりに go というメソッドをオーバーライドすることになります。


*7 一方で Wiki やブログのように、大きな流れがなく、ユーザがアプリケーションとの短いやり取りを繰り返すというものもあります。こうしたものはタスクを使うまでもありません。

4.2 go メソッドの定義

再び MyVoteComponent を取り上げます。 +や−のたびにログインを促すのはあまりにも煩わしいので、一度ログインしたら、自由に操作して良いということにしましょう。

 

まず WATask のサブクラスを定義します。 名前は MyVoteTask です。 クラス定義は以下のようになります。

 
WATask subclass: #MyVoteTask
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SeasideGo-Lesson'
 

次に go メソッドを定義します。

 
MyVoteTask >> go ('actions'カテゴリ)
go
    "ログイン成功するまで繰り返し"
    [self call: MyLoginComponent new]
        whileFalse: [self call: (MyInformComponent new message: 'パスワードが違いますよ?')].

    "ログイン成功"
    self call: (MyInformComponent new message: 'ようこそ').

    "投票の開始"
    self call: MyVoteComponent new
 

call: を、アプリケーションの流れにそって書いていくだけです。 特に最初の"ログインに成功するまで繰り返し"の部分では、whileFalse: を使って true になるまでループさせています。 Smalltalk の通常の文法を使った極めて自然な書き方ができているというわけです。 XML で複雑なフロー言語を書かなければならないようなアプローチとはひと味違い、実にシンプルにフロー制御ができるということがわかります。

 

メソッドを分割すると、上記の流れを、よりはっきりと示せます。

call: を行っているそれぞれの固まりを、private メソッドとして分けてみましょう。

 

まずログイン成功確認のループ部分です。

MyVoteTask >> login ('private'カテゴリ)
login
    "ログインに成功するまで繰り返し"
    [self call: MyLoginComponent new]
        whileFalse: [self call: (MyInformComponent new message: 'パスワードが違いますよ?')]
 

成功時の表示も別メソッドとして分けます。

MyVoteTask >> showWelcome ('private'カテゴリ)
showWelcome
    "ログイン成功"
    self call: (MyInformComponent new message: 'ようこそ')
 

最後に投票の開始を行うメソッドを作りましょう。

MyVoteTask >> vote ('private'カテゴリ)
vote
    "投票の開始"
    self call: MyVoteComponent new
 

これで go はさらにシンプルになります。

 
MyVoteTask >> go ('actions'カテゴリ)
go
    self login.
    self showWelcome.
    self vote
 

メソッドの中身を見なくとも全体の流れが一目瞭然ですね。 こうなるとコメントも、もはや必要ありません。 意図がすっきりと伝わるコードになっています。

 

では MyVoteTask を登録しましょう。 ワークスペースで下記を実行します。

MyVoteTask registerAsApplication: 'voteTask'
 

これで準備完了といいたいところですが、前の節で answer: の動きを見るため、MyStarRankComponent>> renderContentOn: 内で+や−のたびに login メソッドを呼ぶようにしていたのでしたね。 これをもとに戻しましょう。self login ifTrue: [...] の部分を削除します。

 

手で書き直しても良いのですが、Squeak では以前保存したバージョンにすぐに戻せますので、その機能を使ってみましょう。

 

ブラウザで MyStarRankComponent>> renderContentOn: が選択した状態になっているのを確認し、中央のボタンバーにある"バージョン"ボタンを押します。

 
図 22: バージョンブラウザの起動
図 22: バージョンブラウザの起動

過去に保存したバージョンを見るためのバージョンブラウザというツールが立ち上がります。 特定のバージョンを選択して"リバート"ボタンを押すと、メソッドがそのバージョンに戻ります。

 
図 23: リバートボタンによる特定バージョンへの回復
図 23: リバートボタンによる特定バージョンへの回復
 

これで元に戻りました。 念のためメソッドを再掲載しておきます。

MyStarRankComponent >> initialize ('initialization'カテゴリ)
renderContentOn: html 
    "トピックの表示"
    html span style: 'margin-right: 10px';with: [html text: topic]. 

    "☆マークの表示"
    stars timesRepeat: [html text: '☆'].

    "+と-のリンクの表示"
    html span style: 'margin-left: 10px'; with: [.
        html anchor callback: [stars := stars + 1]; with: '+'.
        html space.
        html anchor callback: [stars := stars - 1]; with: '-'.
    ]
 

さて、いよいよ MyVoteTask の動きを確認しましょう。

http://localhost:9090/seaside/voteTask

まずログイン画面が立ち上がります (self login)。

図 24: ログイン画面の起動
図 24: ログイン画面の起動
 

成功すると「ようこそ」と表示されます (self showWelcome)。

図 25: ログイン成功
図 25: ログイン成功
 

閉じるボタンを押すと、MyVoteComponent のアプリケーションが始まります (self vote)。

図 26: 投票の開始
図 26: 投票の開始

想定通り、うまく動いていますね。

ちなみにログイン画面で間違ったパスワードを入れると、「パスワードが違いますよ?」と出て、ログインに成功するまで延々と繰り返しになります (self login)。

 
図 27: ログイン失敗
図 27: ログイン失敗

4.3 isolate: による分離

go を使ってアプリケーションの流れを整然と記述できることがわかりました。 Seaside ではさらに細かく、どこまでを一連の流れ(トランザクション)とするかを決めることができます。

 

例えばショッピングの例で考えてみましょう。 擬似コード風に流れを示します。

 
self login. "ログイン"
self selectGoods. "商品をカートに入れる"
self placeOrder. "注文を確定する"
self showAccepted. "注文受けつけメッセージの表示"
 

"注文受けつけメッセージの表示"の後で、"注文を確定する"や"商品をカートに入れる"に戻れたとしたら、どうなるでしょうか。 既に出したはずの注文を修正したり、二重に注文したりすることになってしまいますね。

 

Seaside の大きな特徴は継続による Back ボタン問題への対処ですが、あえて戻らせたくない、戻ってもそれを無効にしたいという状況が時には存在します。

 

実はこの問題を解く方法も極めてシンプルです。 単に流れを断ち切りたい部分を isolate: で示してやれば良いのです。 擬似コードで書くと以下のようになります。

 
self isolate: [
    self login. "ログイン"
    self selectGoods. "商品をカートに入れる"
    self placeOrder. "注文を確定する"
].
self isolate: [self showAccepted]. "注文受けつけメッセージの表示"
 

isolate: で囲った部分を飛び越えて Back ボタンで戻った場合、そのセッションは破棄され、戻っていないことになります。 具体的には注文受けつけの後で戻って再び注文ボタンを押したとしても、処理は無効となり、戻る前の"注文受けつけメッセージの表示"にリダイレクトされることになります8

 

実際に動きを簡単な例で確認してみましょう。


*8 残念ながらリダイレクトは Seaside 2.8 と IE 7 の組み合わせでは動作しません。単にセッションが無効になります。

4.4 isolate: の例

投票アプリケーションを少し改良して、isolate: を呼び出すようにしてみます。

 

go は以下のようなイメージです。 投票を行った後 (self vote) に、集計結果を表示 (self showResult) しています。 isolate: でそれぞれを分割し、一度結果を表示したら、再び投票に戻れなくするというわけです。

 
MyVoteTask >> go ('actions'カテゴリ)
go
    self isolate: [
        self login.
        self showWelcome.
        self vote
    ].
    self isolate: [self showResult]
 

投票の画面も少し変えます。 投票が終わったことを示すため、「集計」ボタンを配置することにします。

 

以下のような画面イメージとなります。

図 28: 集計ボタン付きの MyVoteComponent
図 28: 集計ボタン付きの MyVoteComponent

では早速実装に入りましょう。 まずは MyVoteComponent のレンダリングを改良し、集計ボタンをつけていきます。

 
MyVoteComponent >> renderContentOn: ('rendering'カテゴリ)
renderContentOn: html 
    html table: [
        ranks do:[:each | 
            html tableRow: [html tableData: [html render: each]]
        ]
    ].
    html horizontalRule.
    html form: [html submitButton callback: [self answer: ranks]; with: '集計']
 

フォームを追加し、集計ボタンを配置しています。 コールバックの中で answer: を使い、ranks のコンポーネント群を丸ごと返すようにしました (self answer: ranks)。

 

MyVoteComponent の変更はこれだけです。

 

次は MyVoteTask です。 投票結果を受け取るためにインスタンス変数 result を定義します。

 
WATask subclass: #MyVoteTask
    instanceVariableNames: 'result'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SeasideGo-Lesson'
 

vote 内では、投票結果を result に入れておくようにします。

MyVoteTask >> vote ('private'カテゴリ)
vote
    "投票の開始"
    result := self call: MyVoteComponent new
 

そして showResult です。 受け取った result を整形して表示します。 本来は整形表示用のコンポーネントに result を渡して綺麗に表示させると良いのですが、ここではシンプルなフォーマットの文字列を作り、MyInformComponent で表示させることにしましょう。

 
MyVoteTask >> showResult ('private'カテゴリ)
showResult
    | sortedRanks resultString |
    "結果のソート"
    sortedRanks := result asSortedCollection: [:a :b | a stars > b stars].

    "フォーマットされた文字列の作成"
    resultString := String streamContents: [:stream | 
            sortedRanks
                do: [:each | 
                    stream nextPutAll: 
                        each topic , ' -> ' , each stars asString]
                separatedBy: [stream space]].

    "MyInformComponentで表示"
    self call: (MyInformComponent new message: resultString)
 

まず、result に入っているコンポーネント群を星の多い順にソートします (result asSortedCollection: [:a :b | a stars > b stars])。

 

結果となる文字列を作るため、ソートされたコンポーネント群をイテレートします (do:separatedBy:)。 String>> streamContents: を使うと、文字列からストリームを簡単に生成でき、ストリームに書き込んだ結果を文字列にして得ることができます9

 

さて、これで準備ができました。 いよいよ go を isolate: を使ったものに書き換えます。

 
MyVoteTask >> go ('actions'カテゴリ)
go
    self isolate: [
        self login.
        self showWelcome.
        self vote
    ].
    self isolate: [self showResult]
 

それでは実際に動かして試してみましょう。

http://localhost:9090/seaside/voteTask
 

ログイン画面は省略します。 投票のページでいくつか投票を行い、集計ボタンを押してみます。

図 29: 投票の確定
図 29: 投票の確定
 

集計結果が出ますが、ここであえて下に出ている「閉じる」ボタンでなく、Web ブラウザの Back ボタンを押します。 isolate: の境界を飛び越えるわけですね。

図 30: 結果を見た後、Back ボタンであえて戻る
図 30: 結果を見た後、Back ボタンであえて戻る
 

再び投票ページが出るので、リンゴも捨てがたいということで、修正を試みます10

図 31: Back した上で修正を試みる
図 31: Back した上で修正を試みる
 

にべもなく試みは拒絶されます。 That page has expired とセッションを切られてしまいました。 不正投票はできないということですね。

図 32: セッションの切断
図 32: セッションの切断
 

isolate: を飛び越えようとしたページに自動的にリダイレクトされ、アプリケーションの一貫性がきちんと保たれます。

図 33: 元のページへの復帰
図 33: 元のページへの復帰
 

これが isolate: による継続の分離になります。


*9 このあたりは Smalltalk のライブラリの知識が必要です。より詳しくは巻末の Smalltalk に関する参考文献などを参照してください。

*10 前述の通り、IE では単にセッションが無効になって終了します。

5. ソースコードの読み込み

今回書いたソースコードの最終版をまとめて取り込めるように、.st ファイルとして置いておきます。

 

下記をダウンロードしてお使い下さい。

SeasideGo-Lesson-Chap3.st
 

読み込むには単に .st ファイルを Squeak の画面にドラッグ&ドロップします。

 

読み込みのオプションがいくつか出てきますが、「全体をファイルイン」を選ぶと全ての内容を読み込むことができます11

図 34: ソースコードの読み込み
図 34: ソースコードの読み込み

*11 「コードブラウザ」を選ぶと、選択した部分のみを読み込むことも可能です。

6. まとめ

今回は投票のアプリケーションを題材にして、コンポーネントの入れ子、呼び出し、さらには Task によるコントロールフローの制御の仕方を学びました。 Seaside 流のコンポーネントを中心とした Web アプリ開発の姿が、だいぶ見えてきたのではないでしょうか。 今回の題材を応用して、ショッピングカートやアンケートのアプリなどを作ってみても面白いでしょう。

次回はいよいよデータベースとの接続に入ります。 今までは意図的に V+C の部分に注力してきましたが、モデルが登場することで、真の MVC の形が出来上がっていきます。 ご期待下さい。

7. 参考文献

Let's "do it"!!

© 2008 Masashi Umezawa
Prev. Index Next
Prev. Index Next