UWP - 介紹 App Service 與新功能

App Service 是一種背景工作運行的服務,提供給其他 Apps 使用就像 Web Service。它本身無使用介面(UI-less),允許 Apps 在同一個設備被引用,甚至 Windows 10 1607 開始允許 remote devices 使用它。

[重點觀念]

  • Windows 10, version 1607 開始, App Service 支持新模式:
  • 想要 App Service 每次被啓動都是新的 instance,在 Package.appmanifest 加入宣告;uap4:SupportsMultipleInstances="true";但需要 Windows 10, version 15063 以上才支援
  • App Service 的生命周期,因爲 Process 有所不同:
    • Background Task (out-of-process):
      • 當它被建立時會進入 Run(),隨著 Run() 執行完畢就會被結束
      • 它被啓動後,基本會維持活著約有 30 秒,可搭配呼叫 GetDeferral() 多加 5 秒來完成任務
    • In-app process model:生命周期則跟著呼叫者一起共存,讓兩個 Apps 之間更容易溝通,不用再分成兩份 code 來串聯與維護
  • App Service 的 OnTaskCancel() 被觸發有幾個原因:
    1. Client app 釋放AppServiceConnection
    2. Client app 被 suspended
    3. 系統關閉或睡眠
    4. 系統執行該 Task 用過高的資源

大略有概念之後,接著介紹怎麽做基本的 App Service (two process),再介紹怎麽整合到 App 的 process 裏面;

* 如何建立 App service 並使用它:

分成兩個 App 做説明:一個是擁有 App Service 的 Host App;一個是使用 App Service 的 Client App;

  1. 建立一個 Windows Runtime Component,並且加入AppServiceConnection 的處理邏輯:
    public sealed class ServiceTask : IBackgroundTask
    {
        private BackgroundTaskDeferral backgroundTaskDeferral;
        private AppServiceConnection appServiceconnection;
    
        public void Run(IBackgroundTaskInstance taskInstance)
        {
            // Background Task 被建立時,取得 deferral 拉長生命周期,避免被結束
            this.backgroundTaskDeferral = taskInstance.GetDeferral();
    
            // 一定要註冊處理 Canceled 事件來正確釋放用到的資源
            taskInstance.Canceled += OnTaskCanceled;
    
            // 根據被啓動的 Instance 類型,建立 App Service Connection,並註冊 Request 事件.
            var details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
            appServiceconnection = details.AppServiceConnection;
            appServiceconnection.RequestReceived += OnRequestReceived;
        }
    
        private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            if (this.backgroundTaskDeferral != null)
            {
                // Complete the service deferral.
                this.backgroundTaskDeferral.Complete();
            }
        }
    
        private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            // 當 App Service 收到請求時,該 method 就會被觸發
            // 先要求取得 取得 deferral 拉長生命周期
            var requestDeferral = args.GetDeferral();
    
            ValueSet message = args.Request.Message;
            string cmd = message["cmd"] as string;
            string id = message["id"] as string;
    
            ValueSet responseMsg = new ValueSet();
    
            switch (cmd)
            {
                case "Query":
                    responseMsg.Add("id", "123456");
                    responseMsg.Add("name", "pou");
                    responseMsg.Add("status", "OK");
                    var result = await args.Request.SendResponseAsync(responseMsg);
                    break;
            }
    
            requestDeferral.Complete();
        }
    }
  2. 在 Host App 的 Package.manifest 宣告 App Service 並設定 Entry Point,記得把 App Service 的專案加入到 Host App 的專案參考:
    <Applications>
        <Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="ServiceHost.App">
            <uap:VisualElements  />
            <Extensions>
                <uap:Extension Category="windows.appService" EntryPoint="MyAppService.ServiceTask">
                    <uap:AppService Name="com.pou.MyApService" />
                </uap:Extension>
            </Extensions>
        </Application>
    </Applications>
    加入專案參考這樣在 Host App 被安裝的時候才會一并加入 App Service。 利用 Windows.ApplicationModel.Package.Current.Id.FamilyName 在 Host App 拿到 package family name,準備交給 Client App。
  3. 在 Client App 利用 AppServiceConnection 呼叫 App Service:
    private async void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        AppServiceConnection connection = new AppServiceConnection();
        connection.AppServiceName = "com.pou.MyApService";
        connection.PackageFamilyName = "f9842749-e4c8-4c15-bac8-bc018db1b2ea_s1mb6h805jdtj";
    
        var status = await connection.OpenAsync();
    
        if (status != AppServiceConnectionStatus.Success)
        {
            Debug.WriteLine("Failed to connect");
            return;
        }
    
        var message = new ValueSet();
        message.Add("cmd", "Query");
        message.Add("id", "1234");
    
        AppServiceResponse response = await connection.SendMessageAsync(message);
        string result = "";
    
        if (response.Status == AppServiceResponseStatus.Success)
        {
            if (response.Message["status"] as string == "OK")
            {
                result = response.Message["name"] as string;
            }
        }
    }
     

