MSC2011 D3-303 “Windows Phone/iOS/Android から Windows Azure を利用する” session follow up - Part 3 : スクラッチでのソリューション開発 - 3 Push Notification

皆様、こんにちは!次は、Push Notification の箇所です。簡単に行きましょう。

Sending Microsoft Push Notifications

新規 Windows Phone Cloud Application の作成の箇所で、Microsoft Push Notificationsのサポートにチェックを入れた場合、Notificationのメッセージを、Windows Phone デバイスに送信することができます。そのためのステップは以下の通りです。

このWPアプリケーションは、Pivotアプリケーションとして作られています。そこで、ユーザー認証が済んだら、次に Notification Pivotアイテムが、アプリケーションに現れます。ここではまだ、Push Notification が有効になっていないことに気づきます。したがって、Connection Status は、未だ Disconnected のままです。

そこで、このチェックボックスで、Enable Push Notifications にチェックを入れ、画面下部に出rてくるメッセージが、 ”Notification updates were received at … ” に変わるまで待ちます。

clip_image0054_thumb2_thumb

注意 : ユーザーが、Push Notificationsを有効にした場合、このアプリケーションは、Microsoft Push Notification Service (MPNS) に登録されます。そして、このための Sample Notification Service が、この Toolkit に含まれています。登録した後、このアプリケーションは、順次、Queue に格納されたメッセージを、可能である限り、Windows Phone デバイスのためにダウンロードします。

Push Notification サブスクライブの仕組みは、この図のとおりです。

image

(1)Phone が単一の Channel をオープンします。

(2)Phone が Web Role に URL を送信します。

(3)Web Role が、当該 URL を使って、Notifications を Push します。

(4)Microsoft Push Notification Service が Phone に通知します。

 

ちなみに、下記は、Azure Storage Explorer で見た、あるPushUserEndPointです。Table に格納されており、こんな感じに見えます。

image

※ MPNSについてさらに知りたい場合には、 Understanding Microsoft Push Notifications for Windows PhonesUnderstanding How Microsoft Push Notification Works – Part 2 、両記事をぜひご一読ください。

簡単に特徴と種類をまとめると下記のとおりです。今後、MSDNでも記述が充実してくるはずなので、ご期待ください。

Push Notificationsの特徴

・Phone と Microsoft Push Notification Service との間の単一のコネクション
・帯域の節約とバッテリ消費の逓減
・配信される保証はない

・有効期間が決まっている

Push Notifications の種類
・Raw – 単一メッセージを単一アプリケーションに送信
・トースト - 単一メッセージをユーザーに送信 (デバイスID)
・タイル – イメージ、タイトル、カウントの更新

それでは次に、Web ブラウザーに戻って、Mobile Cloud Application Services Web サイトを表示します。Log On リンクをクリックし、前の記事と同じように、このアプリケーションにログインします。この Web アプリケーションにログインするには下記のクレデンシャルを使用します:

◦ User Name: admin

◦ Password:clip_image0066_thumb_thumb (with a zero)

ログインすると、今度は今までになかったメニューが表示されています。メニューとしては、(それぞれの権限管理等を行う)Tables、Queues、Users、に加えて、 Micorosoft Push Notifications が追加されているはずです。

注意 : ここでも、Mobile Cloud Application Services Web サイトに表示されるメニューは、アプリケーション作成時に、Wizard 内で選択したオプションにより、若干異なる場合があり得ます。たとえば、SQL Azure データベースを、唯一のデータプロバイダーとして選択した場合、当然ながら、Tables や、Queues メニューは表示されません。.

clip_image0074_thumb2_thumb

Microsoft Push Notifications メニューをクリックして、作成したユーザーに向けて、テキストボックス内に、(例えば) “raw message” とタイプして、 Send Raw リンクをクリックします。うまく行けば、Success というメッセージが表示されるはずです。

clip_image0086_thumb2_thumb

そこで、Windows Phone Emulatorに戻ると、Messages リストに表示されている、Raw Noftificaion メッセージを確認できます。

clip_image0104_thumb2_thumb

次に、テキストボックス内に、(例えば) “tile message” とタイプして、Send Tile リンクをクリックします。 今度はエラーメッセージが表示され、Notification がWindows Phone デバイスで受信できなかった旨の表示がされます。

clip_image0096_thumb2_thumb

このように、Tile(Toastも)メッセージを受信できるようにするには、まずはこのアプリケーションを、Windows Phone Startメニューに、ピン留めしておく必要があります。

そこで、Windows Phone Emulator にある Windows button (clip_image0116_thumb_thumb) をクリックし、Start メニューに移動します。Start メニューの中で、右の矢をクリックして(clip_image012_thumb_thumb) 、すべてのアプリケーションリストを表示します。アプリケーションリスの中で、WA Toolkit WP アイコンを選択し、数秒その状態をホールドします。そうすると、コンテキストメニューが出てきますので、その中から、pin to start をクリックします。これにより、Start メニューにリダイレクトされ、WA Toolkit WP アプリケーションアイコンが、Start メニューに表示されます。

