Condividi tramite


Alive with activity、パート 3: プッシュ通知と Windows Azure モバイル サービス

このシリーズのパート 1 では、"aliveness (常に動きがあること)" はユーザーにとって何を意味するかと、そのエクスペリエンスを生むうえでアプリがどのような働きをするかについて説明しました。パート 2 では、ライブ タイルの定期的な更新をサポートするために、Web サービスを作成し、デバッグする方法を見てきました。このパート 3 では、Windows プッシュ通知サービス (WNS) を利用してタイル更新、トースト、および直接通知を特定のクライアント デバイスにオンデマンドで提供する方法と、Windows Azure モバイル サービスを利用することでプロセス全体を簡略化する方法を学んで、シリーズを締めくくりましょう。

プッシュ通知

パート 2 で見たように、定期的な更新はクライアント側から開始され、ポーリングまたは "プル" 手法でタイルまたはバッジの更新が実行されます。"プッシュ" 通知は、サービスがデバイスに更新を直接送信するときに使用され、その更新はユーザー、アプリ、さらにはセカンダリ タイルに対して固有のものにすることができます。

ポーリングと異なり、プッシュ通知はどのようなときであっても非常に高い頻度で実行できます。ただし、デバイスがバッテリ電源を使っているか、コネクト スタンバイ モードであるか、通知トラフィックが過剰になった場合、Windows によってプッシュ通知のトラフィックの量が調整されます。つまり、すべての通知が配信されるとは限りません (特に、デバイスの電源が切られている場合)。

したがって、プッシュ通知を使った時計のタイルや、同じ頻度または間隔で更新されるその他のタイル ガジェットを実装できるとは考えないでください。タイルや通知を、ユーザーに提供すべき興味深く意味のある情報が保持されているバックエンド サービスと連携して、ユーザーが再びアプリを使いたくなるように誘導するためには、プッシュ通知をどのように使用すればいいかについて考えましょう。

以下で詳しく見ていきましょう。プッシュ通知には 2 種類あります。

  1. タイル更新、バッジ更新、またはトースト通知のペイロードを保持している XML 更新: Windows はこのようなプッシュ通知を処理して、アプリに代わって、更新やトーストを発行できます。必要に応じて、アプリが直接このような通知を処理することもできます。
  2. サービスから送信する任意のデータを保持しているバイナリまたは "直接" 通知: これらの通知はアプリ固有のコードによって処理される必要があります。そうしないと、Windows はデータをどのように処理すべきかわかりません。サイズ制限 (5 KB) やエンコード (base64) など、詳細については「直接通知のガイドラインとチェック リスト」を参照してください。

どちらのケースも、実行中 (フォアグラウンド) のアプリから、PushNotificationChannel クラスとその PushNotificationReceived イベントを利用して直接プッシュ通知を処理できます。XML ペイロードの場合、実行中のアプリはコンテンツやタグの変更などをしてから、ローカルでペイロードを発行できます (または、無視することもできます)。直接通知については、通知がある場合、アプリはコンテンツを処理してから、発行する通知を決定します。

アプリが中断状態であるか実行されていない場合でも、バックグラウンド タスクを提供して同様に処理できます。通知が受信されたら、このようなアプリ固有のコードが実行できるように、アプリからロック画面へのアクセスを要求して、このアクセスが付与される必要があります。

バックグラウンド タスクは、通常、通知の受信時に 1 種類または 2 種類の作業を行います。たとえば、アプリが次にアクティブになるか再開された時点で関連情報を取得できるように、バックグラウンド タスクは通知に保持されている関連情報をローカルのアプリ データに保存する場合があります。また、バックグラウンド タスクは、ローカルのタイル更新やバッジ更新、トースト通知を発行できます。

直接通知についての理解を深めるため、典型的な電子メール アプリを例に考えてみましょう。アプリのバックエンド サービスによってユーザー宛ての新着メッセージが検出されると、電子メール メッセージのヘッダー数を保持している WNS に直接通知が送られます。これは、ユーザーの特定のデバイスの特定のアプリに関連付けられている "チャネル URI" を使って処理されます。

これを受けて WNS は、この通知をクライアントにプッシュします。プッシュが成功すると、Windows がこの通知を取得し、問題のチャネル URI に関連付けられているアプリを検索します。該当するアプリが見つからない場合、通知は無視されます。アプリが存在していて、実行されていない場合、Windows は PushNotificationReceived イベントを発行するか、そのアプリ用に用意されているバックグラウンド タスクを検索してこれを起動します。

どちらの場合も、直接通知は最終的に何かしらのアプリのコードに渡され、そのコードがデータを処理し、バッジ更新をアプリのタイルに発行して新着メッセージの数を示し、メッセージ ヘッダー情報を含むタイルの更新を最大 5 件まで順番に表示します。また、アプリは、受信された新着メッセージごとにトースト通知を発行するか、少なくとも新着メールがあることを示すトースト通知を発行できます。

結果として、トーストによってユーザーに新しい電子メールが到着したことを通知し、スタート画面上のアプリのタイルを利用して、新着メールの状況を直ちに伝えるビューを提供できます。

これらのクライアント側のイベント ハンドラーとバックグラウンド タスクの詳細については、「直接通知のサンプル」(英語)、このブログの記事「バックグラウンドでの生産性を上げる - バックグラウンド タスク」、およびホワイトペーパー「バックグラウンド ネットワーク」(英語) を参照してください。ここでは次に、この電子メールのシナリオでのサービス側の処理について見ていきましょう。

