Special Windows 10 issue 2015

Volume 30 Number 11

アプリの統合 - Windows 10 でのアプリのリンクと統合

Arunjeet Singh | Windows 2015

ほとんどのアプリ開発者は、複数のアプリを構築しているか、定期的にメンテナンスを行っています。アプリが充実するにつれ、複数のアプリが連携するワークフローがユーザーから頻繁に求められるようになります。たとえば、製品の在庫を管理するアプリと、清算を行うアプリがあるとします。購入ワークフローを完成させるには、この 2 つのアプリを連携させるのが理想的です。

これを解決する 1 つの方法は、単純にすべての機能を 1 つのアプリに統合することです。事実、これはデスクトップ クラスのアプリケーションによく見られるアプローチです。ただし、このアプローチは危険を伴います。アプリケーションは肥大化し、大半のユーザーが特定の機能しか使用しなくなります。アプリの開発者は、アプリ全体に対して、UI の複雑さと機能の更新の両方に対応しなければならなくなります。さらに悪いことに、UI が複雑になればなるほど、ユーザー (特にモバイル ユーザー) は特定のオプションだけを重視するようになります。実際、最近の傾向では、個々のエクスペリエンスに分けてアプリが用意されるようになっています。そのため、ユーザーは不要な無関係の機能を考えることなく、必要なものだけをインストールして使用できます。

そこで 2 つ目の解決策として、アプリ間のコミュニケーションにクラウドを利用します。この方法は、データ量が一定のサイズを超えない限り、または接続に制限があるユーザーが出現しない限り適切に機能します。しかし、「こちらでステータスを更新したのに、あちらのアプリに表示されない」というような苦情がユーザーから出るようになります。また個人的には、常々、アプリケーション開発者として、同じデバイス上の 2 つのアプリのコミュニケーションをクラウドに頼るのは少々おかしいと感じています。他にもっと適切な方法があるはずです。

今回は、アプリ間のコミュニケーションを容易にする Windows 10 のツールをいくつか紹介します。アプリ間のコミュニケーションには、なんらかのデータを持つ別のアプリを起動する方法もあれば、別のアプリを起動しないで単にデータを相互に交換する方法もあります。Windows 10 には、このようなシナリオのいずれにも利用できるツールがあります。

ディープ リンクを設定するためのアプリの準備

まず、製品在庫アプリの例から始めます。このアプリは製品の詳細を表示します。次に、このアプリと連携させるのが営業アプリです。営業アプリは、製品の販売地域や、販売ニーズの種類や求められる場所など、広範囲にわたる傾向を表示します。営業アプリにはドリルダウン形式のユーザー エクスペリエンスが組み込まれ、ユーザーが個別の製品についての詳細を表示できるようになっています。当然、製品についての最も詳細な情報を表示できるのは在庫アプリです。図 1 は、このシナリオを図示したものです。

営業アプリから在庫アプリへのディープ リンク
図 1 営業アプリから在庫アプリへのディープ リンク

このシナリオでは、まず在庫アプリを起動できるようにする必要があります。そのためには、在庫アプリのパッケージ マニフェスト (package.appxmanifest) にプロトコル宣言を追加します。プロトコル宣言とは、在庫アプリを他のアプリから起動できることを公にする方法です。図 2 に、この宣言の例を示します。ここで使用しているプロトコル名は com.contoso.showproduct です。Contoso 社が contoso.com ドメインを所持しているため、これは適切な命名手法です。他のアプリ開発者が、誤って同じカスタムのスキームを使用する可能性はほとんどありません。

プロトコル宣言
図 2 プロトコル宣言

プロトコル宣言が生成する XML は以下のようになります。

<uap:Extension Category="windows.protocol">
  <uap:Protocol Name="com.contoso.showproduct" />
</uap:Extension>

次に、在庫アプリが新しいプロトコルを使用して起動されたとき適切に応答できるようにアクティブ化コードを追加します。在庫アプリの Application クラス (App.xaml.cs) にすべてのアクティブ化通知がルーティングされるため、追加のコードはこのクラスに配置します。プロトコルのアクティブ化に応答するには、Application クラスの OnActivated メソッドをオーバーライドします。コードは図 3 のようになります。

図 3 ディープ リンクの処理

protected override void OnActivated(IActivatedEventArgs args)
{
  Frame rootFrame = CreateRootFrame();
  if (args.Kind == ActivationKind.Protocol)
  {
    var protocolArgs = args as ProtocolActivatedEventArgs;
    rootFrame.Navigate(typeof(ProtocolActivationPage), protocolArgs.Uri);
  }
  else
  {
    rootFrame.Navigate(typeof(MainPage));
  }
  // Ensure the current window is active
  Window.Current.Activate();
}

