読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Windows 10 TPのUniversal application platformでのAppService

Windows 10 TP時点の情報です

超参考 tmyt.hateblo.jp

Windows 10から、AppServiceというアプリのバックグラウンドでサービスを立ち上げることが出来るようになりました。サービスを提供するためのサーバーアプリと、サービスを使うクライアントのアプリといった使い方が利用可能です。

例えば、画像解析なんかの難しいことをやってくれるサービスを持ったアプリを作ってストアで売るといったことが出来るようになるんじゃないかと思います。(あとは、それをラップしたライブラリをNuGetあたりで放流するとか)

サービスの作成

サービスは、Windows Runtime Componentとして作成する必要があります。(これを知らなくて2日はまった)

UAPのプロジェクト(ここではAppServiceEduという名前で作りました)を作って、そこにWindows Runtime ComponentのプロジェクトをAppServiceEdu.Taskという名前で作りました。そして、GreetServiceTaskという名前で以下のようなクラスを作りました。

using System;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;

namespace AppServiceEdu.Task
{
    public sealed class GreetServiceTask : IBackgroundTask
    {
        private BackgroundTaskDeferral deferral;

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            this.deferral = taskInstance.GetDeferral();

            var detail = taskInstance.TriggerDetails as AppServiceTriggerDetails;

            if (detail != null && 
                detail.Name == "greetservice")
            {
                detail.AppServiceConnection.RequestReceived += AppServiceConnection_RequestReceived;
            }
        }

        private async void AppServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            var command = args.Request.Message["Command"] as string;
            switch (command)
            {
                case "Greet":
                    var d = args.GetDeferral();
                    var name = args.Request.Message["Name"] as string;
                    var message = new ValueSet();
                    message.Add("GreetMessage", "Hello " + name);
                    await args.Request.SendResponseAsync(message);
                    d.Complete();
                    break;
                default:
                    this.deferral.Complete();
                    break;
            }
        }
    }
}

ポイントは、Runのメソッドの中でAppServiceTriggerDetailsを取得して、そいつのAppServiceConnectionプロパティのRequestRectivedイベントを購読しているところです。

次に、RequestRectivedイベントハンドラ内では引数のRequestプロパティのMessageプロパティにValueSetという型(実質Dictionary<string, object>みたいなもの)が入っています。これを使って、クライアントから渡されたデータを使い処理をします。Deferralを使ってメッセージのやり取りをしたり、してる点もポイントです(これのおかげでサービスの中で、さらに外部のWebサービスを呼んだり時間のかかる処理ができる)

サービスの定義

サービスのTaskクラスが出来たので、このサービスをホストするアプリを作ります。AppServiceEdu.Serverという名前でプロジェクトを作ってPackage.appmanifestに以下の定義を追加します(現時点では男らしく手書き。ここらへん正式版ではGUIになってほしいな)

<?xml version="1.0" encoding="utf-8"?>

<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  IgnorableNamespaces="uap mp">

  <!-- 省略 -->
  <Applications>
    <Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="AppServiceEdu.Server.App">
      <uap:VisualElements
        DisplayName="AppServiceEdu.Server"
        Square150x150Logo="Assets\Logo.png"
        Square44x44Logo="Assets\SmallLogo.png"
        Description="AppServiceEdu.Server"
        ForegroundText="light"
        BackgroundColor="#464646">
        <uap:SplashScreen Image="Assets\SplashScreen.png" />
      </uap:VisualElements>
      <!-- ここから -->
      <Extensions>
        <uap:Extension Category="windows.appService"
                       EntryPoint="AppServiceEdu.Task.GreetServiceTask">
          <uap:AppService Name="greetservice" />
        </uap:Extension>
      </Extensions>
      <!-- ここまで -->
    </Application>
  </Applications>

  <Capabilities>
    <Capability Name="internetClient" />
  </Capabilities>
</Package>

uap:ExtensionのCategoryにwindow.appServiceを指定してEntryPointに先ほど作ったクラスの名前空間を含んだ名前を指定します。そしてuap:AppServiceに適当なNameを指定しておきます。(クライアントから呼び出すのに必要)

クライアントから呼び出すのに必要なFamilyNameの取得

クライアントからサーバーを呼び出すのに、uap:AppServiceに指定した名前の他に、サーバーになるアプリのFamilyNameが必要になります。この2つでサービスを一意に識別しています。

FamilyNameの取得は、以下のようにMainPageのコンストラクタにでも書いておくとデバッグ用のコンソールに出力されます。今回は、7b4c053d-e139-481e-8551-5749ba9117bb_a892ers4d9s3wという値でした。

Debug.WriteLine(Package.Current.Id.FamilyName);

クライアントの作成

ついにクライアントの作成です。クライアントは、AppServiceConnectionを作って接続してメッセージを投げてレスポンスを受け取るといった感じになります。コードを以下に示します。

public sealed partial class MainPage : Page
{
    private AppServiceConnection conn;

    public MainPage()
    {
        this.InitializeComponent();
    }

    private async void button_Click(object sender, RoutedEventArgs e)
    {
        await this.EnsureConnection();

        var message = new ValueSet();
        message.Add("Command", "Greet");
        message.Add("Name", "okazuki");

        var response = await conn.SendMessageAsync(message);
        if (response.Status == AppServiceResponseStatus.Success)
        {
            var res = response.Message["GreetMessage"] as string;
            var dlg = new MessageDialog(res);
            await dlg.ShowAsync();
        }
        else
        {
            Debug.WriteLine("SendMessage failed.");
        }
    }

    private async Task EnsureConnection()
    {
        if (conn != null) { return; }
        // コネクションの取得
        this.conn = new AppServiceConnection();
        conn.PackageFamilyName = "7b4c053d-e139-481e-8551-5749ba9117bb_a892ers4d9s3w";
        conn.AppServiceName = "greetservice";
        var result = await conn.OpenAsync();
        Debug.WriteLine(result);
        conn.ServiceClosed += Conn_ServiceClosed;
    }

    private void Conn_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
    {
        Debug.WriteLine("Connection closed.");
        this.conn = null;
    }
}

ValueSetっていうクラスが、サーバーとクライアントのデータの受け渡しの入れ物なのでそいつにデータをつめてSendMessageAsyncって感じですね。もっとタイプセーフにやりたいところではありますが、そういう場合はタイプセーフなラッパークラスを用意するべきなんでしょうね。

f:id:okazuki:20150328131709p:plain  

サービスのデバッグ方法

サービスをデバッグするには、サーバー側のプロジェクトをスタートアッププロジェクトにして、プロジェクトのデバッグのプロパティで外部からの起動を待つように設定します。

その状態でデバッグ実行して、クライアント側のアプリをふつうに立ち上げてサービス呼び出しをしてやればOKです。