Windows Azure – 技術者でつなぐ日めくりカレンダー の 3/24 の記事です。Windows Azure Notification Hub (通知ハブ)を使って、Windows Store アプリへのプッシュ通知を試してみました。記事が長くなったので、2回に分けて投稿します。
まずは、プッシュ通知の仕組みを理解するために、通知ハブを使わずに実装してみました。こちらのMSDNが分かりやすかったです。今回は、通知ハブがテーマなので、アプリをWindows ストアと関連付けるなどの事前準備は完了しているものとします。
チャネルURIの登録
Windows Store アプリのチャネルURIを受け取る Web API を作ります。POST api/channel へリクエストすると、Azure Table Storage にチャネルURIを保持します。
#ChannelController.cs public class ChannelController : ApiController { private CloudTable table; public ChannelController() { var storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString")); var tableClient = storageAccount.CreateCloudTableClient(); this.table = tableClient.GetTableReference("Channel"); this.table.CreateIfNotExists(); } public async Task<IHttpActionResult> Post([FromBody]string uri) { var channel = new Channel { PartitionKey = "storeapp", RowKey = String.Format("{0:D19}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks), ChannelUri = uri }; await this.table.ExecuteAsync(TableOperation.Insert(channel)); return Ok(); } }
チャネルURIの送信
Windows Store アプリからチャネルURIを送信します。Appクラスの OnLaunched メソッドなどで、先ほど作った Web API にリクエストします。
#App.xaml.cs var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync(); var json = await JsonConvert.SerializeObjectAsync(channel.Uri); var res = await new HttpClient().PostAsync("http://localhost:12101/api/channel", new StringContent(json, Encoding.UTF8, "application/json")); res.EnsureSuccessStatusCode();
ライブタイルのプッシュ通知
Windows プッシュ通知サービス(WNS)に通知を送る Web APIを作ります。WNS に認証を受けて、Azure Table Storage から取得したチャネルURIに通知を送信する NotificationClient クラスを作りました。POST api/tile へリクエストすると、WNS にプッシュ通知します。
#TileController.cs public class TileController : ApiController { public async Task<IHttpActionResult> Post([FromBody]string xml) { await new NotificationClient().PushAsync("wns/tile", xml, new PushSettings()); return Ok(); } }
#NotificationClient.cs internal class NotificationClient { private const string SID = "ms-app://xxx"; private const string ClientSecret = "xxx"; private CloudTable table; public NotificationClient() { var storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString")); var tableClient = storageAccount.CreateCloudTableClient(); this.table = tableClient.GetTableReference("Channel"); this.table.CreateIfNotExists(); } public async Task PushAsync(string type, string xml, PushSettings settings) { // WNSに認証を受ける var oAuthToken = await this.GetOAuthTokenAsync(); // チャネルを取得 var query = new TableQuery<Channel>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "storeapp")); foreach (var channel in this.table.ExecuteQuery(query)) { try { // WNSに通知を送信 await this.PushToWNSAsync(oAuthToken, channel.ChannelUri, xml, type, settings); } catch (Exception ex) { Trace.TraceError(ex.Message); this.table.Execute(TableOperation.Delete(channel)); } } } private async Task<string> GetOAuthTokenAsync() { var content = new FormUrlEncodedContent(new Dictionary<string, string> { { "grant_type", "client_credentials" }, { "client_id", SID }, { "scope", "notify.windows.com" }, { "client_secret",ClientSecret }, }); var res = await new HttpClient().PostAsync("https://login.live.com/accesstoken.srf", content); res.EnsureSuccessStatusCode(); var json = await res.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<OAuthToken>(json).AccessToken; } private async Task PushToWNSAsync(string oAuthToken, string channelUri, string xml, string type, PushSettings settings) { var client = new HttpClient(); client.DefaultRequestHeaders.Add("X-WNS-Type", type); client.DefaultRequestHeaders.Add("Authorization", String.Format("Bearer {0}", oAuthToken)); if (String.IsNullOrEmpty(settings.CachePolicy) == false) { client.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", HttpUtility.UrlEncode(settings.CachePolicy)); } if (String.IsNullOrEmpty(settings.RequestForStatus) == false) { client.DefaultRequestHeaders.Add("X-WNS-RequestForStatus", HttpUtility.UrlEncode(settings.RequestForStatus)); } if (String.IsNullOrEmpty(settings.Tag) == false) { client.DefaultRequestHeaders.Add("X-WNS-Tag", HttpUtility.UrlEncode(settings.Tag)); } if (String.IsNullOrEmpty(settings.TTL) == false) { client.DefaultRequestHeaders.Add("X-WNS-TTL", HttpUtility.UrlEncode(settings.TTL)); } HttpContent httpContent = new StringContent(xml, Encoding.UTF8, "text/xml"); var res = await client.PostAsync(channelUri, httpContent); res.EnsureSuccessStatusCode(); } } [DataContract] internal class OAuthToken { [DataMember(Name = "access_token")] public string AccessToken { get; set; } [DataMember(Name = "token_type")] public string TokenType { get; set; } } public class Channel : TableEntity { public string ChannelUri { get; set; } } internal class PushSettings { public string CachePolicy { get; set; } public string RequestForStatus { get; set; } public string Tag { get; set; } public string TTL { get; set; } }
結果確認
タイルテンプレートの XML を api/tile へ POST すると、プッシュ通知が行われます。"wns/badge"や"wns/toast"を渡す Web API を作れば、バッチやトーストにも対応できます。PushSettingsクラスでは、要求ヘッダーで通知キューや有効期限などの設定を行うことも可能です。
まとめ
チャネルURIの管理やWNSへの送信など、バックエンドロジックの実装が地味に大変です。後編では、通知ハブを使って実装してみます。