Seaside チュートリアル(その5)

お久しぶりです。三猫です。「その1」から1年、最後の更新から10ヶ月が経ちました。放置し出すと早い物です。...ごめんなさい。

さて、今日のお勉強は、A Seaside tutorialInterlude: Cleaning up PersonalInformationViewです。本当は、今まで作ってきた PersonalInfomation をクリンナップ・・・なんですが、もうさすがに 1年近く前になってしまうと、そのころのコードが何処にあるかすっかり解らなくなってしてしまいました。(^^;

同じ物をつくるのもなんか気が乗らないので、上記ページを参考に、今日は Seaside で日記 Webアプリケーションを作ってみます。


* * *

今までは、状態を直接 WAComponent のサブクラスに持っていましたが、今回はそれらを Model オブジェクトに切り離します。

DialyPage は日記一日分のデータエンティティです。インスタンス変数とそのアクセサしかないシンプルなクラスです。

Object subclass: #DialyPage
    instanceVariableNames: 'dateTime title body tags'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MySeasideTutorial'


DialyPage >> title
    ^title

DialyPage >> title: aText
    title _ aText

DialyPage >> dateTime
    ^dateTime

DialyPage >> dateTime: aDateAndTime
    dateTime _ aDateAndTime

DialyPage >> body
    ^body

DialyPage >> body: aText
    body _ aText

DialyPage 表示する Webコンポーネントを作ります。これはまぁ、勝手知ったる Seaside プログラミングです。

WAComponent subclass: #DialyPageView
    instanceVariableNames: 'model'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MySeasideTutorial'


DialyPageView >> model
    ^model

DialyPageView >> model: anObject
    model _ anObject


DialyPageView >> renderContentOn: html
    html heading: model title level:1.
    html divClass: 'date' with: model dateTime.
    html divClass: 'body' with: [html preformatted: model body].
    html hr.
    html anchorWithAction: [self answer] text: 'back'

んー、久しぶりに 書くと html (WAHtmlRenderer) に投げられるメッセージを忘れています。そういう場合は、WAAbstractHtmlBuilder の html プロトコル を見ると良いです。

最後は、この日記アプリケーションのルートです。ホントは 日記の一覧や新規日記のエントリー、過去3日分の表示とかが欲しいところですが、とりあえず最初は、DialyPage の作成のため、テスト用に固定内容のもの表示するリンクだけ作ります。

WAComponent subclass: #DialyHome
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MySeasideTutorial'


DialyHome >> renderContentOn: html
    html heading: 'みねこダイアリー' level: 1.

    html anchorWithAction: [self viewADialy]  text: '日記を見る'

DialyHome >> viewADialy
    | view dialy |
    dialy _ DialyPage new.
    dialy title: '日記Webアプリケーションを作ってみる'.
    dialy body:
'こんにちは、三猫です。

今日は Seaside を使って、簡単な日記アプリケーションを作ってみようと思います。リハビリをかねてまず基本から。'.

    view _ DialyPageView new.
    view model: dialy.
    self call: view


あとは、Workspace で、

DialyHome registerAsApplication: 'dialy'

して、http://localhost:9090/seaside/dialy にアクセスすれば OK。見えましたか?


edit機能の追加

日記を見るだけじゃ寂しいので、日記内容の編集を出来るようにします。編集のためのインターフェイスとして DialyPageEdit を追加します。これを DialyPageView から呼べるようにしようという塩梅です。

ここらへんは チュートリアルのその4 Call/Answer を振り返りつつ実装です。

WAComponent subclass: #DialyPageEdit
    instanceVariableNames: 'model'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MySeasideTutorial'


DialyPageEdit >> model
    ^model

DialyPageEdit >> model: anObject
    model _ anObject


DialyPageEdit >> renderContentOn: html
    html heading: model title , ' の編集' level:1.
    html paragraph: '編集を取り消したい場合は、ブラウザの「戻る」ボタンを押してください。'.

    html form:
        [html spanClass: 'lavel' with: 'タイトル'.
        html spanClass: 'field' with: [html textInputOn: #title of: model].
        html spanClass: 'lavel' with: '本文'.
        html spanClass: 'field' with: [html textAreaOn: #body of: model].
        html spanClass: 'button' with:
            [html submitButtonWithAction: [self save] text: '保存']]

DialyPageEdit >> save
    self answer: true

データを DialyPage オブジェクトに切り離したおかげで、簡単に DialyPageEdit ページを作ることが出来ます。データの受け渡しも楽ちんですね。

あとは、DialyPageView から 作った DialyPageEdit を Call するようにするだけです。

DialyPageView >> renderContentOn: html
    html heading: model title level:1.
    html divClass: 'date' with: model dateTime.
    html divClass: 'body' with:[ html preformatted: model body ].
    html hr.
    html anchorWithAction: [self editDialyPage] text: '編集する'.
    html space.
    html anchorWithAction: [self answer] text: 'back'

DialyPageView >> editDialyPage
    | editView |
    editView _ DialyPageEdit new.
    editView model: model.
    self call: editView

これで編集が可能になりました。

DialyPageEdit では サブミットで直接 編集をモデルに反映してしまっています。ですが、Seaside は継続ベースの Webサーバなので、ブラウザの「戻る」で、たとえ反映してしまった後でも、ちゃんと元通りになってしまいます。 嘘、勘違いです。単に submit しないで 戻っちゃったからです。 ちなみにバックトラップは、Webコンポーネントの #initialize の中でself session registerObjectForBacktracking: self しないと有効になりません。

複数件の日記を扱う

日記を保持するなら 本当は omnibase とかのデータベースを使うところですが、最初はまぁお手軽に、偽のデータベースとして DialyPage のクラス変数に保持させてみます。

Object subclass: #DialyPage
    instanceVariableNames: 'dateTime title body tags'
    classVariableNames: 'Database'
    poolDictionaries: ''
    category: 'MySeasideTutorial'


DialyPage class >> database
    ^Database ifNil: [self resetSampleDatabase]

DialyPage class >> resetSampleDatabase
    Database _ OrderedCollection
                    with: self sample1
                    with: self sample2
                    with: self sample3.
    ^Database

DialyPage class >> sample1
    ^DialyPage new
            title: '日記始めました';
            dateTime: '2007-09-01T09:30:00+09:00' asDateAndTime;
            body: '三猫です。今日から日記始めました。

いろいろ思うところをつれづれと、頑張って付けていきたいと思います。';
            yourself

DialyPage class >> sample2
    ^DialyPage new
            title: '忙しい日';
            dateTime: '2007-09-02T20:40:00+09:00' asDateAndTime;
            body: '三猫です。

うーっ、今日は忙しかったよぉ(T△T) なのでもう寝ます。おやすみなさい。';
            yourself

DialyPage class >> sample3
    ^DialyPage new
            title: '...';
            dateTime: '2007-09-03T23:59:00+09:00' asDateAndTime;
            body: 'かゆ、、、うま';
            yourself


あとは homeページで、全ての日記のリストを出すようにします。

DialyHome >> dialy
    ^DialyPage database


DialyHome >> renderContentOn: html
    html heading: 'みねこダイアリー' level: 1.

    html table:
        [html tableRow:
            [html tableHeading: 'タイトル'.
            html tableHeading: '日付'.].
        self renderDatabaseRowsOn: html] 

DialyHome >> renderDatabaseRowsOn: html
    self dialy do: [:dialyPage |
        html tableRow: [self renderDialyPage: dialyPage on: html]] 

DialyHome >> renderDialyPage: page on: html
    html tableData: [html anchorWithAction: [self viewDialyPage: page] 
                    text: page title]. 
    html tableData: page dateTime asDate yyyymmdd


DialyHome >> viewDialyPage: page
    | view |
    view _ DialyPageView new.
    view model: page.
    self call: view

日記の追加

いよいよ日記の追加機能。ここまでできればとりあえず日記アプリとしての最低限の機能は揃うかな。

Home のページに 新規追加用のリンクを作ってあげます。中身のない物を そのまま database に追加してもしょうがないので、作った後は直ぐ DialyPageEdit 飛びます。

DialyHome >> renderContentOn: html
    html heading: 'みねこダイアリー' level: 1.

    html table:
        [html tableRow:
            [html tableHeading: 'タイトル'.
            html tableHeading: '日付'].
        self renderDatabaseRowsOn: html] .

    html hr.
    html anchorWithAction: [self addNewDialy] text: '新しい日記を追加'

DialyHome >> addNewDialy
    | newDialy editer |
    newDialy _ DialyPage new.
    editer _ DialyPageEdit new.
    editer model: newDialy.
    (self call: editer) ifTrue: [self dialy add: newDialy]

日記の削除

追加が有れば削除も欲しい。日記の削除は、Home のページの一覧から行っても良いのですが、それよりも DialyPageView のページからのほうが自然に感じます。なのでそのように。

DialyPageView >> renderContentOn: html
    html heading: model title level:1.
    html divClass: 'date' with: model dateTime.
    html divClass: 'body' with: [html preformatted: model body].
    html hr.
    html anchorWithAction: [self editDialyPage] text: '編集する'.
    html space.
    html anchorWithAction: [self removeDialyPage] text: 'この日記を削除'.
    html space.
    html anchorWithAction: [self answer] text: 'back'


DialyPageView >> removeDialyPage
    (self confirm: 'この日記を削除します。よろしいですか?') 
        ifTrue:
            [DialyPage database remove: self model.
            self answer: true]

クリック一発で消してあげても良いのですが、やっぱりここは確認をいれたほうが親切です。確認ダイアログを出すために removeDialyPage の頭で送っている #confirm: ですが、チュートリアルその4 で出てきた ビルドイン ダイアログ を表示するためのものです。復習復習♪


キャンセルボタン

戻るボタンでキャンセル! は、とてもカッコイイのだけれど、世の中が「Webアプリの常識」を身につけてしまった昨今では、こういう挙動はかえって混乱を生みがちです。でもそれなりに用は足しますが、UIとしてはイマイチです。

なのでここは普通に キャンセルボタンを作ってあげましょう。

まず、DialyPageEdit に「保存」ボタンに加えて「キャンセル」ボタンを作ってあげます。そしてキャンセルしたときは、#answer: で false を返すようにします。

DialyPageView >> renderContentOn: html

    html heading: model title , ' の編集' level:1.

    html form:
        [html spanClass: 'lavel' with: 'タイトル'.
        html spanClass: 'field' with: [html textInputOn: #title of: model].
        html spanClass: 'lavel' with: '本文'.
        html spanClass: 'field' with: [html textAreaOn: #body of: model].
        html spanClass: 'button' with:
            [html submitButtonWithAction: [self save] text: '保存'.
            html submitButtonWithAction: [self cancel] text: 'キャンセル']]

DialyPageView >> cancel
    self answer: false

あとは呼び出し元の問題。DialyPageEdit には、モノホンのデータではなくコピーを渡してあげて、true で返ってきたときのみ、database 中を差し替える処理を入れればOK。簡単です。

DialyPageView >> editDialyPage
    | editView |
    editView _ DialyPageEdit new.
    editView model: model clone.
    (self call: editView) ifTrue:
        [DialyPage database at: (DialyPage database indexOf: self model)
                            put: editView model.
        self model: editView model]

* * *


と言うわけで、あっという間にできちゃった 日記アプリな訳ですが、Seaside のリハビリとしては なかなかちょうど良いボリュームでした。