2020年的UWP(2)——In Process App Service

樓上那個蜀黍發表於2020-10-21

最早的時候App Service被定義為一種後臺服務,類似於極簡版的Windows Service。App Service作為Background Task在宿主UWP APP中執行,向其他UWP APP提供服務,可用於UWP APP間通訊及交換資料。

早期的App Service應用場景較為單一,但隨著Win10 1607版本對In Process AppService的支援,以及從Visual Studio2017開始支援的Desktop Extension和MSIX Package等一系列技術的應用,如今的App Service可以用於UWP和非UWP程式間的直接通訊,達到無限接近傳統桌面程式的能力。我們今天就先來看一下In Process App Service。
In Process,顧名思義我們不需要額外建立專門的Project用來寫App Service的程式碼。而是直接包含在主UWP工程。首先我們建立空的UWP工程FrontUWPApp,然後新增一個簡單的幫助類AppServiceHandler:

class AppServiceHandler
    {
        private AppServiceConnection AppServiceConnection { get; set; }
        private BackgroundTaskDeferral AppServiceDeferral { get; set; }

        public event EventHandler<string> MessageReceivedEvent;

        private static AppServiceHandler instance;
        public static AppServiceHandler Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new AppServiceHandler();
                }

                return instance;
            }
        }

        private AppServiceHandler()
        {

        }

        public void BackgroundActivated(IBackgroundTaskInstance taskInstance)
        {
            AppServiceTriggerDetails appService = taskInstance.TriggerDetails as AppServiceTriggerDetails;
            AppServiceDeferral = taskInstance.GetDeferral();
            AppServiceConnection = appService.AppServiceConnection;
            AppServiceConnection.RequestReceived += OnAppServiceRequestReceived;
            AppServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed;
        }

        private void OnAppServiceRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            AppServiceDeferral messageDeferral = args.GetDeferral();
            var message = args.Request.Message;
            string text = message["response"] as string;

            MessageReceivedEvent?.Invoke(this, text);
            messageDeferral.Complete();
        }

        private void AppServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
        {
            AppServiceDeferral.Complete();
        }

        public async Task<AppServiceResponse> SendRequestAsync(string message)
        {
            var valueSet = new ValueSet();
            valueSet.Add("request", message);
            return await AppServiceConnection.SendMessageAsync(valueSet);
        }
    }

這其中最重要的方法是

public void BackgroundActivated(IBackgroundTaskInstance taskInstance)

該方法將在App.xaml.cs通過

protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
        {
            base.OnBackgroundActivated(args);
            AppServiceHandler.Instance.BackgroundActivated(args.TaskInstance);
        }

將BackgroundTask的例項傳遞進來。再儲存這個Instance中AppService的AppServiceConnection物件。在取得AppServiceConnection物件後,即可以通過事件

public event TypedEventHandler<AppServiceConnection, AppServiceRequestReceivedEventArgs> RequestReceived;

來監聽訊息,同時又可以通過方法

public IAsyncOperation<AppServiceResponse> SendMessageAsync(ValueSet message);

來傳送訊息。實現一個雙向的通訊過程。
僅通過程式碼也許難以想象要做的事情,不妨由介面來推匯出邏輯,下圖是UWP工程FrontUWPApp的介面,我們希望傳送文字訊息給非UWP工程BackgroundNetProcess。再由BackgroundNetProcess處理訊息後,主動經AppService推給FrontUWPApp。

首先我們在MainPage的OnNavigatedTo方法中通過desktop extension的方式,來啟動.NET Framework的Console程式BackgroundNetProcess(如果對UWP如何使用desktop extension不夠了解,請參考這篇《遷移桌面程式到MS Store(9)——APPX With Desktop Extension》)。同時給AppServiceHandler訂閱MessageReceivedEvent。

protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
            {
                await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
                AppServiceHandler.Instance.MessageReceivedEvent += Instance_MessageReceivedEvent;
            }
        }