着信する IActivatedEventArgs の種類をチェックして、プロトコルのアクティブ化かどうかを確かめます。アクティブ化であれば、着信した引数を ProtocolActivatedEventArgs に型キャストし、着信 URI を ProductDetails ページに送信します。ProductDetails ページを、com.contoso.showproduct:Details?ProductId=3748937 などの URI を解析するようセットアップし、対応する製品の詳細を示します。この時点で在庫アプリは、着信ディープ リンクを処理する準備ができました。

このシナリオの最後の手順は、営業アプリから在庫アプリへのディープ リンクを有効にすることです。これはプロセスの中で最も簡単な手順です。営業アプリは、在庫アプリへのディープ リンクを設定するために、Launcher.LaunchUriAsync API を使用するだけです。コードは以下のようになります。

Uri uri = new Uri("com.contoso.showproduct:?ProductId=3748937");
await Launcher.LaunchUriAsync(uri);

アプリ間でのデータの共有

シナリオによっては、データの共有は必須でも、ユーザーを別のアプリに送信する必要はないこともあります。たとえば、今回のサンプルの営業アプリでは、地域別の販売数を表示し、そこから特定の店舗にドリル ダウンできます。製品別に分類されているこのデータを表示するとき、店舗または地域でのその製品の在庫数も表示できれば便利です。このデータの最適な情報源は在庫アプリですが、このケースでは在庫アプリを起動するとユーザー エクスペリエンスが低下します。これに対処するために設計されたシナリオがまさに、AppService 拡張機能 (msdn.microsoft.com/ja-jp/library/windows/apps/xaml/mt187314.aspx) です。

考え方はシンプルです。在庫アプリが「サービス」を用意し、営業アプリから呼び出せるようにします。営業アプリは、在庫アプリのデータをクエリするためにこのサービスを使用します。営業アプリと在庫アプリとの接続を確立した後は、営業アプリが中断されない限り、開いたままにすることができます。

在庫アプリ サービスの作成

ここでは在庫アプリが、提供対象のアプリ サービスを作成して発行する方法を見ていきます。基本的にアプリ サービスは、特殊なバックグラウンド タスクです。このため、アプリ サービスを追加するには、在庫アプリを含む Visual Studio ソリューションに Windows ランタイム コンポーネント (ユニバーサル Windows) プロジェクトを追加します。Windows ランタイム コンポーネント プロジェクトは、Visual Studio の [新しいプロジェクトの追加] ウィンドウの [Visual C#]、[Windows]、[ユニバーサル] ノードにあります。別の言語でも、同様の場所にプロジェクト テンプレートがあります。

新しい Windows ランタイム コンポーネント プロジェクト内部に、InventoryServiceTask という新しいクラスを追加します。前述のように、このコードは UI を表示せずにバックグラウンドで実行させるため、アプリ サービスは特殊なバックグラウンド タスクです。InventoryServiceTask をバックグラウンド タスクにすることを OS に指示するには、IBackgroundTask インターフェイスを実装するだけです。IBackgroundTask インターフェイスの Run メソッドが、在庫アプリ サービスのエントリ ポイントになります。そこで、クライアント (営業アプリ) が必要とする限りこのタスクを利用可能にしておくことを OS に知らせるために、遅延を利用します。また、アプリ サービス固有の RequestReceived イベントにイベント ハンドラーをアタッチします。このイベント ハンドラーは、アプリ サービスが処理する要求をクライアントが送信したとき常に呼び出されます。図 4 に、在庫アプリ サービスを初期化するコードを示します。

図 4 在庫アプリ サービスを初期化する Run メソッド

namespace Contoso.Inventory.Service
{
  public sealed class InventoryServiceTask : IBackgroundTask
  {
    BackgroundTaskDeferral serviceDeferral;
    AppServiceConnection connection;
    public void Run(IBackgroundTaskInstance taskInstance)
    {
      // Take a service deferral so the service isn't terminated
      serviceDeferral = taskInstance.GetDeferral();
      taskInstance.Canceled += OnTaskCanceled;
      var details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
      connection = details.AppServiceConnection;
      // Listen for incoming app service requests
      connection.RequestReceived += OnRequestReceived;
    }
  }
}

今度は、RequestReceived ハンドラーの実装を見ていきます。ここでも、要求が着信したら即座に遅延を使用します。この遅延は、着信要求を処理し終えたらすぐに解放します。アプリ サービス クライアントとアプリ サービス間のコミュニケーションで交換するのは、ValueSet というデータ構造体です。ValueSet は、整数、浮動小数点数、文字列、バイト配列などの単純な型を収容できるキーと値の辞書です。

図 5 に、在庫アプリ サービスが着信要求を処理する方法を示します。在庫アプリ サービスは、コマンドへの着信メッセージを調査して、適切な結果を応答します。図の例は GetProductUnitCountForRegion コマンドを示します。このコマンドに対して、サービスは製品のユニット数とデータの最終更新時刻を応答します。このサービスは、データを Web サービスから取得するか、オフライン キャッシュから取得します。すばらしいのは、クライアント (営業アプリ) は、データの出所を認識する必要も、考慮する必要もないことです。

図 5 在庫アプリでの要求の受信

async void OnRequestReceived(AppServiceConnection sender,
  AppServiceRequestReceivedEventArgs args)
{
  // Get a deferral so we can use an awaitable API to respond to the message
  var messageDeferral = args.GetDeferral();
  try
  {
    var input = args.Request.Message;
    string command = input["Command"] as string;
    switch(command)
    {
      case "GetProductUnitCountForRegion":
        {
          var productId = (int)input["ProductId"];
          var regionId = (int)input["RegionId"];
          var inventoryData = GetInventoryData(productId, regionId);
          var result = new ValueSet();
          result.Add("UnitCount", inventoryData.UnitCount);
          result.Add("LastUpdated", inventoryData.LastUpdated.ToString());
          await args.Request.SendResponseAsync(result);
        }
        break;
      // Other commands
      default:
        return;
    }
  }
  finally
  {
    // Complete the message deferral so the platform knows we're done responding
    messageDeferral.Complete();
  }
}
// Handle cancellation of this app service background task gracefullyprivate void OnTaskCanceled(IBackgroundTaskInstance sender,
  BackgroundTaskCancellationReason reason)
{
  if (serviceDeferral != null)
  {
    // Complete the service deferral
    serviceDeferral.Complete();
    serviceDeferral = null;
  }
}