Windows プッシュ通知サービス (WNS) の操作

Windows、アプリ、サービス、WNS の連携によって、特定のユーザーの特定のデバイスの特定のアプリのタイル (またはトーストや直接通知ハンドラー) にユーザー固有のデータを表示することが可能になります。以下の図は、これらのすべての要素間の関係を表しています。

 

特定のアプリのタイルへのデータ表示を実現する Windows、アプリ、サービス、WNS 間の連携処理を示すフローチャート

 

この処理全体が協調して機能するには、次のような連携が必要です。

  1. 開発者が Windows ストアにアプリを登録して、プッシュ通知を使用できるようにします。登録すると、サービスが WNS による認証に使用できる SID とクライアント シークレットが提供されます (SID とクライアント シークレットは、セキュリティのため、クライアント デバイスには決して保存しないでください)。
  2. 実行時にアプリは、アプリ自身の各ライブ タイル (プライマリ タイルとセカンダリ タイル) 用、または直接通知用の WNS チャネル URI を Windows に要求します。アプリは、これらのチャネル URI を 30 日ごとに更新する必要もあります。この更新は、別のバックグラウンド タスクを使って行うことができます。
  3. アプリのサービスは、アプリがこれらのチャネル URI とその用法を記述するデータ (気象情報を取得する対象の地域や特定のユーザー アカウントとアクティビティなど) をアップロードできる URI を提供します。サービスは、チャネル URI と関連データを受信すると、後で使用するために保存します。
  4. サービスは、特定のユーザー、デバイス、アプリ、タイルの各組み合わせに該当する変更がないか、バックエンドを監視します。サービスは特定のチャネルに対する通知を発行する条件を検出すると、その通知 (XML または直接通知) のコンテンツを作成し、SID とクライアント シークレットを使って WNS に対して認証を行い、通知をチャネル URI と併せて WNS に送信します。

それでは、各ステップを詳しく見ていきましょう (単なるプレビューに HTTP 要求を使うことに抵抗を感じる場合は、以下で説明するように、Windows Azure モバイル サービスを利用することで、細かな処理の多くを省略できます)。

Windows ストアへのアプリの登録

サービスの SID とクライアント シークレットの取得については、Windows デベロッパー センターで「Windows プッシュ通知サービス (WNS) に対して認証する方法」を参照してください。SID は WNS に対してアプリを識別するもので、クライアント シークレットは WNS からアプリに通知を送信できることをサービスが WNS に伝えるために使用します。この場合も、SID とクライアント シークレットは、サービスにのみ保存されるようにします。

Windows プッシュ通知サービス (WNS) に対して認証する方法」のステップ 4「クラウド サーバーの資格情報を WNS に送信する」は、サービスからプッシュ通知を送る場合にのみ実行する操作であることに注意してください。現時点では通知の送信に必要な重要な要素、つまりチャネル URI がサービスにないため、この点については後で説明します。

チャネル URI の取得と更新

クライアント アプリは実行時に Windows.Networking.PushNotifications.PushNotificationChannelManager オブジェクトを介してチャネル URI を取得します。このオブジェクトには次の 2 つのメソッドしかありません。

  • createPushNotificationChannelForApplicationAsync: アプリのプライマリ タイルと、トーストおよび直接通知用のチャネル URI を作成します。
  • createPushNotificationChannelForSecondaryTileAsync: tileId 引数によって指定された特定のセカンダリ タイルのチャネル URI を作成します。

どちらの非同期操作でも、結果として PushNotificationChannel オブジェクトが生成されます。このオブジェクトには、チャネル URI が設定された Uri プロパティと、そのチャネルの更新期限を示す ExpirationTime が保持されています。Close メソッドは必要に応じて明示的にチャネルを終了します。最も重要なのは PushNotificationReceived イベントで、やはり、アプリがフォアグラウンドで実行されていて、このチャネルを介してプッシュ通知が届いた場合に生成されます。

チャネル URI の有効期間は 30 日で、有効期間が過ぎると WNS はそのチャネルに対するあらゆる要求を拒否します。したがって、アプリのコードは、少なくとも 30 日おきに 1 回、上記の create メソッドを使って URI を更新し、更新した URI をサービスに送信する必要があります。この場合、以下の手法が有効です。

  • 最初の起動時に、チャネル URI を要求して、その文字列をローカル アプリ データの Uri プロパティに保存します。チャネル URI はデバイス固有のため、ローミング アプリ データには保存しないでください。
  • 2 回目以降の起動時には、チャネル URI を再度要求し、その URI と前回保存した URI とを比較します。一致しない場合は、新しい URI をサービスに送信するか、送信して、必要に応じてサービスによって古い URI と置き換えます。
  • アプリが 30 日以上中断状態になることも考えられるため、アプリの Resuming ハンドラーでも上記のステップを実行します (ドキュメントの「起動、再開、マルチタスク」を参照)。
  • アプリが 30 日以内に実行されない可能性を考慮するなら、数日または 1 週間おきに実行されるメンテナンス トリガーを持つバックグラウンド タスクを実装します。詳細については、「バックグラウンドでの生産性を上げる - バックグラウンド タスク」を参照してください。この場合のバックグラウンド タスクでは、アプリがチャネルを要求し、チャネルをサービスに送信するコードと同じコードを実行します。

サービスへのチャネル URI の送信

