ASP.NET MVC 4 で AJAX (WCF サービス) [改訂版]

Visual Studio 2012 (Update 3)を起動して「ファイル」→「新規作成」→「プロジェクト」を選択します。
f:id:akmiyoshi:20131123201735p:plain

ASP.NET MVC 4 Web アプリケーション」を選択します。プロジェクト名は「MvcApplication1」のままで「OK」を押して次の画面に進みます。

「テンプレートの選択」では「インターネットアプリケーション」を選択し、「ビューエンジン」では「Razor」を選択し「OK」を押します。
f:id:akmiyoshi:20131123212925p:plain

「MvcApplication1」プロジェクトが新規に作成されました。

ここでアプリケーション起動時のポート番号と仮想パスを明示的に設定しておきます。
ソリューションエクスプローラー上でプロジェクト名を右クリックしてショートカットメニューから「プロパティ」を選択します。「Web」タブを選択して、「開始動作」セクションで「ページを指定する」を選択してテキストボックスは空白のままにしておきます。同じく「Web」タブ内の「サーバー」セクションから「Visual Studio 開発サーバーを使用する」をクリックして、「ポートを指定する」を選択して「8080」と入力します。最後に「仮想パス」には「/mvc1/」と入力します。
f:id:akmiyoshi:20131125141317p:plain

F5で起動してWebブラウザのアドレスバーに「http://localhost:8080/mvc1/」と表示されることを確認しておいてください。

「Views/Home/Index.cshtml」を開きます。
f:id:akmiyoshi:20131123224516p:plain

「Views/Home/Index.cshtml」(既存)の内容をクリアして以下のように編集します。

@{
    ViewBag.Title = "Home Page";
}
<h3>AJAX テスト</h3>

<p>@ViewBag.Message</p>

<input id="button1" type="button" value="ボタン1" />

@section scripts {
<script type="text/javascript">
    $(document).ready(function () {
        $("#button1").click(function () {
            alert('ボタン1が押されました');
        });
    });
</script>
}

「F5」を押してデバッグ起動します。以下のような画面が既定のブラウザで開かれて、ボタンを押すとメッセージボックスが出力されることを確認します。これで jQuery が動作することが確認できました。
f:id:akmiyoshi:20131123225334p:plain

Visual Studio 2012 に戻ってデバッグを終了するには下図のボタンを押します。
f:id:akmiyoshi:20131125143121p:plain

次に、AJAX で呼び出されるサービスを追加します。ソリューションエクスプローラーからプロジェクト名を右クリックしてショートカットメニューで「追加」→「新しい項目」を選択します。
表示されるダイアログで「Visual C#」→「Web」階層にある「AJAX 対応 WCF サービス」を選択します。名前は「Service1.svc」のままで「追加」を押します。
f:id:akmiyoshi:20131123233245p:plain

[Service1.svc.cs(プロジェクトに追加直後のテンプレート)]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace MvcApplication1
{
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(
        RequirementsMode = 
        AspNetCompatibilityRequirementsMode.Allowed)]
    public class Service1
    {
        [OperationContract]
        public void DoWork()
        {
            // 操作の実装をここに追加してください
            return;
        }
    }
}

Service1.svc.cs を編集して以下のように変更します。追加直後のテンプレートに含まれていた「public void DoWork()」は削除して、。「public string HelloWorld()」を追加します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace MvcApplication1
{
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(
        RequirementsMode = 
        AspNetCompatibilityRequirementsMode.Allowed)]
    public class Service1
    {
        [OperationContract, WebInvoke(Method = "GET")]
        public string HelloWorld()
        {
            return "HelloWorld():" + 
                System.Web.HttpContext.Current.Session["Info"];
        }
    }
}

上の Service1.svc.cs 内の HelloWorld() メソッドではセッションの情報(この例では文字列)を受け取って、その文字列を戻り値に追加して返しています。
コントローラーでセッションの情報を予め設定しておくため、HomeContoroller.cs に一行追加します。
[HomeContoroller.cs]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
            HttpContext.Session["Info"] = "[セッションの情報]"; //この行を追加
            return View();
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your app description page.";
            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";
            return View();
        }
    }
}

「Views/Home/Index.cshtml」を以下のように書き換えます。button1(テキストは「1.HelloWorld」)を押すと AJAX 通信(AJAX 呼び出し)を行って結果(JSON)と戻り値を表示します。
[Views/Home/Index.cshtml]

@{
    ViewBag.Title = "Home Page";
}
<h3>AJAX テスト</h3>

<input id="button1" type="button" value="1.HelloWorld" />
<br />
<b>JSON:</b><div id="json"></div>
<b>戻り値</b><div id="result"></div>

@section scripts {
<script type="text/javascript">
$(document).ready(function () {
    $("#button1").click(function () {
        $.ajax({
            type: "GET",
            url: '@VirtualPathUtility.ToAbsolute("~/Service1.svc/HelloWorld")',
            data: {},
            success: function (data) {
                $("#json").text(JSON.stringify(data));
                $("#result").text(data.d);
            },
            error: function (xhr) {
                alert("failed");
                alert(xhr.responseText);
                return;
            }
        });
    });
});
</script>
}

