しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Knockout.js と ASP.NET MVC の組み合わせ時にモデルバインディングが腐る件

今日も Knockout.js を使って ASP.NET MVC に POST するようなコードを書いていたのですが、モデルバインダが意図したとおりに値をバインドしてくれない時がありました。

まずは ASP.NET MVC 側のコードを見ていきましょう。難しいことはしてません。

public class ChildFormModel
{
    public string Name { get; set; }
}

public class FormModel
{
    public string Name { get; set; }
    public List<ChildFormModel> Items { get; set; }
}

[HttpPost]
public ActionResult Post(FormModel model)
{
    return Json(true);
}

簡単にコードを紹介すると、親のモデルがさらにクラスのリストを持っている形です。

それでは次は Knockout.js 側のコードを見ていきます。

var viewModel = {
    Name: ko.observable("foo"),
    Items: ko.observableArray([
        { Name: ko.observable("bar") }
    ])
};

function saveChange() {
    $.ajax({
        type: "POST",
        url: "/Home/Post",
        data: ko.toJS(viewModel)
    });
}

めんどくさかったので初期データを横着しました。ちなみに Knockout.js の Observable なモデルを普通の JavaScript オブジェクトにするためには ko.toJS メソッドを使います。

これで実際に saveChange メソッドを実行すると、Visual Studio 側でネストされた ChildFormModel の Name プロパティに値がセットされていないことがわかります。

f:id:shiba-yan:20140625211118p:plain

何故動作しないかを調べたら、POST されたデータが以下のようになっていました。

Name:foo
Items[0][Name]:bar

ASP.NET MVC のモデルバインダは Items[0].Name という形式でないと正しくバインディング出来ないようになっているので、jQuery が内部的に変換するために使っているメソッドが原因といえます。

どうやって回避するかですが、わざわざ application/x-www-form-urlencoded 形式でエンコードしなくても、ASP.NET MVC は JSON からもバインディング出来るので、難しく考えずに JSON で送ってしまいます。

var viewModel = {
    Name: ko.observable("foo"),
    Items: ko.observableArray([
        { Name: ko.observable("bar") }
    ])
};

function saveChange() {
    $.ajax({
        type: "POST",
        url: "/Home/Post",
        contentType: "application/json",
        data: ko.toJSON(viewModel)
    });
}

Knockout.js には ko.toJSON という Observable なモデルを JSON にしてくれるメソッドがあるので、簡単に JSON を送るように変更できます。

f:id:shiba-yan:20140625212624p:plain

実際に動かしてみると、今度はちゃんと値がセットされていることがわかります。モデルが複雑な場合には JSON を使うのが無難ですね。