通常、プッシュ通知のチャネルでは、電子メールの状態、インスタント メッセージ、その他の個人情報など、ユーザー固有の更新情報を扱います。サービスが同じ通知をすべてのユーザーやすべてのタイルにブロードキャストしなければならない状況は考えられません。このため、サービスでは各チャネル URI をより詳細な情報に関連付ける必要があります。電子メール アプリの場合、メールを確認するアカウントを示すため、ユーザーの ID が最も重要です。一方、天気情報アプリでは、各チャネル URI を特定の緯度と経度に関連付け、各タイル (プライマリ タイルとセカンダリ タイル) に特定の地域の情報を表示することが考えられます。

そこで、アプリはチャネル URI をサービスに送信するときに、このような詳細も併せて送信し、サービスはこの情報を後で使用できるように保存する必要があります。

ユーザーの本人確認が重要な場合、ベスト プラクティスは、サービス固有の資格情報または Facebook、Twitter、Google、ユーザーの Microsoft Account などの OAuth プロバイダーを使って、アプリとは別にユーザーをサービスに対して認証することです (後で説明しますが、OAuth を使うと Windows Azure モバイル サービスを利用する際に便利です)。何かしらの理由でこれが無理な場合は、必ずサービスに送信するユーザー ID を暗号化するか、HTTPS を介して送信します。

どの場合でも、これらの情報すべてをサービスに送信する手段 (ヘッダーに含める、メッセージ本体のデータに含める、またはサービス URI のパラメーターとして渡す) は、開発者が決定します。この部分の通信は、アプリとそのサービス間でしか発生しません。

簡単な例として、receiveuri.aspx という名前のページがあるサービスがあり (これは次のセクションで説明します)、url という変数にこのページの完全なアドレスが保持されているとします。以下のコードでは、Windows にアプリのプライマリ チャネル URI を要求し、HTTP を介してそのページにこの URI を送信しています (このコードは、「プッシュおよび定期通知のクライアント側のサンプル」(英語) を基に簡略化したものです。このコードでは、どこかで定義されている itemId 変数を使って、セカンダリ タイルを識別しています。また、ここでは紹介しませんが、このサンプルには C++ のバージョンもあります)。

JavaScript:

 

 Windows.Networking.PushNotifications.PushNotificationChannelManager
    .createPushNotificationChannelForApplicationAsync()
    .done(function (channel) {
        //Typically save the channel URI to appdata here.
        WinJS.xhr({ type: "POST", url:url,
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            data: "channelUri=" + encodeURIComponent(channel.uri) 
                + "&itemId=" + encodeURIComponent(itemId)
        }).done(function (request) {
            //Typically update the channel URI in app data here.
        }, function (e) {
            //Error handler
        });
    });

           

C#:

 

 using Windows.Networking.PushNotifications;

PushNotificationChannel channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(url);
webRequest.Method = "POST";
webRequest.ContentType = "application/x-www-form-urlencoded";
byte[] channelUriInBytes = Encoding.UTF8.GetBytes("ChannelUri=" + WebUtility.UrlEncode(newChannel.Uri) + "&ItemId=" + WebUtility.UrlEncode(itemId));

Task<Stream> requestTask = webRequest.GetRequestStreamAsync();
using (Stream requestStream = requestTask.Result)
{
    requestStream.Write(channelUriInBytes, 0, channelUriInBytes.Length);
}

次の ASP.NET コードは、receiveuri.aspx ページの基本の実装です。このコードでは、この HTTP POST を処理し、有効なチャネル URI、ユーザー、およびアイテムの ID が渡されていることを確認します。

このコードは "基本" 的なコードであることに注意してください。見てわかるとおり、SaveChannel 関数は単純に要求のコンテンツを特定のテキスト ファイルに書き込んでいるだけであるため、1 ユーザーにしか対応できません。もちろん、実際のサービスでは、データベースを採用して複数のユーザーに対応できるようにしますが、構造は以下のコードと似ています。

 <%@ Page Language="C#" AutoEventWireup="true" %>

<script runat="server">

protected void Page_Load(object sender, EventArgs e)
{
    //Output page header
    Response.Write("<!DOCTYPE html>\n<head>\n<title>Register Channel URI</title>\n</head>\n<html>\n<body>");
    
    //If called with HTTP GET (as from a browser), just show a message.
    if (Request.HttpMethod == "GET")
    {
        Response.StatusCode = 400;
        Response.Write("<p>This page is set up to receive channel URIs from a push notification client app.</p>");
        Response.Write("</body></html>");
        return;
    }

    if (Request.HttpMethod != "POST") {
        Response.StatusCode = 400;
        Response.Status = "400 This page only accepts POSTs.";
        Response.Write("<p>This page only accepts POSTs.</p>");
        Response.Write("</body></html>");        
        return;
    }
    
    //Otherwise assume a POST and check for parameters    
    try
    {
        //channelUri and itemId are the values posted from the Push and Periodic Notifications Sample in the Windows 8 SDK
        if (Request.Params["channelUri"] != null && Request.Params["itemId"] != null)
        {
            // Obtain the values, along with the user string
            string uri = Request.Params["channelUri"];
            string itemId = Request.Params["itemId"];
            string user = Request.Params["LOGON_USER"];
                 
            //TODO: validate the parameters and return 400 if not.
            
            //Output in response
            Response.Write("<p>Saved channel data:</p><p>channelUri = " + uri + "<br/>" + "itemId = " + itemId + "user = " + user + "</p>");

            //The service should save the URI and itemId here, along with any other unique data from the app such as the user;
            SaveChannel(uri, itemId, user);

            Response.Write("</body></html>");
        }
    }
    catch (Exception ex)
    {
        Trace.Write(ex.Message);
        Response.StatusCode = 500;
        Response.StatusDescription = ex.Message; 
        Response.End();
    }
}
</script>

