前回までの内容
Google App Engine(以下 GAE) + Silverlight でアプリを作ってやろうという本連載。前回でひとまず、GAE/Python でのサービス開発は終了。なんちゃって RESTful な Web API に仕上がりました。
今回から、そのサービスを呼び出す Silverlight クライアントを実装していきます。ここから長いです。
Silverlight プロジェクトを作成
まず Silverlight プロジェクトを作成します。名前は SilverTask。
プロジェクトフォルダは GAE/Python で作成した silvertask-web フォルダと同じディレクトリに配置します。
下図のように、SilverTask ソリューションフォルダ内に、Silverlight プロジェクトフォルダと、GAE/Python プロジェクトフォルダを配置する形になります。
モデルを作成
GAE/Python のモデルから、C# のクラスを生成します。以前作った Kay Framework の管理スクリプトの出番です。
スクリプトの使い方は上の記事を読んでもらうとして、生成したコードがこちら。
namespace Core.Models { [System.Runtime.Serialization.DataContract] public partial class Task { public Task() { OnCreated(); } [System.Runtime.Serialization.DataMember(Name="key")] public string Key { get;set; } [System.Runtime.Serialization.DataMember(Name="created")] public System.DateTime Created { get;set; } [System.Runtime.Serialization.DataMember(Name="done")] public bool Done { get;set; } [System.Runtime.Serialization.DataMember(Name="name")] public string Name { get;set; } [System.Runtime.Serialization.DataMember(Name="user")] public string User { get;set; } partial void OnCreated(); } public static partial class TaskMeta { public static string Key { get { return "key"; } } public static string Created { get { return "created"; } } public static string Done { get { return "done"; } } public static string Name { get { return "name"; } } public static string User { get { return "user"; } } } }
メタクラスは Web API を叩いて取得した JSON からデータを取り出すときに使います。DataContractJsonSerializer を使うかもしれないので、データコントラクトにしています。使わないかもしれないけど。
JSON からモデルにデータを詰めるメソッドや、モデルを JSON にするメソッドを追加します。これらは Web API を呼び出すクラスで利用します。
using System; using System.Json; namespace Core.Models { /// <summary> /// タスクを表します。 /// </summary> public partial class Task { partial void OnCreated() { Created = DateTime.Now; } /// <summary> /// <see cref="Task"/> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="json">初期化に使う JSON オブジェクト。</param> public Task(JsonValue json) : this() { this.Key = json[TaskMeta.Key]; this.User = json[TaskMeta.User]; this.Name = json[TaskMeta.Name]; this.Done = json[TaskMeta.Done]; DateTime created; if (DateTime.TryParse(json[TaskMeta.Done].ToString(), out created)) { this.Created = created; } } /// <summary> /// 現在のインスタンスのクローンを作成します。 /// </summary> /// <returns>現在のインスタンスのクローン。</returns> public Task Clone() { return new Task() { Key = this.Key, Name = this.Name, Done = this.Done, Created = this.Created, User = this.User, }; } /// <summary> /// JSON 文字列に変換します。 /// </summary> /// <returns>JSON 文字列。</returns> public string ToJsonString() { var obj = new JsonObject(DateTimeOffset.Now); obj.Add(TaskMeta.Name, (JsonValue)Name); obj.Add(TaskMeta.Done, (JsonValue)Done); return obj.ToString(); } } }
JsonValue は暗黙的な型変換を提供しているので、C# の基本的なクラスだったら、たいてい、そのまま代入できます。コンバートする必要なし。
Web API を呼び出すクラスを作成
GAE/Python で実装した Web API を、タスクが保存されているリポジトリとみなします。リポジトリパターンってやつです。
テストがしやすくなるように、インタフェースと実装を分けます。こうしておけば、ダミーの実装を使ってビューやビューモデルのテストができるので。
インタフェースの定義がこちら。
using System; using Core.Models; namespace SilverTask.Models { public interface ITaskRepository { /// <summary> /// タスクを取得します。 /// </summary> /// <param name="completed">完了済みを取得するとき true。未完了を取得するとき false。</param> /// <param name="callback">コールバック。</param> void ReadTask(bool completed, Action<TaskResult> callback); /// <summary> /// タスクを作成します。 /// </summary> /// <param name="task">作成するタスク。</param> /// <param name="callback">コールバック。</param> void CreateTask(Task task, Action<TaskResult> callback); /// <summary> /// タスクを削除します。 /// </summary> /// <param name="tasks">削除するタスク。</param> /// <param name="callback">コールバック。</param> void DeleteTask(Task tasks, Action<TaskResult> callback); /// <summary> /// タスクを更新します。 /// </summary> /// <param name="tasks">更新するタスク。</param> /// <param name="callback">コールバック。</param> void UpdateTask(Task tasks, Action<TaskResult> callback); } /// <summary> /// リポジトリ操作の結果を格納します。 /// </summary> public class TaskResult { /// <summary> /// タスクの一覧を取得します。 /// </summary> public Task[] Tasks { get; private set; } /// <summary> /// キャンセルされたかどうか示す値を取得します。 /// </summary> public bool Cancelled { get; private set; } /// <summary> /// 発生したエラーを取得します。 /// </summary> public Exception Error { get; private set; } /// <summary> /// <see cref="TaskResult"/> クラスの新しいインスタンスを初期化します。 /// </summary> /// <param name="tasks">タスク。</param> /// <param name="cancelled">キャンセルされたかどうか。</param> /// <param name="error">発生したエラー。</param> public TaskResult(Task[] tasks, bool cancelled = false, Exception error = null) { this.Tasks = tasks; this.Cancelled = cancelled; this.Error = error; } } }
Silverlight の非同期サービス呼び出しは結果をイベントで受け取るのがほとんどですけど、今回はコールバックで受け取るようにしています。個人的に、コールバックの方が使いやすいので。
そして実装。
using System; using System.Json; using System.Linq; using System.Net; using System.Text; using System.Windows.Browser; using Core.Models; namespace SilverTask.Models { public partial class TaskRepository : ITaskRepository { /// <summary> /// サービスのベースアドレス。 /// </summary> private static readonly string _BaseAddress; static TaskRepository() { _BaseAddress = HtmlPage.Document.DocumentUri.ToString(); } void ITaskRepository.ReadTask(bool completed, Action<TaskResult> callback) { string query = _BaseAddress + string.Format( "tasks?{0}={1}", TaskMeta.Done, completed); var client = new WebClient(); client.DownloadStringCompleted += (sender, e) => { if (null != e.Error) { callback(new TaskResult(new Task[0], e.Cancelled, e.Error)); } else { var tasks = from obj in (JsonArray)JsonArray.Parse(e.Result) select new Task(obj); callback(new TaskResult(tasks.ToArray())); } }; client.DownloadStringAsync(new Uri(query)); } void ITaskRepository.CreateTask(Task task, Action<TaskResult> callback) { string query = _BaseAddress + "tasks"; string parameters = string.Format( "{0}={1}", TaskMeta.Name, HttpUtility.UrlEncode(task.Name)); var client = new WebClient(); client.UploadStringCompleted += (sender, e) => { if (null != e.Error) { callback(new TaskResult(new Task[0], e.Cancelled, e.Error)); } else { var result = new Task(JsonObject.Parse(e.Result)); callback(new TaskResult(new Task[] { result })); } }; client.UploadStringAsync(new Uri(query), parameters); } void ITaskRepository.DeleteTask(Task task, Action<TaskResult> callback) { string uri = _BaseAddress + "tasks/" + task.Key; var request = WebRequest.CreateHttp(uri); request.Method = "DELETE"; request.ContentType = "application/json"; request.BeginGetResponse(result => { var response = (HttpWebResponse)request.EndGetResponse(result); if (response.StatusCode != HttpStatusCode.OK) { var error = new WebException(response.StatusDescription); callback(new TaskResult(new Task[0], error: error)); } else { callback(new TaskResult(new Task[] { task.Clone() })); } }, null); } void ITaskRepository.UpdateTask(Task task, Action<TaskResult> callback) { string uri = _BaseAddress + "tasks/" + task.Key; byte[] bytes = Encoding.UTF8.GetBytes(task.ToJsonString()); var request = WebRequest.CreateHttp(uri); request.Method = "PUT"; request.ContentType = "application/json"; request.ContentLength = bytes.Length; request.BeginGetRequestStream(result => { using (var stream = request.EndGetRequestStream(result)) { stream.Write(bytes, 0, bytes.Length); } request.BeginGetResponse(result2 => { var response = (HttpWebResponse)request.EndGetResponse(result2); if (response.StatusCode != HttpStatusCode.OK) { var error = new WebException(response.StatusDescription); callback(new TaskResult(new Task[0], error: error)); } else { callback(new TaskResult(new Task[] { task.Clone() })); } }, null); }, null); } } }
WebClient には PUT や DELETE に対応するメソッドが無いので、更新と削除は HttpWebRequest を使用しています。WebCliet に用意してくれればいいのに。
今回はここまで
変更内容の詳細は下記URLで確認できます。
今回のコードは、WebRequest や WebClient を使って RESTful なサービスの API を呼び出すサンプルにするといいです。PUT や DELETE を使った例はほどんど見かけないから、意外と貴重かも。
次は ViewModel を実装する予定です。
関連記事
- Google App Engine + Silverlight でタスク管理アプリケーション開発(1) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(2) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(4) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(5) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(6) - present