図 5 はキャンセル ハンドラーの実装も示しています。キャンセルが要求された場合、アプリ サービスは利用している遅延を適切に解放することが重要です。アプリ サービス バックグラウンド タスクは、クライアントがアプリ サービスとの接続を切断するか、システムがリソースを使い果たすかのいずれかの理由でキャンセルされることがあります。いずれにしても、適切にキャンセルが行われれば、プラットフォームはクラッシュと見なしません。

在庫アプリ サービスを呼び出し可能にする前に、サービスを公開して、エンドポイントを提供する必要があります。まず、在庫アプリ プロジェクトの新しい Windows ランタイム コンポーネントへの参照を追加します。次に、在庫アプリ プロジェクトにアプリ サービスの宣言を追加します (図 6 参照)。[エントリ ポイント] を InventoryServiceTask クラスの完全修飾名に設定し、[名前] にはアプリ サービスのこのエンドポイントの特定に使用する名前を指定します。この名前は、アプリ サービス クライアントがエンドポイントにアクセスするために使用します。

アプリ サービスの宣言
図 6 アプリ サービスの宣言

アプリ サービスの宣言が生成する XML は以下のようになります。

<uap:Extension Category="windows.appService"
  EntryPoint="Contoso.Inventory.Service.InventoryServiceTask">
  <uap:AppService Name="com.contoso.inventoryservice"/>
</uap:Extension>

その他、クライアントが在庫アプリ サービスとコミュニケーションをとるには、在庫アプリのパッケージ ファミリ名も必要です。この値を取得する最も簡単な方法は、在庫アプリ内で Windows.ApplicationModel.Package.Current.Id.FamilyName API を使用することです。デバッグ ウィンドウにこの値を出力して、それを取得する方法もよく使われます。

アプリ サービスの呼び出し

これで在庫アプリ サービスを作成したので、営業アプリから呼び出すことができます。クライアントでは AppServiceConnection API を使用して、アプリ サービスを呼び出します。AppServiceConnection クラスのインスタンスは、アプリ サービスのエンドポイント名と、サービスを含むパッケージのパッケージ ファミリ名を必要とします。これら 2 つの値は、アプリ サービスのアドレスと考えることができます。

図 7 は、営業アプリからアプリ サービスに接続するために使用するコードです。AppServiceConnection.AppServiceName プロパティを、在庫アプリのパッケージ マニフェストで宣言されているエンドポイント名に設定しているのがわかります。また、在庫アプリのパッケージ ファミリ名を、AppServiceConnection.PackageFamilyName プロパティに設定しています。準備ができたら、AppServiceConnection.OpenAsync API を呼び出して、接続を開きます。OpenAsync API は完了時にステータスを返しますが、接続が正常に確立されたかどうかを判断するためにこのステータスを使用します。

図 7 在庫アプリ サービスの呼び出し

