Tag Archives: Windows Store App

Windows Azure Notification Hub を使ってプッシュ通知する(後編)

前回の投稿では、Windows Azure Notification Hub(通知ハブ)を使わずにプッシュ通知したので、今回は通知ハブを使って実装してみました。通知ハブについては、こちらのMSDNが分かりやすかったです。

Windows Azure Notification Hub の作成

Azure ポータルから、通知ハブを作成します。ここでは、「goonerhub」という名前で作成しました。後ほど必要になるので、接続情報をメモしておきます。

  • DefaultListenSharedAccessSignature
  • DefaultFullSharedAccessSignature

チャネルURIの送信

Windows Store アプリからチャネルURIを送信します。通知ハブ用のライブラリを Nuget からインストールしておきます。

  • Install-Package WindowsAzure.Messaging.Managed

Appクラスの OnLaunched メソッドなどで、通知ハブに送信します。DefaultListenSharedAccessSignatureを設定して、NotificationHub クラスをインスタンス化します。

var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
var hub = new NotificationHub("goonerhub", "DefaultListenSharedAccessSignature");
var result = await hub.RegisterNativeAsync(channel.Uri);

ライブタイルのプッシュ通知

通知ハブに送信する Web API を作ります。POST api/hub へリクエストすると、通知ハブに送信します。通知ハブ用のライブラリを
Nuget からインストールしておきます。

  • Install-Package WindowsAzure.ServiceBus

DefaultFullSharedAccessSignatureを設定して、NotificationHubClient クラスをインスタンス化します。

public class HubController : ApiController
{
    public async Task<IHttpActionResult> Post([FromBody]string payload)
    {
        var client = NotificationHubClient.CreateClientFromConnectionString("DefaultFullSharedAccessSignature", "goonerhub");
        await client.SendWindowsNativeNotificationAsync(payload);
        return Ok();
    }
}

結果確認

タイルテンプレートの XML を api/hub へ POST すると、プッシュ通知が行われます。

まとめ

通知ハブを使うと、バックエンドロジックの実装コストを大幅に削減できます。iOS や Android など対応デバイスが増えると、そのメリットはさらに大きくなります。また、数百台というデバイスにプッシュ通知する場合でも、通知ハブ側でスケーリングしてくれることも、大きなメリットです。気になったのは、WNS への要求ヘッダーで指定できた通知キューや有効期限などの設定が難しそうなことぐらいです。モバイルアプリを作るうえで、プッシュ通知は上手く活用したい技術なので、通知ハブは便利なサービスです。

まだプレビュー版ですが、ASP.NET Web API に対応した Windows Azure Mobile Services を利用すると、さらに便利に使えそうなので、時間を見つけて確認してみたいと思います。

おまけ

こちらにも記載されていますが、Windows 8.1 から大きなタイルが増えたので、タイルテンプレートの XML が Version 2 に更新されています。Windows 8 に新しいタイルテンプレートを通知してよいものか少し心配でしたが、fallback 属性があるので、8.0と8.1のチャネルURIを区別する必要はありません。実際に試してみましたが、それぞれのOSでライブタイルが表示されました。

Windows Azure Notification Hub を使ってプッシュ通知する(前編)

Windows Azure – 技術者でつなぐ日めくりカレンダー の 3/24 の記事です。Windows Azure Notification Hub (通知ハブ)を使って、Windows Store アプリへのプッシュ通知を試してみました。記事が長くなったので、2回に分けて投稿します。

Windows Azure Notification Hub を使ったプッシュ通知(後編)

まずは、プッシュ通知の仕組みを理解するために、通知ハブを使わずに実装してみました。こちらのMSDNが分かりやすかったです。今回は、通知ハブがテーマなので、アプリをWindows ストアと関連付けるなどの事前準備は完了しているものとします。

チャネルURIの登録

Windows Store アプリのチャネルURIを受け取る Web API を作ります。POST api/channel  へリクエストすると、Azure Table Storage にチャネルURIを保持します。

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 にリクエストします。

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 にプッシュ通知します。

public class TileController : ApiController
{
    public async Task<IHttpActionResult> Post([FromBody]string xml)
    {
        await new NotificationClient().PushAsync("wns/tile", xml, new PushSettings());
        return Ok();
    }
}
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への送信など、バックエンドロジックの実装が地味に大変です。後編では、通知ハブを使って実装してみます。

Windowsストアアプリの未処理例外を捕捉する

Windows 8 では、ストアアプリで処理されなかった例外を UnhandledException イベントで捕捉できませんでした。予期せぬ例外が発生すると、ストアアプリがストンと落ちてしまうアレです。Silverlight のときは、非同期処理の中で発生した例外であっても、UnhandledException イベントで捕捉できていたので困っていましたが、Windows 8.1 から改善され、async/await に対応しました。

さらに、C++などのアンマネージコードで発生した例外を捕捉できる API も追加されています。

CoreApplication.UnhandledErrorDetectedイベント

Build 2013 のセッションでも、紹介されています。(スライドの22ページ)

Making your Windows Store Apps More Reliable

新たに追加された UnhandledErrorDetected イベントを使うことで、非同期処理(async / await)の中で発生した例外も捕捉できます。

public App()
{
    this.InitializeComponent();
    this.Suspending += OnSuspending;
    CoreApplication.UnhandledErrorDetected += CoreApplication_UnhandledErrorDetected;
}

private async void CoreApplication_UnhandledErrorDetected(object sender, UnhandledErrorDetectedEventArgs e)
{
    var message = "";

    try
    {
        e.UnhandledError.Propagate();
    }
    catch (Exception ex)
    {
        message = ex.Message;
    }
    await new MessageDialog(message, "エラーメッセージ").ShowAsync();
}

今さら感のある内容ですが、アプリを作る上で例外管理は重要なので、地味に嬉しかった Windows 8.1 での改善でした。