protected void SaveChannel(String uri, String itemId, String user)
{
    //Typically this would be saved to a database of some kind; to keep this demonstration very simple, we'll just use
    //the complete hack of writing the data to a file, paying no heed to overwriting previous data.
    
    //If running in the debugger on localhost, this will save to the project folder
    string saveLocation = Server.MapPath(".") + "\\" + "channeldata_aspx.txt";
    string data = uri + "~" + itemId + "~" + user;

    System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
    byte[] dataBytes = encoding.GetBytes(data);

    using (System.IO.FileStream fs = new System.IO.FileStream(saveLocation, System.IO.FileMode.Create))
    {
        fs.Write(dataBytes, 0, data.Length);
    }

    return;
}

このサービスのコードは、著者の無料の電子ブック『HTML、CSS、JavaScript を使った Windows 8 アプリ開発』(英語) の第 13 章にあります。このブックに付属のコンテンツの HelloTiles サンプルを参照してください。このコードは、前出のクライアント側の SDK サンプルと連携するように設計されています。localhost が有効なデバッガー (Visual Studio 2012 Ultimate または Visual Studio Express 2012 for Web) で HelloTiles を実行すると、"https://localhost:52568/HelloTiles/receiveuri.aspx" のような URL が生成され、これをクライアント側 SDK サンプルに貼り付けることができます。その後、サンプルからその URL に要求を送ると、サービス内のブレークポイントで停止するため、コードのステップ実行を行えます。

通知の送信

実際のサービスでは、データ ソースを監視し、適宜プッシュ通知を特定のユーザーに送信する、何かしらの常時実行プロセスを用意します。これにはさまざまな用途が考えられます。

  • スケジュールされたジョブを使って、おおよそ 15 ~ 30 分間隔 (気象警報が発表される頻度によります) で特定の地域の気象警報の有無を確認し、それに応じてプッシュ通知を発行できます。
  • 新しいメッセージが到着したら、メッセージング バックエンドなど、別のサービスによって、サーバーのページに要求を送ることができます。ページは要求を受け取ったら、適切な通知を発行できます。
  • ユーザーがサーバーの Web ページを操作している場合、ユーザーのアクティビティをトリガーにして、プッシュ通知を特定のチャネルに送信できます。

つまり、バックエンド サービスや Web サイトの性質によっては、プッシュ通知のトリガーにできる要素は多数あります。この記事では、前述の『HTML、CSS、JavaScript を使った Windows 8 アプリ開発』(英語) の第 13 章の HelloTiles サンプルにある sendBadgeToWNS.aspx というページを使用して、ブラウザーからこのページにアクセスするたびに、以前テキスト ファイルに保存したチャネル URI を使ってプッシュ通知を送信します。実際のサービスでは、ファイルの内容を読み取るのではなく、何かしらのデータベース参照を実行してチャネル URI を取得しますが、やはり全体的な構造は非常に似ています。

ASP.NET ページ:

 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="sendBadgeToWNS.aspx.cs" Inherits="sendBadgeToWNS" %>

<!DOCTYPE html>

<html xmlns="https://www.w3.org/1999/xhtml">
<head runat="server">    
    <title>Send WNS Update</title>
</head>
<body>
    <p>Sending badge update to WNS</p>    
</body>
</html>

 

C# の分離コード:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Net;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;

public partial class sendBadgeToWNS : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        //Load our data that was previously saved. A real service would do a database lookup here
        //with user- or tile-specific criteria.
        string loadLocation = Server.MapPath(".") + "\\" + "channeldata_aspx.txt
        byte[] dataBytes;
        
        using (System.IO.FileStream fs = new System.IO.FileStream(loadLocation, System.IO.FileMode.Open))
        {
            dataBytes = new byte[fs.Length];
            fs.Read(dataBytes, 0, dataBytes.Length);
        }

        if (dataBytes.Length == 0)
        {
            return;
        }
        
        System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();

        string data = encoding.GetString(dataBytes);
        string[] values = data.Split(new Char[] { '~' });
        string uri = values[0]; //Channel URI
        string secret = "9ttsZT0JgHAFveYahK1B6jQbvMOIWYbm";
        string sid = "ms-app://s-1-15-2-2676450768-845737348-110814325-22306146-1119600341-293560589-2707026538";
        
        //Create some simple XML for a badge update
        string xml = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>";
        xml += "<badge value='alert'/>";
                    
        PostToWns(secret, sid, uri, xml, "wns/badge");
    }
}

 