F5 で起動して「1.HelloWorld」ボタンをクリックすると戻り値が「HelloWorld():[セッションの情報]」となっていて、セッションの情報を受け継いでいることが確認できます。
f:id:akmiyoshi:20131124001913p:plain

次に引数のあるサービスメソッドの呼び出しを行ってみます。最終的には構造体や構造体の配列を引数や戻り値として受け渡すサンプルを提示します。

二つの整数型の引数 x, y を取って x と y の合計を返すというサービスメソッド「Add2()」を「Service1.svc.cs」に以下のソースコードのように追加します。

[Service1.svc.cs]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace MvcApplication1
{
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(
        RequirementsMode = 
        AspNetCompatibilityRequirementsMode.Allowed)]
    public class Service1
    {
        [OperationContract, WebInvoke(Method = "GET")]
        public string HelloWorld()
        {
            return "HelloWorld():" + 
                System.Web.HttpContext.Current.Session["Info"];
        }
        [OperationContract, WebInvoke(Method = "GET")]
        public int Add2(int x, int y)
        {
            return x + y;
        }
    }
}

「Views/Home/Index.cshtml」に button2 (キャプション=「2.Add2」)を追加します。
button2 が押されると { x: 11, y: 22 } の引数を渡してその合計を取得します。
[Views/Home/Index.cshtml]

@{
    ViewBag.Title = "Home Page";
}
<h3>AJAX テスト</h3>

<input id="button1" type="button" value="1.HelloWorld" />
<input id="button2" type="button" value="2.Add2" />
<br />
<b>JSON:</b><div id="json"></div>
<b>戻り値</b><div id="result"></div>

@section scripts {
<script type="text/javascript">
$(document).ready(function () {
    $("#button1").click(function () {
        $.ajax({
            (中略)
        });
    });
    $("#button2").click(function () {
        $.ajax({
            type: "GET",
            url: '@VirtualPathUtility.ToAbsolute("~/Service1.svc/Add2")',
            data: { x: 11, y: 22 },
            success: function (data) {
                $("#json").text(JSON.stringify(data));
                $("#result").text(data.d);
            },
            error: function (xhr) {
                alert("failed");
                alert(xhr.responseText);
                return;
            }
        });
    });
});
</script>
}

f:id:akmiyoshi:20131124010025p:plain


次に以下のような構造体(POCO)を戻り地として返すサンプルを提示します。name(文字列)と age(整数)をメンバーとして持った Person クラスです。クラスには [DataContract]属性をつけて、name と age のメンバーには [DataMember]属性をつけておきます。これらの属性をつけてないとデータの受け渡しが行えません。

    [DataContract]
    public class Person
    {
        [DataMember]
        public string name { get; set; }
        [DataMember]
        public int age { get; set; }
    }

サービスに新しい公開メソッドを追加します。

[Service1.svc.cs]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace MvcApplication1
{
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(
        RequirementsMode = 
        AspNetCompatibilityRequirementsMode.Allowed)]
    public class Service1
    {
        [OperationContract, WebInvoke(Method = "GET")]
        public string HelloWorld()
        {
            return "HelloWorld():" + 
                System.Web.HttpContext.Current.Session["Info"];
        }
        [OperationContract, WebInvoke(Method = "GET")]
        public int Add2(int x, int y)
        {
            return x + y;
        }
        [OperationContract, WebInvoke(Method = "GET")]
        public Person GetPerson() // ←追加
        {
            return new Person { name = "トム", age = 23, };
        }
    }
    [DataContract]
    public class Person // ←追加
    {
        [DataMember]
        public string name { get; set; }
        [DataMember]
        public int age { get; set; }
    }
}

[Views/Home/Index.cshtml]

@{
    ViewBag.Title = "Home Page";
}
<h3>AJAX テスト</h3>

<input id="button1" type="button" value="1.HelloWorld" />
<input id="button2" type="button" value="2.Add2" />
<input id="button3" type="button" value="3.GetPerson" />
<br />
<b>JSON:</b><div id="json"></div>
<b>戻り値</b><div id="result"></div>

@section scripts {
<script type="text/javascript">
$(document).ready(function () {
    $("#button1").click(function () {
        (中略)
    });
    $("#button2").click(function () {
        (中略)
    });
    $("#button3").click(function () {
        $.ajax({
            type: "GET",
            url: '@VirtualPathUtility.ToAbsolute("~/Service1.svc/GetPerson")',
            data: {},
            success: function (data) {
                $("#json").text(JSON.stringify(data));
                $("#result").text(data.d.name +
                 " & " + data.d.age);
            },
            error: function (xhr) {
                alert("failed");
                alert(xhr.responseText);
                return;
            }
        });
    });
});
</script>
}