Instance_MesssageReceivedEvent就是簡單的把從BackgroundNetProcess中返回的訊息顯示在介面上。

        private async void Instance_MessageReceivedEvent(object sender, string e)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
             {
                 textBoxResponses.Text += e + "\r\n";
             });
        }

同時MainPage上的Button按鈕會通過AppServiceHandler例項中儲存的AppServiceConnection物件來傳送request給BackgroundNetProcess程式。

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            var response = await AppServiceHandler.Instance.SendRequestAsync(textBoxRequest.Text);
        }

我們轉到BackgroundNetProcess工程,在Main方法中僅僅是建立類BackgroundProcess的例項,並且讓Console保持執行。

        static void Main(string[] args)
        {
            var backgroundProcess = new BackgroundProcess();
            Console.ReadKey();
        }

而在BackgroundProcess類中,我們通過InitializeAsync方法來建立AppServiceConnection物件,在成功開啟Connection的情況下,訂閱ReqeustReceived事件。這是為了能接受到上文提到的,UWP APP傳送過來的request。

    public class BackgroundProcess
    {
        private AppServiceConnection Connection { get;  set; }

        public Task InitializeTask { get; private set; }

        public BackgroundProcess()
        {
            InitializeTask = InitializeAsync();
        }

        public async Task InitializeAsync()
        {
            Connection = new AppServiceConnection();
            Connection.PackageFamilyName = Package.Current.Id.FamilyName;
            Connection.AppServiceName = "NotificationAppService";
            AppServiceConnectionStatus status = await Connection.OpenAsync();
            if (status != AppServiceConnectionStatus.Success)
            {
                Console.WriteLine(status);
            }
            else
            {
                Console.WriteLine(status);
                Connection.RequestReceived += Connection_RequestReceived;
            }
        }

        private async void Connection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            var deferral = args.GetDeferral();
            var content = args.Request.Message["request"];
            var message = new ValueSet();
            message.Add("response", $"Received request content: {content}");
            await Connection.SendMessageAsync(message);
            deferral.Complete();
        }
    }

這裡需要注意的是,Connection.AppServiceName需要和最終Package.appmanifest檔案中配置的ServiceName一致(appmanifest檔案的修改我們後面一點再介紹)。

在BackgroundProcess類中,一旦我們收到了UWP APP發來的request,就會觸發Connection_RequestReceived方法。在該方法裡,我們對收到的字串做了簡單處理,然後通過SendMessageAsync方法反向給UWP APP傳送訊息。
當然,並沒有規定收到request就一定要立即返回訊息。我們可以在BackgroundProcess這樣的desktop extension程式中,實現一些UWP限制的功能,諸如查詢登錄檔,啟動其他exe程式等等。甚至可以掛個鍵盤鉤子,在捕捉到熱鍵時,通知UWP APP。
前後端的FrontUWP和BackgroundNetProcess都介紹完了,接著就是通過Packaging工程將它們整合打包成MSIX package。

記得在Package工程的Applications中,新增對FrontUWPApp和BackgroundNetProcess的引用。同時設定FrontUWPApp為入口點。

最後我們來編輯Package工程的appxmanifest檔案,主要就是新增Extensions節點。

      <Extensions>
        <uap:Extension Category="windows.appService">
          <uap:AppService Name="NotificationAppService" />
        </uap:Extension>
        <desktop:Extension Category="windows.fullTrustProcess" Executable="BackgroundNetProcess\BackgroundNetProcess.exe"></desktop:Extension>
      </Extensions>

在完成以上操作之後,我們的AppServiceCommunicaton工程就編寫完畢了。在Visual Studio 2019中按F5執行的話,應該可以實現FrontUWPApp和BackgroundNetProcess之間的訊息傳遞了。
本篇的示例程式碼依然放在這個Repository中,Clone後通過VS開啟,找到InProcessAppService資料夾即可。
https://github.com/manupstairs/UWPSamples

 

相關文章