ここでは、チャネル URI を取得して XML ペイロードを作成し、WNS との認証を行い、取得したチャネル URI に対して HTTP POST を実行しています。最後の 2 ステップには、Windows デベロッパー センターの「クイック スタート: プッシュ通知の送信」の PostToWns 関数を使用しています。この関数のコードの大半は、Windows ストアから取得したクライアント シークレットと SID を使って、OAuth (https://login.live.com/accesstoken.srf) から WNS との認証を行っているだけであるため、この記事ではこの関数全体を引用することはしませんでした。この認証の結果、アクセス トークンが生成されます。このトークンをチャネル URI に送信する HTTP POST に含めます。

C#:

 public string PostToWns(string secret, string sid, string uri, string xml, string type = "wns/badge")
{
    try
    {
        // You should cache this access token
        var accessToken = GetAccessToken(secret, sid);

        byte[] contentInBytes = Encoding.UTF8.GetBytes(xml);

        // uri is the channel URI
        HttpWebRequest request = HttpWebRequest.Create(uri) as HttpWebRequest;
        request.Method = "POST";
        request.Headers.Add("X-WNS-Type", type);
        request.Headers.Add("Authorization", String.Format("Bearer {0}", accessToken.AccessToken));

        using (Stream requestStream = request.GetRequestStream())
            requestStream.Write(contentInBytes, 0, contentInBytes.Length);

        using (HttpWebResponse webResponse = (HttpWebResponse)request.GetResponse())
            return webResponse.StatusCode.ToString();
    }
    catch (WebException webException)
    {
        // Implements a maximum retry policy (omitted)
    }
}

この例では、HTTP 要求の X-WNS-Type ヘッダーが wns/badge に設定され、Content-Type ヘッダーが既定で wns/xml に設定されていることに注意してください。タイルであれば種類を示す値は wns/tile、トーストであれば wns/toast です。直接通知の場合は wns/raw を使用し、Content-Type を application/octet-stream に設定します。ヘッダーの完全な詳細については、Windows ストア アプリのドキュメントの「プッシュ通知サービスの要求ヘッダーと応答ヘッダー」を参照してください。

プッシュ通知のエラー

もちろん、このような HTTP 要求の送信は、常に成功するとは限りません。WNS から 200 コード (成功) 以外のコードが返される原因は多数あります。詳細については、「プッシュ通知サービスの要求ヘッダーと応答ヘッダー」の「応答コード」のセクションを参照してください。以下では、よくあるエラーとその原因を紹介します。

  • チャネル URI が無効である (404 Not Found) か、有効期限が切れている (410 Gone)。このような場合、サービスがデータベースから問題のチャネル URI を削除し、この URI に対してこれ以上の要求を行わないようにします。
  • クライアント シークレットと SID が無効である (401 Unauthorized) か、アプリのマニフェストに含まれるアプリのパッケージ ID と Windows ストア内のパッケージ ID が一致しない (403 Forbidden)。この 2 つのパッケージ ID を確実に一致させる最も有効な方法は、Visual Studio の [ストア] の [アプリケーションをストアと関連付ける] を使用することです (このコマンドは、Visual Studio 2012 Ultimate では [プロジェクト] メニューにあります)。
  • 直接通知のペイロードが 5 KB を超えている (413 Request Entity Too Large)。
  • クライアントがオフラインの可能性がある。この場合、WNS は自動的に再試行しますが、最終的にはエラーを報告します。XML 通知の場合、既定では、プッシュ通知がキャッシュされ、クライアントがオンラインになった時点で送信されます。直接通知の場合、既定ではキャッシュは無効です。この動作は、チャネル URI への要求の X-WNS-Cache-Policy ヘッダーを cache に設定することで変更できます。

その他のエラー (400 Bad Request) の場合、XML ペイロードにエンコードが UTF-8 のテキストが保持されていること、および直接通知が base64 であり、Content-Type ヘッダーが application/octet-stream に設定されていることを確認してください。また、ある時間内で送信した通知が多すぎるために、WNS によって配信が調整されている可能性もあります。

アプリがロック画面に存在せず、デバイスがコネクト スタンバイ モードの場合も、直接プッシュ通知が拒否される可能性があります。この状態でロック画面にないアプリに対する直接通知は Windows によって阻止されるため、WNS には、配信されないことがわかっている通知を削除する最適化機能があります。

Windows Azure モバイル サービス

ストレージの問題を省いても複雑な、プッシュ通知の処理について詳細に見てきたため、「これをもっと簡単にする方法はないか?」とお考えだと思います。顧客ベースの規模が大きく、拡大し続けている場合に、数千または数万のチャネル URI を管理しなければならない状況を考えてみてください。

そのような疑問を抱かれるのは、皆さんが初めてではありません。Urban Airship が提供するソリューションなど、サードパーティのソリューションのほか、Windows Azure モバイル サービスを使うことで、作業量を大幅に削減できます。

Windows Azure モバイル サービス (略記「AMS」) は、これまで説明したサービスの細かな処理の大半に対応できる、既成のソリューション (基本的に、さまざまな REST エンドポイント) を提供します。"モバイル サービス" は、基本的に、開発者に代わってデータベースを管理し、WNS にペイロードを簡単に送信するためのライブラリ機能を提供するサービスです。AMS の概要については、「Windows Azure モバイル サービスを使って、クラウド サービスをアプリに追加する」を参照してください。

特にプッシュ通知の場合は、最初に Windows 8 用の Windows Azure モバイル サービス SDK に含まれるクライアント側のライブラリを取得します。次に、これまで説明した連携のすべての要件を満たすうえで AMS がどのように役立つかを説明している「モバイル サービスにおけるプッシュ通知の概要」(英語) (同じトピックで JavaScript バージョン (英語) もあります) を参照してください。

  • Windows ストアへのアプリの登録: Windows ストアからアプリ用のクライアント シークレットと SID を取得したら、それらの値をモバイル サービスの構成に保存します。前述の概要 (英語) トピックの「Windows ストアへのアプリの登録」(英語) セクションを参照してください。
  • チャネル URI の取得と更新: アプリでのチャネル URI の要求と管理は、完全にクライアント側の問題であり、前に説明したとおりです。
  • サービスへのチャネル URI の送信: AMS を使うと、このステップは非常に簡単になります。まず、(Windows Azure ポータルから) モバイル サービスにデータベース テーブルを作成します。作成できたら、アプリから、チャネル URI やそのほかにアタッチが必要な重要な情報を保持するレコードをそのテーブルに挿入できます。この挿入操作の基盤となる HTTP 要求や、サーバー上で発生した変更のクライアント側のレコードへの反映でさえ、AMS クライアント ライブラリによって処理されます。さらに、AMS では、ユーザーの Microsoft アカウントまたは他の 3 種類の OAuth プロバイダー (Facebook、Twitter、または Google) のいずれかを使ってアプリを登録している場合、それらを利用して認証を自動的に処理できます。「モバイル サービスでの認証の概要」(英語) を参照してください。
  • 通知の送信: モバイル サービス内で、スクリプト (Node.js という JavaScript の一種で作成) をデータベース操作にアタッチしたり、JavaScript を使ってスケジュールされたジョブを作成したりできます。これらのスクリプトで、チャネル URI とペイロードを使った push.wns オブジェクトへのシンプルな呼び出しを行うことで、チャネルに対して必要な HTTP 要求が生成されます。また、プッシュ エラーのキャプチャと、応答の console.log への記録も簡単に行えます。このログは、Windows Azure ポータルから簡単に確認できます。

詳細については、サンプルを使った 2 つのチュートリアル、「Windows Azure モバイル サービスを使ったタイル、トースト、バッジのプッシュ通知」(英語) および「Windows Azure モバイル サービスを使った直接通知」(英語) を参照してください。ここでは、これらのチュートリアルで説明されているすべての手順を再度説明するのではなく、主なポイントだけをいくつか見ていきましょう。

モバイル サービスを設定すると、特定のサービス URL が用意されます。AMS SDK の MobileServiceClient オブジェクトのインスタンスを作成するときには、この URL を使います。

JavaScript:

 var mobileService = new Microsoft.WindowsAzure.MobileServices.MobileServiceClient(
    "https://{mobile-service-url}.azure-mobile.net/",
    "{mobile-service-key}");

C#:

 using Microsoft.WindowsAzure.MobileServices;

MobileServiceClient MobileService = new MobileServiceClient(
    "https://{mobile-service-url}.azure-mobile.net/",
    "{mobile-service-key}");

C++ (別のサンプル (英語) から抜粋):

 using namespace Microsoft::WindowsAzure::MobileServices;

auto MobileService = ref new MobileServiceClient(
    ref new Uri(L" https://{mobile-service-url}.azure-mobile.net/"), 
    ref new String(L"{mobile-service-key}"));

このクラスは、サービスとのすべての HTTP 通信をカプセル化し、基盤のコードを作成するという手間のかかる作業から開発者を解放します。

特定の OAuth プロバイダーとの認証を行うには、login (英語) または LoginAsync (英語) メソッドを使います。その結果、アプリにログイン情報を提供する User (英語) オブジェクトが生成されます (認証されると、クライアント オブジェクトの CurrentUser プロパティにも、ユーザー ID が設定されます)。このようにしてモバイル サービスを直接認証すると、サービスからユーザー ID にアクセスでき、クライアントがユーザー ID を明示的に送信する必要がありません。

JavaScript:

 mobileService.login("facebook").done(function (user) { /* ... */ });
mobileService.login("twitter").done(function (user) { /* ... */ });
mobileService.login("google").done(function (user) { /* ... */ });
mobileService.login("microsoftaccount").done(function (user) { /* ... */ });

C#:

 MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Twitter);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.Google);
