いや、わかりました。確かに認めましょう。私のプログラミング経験は 1970年代の中頃にさかのぼります。私は非常に単純な理由からプログラミングを始めました。私は当時、ダンジョンズ&ドラゴンズ (Dungeons and Dragons (訳注: 米国製のロール・プレイング・ゲーム。下記「ゴブリン」、「ダンジョンマスター」はゲームに出てくる用語。)) をプレイしながら、20 面体のさいころで出た 14 の目が、私のゴブリン (私は通常、ダンジョンマスターでした) が誰か他のプレーヤーの冒険者を打ち負かすのに十分かどうかを判断するための正しい表を見つけようと、解説本のページをめくるといったことに、うんざりしていたのです。ちょっと挙手で調べてみましょう。皆さんの中で、フィネア人のすりがおせっかいなオドーからすりを働くことができるか (訳注: 「フィネア人」も「オドー」も米国の SF テレビ・ドラマ「スタートレック」に登場する。) 判断しようとして、プログラミングのスキルを磨いた人はいないでしょうか。騙されませんよ。皆さんも同じようなことをしていたはずです。
まじめな話 (いや、いくらかまじめな話ですが)、初期のゲームである Dungeon や Star Trek から、現在の 3D 対話型の「世界」に至るまで、ゲームとプログラミングは長い間、一体のものとされてきました (ベジェ曲線によるララ・クロフトの曲線美は言うまでもありません (訳注: 「ララ・クロフト」はアクション・アドベンチャー・ゲーム「トゥームレイダー」のヒロイン。)。
私はこれを偶然とは思いません。どちらも究極的には、世界のモデルの構築 (あるいは、少なくとも世界の構築) に関与します。それを考えれば、ロール・プレイング・ゲームが、この地球上のコンピューターによる娯楽の最も重要な領域となるであろうことは、それほど驚くべきことではありません。
この、XForms アプリケーション開発に関するシリーズでは、まずロール・プレイング・ゲームに焦点を当てます。最初は (今回の記事の中心である) 非常に単純なシナリオのゲームから、より複雑なシミュレーションに移ります。これらは XForms の内部動作を示すのみならず、他のアプリケーション、特にテストや教育の領域でのアプリケーションの基礎ともなるはずです。
また、この領域で私の好みの実装もいくつか紹介します (例えば eXist XML データベースや、(XSLT2 サポートに対する) Saxon 8.9 プロセッサー、Firefox 用の Mozilla XForms プラグインなど)。この同じ技術を、他の XML データベースや XForms の実装でも使えるように適応させられるはずですが (そしてこれによる影響についてはこの記事の後の方で説明します)、これらのツールには、まずこうした技術を試し、XML バインディングを扱うための、迅速な方法が用意されています。
eXist サーバー (http://exist.sourceforge.net) は Java™ ベースの XML データベースであり、Jetty の中でサーブレットとして実行されます。インストールは簡単です。eXist 1.1.1 の .jar ファイル (eXist-1.1.1-newcore.jar) をダウンロードし、この .jar ファイルを java -jar exist-1.1.1-newcore.jar 命令を使って実行して、インストール・ダイアログを起動します。これによって eXist データベースが自動的にポート 8080 にインストールされます。
あるいは、既にサーブレット・エンジン (Jetty あるいは Tomcat) がアクティブであれば、eXist を WAR リソースとしてインストールすることもできます (eXist-1.1.1-newcore.war をダウンロードし、サーブレット・エンジンを使って WAR をシステムにアップロードします)。この場合 eXist は、サーブレット・エンジンと同じソケット・ポートの下で実行します。これは他のサーバーと組み合わせて eXist を実行する場合に特に便利です。
注意:この記事では、ソケット 8080 を使って、eXist がデフォルト・モードでインストールされるものと想定しています。
Mozilla XForms の拡張機能のインストールも、同じくらい簡単です。Mozilla XForms Extension (2.0 以上) の中から Mozilla XForms 拡張機能 までナビゲートし、そして (使用しているオペレーティング・システムに応じて) Install Now for Windows/Linux/Macintosh ボタンをクリックし、次に Addons ダイアログの Install Now ボタンをクリックします。次に Firefox を再起動する必要があります。これは Addons ダイアログの Restart Firefox ボタンから行うことができます。
命令型プログラミングと宣言型プログラミングの手法の重要な違いの 1 つとして、完全性の概念をあげることができます。大部分の手続き型言語では、最初に何も設計せずにコードを書き始めることは、まったく問題ありません。コードは、スニペットやあるまとまりで動作させ、後に他のコードとひとまとめにすることができるからです。
一般的に XML では (特に XForms では)、少なくとも最低限のデータ・モデルを用意してからでないと動作の確認をすることができません。しかしいったんモデルが用意できれば、通常、そのモデル (フォーム要素) を表現する作業は非常に速く進みます。したがって XForms では、よく練られたプログラミング方法を採用する傾向があり、それによって後に続く開発作業を迅速に進めるためには、最初の設計に多くの時間を費やす必要があるのです。
この記事では、データ・モデルを「シナリオ」と呼ぶことにします。シナリオは、お互いがリンクで接続されたシーンの集合と考えることができます (図 1)。
図 1. シナリオの構造
シナリオ・ヘッドには、RSS フィードにあるような一種のメタデータ (タイトルやサブタイトル、作成者など) が含まれています (もちろん、Atom あるいは RSS2 をモデルの「ベース」として使うこともできるはずですが、この記事ではわかりやすくするために、そうはしません)。また、startScene プロパティーには、表示する最初のシーンの参照 ID が含まれています (もしこのプロパティーが含まれていない場合には、通常の場合と同様に、シナリオにリストされた最初のシーンを開始シーンにするという暗黙の前提があります。)。
各シーンには、順番に、その文書内での固有の ID が付けられています。私が最初にモデルを設計したときは、文書内での位置を使うことも考えましたが、位置を利用したオフセットは、文書の本体に新しいシーンが挿入されると、すぐに狂ってしまいます。また ID を使うことで、外部の文書からシーンを参照できる可能性も広がります。
シーンを「記述する」主要なコンテンツは、4 つのセクションに分けられます (リスト 1)。リスト 1 に含まれるリンクを見るためには、theProject という XML ファイルを開く必要があることに注意してください (このファイルは記事の最後の「ダウンロード」セクションで入手することができます)。
リスト 1. 典型的なシーン
<scene id="sc7">
<title>A Less Than Sterling Demo</title>
<content>
<p>A couple of weeks pass, and you put together an initial demo -
at which point it becomes pretty obvious that there's some fairly
major holes that weren't specifically covered by your initial assignments.
Your manager isn't happy that time is already slipping away and
there's very little to show for your efforts, and your development team is
beginning to become frustrated
because there's no real coordination going on.</p>
<p>Do you ...</p>
</content>
<links>
<link ref="sc4">Set up an architectural review and reassign the tasks
to insure that the core works gets done?</link>
<link ref="sc8">Assess your best programmers and assign them
the additional tasks, knowing that they should be able to handle the
additional load (especially if you pull a
few all nighters)?</link>
<link ref="sc8" minvalue="-3" maxvalue="0">Go to your manager
and tell her that there were some unexpected delays in the coding,
due to the slackness of your developers, and that if you could have
a little more time, you'd have an incredible demo to show them?</link>
<link ref="sc10" minvalue="1" maxvalue="3">Go to your manager and
tell her that there were some unexpected delays in the coding,
due to your own learning curve and that if you could have a little
more time, you'd have an incredible demo to show them?</link>
</links>
<score action="add" value="-1"/>
</scene> |
最初の <title> ブロックはこのシーンのタイトルであり、主として編集用に使われます。アドベンチャー・ビューアーでは実際にはオフされるかもしれません。
2 番目のブロックである <content> 要素は、<p> (段落) 要素の集合になっています。この方法を使わず、もっと一般的な「リッチ・テキスト」コンポーネントを使うこともできますが、そうすると特定の XForms 実装に依存することになります。これについては次回の記事で詳しく説明します。このコンテンツはシーンの中心となる部分であり、モンスターの住むダンジョンから上司のオフィスまで (両者は実は非常に似ているかもしれません)、特定のシーンを設定します。
1 つのシーンに合成する仕組みは、ご想像通り <link> タグであり、シーンの 3 番目のブロックとして <links> 集合の中にまとめられています。この最初の場合でのリンク・タグのインライン・コンテンツは、実行する必要がある指定のアクションについて記述する単純なテキスト・ストリングであり、一方 <links> の ref 属性は、そのアクションが選択された時に進む、シナリオの中の次のシーンの ID です。リンクの数に制限はありませんが、1 つのシーンには少なくとも必ず 1 つのリンクがあるものです。
最後のシーンに達した場合 (宝物を見つけた、モンスターに食べられた、あるいは上司にクビにされた、など) であっても、やはりリンクがあるはずです。しかしこの場合、リンクの ref 属性は値「null」に設定され、ヌル・リンクのコンテンツを表示するフラグが true に設定されていない限り、リンクのテキスト・コンテンツは非表示になります。これは実際にデバッグ用のツールとして、リンク・テキストはわかっていても、すべてのシーンはまだ定義していないようなシナリオを設定する際に便利です。
最後に、各シーンには、スコアに関連する @value 属性を持つ <score> 要素が関連付けられています。この score 要素は、シナリオの中でどのくらい「うまく」できたかを判断する 1 つの方法として便利なのですが、もっと重要なこととして、(minvalue 属性と maxvalue 属性を使うことで) 条件式を処理する仕組みを導入する方法をも示しています。これを利用すると、各シーンの迷路の進み具合が外部の条件に応じて変わるようにすることができます。
各 <score> 要素には action 属性が関連付けられています。action 属性は、この場合に関しては、対応する value 属性と共に、「"set"」あるいは「"add"」の値を取ることができます。指定されたシーンに達すると、アクションは、スコアがこの値に設定されるのか、あるいはこの値がスコアに追加されるのかを判断します。もちろん、この方法で、(XForms がどのようなものかによって) もっと複雑な操作を設定することもできます。
次にスコアは、リンクがアクションを判断するための補助に使われます。例えば、リスト 1 の 3 番目と 4 番目のリンクでは、ほとんど同じテキストを持つ 2 つのリンクがあります。両者の主な違いは、最初のリンクは minvalue 属性と maxvalue 属性がそれぞれ -3 と 0 であり、2 番目のリンクは minvalue 属性と maxvalue 属性がそれぞれ 1 と 3 であることです。これはつまり、もしスコアが -3 と 0 の間であれば最初のリンクのみが表示され、もしスコアが 1 と 3 の間にあれば 2 番目のリンクのみが表示されるということです。もし両者の範囲が重複している場合には、重複したスコアでは両方のリンクが表示されます。
そうすると、この解釈は非常に単純です。指定されたシナリオで、間違ったアクションには負の値が与えられ、適切なアクションには正の値が与えられます。もし間違ったアクションを続けた結果「悪いデモ」の段階に達してしまったら、おそらく最後のデモは見られないでしょうが、大部分は正しいアクションを行い、1 つか 2 つの小さな間違いをしただけであれば、上司はきっと寛容に扱ってくれるはずであり、間違いを修正できる可能性が高くなります。従ってこのシミュレーションは、皆さんがどのくらいうまくプレイしているかを「意識する」ようになり、それに基づいて他の方法を提供してくれるのです。
これは明らかに非常に大まかなフィルタリング機構です。本物のシーン・エンジンであれば、シナリオに従ってプレーヤーの傾向に影響を与える、一連の変数を持っているはずです。これについての詳細は、次回の記事で説明します。
宣言型プログラミングによるアプリケーションを構成する上では、適切なモデルの設計は重要な部分ですが、それだけが重要というわけではありません。XML 文書は、XML をアプリケーションの実行エンジンに変える、データの「ビュー」を提供する方法がない限り、単なるマークアップにすぎません。そこに XForms が登場します。
これはある範囲までは、HTML や SVG などのある種の XML フォーマット (そして XML のようなフォーマット) に対してブラウザーが行うことです。基本的にブラウザーは、各要素と属性を (そして多くの場合は要素の集約を)、何らかの出力を描画するための、あるいは何らかのアクションを実行するための命令として解釈します。XSLT 変換あるいは CSS (あるいは両方) を使うことで XML を HTML あるいは SVG に逆変換することはできますが、最終的に描画されるのは相変わらず静的な HTML あるいは SVG のページであり、たとえ JavaScript コードを追加したとしても、XML から変換された大量の有用な情報を持ったコンテンツを処理しているという事実が変わるわけではありません。
XForms によって、その関係が変わります。つまり XForms を使うことによって、ブラウザーが HTML を処理するのと同じ方法で XML データの内容を処理してくれる特別な「インタープリター」を作成しているのです。例えば、このゲーム・アプリケーションでは、シナリオ文書は、HTML とまったく同じように妥当性のある XML 「言語」です。しかし通常は、そのシナリオのコンテンツを使って何か有用なことをするための唯一の正しい方法は、大量のスクリプト・コードを作成することです。そしてコードが複雑であればあるほど、異なるプラットフォームに移植するためには大量の作業が必要になります。
一方 XForms を使うと、データ・モデルに直接バインドされたさまざまな XForms コンポーネントによって、XML インスタンスを視覚的に (あるいは他の方法で) 表現することができます。つまりデータ・モデルを変更すると、プレゼンテーション層を変更することになり、その逆もまた同様です。これは要するに文書オブジェクト・モデルの原則であり、実際、DOM の最も便利な側面の 1 つでもあります。
従って、このアプリケーションを見るに際しては、XForms を「フォームを作成する」手段として考えるというよりもむしろ、カスタムの XML フォーマットを表示したり操作したりできる「ミニ・ブラウザー」を作成する手段として考えることができ、誰かがシナリオや他のビジネス・オブジェクトのための特別なビューアーを作成してくれるのを何年も待たなくてもよくなるのです。XML 標準 (そして XML フォーマット) の数が爆発的に増えるにつれ、それぞれに対して「そのためだけの」ビューアーを作成することを考えると、気が遠くなるほどです。そのため、XForms によって提供される宣言型バインディングの方法が、検討に値する有効な技術になってくるのです。
この記事のシナリオのワン・シーンのスクリーンショットを図 2 に示します。
図 2. サンプル・シーンのスクリーンショット
ビューアーを作成するためには、大部分の XForms には共通の、かなり古典的な区分が必要です。その区分とは、インスタンス・データに分割される XForms モデル (対象としているシナリオ) と、ローカルの状態データです。この区分は変に思えるかも知れませんが、実際には非常に便利なのです。多くの場合、XForms の中には (大部分の命令型プログラミングによるアプリケーションの場合と同様に)、アプリケーションの実行には関係してもデータ・モデル自体には必ずしも密接な関係はなく、従ってインスタンス・データと一緒にしておく必要はない情報があるものです。具体的なデータ・モデルをリスト 2 に示します。
リスト 2. データ・モデルの宣言
<xf:model id="scenario_model">
<!-- The data instance contains the relevant scenario -->
<xf:instance id="data" src="theProject.xml"/>
<!-- The localState instance contains structure that
holds the local state variables -->
<xf:instance id="localState">
<state>
<activeScene/>
<activeLink/>
<previousScene/>
<nextScene/>
<count>0</count>
<displayControls>false</displayControls>
<displayNullLinks>false</displayNullLinks>
<score>0</score>
<sequence>
<scene ref="" title=""/>
</sequence>
</state>
</xf:instance>
<!-- multiple xf:bindings and actions... covered later -->
</xf:model>
|
この「data」インスタンスは、対象となるシナリオ XML へのポインターを含んでいるにすぎません (この場合では対象は「theProject.xml」というシナリオであり、そのワン・シーンを先ほど説明しました)。このインスタンスは最初のデータ・モデルで最初と指定されたインスタンスでもあるので、ref 属性または nodeset 属性で instance('data')/ を使って修飾しなくても、この特定インスタンスを参照することは可能です。
2 番目のインスタンス「localState」は、ご想像通りローカルの状態情報を含んでいます。この特定の状態記述の開発は、多くの場合は試行錯誤で、コーディングをしているときにプライベート変数をオブジェクトへと組み込み、定期的にコードをリファクタリングして余分な変数を削除する、というのとほぼ同じです。ほとんどの場合、activeScene や activeLink、そして関連の変数は、シナリオ自体の中のさまざまなシーンや関連リンクの ID を指しています。
<displayControls> や <displayNullLinks> などの変数は、対象のインターフェース要素を表示するかどうかをアプリケーションに示す役割を果たします。最初の方の <displayControls> は、previous/next ボタンを表示、あるいは非表示にします (このボタンによって、リンクに頼らずに順番にシーンをウォークスルーすることができます)。また <displayNullLinks> はヌル・リンクを表示すべきかどうかを示すために使われます。これらのプロパティーは、ボタンによってコントロールされるか、あるいはプロパティー自身がトグル動作を行います。
一方、<score> 要素は、プレーヤーが文書間を行き来する間に累積されるスコアの「保存レジスター」を提供するために使われます。また sequence は、ゲーム中にプレーヤーが行き来するシーンの実行記録を含みます (初期ノードを、その後のノードを生成するためのテンプレートとして使います)。
この状態情報は本来データ・インスタンスの中に含まれるべきだ、という少し正当な主張もありえます。しかし私は、この情報を別の実体として作成することに、いくつかの大きな利点があるとも信じています。
まず、2 番目のインスタンスは、グラフのテキストと説明自体を実際に含まなくても、シナリオ・グラフ全体の行き来を再現するために十分な情報を含んでいます。そのため、処理がずっと効率的になります。多くの場合、人間にとっても、このようなシナリオの自動評価システムにとっても、シナリオをたどったプレーヤーがどこで終わるかよりも、どのようなパスをたどったかの方がずっと重要です。そのことを考えると、この情報は結局、心理学的な評価からソフトウェアの生成まで、すべてに使えるのです。
すべての XForms コンテキスト (ref 属性と nodeset 属性の中にあるもの) は XPath 式です。しかしそこには、複数のモデルとモデル・インスタンスが存在する可能性があるため、XForms は XForms XPath に対して instance() 拡張関数も提供しています。この拡張関数は、指定されたインスタンスの最上位ノード要素を返します。従って、instance('localState') は localState インスタンスから <state> ノードを返します。そのため、<activeScene> を参照するためには instance('localState')/activeScene という式を使います。
これはシナリオのヘッダー表示の中に見ることができます (リスト 3)。
リスト 3. データ・インスタンスを参照する
<h:body>
<xf:group ref="instance('data')/head">
<h:h1>
<xf:output ref="title"/>
</h:h1>
<h:h2> By <xf:output ref="author"/>
</h:h2>
</xf:group>
<!-- more follows -->
</h:body>
|
この場合、group には 2 つの目的があります。それは、グループ内部の要素のための汎用コンテナーを提供すること、そして XPath コンテキスト (この場合はデータ・インスタンスからの <head> 要素) を設定することです。この利点は、コンテナー内でコンテキストを設定することでグループ内のすべてがその特定のコンテキストを共有するため、その後の参照がすべて、そのグループのコンテキストとの相対参照になることです。従って title と author は今や、どちらも instance('data')/head のコンテキストで提供されています。
次のセクションでは、もう少し複雑になります (リスト 4)。
リスト 4. Previous/Next ボタンを表示する
<xf:group bind="showControls">
<xf:trigger bind="previousScene">
<xf:label><</xf:label>
<xf:dispatch
name="go-previous-scene"
target="scenario_model"
ev:event="DOMActivate"/>
</xf:trigger>
<xf:output ref="instance('localState')//activeScene"/>
<xf:trigger bind="nextScene">
<xf:label>></xf:label>
<xf:dispatch
name="go-next-scene"
target="scenario_model"
ev:event="DOMActivate"/>
</xf:trigger>
</xf:group>
|
コードのこのセクションは、Previous と Next という 2 つのボタンを提供しています (それぞれ「<」と「>」で示されています)。これらのボタンを使うことで、シナリオ文書に現れる順序でシナリオをウォークスルーすることができます。これは通常、デバッグ・ツールとしてのみ使われますが、「スライドショー」風の効果を作成するためにも使うことができます。
XForms に関してわかりにくい概念のひとつに、bind 属性があります。bind 属性はまた、XForms の最も強力な機能の 1 つでもあり、そして混乱しやすいものの 1 つでもあります。bind は変数を定義するための方法と考えられがちですが、そうではありません (これが混乱しがちな点の 1 つです)。
実はバインディングは、ある 1 つのノード、あるいは一連のノードを識別し、そうしたノードに特定の制限を課すのです。そしてそれが、ノードの動作を定義する上で役立つのです。例えば、<xf:group> 要素に関連付けられた bind を考えてみてください (リスト 5)。この bind 参照は、モデルの中の <xf:bind> 要素を指しています。
リスト 5. bind 要素
<xf:bind nodeset="instance('localState')//displayControls"
id="showControls" relevant=".='true'"/>
|
このステートメントは、「showControls」バインディングを作成し、このバインディングをデータ・モデルの中の instance('localState')//displayControls 要素と関連付けます。モデルの中の変更がバインディングで定義される要素に影響する場合は、バインディング自体の出番です。showControls の場合、関連する属性は relevant 属性です。つまりノード・セットの中で定義されるコンテキスト (<displayControls> 要素のテキスト値) がストリング値 "true" に等しい場合には必ず、バインディングの relevant プロパティーは true に設定され、そしてこのバインディングを参照するすべてのコントロールが表示されます。
一方、もしこのテキスト値が "false" に設定されると、relevant プロパティーはもはや true ではなくなるため、それを信号として XForms プロセッサーはそのコンポーネントを非表示にします。これは、XForms の指定された一連の項目の表示/非表示を CSS 宣言を使わずにコントロールするための方法として適切です。
Previous ボタンと Next ボタンのバインディングは似ています。ただし値が真か偽かをチェックするのではなく、それぞれの要素の中身が空でないかどうかをチェックします (下記のリスト 6 と 7)。
<xf:bind id="previousScene"
nodeset="instance('localState')//previousScene"
calculate="instance('data')//scene[@id=instance('localState')//
activeScene]/preceding-sibling::*[1]/@id"
relevant=". != ''"/>
|
リスト 7. nextScene バインディング
<!-- The nextScene binding determines the next scene id from the current one -->
<xf:bind id="nextScene" nodeset="instance('localState')//nextScene"
calculate="instance('data')//scene[@id=instance('localState')//
activeScene]/following-sibling::*[1]/@id"
relevant=". != ''"/>
|
また、これら 2 つのバインディングはそれぞれ、追加の計算を行います (正直、厄介な計算です)。つまり、activeScene プロパティーで指定されるシーンの前のシーンあるいは次のシーンを知ることで、activeScene プロパティーが変化するごとに前のシーンと次のシーンの ID 参照を再計算するのです。
これは、バインディングの重要な特徴の 2 番目をよく示しています。バインディングは、大きく異なる複数の動作を処理することが多いため、複数の関連属性を持つことができます。ノードに対する値を計算することは、そのノードがモデル全体に関係するかどうかを判断することとは若干異なっており、また指定されたノードが図式的に有効かどうかを判断することとも多少異なっています。
この点の他に、XForms 文書と HTML/JavaScript フォームとの基本的な違いを再度思い出す必要があります。前者では、コントロールの大部分が、単純にデータ・モデルの反映です。つまりコントロールの中の値を変更するとデータ・モデルが変化し、そして (もし変化するモデルの一部に他のコントロールがバインドされていると) その変更が他のコントロールに反映されます。一方 HTML/JavaScript フォームは通常、他のコントロールの状態を直接変更する 1 つのコントロールを持つため、最終的なページでは非常に脆弱なものになりがちです。
トリガーは、一度押されると、カスタム・イベントをディスパッチします。カスタム・イベントも XForms の機能ですが、これを完全に理解するには時間がかかります。多くの点でカスタム・イベントは、要素上で inXXX スクリプトから呼びだされる宣言関数と等価な役割を果たします。例えば Next Button の場合、トリガー・コード (リスト 8) は、シナリオ・モデルに対して別のイベント (go-next-scene) を起動することで DOMActivate イベント (これは汎用の onclick イベントとほとんど同じです) に応答します。
リスト 8. トリガーからイベントのディスパッチを呼び出す
<xf:trigger bind="nextScene">
<xf:label>></xf:label>
<xf:dispatch
name="go-next-scene"
target="scenario_model"
ev:event="DOMActivate"/>
</xf:trigger>
|
問題のモデルは定義とバインディングを持つ他に、下記のディスパッチされたイベントを受信するように ev:event 属性が設定された、<xf:action> 要素も持ちます。
リスト 9. go-next-scene イベント・ハンドラー
<xf:action ev:event="go-next-scene">
<xf:setvalue ref="instance('localState')//activeScene"
value="instance('localState')//nextScene"/>
</xf:action>
|
このイベントが受信されると、アクションはローカルの状態の <activeScene> 要素を、<nextScene> に含まれる値に設定します。この特定の場合では、<xf:action> 要素は少し冗長です。受信アクションを <xf:setvalue> タグに直接置くこともできたのですが、前者の方法の方が、イベント・ハンドラーが定義されていることを明白に示すことができます。
同様のアクションは、まず activeScene を初期化するためのアクションとして存在しています (リスト 10)。
リスト 10. xforms-model-construct-done ハンドラーを使った「初期化コード」
<xf:action ev:event="xforms-model-construct-done">
<xf:setvalue
ref="instance('localState')//activeScene"
value="if (instance('data')/head/startScene != '',
instance('data')/head/startScene,instance('data')//scene[1]/@id)"/>
<xf:setvalue ref="instance('localState')//sequence/scene/@ref"
value="instance('localState')//activeScene"/>
<xf:setvalue ref="instance('localState')//sequence/scene/@title"
value="instance('data')//scene[@id=instance('localState')/activeScene]/title"/>
</xf:action>
|
xforms-model-construct-done は go-next-sceen イベントとは異なり、新しいモデルが作成されるごとに XForms モデルによって自動的に起動されます。これは初期化コードを処理する際、特にモードを定期的にリロードするようになった場合に非常に便利です。
アクションの中の <xf:setvalue> ステートメント群はかなり複雑ですが、最初の <xf:setvalue> は、if() 文を使っているという点で注目する価値があります。大部分の XForms 実装では (そして正式な XForms の仕様では)、XPath 1.0 実装を使っていますが、XPath 1.0 の実装には、特定のノード・セットに結びつけられていない条件式を簡潔に評価する方法が含まれていません。しかし、一部の計算は、条件式がないと実行不可能です
そのため XForms 1.0 仕様では、3 つのパラメーターをとる if() 関数を定義しています。3 つのパラメーターとは、条件式と、条件の評価結果が真の場合の XPath 式、そして式の評価結果が偽の場合の 2 番目の if です。Orbeon などの一部の実装は XPath 2.0 をサポートしているため、if/then/else 形式の文を提供しています。しかし XPath 2.0 が仕様の標準となるまで (そしてその日はすぐには来ないようです)、代わりに XForms の拡張関数を使う必要があります。
2 番目と 3 番目の <xf:setaction> ステートメントは、<sequence> 要素の中の、最初のローカル状態の <scene> 要素の初期化も行います。この要素は、シナリオを進む中で、どのシーンを通ってきたかを追跡するために使われます。
最後のコード・ブロックは、アクティブなシーンのコンテキストの中で発生します (リスト 11)。
リスト 11. 個々のシーンの表示
<xf:group ref="instance('data')//scene[@id = instance('localState')//activeScene]">
<h:div>
<h:h3>
<xf:output ref="title"/>
</h:h3>
<xf:repeat nodeset="content/p">
<h:p>
<xf:output ref="."/>
</h:p>
</xf:repeat>
</h:div>
<h:ol>
<xf:repeat nodeset="links/link" id="links">
<xf:group ref="self::node()[((not(@minvalue) and not(@maxvalue)) or
((instance('localState')//score >= @minvalue)
and (instance('localState')//score <= @maxvalue))) and
(@ref!='null')]">
<h:li>
<xf:trigger ref="." appearance="minimal">
<xf:label ref="."/>
<xf:dispatch name="link-selected"
target="scenario_model"
ev:event="DOMActivate"/>
</xf:trigger>
</h:li>
</xf:group>
</xf:repeat>
<xf:repeat nodeset="links/link[@ref='null' and
instance('localState')//displayNullLinks='true']">
<h:li class="nullLink">
<xf:output value="if (string(.)!='',.,'Null Link')"/>
</h:li>
</xf:repeat>
</h:ol>
<xf:output ref="instance('localState')//score">
<xf:label>Current Score =</xf:label>
</xf:output>
<h:h3>History</h:h3>
<h:ol>
<xf:repeat nodeset="instance('localState')/sequence/scene" id="visited">
<h:li>
<xf:output ref="@ref"/>:<xf:output ref="@title"/>
</h:li>
</xf:repeat>
</h:ol>
</xf:group>
|
最初のグループは、ローカル状態からの activeScene 参照を「参照解除」して実際の XML オブジェクト (対象のシーン) を返し、それをコンテキストとして設定します。(注意: XForms 1.1 では、この参照解除を id() 関数を使って行うことができますが、XForms 1.0 ではサポートされていません。)
最初のサブ・パートは、単純にシーンのタイトルと最初の段落を表示します。これはかなり単純です。2 番目のブロックでは少し面白くなってきて、リンクが表示されています。XForms 1.0 で困難なことの 1 つ (XForms 1.1 では処理されます) は、ノードの集合が繰り返されている場合、新しいノードを追加するためには、その集合の中にある既存ノードから作業をしなければならないということです。
これは XForms の表示という点では、さほど大きな問題ではありませんが、この問題によって、そもそも、そうしたノードを編集するということ自体が非常に困難になります。この理由から、正式なリンクを持たないシーンは、その中に必ず最低 1 つのリンクを持ちますが、このリンクの ref 値は "null" なのです。次回の記事では、そのようなシナリオを作成するためのエディターに焦点を当て、この問題の重要性をそこで明らかにする予定です。しかしここでは、出力を生成する際に、ヌル・リンクとヌルではないリンクとを区別する必要があります。これが、2 つの明確な繰り返しセクションがある理由です
ヌルではないリンクのセクション内のグループ自体は、非常に難しいですが、それには理由があります (リスト 12)。
リスト 12. ノードを条件付きで表示する
<xf:group ref="self::node()[(not(@minvalue) and not(@maxvalue)) or
((instance('localState')//score >= @minvalue)
and (instance('localState')//score <= @maxvalue))]">
<h:li>
<xf:trigger ref="." appearance="minimal">
<xf:label ref="."/>
<xf:dispatch name="link-selected"
target="scenario_model"
ev:event="DOMActivate"/>
</xf:trigger>
</h:li>
</xf:group>
|
このコードは、スコア・セクションの動作を示しています。このスキーマは暗黙の前提として、2 つの条件のうちの 1 つが成立するとしています。つまり、リンク値が minvalue 属性と maxvalue 属性の両方を持つか、あるいはどちらも持たないとしています (もっと完全なテストでは、起こりうる 4 つの状態すべてを網羅するはずです)。要は、もしスコアが minvalue 以上で maxvalue 以下の範囲にあればスコアが描画され、もしそうでなければ描画されない、ということです。
これによって、同じターゲットを指す 2 つのリンクを持つことが可能になります。もし 1 つのリンクがあるスコアの範囲をカバーし、2 番目のリンクが別のスコアの範囲をカバーしているとすると、もしスコアが最初のスコアの範囲内でも 2 番目のスコアの範囲内ではない場合には、最初のリンクのみが有効になります。重複させる (そして両方のリンクを表示する) ことは可能ですが、このかなり単純なスコア・システムでは、その具体的な用途は限定されています。
リンク自体はトリガーですが、appearance="minimal" を使うと、そのリンクはボタンではなくハイパーテキスト・リンクとして表示されます。このリンクも、選択されるとカスタム・イベント link-selected をディスパッチします (リスト 13)。
リスト 13. link-selected イベント・ハンドラー
<xf:action ev:event="link-selected">
<xf:setvalue ref="instance('localState')//activeScene"
value="instance('data')//scene[@id=
instance('localState')//activeScene]//link[index('links')]/@ref"/>
<xf:setvalue ref="instance('localState')//score"
value="if(instance('data')//scene[@id =
instance('localState')//activeScene]//score/@action = 'add',
instance('localState')//score + instance('data')//scene[@id =
instance('localState')//activeScene]//score/@value,
instance('data')//scene[@id =
instance('localState')//activeScene]//score/@value
)"/>
<xf:insert nodeset="instance('localState')/sequence/scene"
at="last()" position="after"/>
<xf:setvalue ref="instance('localState')/sequence/scene[last()]/@ref"
value="instance('localState')//activeScene"/>
<xf:setvalue ref="instance('localState')/
sequence/scene[last()]/@title"
value="instance('data')/scene[@id=instance('localState')//
activeScene]/title"/>
</xf:action>
|
繰り返しシーケンス内の各出力コード・ブロックは、ブロック内での場所を示す各ブロック独自の標識を持っています。そしてこれらのブロック内の、どのコントロールも、index() という XPath 拡張関数を使ってその標識にアクセスすることができます。従って index('links') はトリガーされたリンクのインデックスを取得し、そしてこのインデックスから、インデックスに関連付けられた id 値を取得することができます。スコアはターゲット・シーンの action 属性を参照することで更新され、そしてこの action 属性を使って、ローカルの状態モデルのスコアに設定したり追加したりします。
ひとたびスコアが更新されると、ローカルの状態シーケンスの最初のシーンが複製され、そして新しいシーンに対して ref 属性と title 属性が適切に設定されます。
最後のコード・ブロックもこのシーケンスを利用して、そのプレーヤーがある時点でいる場所に達するまでに、どのシーンを既に訪れたかの「行程」を生成します。
の完全なリストとサンプル・シナリオは、下記の「ダウンロード」セクションにあります。
この特定のアプリケーションは (非常に難解な XPath 式すべてに対して) 比較的単純になっていますが、当然ながら、もっと他の方法も可能です。例えば、ここで取り上げたサンプルでは、一部の段落とコンテンツのリテラル出力のみを使ってシナリオの本体とリンクを表示していますが、さまざまな点で、そのコンテンツが何らかのマークアップ・コードであった方が適切と言えます。そうすれば、画像や、テキストの書式、その他関連の操作を含めることができます。
さらに、他の XForms コントロール要素を追加し、それらを例えば別のデータ・モデルに提供することもできます。この場合のシナリオ・フォーマットでは、さまざまなシナリオを作成してそれらをウォークスルーし、適当なフォームを埋めることができます。
このシリーズの第 2 回では、リッチ・テキスト版のシナリオ・エディターの作成と、新しいシナリオを即座に生成して XML データベースに保存できるエディターの作成の両方について解説する予定です。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Sample scenarios for this article | scenarios.zip | 5KB | HTTP |
学ぶために
-
XForms の基本への入門として、「XForms 入門、第 1 回: フォームのための新しい Web 標準」(Chris Herborth 著、developerWorks、2006年9月) と「XForms 入門、第 2 回: フォーム、モデル、コントロール、そして送信アクション」(Chris Herborth 著、developerWorks、2006年9月)、そして「XForms 入門、第 3 回: アクションとイベントを使う」(Chris Herborth 著、developerWorks、2006年9月) を読んでください。
-
IBM developerWorks の XML ゾーンで XForms について学んでください。
-
XML および関連技術において IBM 認証開発者になる方法については、IBM XML certification を参照してください。
-
developerWorks の XML ゾーンは XML の技術ライブラリーとして、広範な話題を網羅した技術記事やヒント、チュートリアル、技術標準、IBM レッドブックなどを用意しています。
-
XForms に関連する情報交換の場、XForms.org を訪れてください。
-
developerWorks technical events and webcasts で最新情報を入手してください。
-
XForms に関するすべてに焦点を当てた、コミュニティーのマイクロ・サイトである、新しい XForms community topic をぜひ訪れてください。
製品や技術を入手するために
-
XForms Recommendation は W3C によって維持管理されています。
-
XPath Recommendation に関する詳しい情報については、XPath 勧告を調べてください。
-
Internet Explorer で XForms を描画できるオープンソースのコントロール、MozzIE を入手してください。
-
Google Data API から XForms のテンプレートを入手してください。
議論するために