上面介紹的 App Service 是比較一般的用法, 把 App Service 放到 Background Task 的架構。

* 把 App Service 合併到 App.xaml.cs 裏面,作爲 Same Process:

AppServiceConnection 允許其他 App 叫醒在背景中自己的 App 並傳入指令。它與上方的 out-of-process 最大不同有兩個:

  1. Package.manifest 宣告 <uap:Extension Category="windows.appService"> 不用 Entry Point,改用 OnBackgroundActivated()
    <Package>
      <Applications>
          <Application>
              <Extensions>
                  <uap:Extension Category="windows.appService">
                      <uap:AppService Name="com.pou.MyApService" />
                  </uap:Extension>
              </Extensions>
          </Application>
      </Applications>
    OnBackgroundActivated 代表 App 在背景時被啓動,Life cycle 可以參考 Windows 10 universal Windows platform (UWP) app lifecycle
  2. 在 App.xaml.cs 加入 OnBackgroundActivated() 的處理邏輯。
    sealed partial class App : Application
    {
        private AppServiceConnection appServiceConnection;
        private BackgroundTaskDeferral appServiceDeferral;
    
        protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
        {
            base.OnBackgroundActivated(args);
    
            AppServiceTriggerDetails appService = args.TaskInstance.TriggerDetails as AppServiceTriggerDetails;
                
            if (appService ==null)
            {
                return;
            }
                
            args.TaskInstance.Canceled += OnAppServicesCanceled;
    
            // appServiceDeferral 與 appServiceConnection 需要變成公用變數
            // 因爲其他時間需要用到,已維持連線的一致性
            appServiceDeferral = args.TaskInstance.GetDeferral();
            appServiceConnection = appService.AppServiceConnection;
    
            appServiceConnection.RequestReceived += AppServiceConnection_RequestReceived;
            appServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed;
        }
    
        private async void AppServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            // 當 App Service 收到請求時,該 method 就會被觸發
    
            // 先要求取得 取得 deferral 拉長生命周期
            var requestDeferral = args.GetDeferral();
    
            ValueSet message = args.Request.Message;
    
            string cmd = message["cmd"] as string;
            string id = message["id"] as string;
    
            ValueSet responseMsg = new ValueSet();
    
            switch (cmd)
            {
                case "Query":
                    responseMsg.Add("id", "123456");
                    responseMsg.Add("name", "pou");
                    responseMsg.Add("status", "OK");
                    var result = await args.Request.SendResponseAsync(responseMsg);
                    break;
            }
    
            requestDeferral.Complete();
        }
    
        private void AppServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
        {
            appServiceDeferral?.Complete();
            appServiceConnection?.Dispose();
        }
    
        private void OnAppServicesCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            appServiceDeferral?.Complete();
            appServiceConnection?.Dispose();
        }
    }

要支援 in-process model 就是這樣簡單,而且讓原本的 App Service 邏輯回到 App 本身,讓邏輯更乾净。OnBackgroundActivated() 負責處理 App Service 的啓用,並儲存 Deferral 保持服務的生命周期,詳細可以參考 Windows 10 通用 Windows 平台 (UWP) app 週期

介紹完怎麽實作之後,下面補充幾個重要的元件:

  • AppServiceConnection
    代表連線到 App Service 的端點,App Service 允許 UWP App 之間互相溝通。幾個重點:
    Type Name Description
    Properties AppServiceName 取得或設定想要操作的 App Service Name
      PackageFamilyName 取得或設定該 App Service 所屬 package 的 family name
    Methods Dispose() Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
      OpenAsync() Opens a connection to the endpoint for the app service.
      OpenRemoteAsync(RemoteSystemConnectionRequest) Opens a connection to the endpoint on another device for the app service.
      SendMessageAsync(ValueSet) 傳送 ValueSet 内容到 App Service
    Events RequestReceived Occurs when a message arrives from the other endpoint of the app service connection.
      ServiceClosed Occurs when the other endpoint closes the connection to the app service.
  • ValueSet Class
    實現來傳遞交換訊息用的結構,利用 string 做為 Key,Value 則是 Object。這個結構不能放不可被序列化的結構。
  • AppServiceResponseStatus
    代表 App 傳送訊息到 App Service 的結果
  • AppServiceClosedStatus
    代表 App Service 被關閉連線時的描述

======

從上面的介紹,如果您的 App Service 是比較工具型跟 App 本身不需要共用資料或邏輯,建議獨立成 Background Task;相反地,需要讓 App Service 跟 App 有互動,建議改為 Same Process 的架構。更多介紹可以參考 Extend your app with services, extensions, and packages

Connected apps and devices (Project Rome) 支援更豐富的功能,下一篇將有更詳細的説明。

希望對大家有所幫助。

References