MobileServiceUser user = await MobileService.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);

C++:

 task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Facebook))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Twitter))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::Google))
    .then([this](MobileServiceUser^ user) { /* */ } );
task<MobileServiceUser^> (MobileService->LoginAsync(MobileServiceAuthenticationProvider::MicrosoftAccount))
    .then([this](MobileServiceUser^ user) { /* */ } );

チャネル URI のサービスへの送信も、サービスのデータベースにレコードを保存し、クライアント オブジェクトから HTTP 要求を行うだけです。それには、以下の例 (前出のプッシュ通知のチュートリアルのサンプルから抜粋) のように、データベース オブジェクトを要求し、レコードを挿入します。各コード スニペットでは、ch に WinRT API の PushNotificationChannel オブジェクトが保持されていることを前提にしています。また、セカンダリ タイル ID やチャネルの用途を示すその他のデータなど、作成された channel オブジェクトに、他のプロパティを含めることもできます。

JavaScript:

 var channelTable = MobileServicesSample.mobileService.getTable('Channels');

var channel = {
    uri: ch.uri, 
    expirationTime: ch.expirationTime.
};

channelTable.insert(channel).done(function (item) {

    },
    function () {
        // Error on the insertion.
    });
}

C#:

 var channel = new Channel { Uri = ch.Uri, ExpirationTime = ch.ExpirationTime };
var channelTable = privateClient.GetTable<Channel>();

if (ApplicationData.Current.LocalSettings.Values["ChannelId"] == null)
{
    // Use try/catch block here to handle exceptions
    await channelTable.InsertAsync(channel);
}

C++:

 auto channel = ref new JsonObject();