f:id:akmiyoshi:20131124010735p:plain

下のソースコードのように3つのサービスメソッド(GetPersons(), SetPerson(), SetPersons())を「Service1.svc.cs」に追加します。
[Service1.svc.cs]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace MvcApplication1
{
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(
        RequirementsMode = 
        AspNetCompatibilityRequirementsMode.Allowed)]
    public class Service1
    {
        [OperationContract, WebInvoke(Method = "GET")]
        public string HelloWorld()
        {
            return "HelloWorld():" + 
                System.Web.HttpContext.Current.Session["Info"];
        }
        [OperationContract, WebInvoke(Method = "GET")]
        public int Add2(int x, int y)
        {
            return x + y;
        }
        [OperationContract, WebInvoke(Method = "GET")]
        public Person GetPerson()
        {
            return new Person { name = "トム", age = 23, };
        }
        [OperationContract, WebInvoke(Method = "GET")]
        public List<Person> GetPersons() // ←追加
        {
            return new List<Person> {
                new Person { name = "トム", age = 23, },
                new Person { name = "ジョン", age = 35, },
            };
        }
        [OperationContract, WebInvoke(Method = "POST")]
        public string SetPerson(Person person) // ←追加
        {
            if (person == null) return "(null)";
            return "[" + person.GetType().ToString() +
                   ":" + person.name + "|" + person.age + "]";
        }
        [OperationContract, WebInvoke(Method = "POST")]
        public string SetPersons(List<Person> persons) // ←追加
        {
            if (persons == null) return "(null)";
            var stream1 = new System.IO.MemoryStream();
            Type[] knownTypes = new Type[] {
                typeof(List<Person>)
            };
            var serializer =
                new DataContractJsonSerializer(
                    typeof(Person), knownTypes
                    );
            serializer.WriteObject(stream1, persons);
            return Encoding.UTF8.GetString(stream1.ToArray());
        }
    }
    [DataContract]
    public class Person
    {
        [DataMember]
        public string name { get; set; }
        [DataMember]
        public int age { get; set; }
    }
}

[Views/Home/Index.cshtml

@{
    ViewBag.Title = "Home Page";
}
<h3>AJAX テスト</h3>

<input id="button1" type="button" value="1.HelloWorld" />
<input id="button2" type="button" value="2.Add2" />
<input id="button3" type="button" value="3.GetPerson" />
<br />
<input id="button4" type="button" value="4.GetPersons" />
<input id="button5" type="button" value="5.SetPerson" />
<input id="button6" type="button" value="6.SetPersons" />
<br />
<b>JSON:</b><div id="json"></div>
<b>戻り値</b><div id="result"></div>

@section scripts {
<script type="text/javascript">
$(document).ready(function () {
    $("#button1").click(function () {
        (中略)
    });
    $("#button2").click(function () {
        (中略)
    });
    $("#button3").click(function () {
        (中略)
    });
    $("#button4").click(function () {
        $.ajax({
            type: "GET",
            url: '@VirtualPathUtility.ToAbsolute("~/Service1.svc/GetPersons")',
            data: {},
            success: function (data) {
                $("#json").text(JSON.stringify(data));
                $("#result").text("(" + data.d.length + ")" +
                                  data.d[0].name + " & " + data.d[0].age);
            },
            error: function (xhr) {
                alert("failed");
                alert(xhr.responseText);
                return;
            }
        });
    });
    $("#button5").click(function () {
        var jsonObj = { person: { name: "Tom-トム", age: 25 } };
        $.ajax({
            type: "POST",
            url: '@VirtualPathUtility.ToAbsolute("~/Service1.svc/SetPerson")',
            data: JSON.stringify(jsonObj),
            contentType: "application/json",
            success: function (data) {
                $("#json").text(JSON.stringify(data));
                $("#result").text(data.d);
            },
            error: function (xhr) {
                alert("failed");
                alert(xhr.responseText);
                return;
            }
        });
    });
    $("#button6").click(function () {
        var jsonObj = { persons: [{ name: "Tom-トム", age: 25 },
                                  { name: "John-ジョン", age: 36 }] };
        $.ajax({
            type: "POST",
            url: '@VirtualPathUtility.ToAbsolute("~/Service1.svc/SetPersons")',
            data: JSON.stringify(jsonObj),
            contentType: "application/json",
            success: function (data) {
                $("#json").text(JSON.stringify(data));
                $("#result").text(data.d);
            },
            error: function (xhr) {
                alert("failed");
                alert(xhr.responseText);
                return;
            }
        });
    });
});
</script>
}

「4.GetPersons(button4)」を押した時の実行結果
f:id:akmiyoshi:20131124173659p:plain

「5.SetPerson(button5)」を押した時の実行結果
f:id:akmiyoshi:20131124173945p:plain

「6.SetPersons(button6)」を押した時の実行結果
f:id:akmiyoshi:20131124174209p:plain