using (var connection = new AppServiceConnection())
{
  // Set up a new app service connection
  connection.AppServiceName = "com.contoso.inventoryservice";
  connection.PackageFamilyName = "Contoso.Inventory_876gvmnfevegr";
  AppServiceConnectionStatus status = await connection.OpenAsync();
  // The new connection opened successfully
  if (status != AppServiceConnectionStatus.Success)
  {
    return;
  }
  // Set up the inputs and send a message to the service
  var inputs = new ValueSet();
  inputs.Add("Command", "GetProductUnitCountForRegion");
  inputs.Add("ProductId",productId);
  inputs.Add("RegionId", regionId);
  AppServiceResponse response = await connection.SendMessageAsync(inputs);
  // If the service responded with success display the result and walk away
  if (response.Status == AppServiceResponseStatus.Success)
  {
    var unitCount = response.Message["UnitCount"] as string;
    var lastUpdated = response.Message["LastUpdated"] as string;
    // Display values from service
  }
}

接続したら、クライアントは AppServiceConnection.SendMessageAsync API を使用して ValueSet の一連の値をアプリ サービスに送信します。ValueSet の Command プロパティを GetProductUnitCountForRegion に設定しているのがわかります。これが、アプリ サービスによって認識されるコマンドです。SendMessageAsync が返す応答には、アプリ サービスが返す ValueSet が含まれています。アプリ サービスとコミュニケーションをとるには、UnitCount 値および LastUpdated 値を解析して表示するだけです。AppServiceConnection を using ブロックに配置すると、ブロックの終了後すぐに AppServiceConnection の Dispose メソッドが呼び出されます。また Dispose を呼び出すと、アプリ サービスとの対話が終わったので終了可能であることをクライアントから通知できます。

マイクロソフトもこれらの API を使用

もちろん、マイクロソフトもこれらの API を使用しています。実は、Windows 10 に同梱されるマイクロソフト アプリのほとんどがユニバーサル Windows プラットフォーム アプリです。これには、フォト、カメラ、メール、カレンダー、Groove ミュージック、ストアなどのアプリが含まれます。これらのアプリを作成した開発者は、統合シナリオを実装するために、ここで説明した API の多くを使用しています。たとえば、Groove ミュージック アプリには [ストアで音楽を取得] というリンクがあります。このリンクをタップまたはクリックすると、Groove ミュージック アプリがストア アプリに接続するために Launcher.LaunchUriAsync API を使用します。

もう 1 つの好例が設定アプリです。[アカウント]、[お使いのアカウント] の順にタップまたはクリックし、新しいプロフィール画像を撮影するためにカメラを使おうとすると、アプリは Launcher.LaunchUriForResultsAsync という API を使用して、その写真を撮影するためにカメラ アプリを起動します。LaunchUriForResultsAsync は LaunchUriAsync の特殊な形式で、msdn.microsoft.com/ja-jp/library/windows/apps/xaml/mt269386.aspx で詳しく説明しています。

これ以外にも多くのアプリが、Cortana との間で情報を通信するためにアプリ サービスを使用しています。たとえば、Cortana が応答する音声コマンドをアプリがインストールする場合、実際には Cortana によって提供されるアプリ サービスを呼び出しています。

まとめ

Windows 10 には、1 つのデバイスで実行されているアプリどうしのコミュニケーションをサポートする強力なツールが付属しています。これらのツールは、相互に対話できるアプリや、交換できるデータの種類について制限を設けません。まさに、このことを考慮して設計されています。目的は、アプリが相互に固有のコントラクトを定義して、互いの機能を拡張できるようにすることです。これによりアプリ開発者が、自作のアプリをメンテナンス、更新、使用するのが簡単になるように、アプリを細かいエクスペリエンスに分解することも可能になります。複数のデバイスを所持し、タスクごとに使い分けるユーザーが増えているため、これは非常に重要です。これらの API はすべてユニバーサルなので、デスクトップ、ノート PC、タブレット、スマートフォンのいずれでも機能します。Xbox、Surface Hub、HoloLens は近日中に対応予定です。


Arun Singhは、ユニバーサル Windows プラットフォーム チームの上級プログラム マネージャーです。彼の Twitter は @aruntalkstech (英語)、ブログは aruntalkstech.com (英語) です。

この記事のレビューに協力してくれた技術スタッフの Hector Barbera、Jill Bender、Howard Kapustein、Abdul Hadi Sheikh、Stefan Wick、および Jon Wiswall に心より感謝いたします。
Stefan Wick はマイクロソフトのプログラム マネージャーで、アプリの実行と配置を担当しています。
Hector Barbera はマイクロソフトのプログラム マネージャーで、アプリのバックアップとローミングを担当しています。
Howard Kapustein はマイクロソフトのエンジニアで、アプリの状態管理を担当しています。
Howard Kapustein はマイクロソフトのエンジニアで、アプリの実行を担当しています。
Stefan Wick はマイクロソフトのプログラム マネージャーで、アプリの統合シナリオを担当しています。
Howard Kapustein はマイクロソフトのエンジニアで、アプリ モデル API を担当しています。