channel->Insert(L"Uri", JsonValue::CreateStringValue(ch->Uri));
channel->Insert(L"ExpirationTime", JsonValue::CreateBooleanValue(ch->ExpirationTime));

auto table = MobileService->GetTable("Channel");
task<IJsonValue^> (table->InsertAsync(channel))
    .then([this, item](IJsonValue^ V) { /* ... */ });

チャネル レコードが挿入されていれば、サービスがレコードに対して実行した変更や追加があった場合、その変更や追加はクライアントに反映されます。

また、GetTable や getTable の呼び出しに指定したデータベース名のスペルが正しくない場合、レコードの挿入を実行しない限り、例外は発生しません。これは見つけることが難しいバグとなる可能性があります。すべてが正常に機能するはずなのに機能しない場合は、このデータベース名を確認してください。

また、クライアント側でのこれらの挿入はサービスに対する HTTP 要求に変換されますが、サービス側でもこの処理は開発者から隠されます。要求を受け取って処理するのではなく、カスタム スクリプトを各データベース操作 (挿入、読み取り、更新、削除) にアタッチします。

これらのスクリプトは、Node.js に用意されている、同じ組み込みのオブジェクトとメソッドを使って JavaScript 関数として作成されています (これらの関数はいずれも、アプリのクライアント側の JavaScript とは無関係です)。各関数は、該当するパラメーターを受け取ります。つまり、insert と update は新しいレコードを、delete は対象の項目の ID を、read はクエリを受け取ります。また、どの関数も、アプリがモバイル サービスに対してユーザーを認証している場合は user オブジェクトと、目的の操作を実行して HTTP 応答になるデータを生成するための request オブジェクトを受け取ります。

最もシンプルな (既定の) insert スクリプトは、要求を実行して、レコードの item をそのまま挿入するだけです。

 function insert(item, user, request) {
    request.execute();
}

タイムスタンプとユーザー ID をレコードにアタッチする場合は、要求を実行する前に item パラメーターにこれらのプロパティを追加するだけです。

 function insert(item, user, request) {
    item.user = user.userId;
    item.createdAt = new Date();
    request.execute();
}

データベースへの挿入が実行される前に、スクリプトで item に対して実行した変更は、自動的にクライアントに反映されます。上のコードでは、挿入が成功すると、クライアントの channel オブジェクトに user プロパティと createdAt プロパティが設定されます。非常に便利です。

このサービス スクリプトでは、特に操作の成功またはエラーに応じて、request.execute の後に追加の操作を実行することもできますが、その詳細については、サーバー スクリプトの各種サンプル ドキュメント (英語) を参照してください。

プッシュ通知の話に戻ると、チャネル URI のテーブルへの保存は、プロセスの一部に過ぎません。サービスはこのイベントに応じて通知を送信する場合も、送信しない場合もあります。おそらく、サービスには、その他の情報が設定されている他のテーブルがあり、それらのテーブルに対して実行される操作によって、特定のチャネル URI に通知が発行されます。次のセクションで、いくつか例を挙げます。どのようなケースでも、push.wns (英語) オブジェクトを使って、スクリプトからプッシュ通知を送信します。この場合も、特定の種類の更新を送信する方法は (直接通知も含め) 多数あり、names メソッドを介して直接タイル、トースト、バッジを操作します。以下に例を示します。

 push.wns.sendTileSquarePeekImageAndText02(channel.uri, {
    image1src: baseImageUrl + "image1.png",
    text1: "Notification Received",
    text2: item.text
}, {
    success: function (pushResponse) {
        console.log("Sent Tile Square:", pushResponse);
    },
    error: function (err) {
        console.log("Error sending tile:", err);
    }

});

push.wns.sendToastImageAndText02(channel.uri, {
    image1src: baseImageUrl + "image2.png",
    text1: "Notification Received",
    text2: item.text
}, {
    success: function (pushResponse) {
        console.log("Sent Toast:", pushResponse);
    }
});

push.wns.sendBadge(channel.uri, {
    value: value,
    text1: "Notification Received"
}, {
    success: function (pushResponse) {
        console.log("Sent Badge:", pushResponse);
    }
});

このコードでも、console.log 関数によってログにエントリが作成され、このログは Azure モバイル サービスのポータルから参照できます。通常は、上のタイル通知のコードにあるように、エラー ハンドラーにログ呼び出しを含めます。

各 send* メソッドは特定のテンプレートに関連付けられています。したがって、タイルの場合、ワイド用のペイロードと正方形用のペイロードは、2 つの通知に分けて送信される必要があります。スタート画面のタイルの表示はユーザーが制御するため、ほとんどの場合、両方のサイズを一緒に送信すべきです。したがって、push.wns の send 関数のテンプレートを使う場合は、2 つの連続する呼び出しが実行され、それぞれの呼び出しによって個別のプッシュ通知が生成されます。

複数のタイル更新に別のタグを付けて同時に送信する、複数のトーストを送信するなど、複数の更新を組み合わせるには、push.wns.sendTile メソッドと push.wns.sendToast メソッドを使います。以下に例を示します。

 var channel = '{channel_url}';

push.wns.sendTile(channel,
    { type: 'TileSquareText04', text1: 'Hello' },
    { type: 'TileWideText09', text1: 'Hello', text2: 'How are you?' },
    { client_id: '{your Package Security Identifier}', client_secret: '{your Client Secret}' },

    function (error, result) {
        // ...
    });

