ソートの例
DojoがどのようにStrutsやTilesと連携するのかを理解するため、具体的な問題に取り組んでみましょう。付録のサンプルコードでは、まず有名人のリストを図1のような形式でユーザーに表示します。
このデータを、最初のページ読み込み時と異なる順序でソートして表示するには、2つの方法があります。1つ目は、従来どおりの、フォーム全体をサーバーに送り返してデータを並べ替え、ページ全体をリロードするという方法です。2つ目は、もうお分かりでしょうが、AJAXを利用する方法です。このAJAXによる方法を実装するには、ページのマークアップを編集して、divブロック内部にソート可能なリストを含め、ページ上のどこかのボタンクリックまたはその他のクライアントサイドのイベントから、図2のようにデータを並べ替えるためのAJAXコールを呼び出すようにします。
具体的には、次のようなdivタグを記述することが考えられます。
<div id="famousPeople"> <table> <tr> <td> <ul> <li>Dave Thomas</li> <li>Ronald McDonald</li> <li>George Washington</li> </ul> </td> </tr> </table> </div>
ただし、上記のdivマークアップは手で書いたものではなく、生成されたHTMLです。このような単純なコードを繰り返し書くよりも、Tilesを使用して、ブロック全体を再利用しやすく、エラーの起こりにくいものにした方が得策です。そこで、次のようにします。
<div id="famousPeople"> <tiles:insert definition="famousPeopleList" flush="true"> <tiles:put name="listbean" beanName="famousFolkForm" beanProperty="famousFolkList" /> </tiles:insert> </div>
このサンプルコードでは、有名人の名前のキャッシュリストを「famousFolks」というセッション属性で定義しています。このリストは、Dojoが次のようなURLに対する標準的なStrutsマッピングを使って呼び出すサーブレットによって再ソートされます。
<action path="/famousPeopleSort" name="famousFolkForm" scope="request" validate="false" type="com.tillman.dojo.action.FamousPersonageSort"> <forward name="success.sort" path="/sortUpdateXml.jsp"/> </action>
新しくソートされたリストは「sortUpdateXml.jsp」に渡されます。このJSPには、<tiles: insert>タグと、AJAX(Dojo)の応答において返されるマークアップを作成する際に名前のリストを受け取るTilesコンテキストが含まれています。
それでは、もう少し機能を追加してみましょう。このAJAXコールを含んでいるページには、sortByという名前のドロップダウンリストも含まれています。このドロップダウンリストのonchangeイベントから、JavaScriptメソッドを呼び出すようにします。sortByドロップダウンのマークアップは次のようになります(コード全体についてはダウンロードサンプルを参照してください)。
<select name="sortBy" styleId="sortBy" onchange="javascript(doSort(this.value));"> <option name="person" value="George Washington">George Washington ... </select>
新しいソートパラメータが選択されると、<head>タグで定義された(またはJavaScript(.js)リソースファイルからインクルードされた)doSort()というJavaScriptメソッドが呼び出されます。こうした事情を念頭に置きながら、今度はDojoコールのためのコードを調べてみましょう。
Dojoには堅牢なAPIが多数用意されており、パッケージ化の構造もおおむね直感的です。そうは言っても、AJAXコールのセットアップにはdojo.ioパッケージのdojo.io.bind()メソッドを使用するのだということを私が理解するまでには少し時間がかかりました(この「io」はフォームに対する入出力を意味するに違いありません。個人的には、Javaにならって.net.packageにした方がいいのではないかと思うのですが)。
function doSort(sortTerm) { dojo.io.bind( { url: "famousPeopleSort.shtml", content: {sortedBy: sortTerm}, method: "POST", mimetype: "text/html", load: function(type, value, evt) { processReturnValue(value); }, backButton: function(){ //for maintaining Dojo back button stack dojo.io.bind({ url: "famousPeopleSort.shtml", content: {sortedBy: previousSortTerm}, method: "POST", mimetype: "text/html", load: function(type, value, evt) { resetDropDown(previousSortTerm); processAjaxResponse(value); }, error: function(type, error) { alert("Error: " + error); } }); }, error: function(type, error) { alert("Error: " + error.reason); } });
このメソッド呼び出しでは、HTTP PostのURL引数に、「struts-config.xml」(サンプルコードを参照)内で「famousPeopleSort.shtml」にマッピングされているサーブレットを割り当てます。content引数は、ドロップダウンリストのonchangeイベントからこの関数に渡されるsortTerm引数を保持しており、これをPostの要求パラメータとして設定します。method引数はHTMLのformタグのmethod属性に似ており、mimetype引数はAJAXの応答時に期待されるデータの型を指定します。load引数は、AJAXの応答が返されたときに呼び出される関数を定義しています。
上記のJavaScriptメソッドから、Dojoがブラウザの[戻る]ボタンの動作を管理できることがお分かりになったかもしれません。Dojoは独自のコールスタックを作成することでこれを行っています。これが便利なのは、もちろん、多くのユーザーは[戻る]ボタンを使って前のページに戻ろうとするからです。こうした状況では、ページ全体がリロードされず、Dojoによる処理が失われるため、この動作は混乱を招くかもしれません。個々のDojoコールは直前のコールの上にスタックされており、ユーザーが[戻る]ボタンをクリックすると、一番上のバインドされた(つまり、io.bindメソッドによって呼び出された)コールがスタックからポップされて使われます。
このスタックは、「dojo.js」と同じディレクトリにあるHTMLファイル「iframe_history.html」の助けを得て管理されています。私の場合はDojoの[戻る]ボタン管理機能をうまく実装できましたが、他の開発者からはうまくいかなかったという報告も聞いています。問題に遭遇したときには、私の事例が少しでも参考になれば幸いです。
[戻る]ボタンをプログラムで管理するのは素晴らしいことですが、仕事の関係者がDojoの[戻る]ボタンの動作に不慣れな場合は、開発に入る前に、彼らのためにプロトタイプやデモを作成すべきです。AJAXコールは、「想定されるもの」とはかけ離れたナビゲーションやページフローのパラダイムを作り上げます。さらに、サイトの反応を鈍らせる何か未知の問題(または目に見えない問題)が起こっていると、大抵はユーザーに嫌な思いをさせることになり、アプリケーションは失敗に終わる可能性が高くなります。そのため、Dojoコールの実行開始時に起動し、完了時には消えるプログレスバーなどのビジュアルキュー(視覚的な合図)の追加を検討したくなるかもしれません。
返されたデータがどのように処理されるかを説明する前に、そのデータを保持する応答ドキュメントを見てみましょう。今回の有名人ソートのための応答ドキュメントはXML形式になっており、これが、前述のdiv内のタイル挿入ステートメントで参照されるJSPタイルのコンテナとして働きます。
<%@page contentType="text/xml" %> <%@taglib prefix="tiles" uri="/WEB-INF/tiles.tld" %> <%@taglib prefix="html" uri="/WEB-INF/struts-html.tld" %> <%@taglib prefix="bean" uri="/WEB-INF/struts-bean.tld" %> <ajax-response> <field id="famousFolk1" attribute="innerHTML"> <![CDATA[ <tiles:insert definition="famousPeople" flush="true"> <tiles:put name="famousFolkList" beanName="famousFolkForm" beanProperty="famousFolkList"/> </tiles:insert> ]]> </field> </ajax-response>
tiles:insertはサーバー上で処理されるので、ソート操作に関するDojoコールはリストを再び処理し、その結果をタイルによって作成されたHTMLに組み込みます。AJAXコールが呼び出されると、動的に生成されたこのHTMLが、bind()メソッドで指定したJavaScript関数の処理のためにajax-responseというXML要素内にネストされます。
function processResponse(response) { if (djConfig["isDebug"]) { dojo.debug("ajax response: " + response); } //if the XML element ?ajax-response? //isn't found then write out what was returned if (response.toLowerCase().indexOf("<ajax-response>") < 0) { document.write(response); document.close(); return; } //parse the AJAX XML response into a document. var xmlDoc = dojo.dom.createDocumentFromText(response); //refer to the XML response above ? the ?field? element is inside //the response document. var fields = xmlDoc.getElementsByTagName("field"); for (var i = 0; i < fields.length; i++) { var id = fields[i].getAttribute("id"); var attribute = fields[i].getAttribute("attribute"); var value = null; if (fields[i].hasChildNodes()) { for (var j=0; j<fields[i].childNodes.length; j++) { var currentNode = fields[i].childNodes[j]; if(currentNode.nodeName.toLowerCase()== "#cdata-section") { value = currentNode.nodeValue; } } } //Dynamically builds a function //called replaceValue and then invokes it eval("window.replaceValue = function(value) { dojo.byId('"+ id + "')." + attribute + " = value;}"); replaceValue(value); } }
この応答の処理は、返されるXMLドキュメントの構文解析の途中で行われます。上記のコードは、この応答内にあるフィールド要素を探し、その内容を取り出します。続いて、ページ上のdivをこの応答の指定された#cdata-sectionの値で置き換えます。もちろん、この置き換えはDOMの更新に対応しており、ブラウザはコンテンツの変更を反映するために表示の更新を行います。他にもきっとあるでしょうが、これは私自身が機能させることができた唯一の、ブラウザに依存しない形の構文解析でした。
ここ数年、StrutsとTilesは企業でのJava開発における重要なフレームワークになっています。また、AJAXとDojoツールキットは、問題を分離し、コードおよび表示要素の双方の再利用を促進する大まかなサービス指向の方法でStrutsとTilesを利用するための優れた組み合わせです。こうした強力なフレームワークどうしを統合するためには習熟が必要ですが、その努力に見合うだけの結果が得られます。
dojo.byIdの呼び出しにいくつか問題がありました。Strutsはidプロパティをhtml:hiddenタグのプロパティとしてサポートしていません。IEの場合は、dojo.byId("field")の呼び出し途中でこのプロパティが見当たらなくても、うまくやり過ごしてそのフィールドの名前を参照します。一方、Firefoxの場合は、エラーをスローして、このフィールドがそのようなプロパティ(この場合はidプロパティ)をサポートしていないことを知らせます。私にとって幸運だったのは、同僚の1人が、html:hiddenタグのstyleldフィールドがJSPのレンダリング時にidプロパティに変換されるんだよとヒントを与えてくれたことでした。さらに運が良かったのは、そのときの会話でもう1人の同僚が、[戻る]ボタンに類似した機能を実行するためにはchangeURLパラメータをdojo.bind()メソッドに追加しなければならない、と教えてくれたことです。これで問題の全容が見えてきたのではないでしょうか。私もそうでした。私の同僚たちは、彼らが[戻る]ボタンを動作させるために行ったさまざまなハッキングに関する知識を惜しみなく与えてくれました。しかし彼らのヒントがあったにもかかわらず、これらの問題や次から次へと現れる新たな問題への対処方法を思い出すには、思ったよりも時間がかかりました。本稿の発表前にこれらすべての問題の原因を探り当て、あらゆる問題を解決したサンプルコードを紹介できればよかったのですが、残念ながらそれはできていません。また、インストールされているFirefox拡張機能の数など、ある種の特異な環境上の問題も、不安定な動作につながる可能性があります。
これらの問題のいくつかに遭遇した後、自分のローカル環境にあったDojoツールキットのアップグレードを行ったため、現在、私のDojoに関する不完全ながらも増えつつある知識は、バージョン0.2.2と0.3.1の両方に対応しています。ちょっとした調整:Webを検索してみたところ、「bootstrap1.js」ファイルでは、
djConfigのpreventBackButtonFixのデフォルト値がtrueに設定されていることが分かりました。それまで私のコードではこれをfalseに設定していたのですが、この「bootstrap1.js」の値を同じように変えると、驚いたことに、Firefoxだけに見られたページ読み込みのエラーが消えたのです。普通、[戻る]ボタンはFirefoxと連動するのですが、バージョン0.2.2でうまくいったのと同じテクニックを0.3.1で使うと、フィールドの一部の更新がスムーズに行われませんでした。また、私のサンプルをOperaで読み込んだところ、[戻る/進む]ボタンの機能はOperaではサポートされていません、というメッセージが表示されましたが、それ以外のコードは正常に動作していました。奇妙なことに、ある同僚がバージョン0.2.2を使ってテストした際にはOperaでも正常に動作したそうです。要するに、Dojoにはまだ何らかの問題が残っているので、正しい方向に向かってDojoの開発が活発に進められている限り、当面は困難に遭遇する覚悟が必要と言えそうです。


![図2 ドロップダウンリストから[Ascending(昇順)]を選択すると、サーバーにアクセスすることなく、データの再ソートを行うAJAXコールが呼び出され、リストがすばやく並べ替えられる 図2 ドロップダウンリストから[Ascending(昇順)]を選択すると、サーバーにアクセスすることなく、データの再ソートを行うAJAXコールが呼び出され、リストがすばやく並べ替えられる](http://cz-cdn.shoeisha.jp/static/images/article/568/16338_s.jpg)
+ id +