ASP.NET MVCとknockout.jsを使って以下の割と基本的なことをやろうとしたら結構はまったのでまとめてみました。
- データベースから取得したデータを元にJSONを生成する
- それをAJAXで取得しknockout.mappingでビューモデルにする
- AJAXでJSONをPOSTしてサーバに保存する
Entity Frameworkのデータオブジェクトを直接JSONシリアライズするとエラーになる
まずはサーバからJSONを返すためにコントローラのJson()メソッドを呼んでJSONを生成するわけですが、ここに横着してデータベースから取得した値、つまりEntity Frameworkのデータオブジェクトをそのまま入れようとすると、よほど単純なモデルでないかぎり「オブジェクトのシリアル化を実行中に循環参照が見つかりました」という例外が発生します。
これはオブジェクトに含まれるすべてのpublicなプロパティがシリアライズされる時に、関連付けのプロパティもシリアライズされてしまうためです。関連付けの接続先がめぐりめぐって自分のデータオブジェクトに返ってきていると循環が発生してしまうためシリアライズできません。双方向の関連付けがある場合、もうそれだけでアウトです。
なので、ASP.NET MVCのビューモデルを作って、モデルに必要なプロパティをひとつずつ移して余計なものが含まれないようにする必要があります。また、knockout.mappingを使うとサーバから来たものだけサーバに送り返す挙動になるので、サーバから返す必要がなくてもプロパティは用意しておく必要があります。つまり、出力でも入力に使うビューモデルと同じものを使用することになります。
JSONに日付を出力すると変なフォーマットの文字列になる
dateTime型のオブジェクトをJSONにシリアライズすると/Date(62831853071)/
みたいな文字列になります。これはそういう仕様なのでしかたありません。
ASP.NET AJAX: Inside JSON date and time string
https://msdn.microsoft.com/en-us/library/bb299886.aspx#intro_to_json_sidebarb
幸いPOST時はモデルバインダがさまざまなフォーマットを受け付けてくれるので、knockout.jsのビューモデルを生成するときに、この謎フォーマットから人間が編集するためのフォーマットに変換するだけで対応できます。
なので以下のような謎フォーマットからYYYY/MM/DD形式の文字列への変換関数を用意して、
var fromJsonDateStr = function (value) {
var match = /\/Date\((\d+)\)\//.exec(value);
if (match) {
var date = new Date(parseInt(match[1]));
value = date.getFullYear() + '/' + ('00' + (date.getMonth() + 1)).slice(-2) + '/' + ('00' + date.getDate()).slice(-2);
}
return value;
};
knockout.mapping用のマッピングを以下のように用意すれば対応できます。
var mapping = {
Date: {
create: function (options) {
return ko.observable(fromJsonDateStr(options.data));
},
update: function (options) {
return fromJsonDateStr(options.data);
}
}
};
createではobservableを返さなければならないのに、updateではむしろobservableではダメというのは罠っぽいです。
nullが"null"という文字列でPOSTされてしまう
x-www-form-urlencoded形式でPOSTするとnullが"null"という文字列でPOSTされてしまいます。単純にnullを文字列化したものを送るのでこうなってしまうようです。受ける側の型がint?
だったりするとモデルバインダが数値に変換できないというエラーを出します。
解決方法はJSONをPOSTするようにすることしかありません。
以下のようにすればJSONでPOSTすることができます。
$.ajax({
url: "...",
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(model),
});
JSON形式でPOSTすると例外HttpAntiForgeryExceptionが投げられる
アクションにValidateAntiForgeryToken属性を付けている時にJSON形式でPOSTすると以下の例外が投げられます。
System.Web.Mvc.HttpAntiForgeryException: 必要な偽造防止フォーム フィールド "__RequestVerificationToken" が存在しません。
おそらく、モデルバインダは何事も無くJSON形式のリクエストを解釈するが、その他の部分はx-www-form-urlencoded形式しか解釈できないためリクエストから取り出すことができないのではないかと思います。
この問題は割とどうにもならなくて、自前でトークンを検証する仕組みを用意するしか方法が無いようです。以下のページにトークンをリクエストヘッダに入れてサーバに渡す実装の例があるので、それを参考に用意するなどします。
Validating .NET MVC 4 anti forgery tokens in ajax requests
http://richiban.uk/2013/02/06/validating-net-mvc-4-anti-forgery-tokens-in-ajax-requests/
トークンの検証関係はSystem.Web.Helpers.AntiForgeryクラスに閉じ込められていて外からは割と何もできないのですが、なぜかトークンを与えて検証できるpublicなメソッドValidate(cookieToken, formToken)
が用意されています。前述のページはこれを利用しています。トークンを取得するメソッドは無いのでそこはがんばって何とかする必要があります。ちなみにcookieTokenがクッキーに保存されている検証元のトークン、formTokenがフォームに入れられる検証対象のトークンになります。