さらに下のレベルでも、push.wns.send では通知のコンテンツを非常に正確に処理できます。push.wns.sendRaw は直接通知にも利用できます。詳細については、再度 push.wns (英語) オブジェクトのドキュメントを参照してください。

Windows Azure モバイル サービスを使った実際のシナリオ

Windows Azure モバイル サービスを使ったタイル、トースト、バッジのプッシュ通知」(英語) のサンプル アプリは、データベース テーブルに挿入する新しいメッセージがあると、それに応じてプッシュ通知を送信する方法を示しています。しかし、これはプッシュ通知をアプリ自身に送信することになり、このような処理が必要になることは通常ありません (おそらく、ユーザーの他のデバイスにインストールされている同じアプリに通知を送信する場合を除く)。

それよりも可能性が高いのは、最終的にプッシュ通知を受け取るアプリやタイルの外部で発生したイベントに応じて、サービスがプッシュ通知を送信するケースです。以下でいくつかのシナリオを検討してみましょう。

  • ソーシャル ネットワークの使用:

アプリは、ユーザーのソーシャル ネットワークを使って、ユーザーの友人にチャレンジを発行するなどの機能を実装できます。たとえば、あるユーザーがゲームで新しいハイ スコアを達成したら、タイル更新またはトーストを使って、同じゲームをインストールしているユーザーの友人にチャレンジを発行できます。フィットネス アプリでも同様に、特定の運動でのベスト タイムを更新できたら、これを投稿できます。

その方法として、アプリから新しいレコードを該当するモバイル サービスのテーブル (Scores、BestTimes など) に挿入できます。挿入のスクリプト内で、サービスからデータベースを照会して、現在のユーザーの友人の中で該当する友人を検索し、友人のチャネル URI に通知を送信します。追加のクエリ条件では、ゲームの正確な状況、運動の種類 (セカンダリ タイルに表示) などを記述できます。

  • 天気の更新情報と気象警報:

天気情報アプリでは、通常、特定の地域をアプリのプライマリ タイルに割り当て、その他の地域の情報を表示するセカンダリ タイルを作成できます。各タイルで重要な情報は、対象地域の緯度と経度です。この情報は各チャネル URI と併せて (テーブルに挿入することで) アプリからサービスに提供されます。チャネルへの更新をトリガーするには、定期的に中央の気象サービスにクエリを送って最新情報や警報の有無を確認し、クエリの応答を処理して、適切なメッセージをモバイル サービスの警報テーブルに挿入する別のプロセス (以下で説明するスケジュールされたジョブなど) をサービスに用意できます。次に、挿入のスクリプトでは、適切なチャネル URI を取得し、更新情報を送信します。または、天気サービス自体が、ユーザーに警報や定期的な更新を登録できるようにしている場合は、サービスの別のページでこれらの要求 (おそらく HTTP PUT) を受け取り、処理し、モバイル サービスを起動してレコードを挿入することで、更新がトリガーされるようにします。

  • メッセージング:

インスタント メッセージの処理は、最新の気象情報の受信とほぼ同じ方法で行います。この場合も、定期的に着信メッセージを確認するプロセスなど、別のプロセスによって新しいメッセージの受信を監視するか、新しいメッセージが到着したときにアラートを表示するようにメッセージ ソースに登録します。どちらのケースも、新しいメッセージの到着がトリガーになって、プッシュ通知が該当するチャネルに送られます。この場合、チャネル URI は、特定のメッセージ情報を表示するユーザーのタイルに関連付けられます。この場合は、この記事の最初で説明したメールのシナリオに似ているため、おそらく直接通知を使用することになります。

どのシナリオでも、データベース テーブルにデータを実際に挿入する必要はありません。挿入のスクリプトで request.execute を呼び出さなければ、データベースには何も挿入されませんが、それでも、通知の送信などその他のタスクをこのスクリプト内で実行できます。つまり、特にデータを保存する処理にはコストがかかるため、後で使うことのないレコードでデータベースをいっぱいにしてしまう必要はありません。

Azure モバイル サービスには、ジョブをスケジュールするユーティリティがあることにも注意してください。この機能については、「モバイル サービスでの定期的なジョブのスケジュール」(英語) を参照してください。このようなジョブを使うと、他のサービスから定期的にデータを取得して処理し、レコードをサービスのデータベースに挿入して、プッシュ通知をトリガーできます。同様に、このシリーズのパート 2 で指摘したように、他の Web ページやモバイル アプリを使って、そのデータベースを変更し、プッシュ通知をトリガーすることもできます。モバイル サービスのデータベース スクリプトは、上記のすべてのケースで実行されます。

終わりに

このシリーズでは、"alive with activity (常に動きのある)" エクスペリエンスが総体的に見て、ユーザー、開発者、アプリ、サービスのそれぞれに対してどのような意味を持つかを確認しました。タイル、バッジ、およびトーストの更新機能、定期的な通知を設定する方法、更新プロセスの一環でバックグラウンド タスクを使用する方法、定期通知やプッシュ通知を処理するサービスの構造について見てきました。明らかに、Windows Azure モバイル サービスを利用すると、プッシュ通知を操作するプロセス全体を、はるかに高い観点から把握できます。Azure モバイル サービスを使えば、最初から新しいサービスを作成する場合に比べて、間違いなく生産性の大幅な向上が期待でき、多くの悩みの種が取り除かれることでしょう。

Kraig Brockschmidt

- Windows エコシステム担当チーム、プログラム マネージャー

著書『HTML、CSS、JavaScript を使った Windows 8 アプリ開発』(英語)