clip_image0134_thumb2_thumb

再度、Web ブラウザーに戻り、Send Tile リンクをクリックして、'tile message' Notification を再度送信します。今度は Success メッセージを確認できるでしょう。

clip_image014_thumb2_thumb

今度は、テキストボックスに、(例えば)“toast message”と入力して、Send Toast リンクをクリックします。こちらも Success メッセージを確認できるでしょう。

clip_image0154_thumb2_thumb

Windows Phone Emulator に戻ります。WAT Windows Phone アイコンに表示されているTile Notification の数字 (ここでは1)と、上部に ある Toast Message が確認できます。いずれかをクリックすると、WAT Windows Phone アプリケーションが開き、Notifications ページに遷移します。ここで表示される Messages リストの中に、両方のメッセージ('tile message' と 'toast message') があるのが確認できるでしょう。

clip_image0164_thumb2_thumb

なお、ログインページにリダイレクトされないのは、認証トークンを、最初にログインした時に受け取っており、そのトークンは、標準で、当該デバイス(エミュレーター)の Isolated Storage に格納されているためです。

さて、このメッセージ群も、Azure Storage Explorer で見てみましょう。これは Queue に格納されており、例えば、このように見えます。

image image

以上で、Push Notification の部分のデモの解説は終了です。このあたり、サンプル実装が非常によくできていますので、ぜひ内容をご覧ください。

