Ajaxに絡んでもう1つ。ASP.NET MVCでは、アクション・メソッドの結果をJavaScriptコードの形式で出力するためのJavaScriptResultオブジェクトを公開している(対応するヘルパー・メソッドはJavaScriptメソッド)。この方式では、Ajax通信のたびにコードとデータの双方を出力することになるため、クライアント/サーバ間を行き来するデータ・サイズはおのずと大きくなる。通常は、Ajax通信で必要なデータはJsonResult、またはContentResultオブジェクトを使って、都度の通信では必要最小限のデータだけをやり取りするのが好ましい。
もちろん、例外的に、サーバサイドの処理結果に応じて動的にJavaScriptのコードを生成したいというケースもあるだろうが、多くの場合、JavaScriptResultオブジェクトを利用するのはJSONPを利用する場合、ということになるはずだ。
JSONPとは、ひと言でいってしまうならば、クロス・ドメイン環境でAjax通信を行うための応用技術である。JSONPそのものについては、本題から外れてしまうため、「リッチクライアント & 帳票」フォーラムの別記事「jQueryを使ってTwitterをおいしくマッシュアップ」も併せて参照いただくとよい。
ここでは、先ほど紹介した書籍検索のサンプルを、JSONP技術を使って置き換えてみよう。
[1]アクション・メソッドを作成する
先ほどのResult/Searchアクションを基に、JSONPに対応したResult/Jsonpアクションを定義してみよう。ここでは、先のコードと異なる部分にのみ注目してみていただきたい。
using System.Web.Script.Serialization;
……中略……
public ActionResult Jsonp(String isbn, String callback) {
var _db = new MyMvcEntities();
// 引数isbnの値をキーに、bookテーブルから該当するレコードを取得
var bok = (from b in _db.Book where b.isbn == isbn
select new {b.title, b.price, b.publish}).FirstOrDefault();
// 取得したBookオブジェクトをシリアライズし、
// JavaScriptのコードを生成
var ser = new JavaScriptSerializer();
var code = String.Format("{0}({1})",
callback, ser.Serialize(bok));
// 生成した文字列をJavaScriptのコードとして返す
return JavaScript(code);
}
Imports System.Web.Script.Serialization
……中略……
Function Jsonp(ByVal isbn As String, ByVal callback As String) As ActionResult
Dim _db = New MyMvcEntities()
' 引数isbnの値をキーに、bookテーブルから該当するレコードを取得
Dim bok = (From b In _db.Book Where b.isbn = isbn _
Select b.title, b.price, b.publish).FirstOrDefault()
' 取得したBookオブジェクトをシリアライズし、
' JavaScriptのコードを生成
Dim ser As New JavaScriptSerializer()
Dim code As String = String.Format("{0}({1})", _
callback, ser.Serialize(bok))
' 生成した文字列をJavaScriptのコードとして返す
Return JavaScript(code)
End Function
JSONPでは、JSONデータを以下のような形式で送信するのが一般的である。
コールバック関数名(JSONデータ)
クライアント側では、これをJavaScriptのコードとして解釈することで(<script>タグから呼び出すことで)、あらかじめ用意されたコールバック関数にJSONデータを引き渡しているわけだ。
ここでは、リスト9の太字の部分で、引数callbackの値と、LINQ to Entitiesで得たデータをJSON形式にシリアライズした結果とから、以下のようなJavaScriptコードを生成している。
disp({"title":"今日からつかえるPHP5サンプル集 PEAR&Zend Framework活用版 ","price":3360,"publish":"秀和システム"})
なお、JsonResultオブジェクトでは自動的にシリアライズ処理が行われていたが、JavaScriptResultオブジェクトではシリアライズは考慮されていない(JSONデータを送信するのが目的ではないので、当然といえば当然だろう)。シリアライズ処理は、JavaScriptSerializer.serializeメソッド(System.Web.Script.Serialization名前空間)で自ら行う必要がある。
[2]クライアント・ページを作成する
クライアント・ページ(JsonpForm.html)は、非ASP.NET環境であることも想定して、ここでは純粋なHTML+JavaScriptで記述している。なお、このファイルは関連するJavaScriptファイル(MicrosoftAjax.js、jquery.js)と合わせて、ASP.NET MVCが動作しているサーバとは異なるサーバに配置するものとする。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>JavaScriptResultオブジェクト</title>
<!--処理に必要なスクリプトをインポート-->
<script src="jquery.js" type="text/javascript"></script>
<script type="text/javascript">
// サーバから受信したJavaScriptコードによって
// 呼び出されるコールバック関数
function disp(result) {
if (result == null) {
window.alert("該当するデータがありません。");
} else {
var builder = new Sys.StringBuilder();
builder.append("タイトル:" + result.title);
builder.append("価格:" + result.price + "円");
builder.append("出版社:" + result.publish);
window.alert(builder.toString("\r"));
}
}
// [検索]ボタンのクリック時に実行されるイベント・ハンドラ
function btn_click() {
$("<script>")
.attr('type', 'text/javascript')
.attr('src', String.format(
"http://localhost:4419/Result/Jsonp?isbn={0}&callback=disp",
$('#isbn').attr('value')))
.appendTo($("head"));
}
</script>
</head>
<body>
<form>
ISBNコード:
<input type="text" id="isbn" />
<input type="button" value="検索" onclick="btn_click()" />
</form>
</body>
</html>
ポイントとなるのは太字の部分だ。基本的なjQueryの構文については、拙稿「ASP.NETプログラマのためのjQuery入門」も併せて参照いただきたい。ここでは最低限、以下のような<script>タグを動的に生成し、<head>タグの配下に埋め込んでいるとだけ理解しておけばよいだろう。
<script type="text/javascript" src="http://localhost:4419/Result/Jsonp?isbn=978-4-7980-2004-4&callback=disp"></script>
この<script>タグによって、Result/Jsonpアクションの結果(JavaScriptコード)が埋め込まれ、指定されたコールバック関数(ここではdisp関数)が呼び出されるというわけだ。先ほども見たように、呼び出しのコードにはJSONデータが埋め込まれているので、後はdisp関数でこのデータが解析され、先ほど同様の結果が得られる。
以上を理解したら、別サーバに配置したJsonpForm.htmlを実行してみよう。クロス・ドメイン環境であっても、先ほどと同じく、入力したISBNコードに対応する書籍情報がダイアログ表示されることが確認できるはずだ。
テキスト・データを出力するContentResult/JsonResultオブジェクトに対して、ファイルやバイナリ・データを出力するのがFileResultオブジェクトである*6。ヘルパー・メソッドの1つであるFileメソッドで指定できる。
*6 厳密には、ファイルの指定方法によって、その派生オブジェクトであるFileContentResult/FilePathResult/FileStreamResultオブジェクトに分類される。が、基本的に開発者がこれらを意識することはないだろう。
Fileメソッドの構文は、次のとおりである(ここでは3つのオーバーロードを示している)。
File(contents As Byte(), type As String [,name As String])
File(contents As Stream(),type As String [,name As String])
File(path As String, type As String [,name As String])
' contents:ダウンロードするデータ本体
' path:ダウンロードファイルのパス
' type:ファイルのコンテンツ・タイプ
' name:ダウンロード時のファイル名
最後の引数nameは、内部的にはContent-Disposition応答ヘッダにセットされる値となる。ダウンロード時にデフォルトで表示されるファイル名を指定したい場合に使用するが、この指定は、クライアントに対してそのファイル名で保存することを「強制するものではない」ので注意されたい。
では具体的なサンプルを見ていこう。
■例:指定されたファイルをダウンロードする
まずは、サーバ上に用意された画像ファイルをダウンロードするための例を見てみよう。例えば、アクセス先を「http://localhost:4419/Image/1125」とした場合には、サーバ上の「C:/Data/RIMG1125.JPG」がダウンロードされるようにする。
もっとも、ファイルを直接にダウンロードさせるならば、直接、ファイルにリンクを張った方がシンプルに思われるかもしれない。しかし、ファイル自体は公開フォルダに配置せず、間にアクションを挟むことで、アプリケーションでアクセス制御やアクセス・カウントなど任意の付随処理を適用できるというメリットがある。
[1]アクション・メソッドを用意する
Result/Downloadアクションは、ダウンロード処理を実装したアクションだ。引数としてファイルを識別するための変数idを受け取り、ここからサーバ上のパス「C:/Data/RIMG<id値>.JPG」を決定する。
public ActionResult Download(String id) {
var path = String.Format("C:/Data/RIMG{0}.JPG", id);
return File(path, "image/jpeg", "sample.jpg");
}
Function Download(ByVal id As String)
Dim path As String = String.Format("C:/Data/RIMG{0}.JPG", id)
Return File(path, "image/jpeg", "sample.jpg")
End Function
Fileメソッドの構文については前述したとおりであり、ここでことさらに特筆すべき点はない。
[2]ルーティング設定を追加する
Result/Downloadアクションを呼び出すために、デフォルトのルーティング設定を使って、
http://localhost:4419/Result/Download/1125
というURLで呼び出しても構わない。
しかし、こうしたよく使うであろうアクションは、もっと短いURLで呼び出せるようにしておきたい、という場合もあるだろう。また、セキュリティ的な観点から、id値(ここでは「1125」の部分)に自由な値を設定できるのは好ましくないという懸念もある。
そこで、ここではより短く、
http://localhost:4419/Image/1125
というURLでResult/Downloadアクションを呼び出せるようにするとともに、id値には4桁の数字のみを設定できるように、ルーティング設定を追加しておこう。
既存のGlobal.asaxに追加すべきコードは、次のとおりである(追記部分は濃い文字で表している)。
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("Result/Download/{id}");
routes.MapRoute(
"Image",
"Image/{id}",
new {controller = "Result", action = "Download"},
new {id = "\\d{4}"}
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
routes.IgnoreRoute("Result/Download/{*pathinfo}")
routes.MapRoute( _
"Image", _
"Image/{id}", _
New With {.controller = "Result", .action = "Download"}, _
New With {.id = "\d{4}"} _
)
routes.MapRoute( _
"Default", _
"{controller}/{action}/{id}", _
New With {.controller = "Home", .action = "Index", .id = ""} _
)
End Sub
ここでのポイントは2点である。
(1)ルーティング設定を追加するのはMapRouteメソッド
ルーティング設定を追加するのは、MapRouteメソッドの役割である。MapRouteメソッドの構文については、第1回でも紹介しているが、ここでもう一度おさらいしておこう。
MapRoute(name As String, url As String, defaults As Object, constraint As Object)
' name:ルール名
' url:ルーティング方式
' defaults:初期値
' constraint:制約
引数urlは、ルーティングの際に実際にマッチングを行うURIパターンを表すものだ。リスト14では、
Image/{id}
がそれである。“{名前}”の部分は変数のプレイスホルダを表すのだった。
デフォルトのルーティング設定では、ここで「{controller}/{action}/{id}」として、コントローラ名やアクション名を指定していたが、今回はこれらの情報が見当たらない。従来、コントローラ名とアクション名をURIパターンから動的に取得していたのに対して、ここでは引数defaultsで指定しているのである。つまり、ここでは「Image/{id}」で呼び出された場合に、固定的にResult/Downloadアクションが呼び出される、ということを意味する。
そして引数constraintsは、第1回には登場しなかった新出の引数である。これは、ルーティング設定に、より細かな制約条件を設定するためのものだ。ここでは、変数{id}に対する正規表現として「\d{4}」を指定することで、値が「4けたの数値」であるという制限を課しているわけだ。これによって、(例えば)「Image/Xyz」のようなURIパターンにはマッチしなくなる。
(2)特定のURIパターンを無効化するのはIgnoreRouteメソッド
(1)では「Image/{id}」というルーティング設定を追加したが、このままでは依然として「Result/Download/〜」というアドレスでもResult/Downloadアクションを呼び出せてしまう。
そこで、明示的に「Result/Download/〜」がデフォルトのルーティング規則にマッチしないように無効化しておこう。このように特定のURIパターンを無効化するのはIgnoreRouteメソッドの役割だ。
routes.IgnoreRoute("Result/Download/{id}")
IgnoreRouteメソッドには、無効にしたいURIパターンを指定するだけでよい。
以上を理解したら、さっそく、サンプルを実行してみよう。「C:/Data」フォルダの配下に「RIMG9999.JPG」のような名前のファイルを配置した状態で、ブラウザから以下のアドレスでアクセスしてみよう(ポート番号部分は環境によって異なる可能性がある)。
http://localhost:3568/Image/9999
ファイルをダウンロードするための以下のようなダイアログが表示され、サーバ上に配置した画像ファイルがダウンロードできれば成功である。
[参考]FileResultオブジェクトによる動的なバイナリ・データ出力
FileResultオブジェクトの役割は、サーバ上にあらかじめ(静的に)用意されたファイルを出力することばかりではない。動的に生成したバイナリ・データを出力することも可能だ。具体的な例については、後日、「.NET TIPS:ASP.NET MVCで動的にPDF文書を生成するには?」で解説の予定である
以上、今回はActionResult(派生)オブジェクトと、それらに対応するヘルパー・メソッドについて解説した。本稿をご覧いただいても分かるように、アクション・メソッドを記述するうえで、ActionResultオブジェクトの理解は欠かせないものである。ぜひとも、ここで紹介した内容は具体的な用法とともに、きちんと押さえておいていただきたい。
次回は、アクション・メソッドに対して、認証やキャッシュ、検証などの機能を追加するフィルタ機能について扱う予定である。
Copyright© Digital Advantage Corp. All Rights Reserved.