次のソースは、自動生成されている SamplePushUserRegistrationResponse.cs です。Connect メソッドで、Table に、Push の Channel URIを書き込んでいます。上記のPush Notification サブスクライブの仕組みを再度見ながら、流れを追ってみてください。

    1: namespace WPCloudApp5.Phone.Push
    2: {
    3:     using System;
    4:     using System.Globalization;
    5:     using System.IO;
    6:     using System.Net;
    7:     using System.Net.Browser;
    8:     using System.Runtime.Serialization;
    9:     using System.Text;
   10:     using Microsoft.Samples.WindowsPhoneCloud.StorageClient;
   11:     using Microsoft.Samples.WindowsPhoneCloud.StorageClient.Credentials;
   12:     using WPCloudApp5.Phone.Models;
   13:  
   14:     public class SamplePushUserRegistrationClient : ISamplePushUserRegistrationClient
   15:     {
   16:         private const string RegisterNotificationOperation = "/register";
   17:         private const string UnregisterNotificationOperation = "/unregister";
   18:         private const string GetUpdatesNotificationOperation = "/updates";
   19:  
   20:         private readonly Uri serviceEndpoint;
   21:         private readonly IStorageCredentials storageCredentials;
   22:         private readonly string applicationId;
   23:  
   24:         public SamplePushUserRegistrationClient(Uri serviceEndpoint, IStorageCredentials storageCredentials, string applicationId)
   25:         {
   26:             this.serviceEndpoint = serviceEndpoint;
   27:             this.storageCredentials = storageCredentials;
   28:             this.applicationId = applicationId;
   29:         }
   30:  
   31:         public void Connect(Action<SamplePushUserRegistrationResponse<string>> callback)
   32:         {
   33:             var registerOperationUriBuilder = new UriBuilder(this.serviceEndpoint);
   34:             registerOperationUriBuilder.Path += RegisterNotificationOperation;
   35:  
   36:             PushContext.Current.Connect(c => ExecutePostServiceOperation<string>(registerOperationUriBuilder.Uri, this.storageCredentials, c.ChannelUri, this.applicationId, callback));
   37:         }
   38:  
   39:         public void Disconnect(Action<SamplePushUserRegistrationResponse<string>> callback)
   40:         {
   41:             if (PushContext.Current.NotificationChannel != null)
   42:             {
   43:                 var channelUri = PushContext.Current.NotificationChannel.ChannelUri;
   44:                 var unregisterOperationUriBuilder = new UriBuilder(this.serviceEndpoint);
   45:  
   46:                 unregisterOperationUriBuilder.Path += UnregisterNotificationOperation;
   47:                 ExecutePostServiceOperation<string>(unregisterOperationUriBuilder.Uri, this.storageCredentials, channelUri, this.applicationId, callback);
   48:             }
   49:             else
   50:             {
   51:                 callback(new SamplePushUserRegistrationResponse<string>(string.Empty, null));
   52:             }
   53:  
   54:             PushContext.Current.Disconnect();
   55:         }
   56:  
   57:         public void GetUpdates(Action<SamplePushUserRegistrationResponse<string[]>> callback)
   58:         {
   59:             if (PushContext.Current.NotificationChannel != null)
   60:             {
   61:                 var channelUri = PushContext.Current.NotificationChannel.ChannelUri;
   62:                 var getUpdatesOperationUriBuilder = new UriBuilder(this.serviceEndpoint);
   63:                 getUpdatesOperationUriBuilder.Path += GetUpdatesNotificationOperation;
   64:  
   65:                 ExecuteGetServiceOperation<string[]>(getUpdatesOperationUriBuilder.Uri, this.storageCredentials, channelUri, this.applicationId, callback);
   66:             }
   67:         }
   68:  
   69:         private static void ExecuteGetServiceOperation<T>(Uri serviceOperationUri, IStorageCredentials storageCredentials, Uri channelUri, string applicationId, Action<SamplePushUserRegistrationResponse<T>> callback)
   70:         {
   71:             string deviceId = App.GetDeviceId();
   72:             var getOperationUri = new Uri(string.Format(CultureInfo.InvariantCulture, "{0}?applicationId={1}&deviceId={2}", serviceOperationUri, applicationId, deviceId));
   73:  
   74:             var request = WebRequestCreator.ClientHttp.Create(getOperationUri);
   75:             request.Method = "GET";
   76:  
   77:             try
   78:             {
   79:                 storageCredentials.SignRequest(request, -1);
   80:                 request.BeginGetResponse(
   81:                     ar =>
   82:                     {
   83:                         var result = default(T);
   84:                         try
   85:                         {
   86:                             var response = request.EndGetResponse(ar);
   87:                             var serializer = new DataContractSerializer(typeof(T));
   88:  
   89:                             result = (T)serializer.ReadObject(response.GetResponseStream());
   90:                             callback(new SamplePushUserRegistrationResponse<T>(result, null));
   91:                         }
   92:                         catch (Exception exception)
   93:                         {
   94:                             callback(new SamplePushUserRegistrationResponse<T>(default(T), StorageClientExceptionParser.ParseStringWebException(exception as WebException) ?? exception));
   95:                         }
   96:                     },
   97:                     null);
   98:             }
   99:             catch (Exception exception)
  100:             {
  101:                 callback(new SamplePushUserRegistrationResponse<T>(default(T), exception));
  102:             }
  103:         }
  104:  
  105:         private static void ExecutePostServiceOperation<T>(Uri serviceOperationUri, IStorageCredentials storageCredentials, Uri channelUri, string applicationId, Action<SamplePushUserRegistrationResponse<T>> callback)
  106:         {
  107:             var request = WebRequestCreator.ClientHttp.Create(serviceOperationUri);
  108:             request.Method = "POST";
  109:             request.ContentType = "text/xml";
  110:  
  111:             var postData = string.Empty;
  112:             using (var stream = new MemoryStream())
  113:             {
  114:                 var serializer = new DataContractSerializer(typeof(PushUserServiceRequest));
  115:                 string deviceId = App.GetDeviceId();
  116:                 var newPushUserRegistration = new PushUserServiceRequest { ApplicationId = applicationId, DeviceId = deviceId, ChannelUri = channelUri };
  117:                 serializer.WriteObject(stream, newPushUserRegistration);
  118:  
  119:                 byte[] contextAsByteArray = stream.ToArray();
  120:                 postData = Encoding.UTF8.GetString(contextAsByteArray, 0, contextAsByteArray.Length);
  121:             }
  122:  
  123:             try
  124:             {
  125:                 storageCredentials.SignRequest(request, postData.Length);
  126:                 request.BeginGetRequestStream(
  127:                     ar =>
  128:                     {
  129:                         var postStream = request.EndGetRequestStream(ar);
  130:                         var byteArray = Encoding.UTF8.GetBytes(postData);
  131:  
  132:                         postStream.Write(byteArray, 0, postData.Length);
  133:                         postStream.Close();
  134:  
  135:                         request.BeginGetResponse(
  136:                             asyncResult =>
  137:                             {
  138:                                 var result = default(T);
  139:                                 try
  140:                                 {
  141:                                     var response = request.EndGetResponse(asyncResult);
  142:                                     var serializer = new DataContractSerializer(typeof(T));
  143:  
  144:                                     result = (T)serializer.ReadObject(response.GetResponseStream());
  145:                                     callback(new SamplePushUserRegistrationResponse<T>(result, null));
  146:                                 }
  147:                                 catch (Exception exception)
  148:                                 {
  149:                                     callback(new SamplePushUserRegistrationResponse<T>(default(T), StorageClientExceptionParser.ParseStringWebException(exception as WebException) ?? exception));
  150:                                 }
  151:                             },
  152:                         null);
  153:                     },
  154:                 request);
  155:             }
  156:             catch (Exception exception)
  157:             {
  158:                 callback(new SamplePushUserRegistrationResponse<T>(default(T), exception));
  159:             }
  160:         }
  161:     }
  162: }

以上です。次のエントリは、Table/Blob/Queue 及び SQL Azure の操作、と、ソースコードの紹介(おもに、ListBlobsPageViewModel()、UploadPhotoPageViewModel()等)です。

これが終わり次第、iOS + Windows Azure 編に入ります。

鈴木 章太郎