1. 程式人生 > >2021年的UWP(6)——長生命週期Desktop Extension向UWP的反向通知

2021年的UWP(6)——長生命週期Desktop Extension向UWP的反向通知

上一篇我們討論了UWP和Desktop Extension間的雙向通訊,適用於Desktop Extension中存在使用者互動的場景。本篇我們討論最後一種情況,與前者不同的是,Desktop Extension和UWP保持相同的生命週期,同時規避AppServiceConnection可能被Windows回收的限制,在任意時刻能夠反向通知UWP的場景。
首先回顧之前總結的四個場景分類:

  • 執行後立即退出
  • 等待request,處理完後退出
  • 一或多個request/response週期
  • 與UWP相同生命週期,且保持由Desktop Extension發起通知的能力

在長生命週期Desktop Extension向UWP的反向通知場景中,有以下特徵:

  1. 通知發起方是Desktop Extension
  2. 通過request傳遞引數
  3. 不關心返回結果
  4. Desktop Extension和UWP相同生命週期

示意圖如下:

在我們接下來的Sample工程中,將通過Desktop Extension來監聽全域性鍵盤事件。在使用者按下W, A, S, D四個鍵時列印在UWP的介面上。其實UWP程式在前臺執行的狀態下,也是可以捕獲鍵盤事件的。但在最小化的狀態下,就只能依靠Desktop Extension來實現了。
在上一篇《2020年的UWP(5)——UWP和Desktop Extension的雙向互動》中,我們提到了AppServiceConnection在UWP程式處於最小化時,會被Windows回收導致失去連線。而在長生命週期的Desktop Extension中,我們規避該限制的方式,是在每次從Desktop Extension發起通知時,均建立新的AppConnection物件,這一點非常重要。

整體的工程結構和之前的三篇保持一致,分為ReverseNotification.FrontUWP,ReverseNotification.Desktop以及打包用的ReverseNotification.Package工程。

我們先從FrontUWP工程講起,AppServiceHandler.cs是我建立的幫助Class,用來處理AppServiceConnectoin的Connected和RequestReceived事件。

        public void OnBackgroundActivated(AppServiceTriggerDetails details)
        {
            Connected?.Invoke(this, new AppServiceConnectionConnectedEventArgs(details.AppServiceConnection));
            Connection = details.AppServiceConnection;
            Connection.RequestReceived += Connection_RequestReceived;
        }

        private void Connection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            RequestReceived?.Invoke(this, args);
        }

而OnBackgroundActivated事件則是在App.xaml.cs中,通過override UWP Application物件的OnBackgroundActivated方法來觸發。這裡是AppServiceConnection連線的起點,即源頭。

        protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
        {
            base.OnBackgroundActivated(args);
            if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails details)
            {
                if (details.CallerPackageFamilyName == Package.Current.Id.FamilyName)
                {
                    var deferral = args.TaskInstance.GetDeferral();
                    args.TaskInstance.Canceled += (sender, e) => { deferral?.Complete(); };
                    AppServiceHandler.Instance.OnBackgroundActivated(details);
                }
            }
        }

以上這些在前面幾篇中都有提及,這裡不再贅述。在UWP工程的MainPage中,我們記錄了UWP程序的process id,Desktop Extension段會讀取該值,用以檢測UWP process的Exit事件,在UWP被關閉時釋放資源。同時通過RequestReceived事件來將Desktop Extension反向通知的HotKey的值,通過HotKeyList繫結顯示到UWP的介面上。

        protected async override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            Process process = Process.GetCurrentProcess();
            ApplicationData.Current.LocalSettings.Values["processId"] = process.Id;
            if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
            {
                await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
            }
            AppServiceHandler.Instance.RequestReceived += Instance_RequestReceived;
        }

        private async void Instance_RequestReceived(object sender, Windows.ApplicationModel.AppService.AppServiceRequestReceivedEventArgs e)
        {
            var message = e.Request.Message;
            if (message.TryGetValue("HotKey", out object keyCode))
            {
                await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High, () => { HotKeyList.Add(keyCode.ToString()); });
            }
        }

最後不要忘記給FrontUWP工程新增對Windows Desktop Extension for the UWP的引用。

我們轉到Desktop這一邊,ReverseNotificatio.Desktop是一個WinForms的程式,通過RegisterHotKey這個Win32的API來監聽熱鍵。如何實現監聽熱鍵我不做過多介紹,具體請參考示例程式碼。

        [DllImport("user32.dll")]
        public static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vlc);

同時為了使用AppServiceConnection,添加了對Win10 API的引用,主要是WindowsRuntime和Windows.winmd這兩個檔案。前者通過Nuget新增,後者請參考《遷移桌面程式到MS Store(4)——桌面程式呼叫Win10 API》。

我想強調的是,不要在長生命週期的Desktop Extension程序中,去維護一個全域性的AppServiceConnection,某軟的文件並沒有提到細節,但也明確指出AppServiceConnection在UWP進入suspended狀態時,可能被釋放。我們要做的事情,是在每一次的熱鍵響應事件中,建立新的AppServiceConnection去傳送訊息。

        private async void hotkeys_HotkeyPressed(int ID)
        {
            var key = Enum.GetName(typeof(VirtualKey), ID);

            var message = new ValueSet
            {
                { "HotKey", key }
            };

            var connection = new AppServiceConnection
            {
                PackageFamilyName = Package.Current.Id.FamilyName,
                AppServiceName = "ReverseNotificationAppService"
            };
            connection.ServiceClosed += Connection_ServiceClosed;

            var status = await connection.OpenAsync();
            if (status == AppServiceConnectionStatus.Success)
            {
                var response = await connection.SendMessageAsync(message);
            }
        }

不能儲存已建立的AppServiceConnection來重複使用,有時會造成不便。但這也正是我將Desktop Extension分為4個場景的原因,針對不同的用途來建立特定型別的background process。

ReverseNotification.Package作為打包工程,我們需要注意新增對FrontUWP和Desktop的引用。以及編輯Package.appxmanifest檔案,提供對AppService和Desktop Extension的支援。

    <Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="$targetentrypoint$">
      <uap:VisualElements
        DisplayName="ReverseNotification.Package"
        Description="ReverseNotification.Package"
        BackgroundColor="transparent"
        Square150x150Logo="Images\Square150x150Logo.png"
        Square44x44Logo="Images\Square44x44Logo.png">
        <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" />
        <uap:SplashScreen Image="Images\SplashScreen.png" />
      </uap:VisualElements>
      <Extensions>
        <uap:Extension Category="windows.appService">
          <uap:AppService Name="ReverseNotificationAppService" />
        </uap:Extension>
        <desktop:Extension Category="windows.fullTrustProcess" Executable="ReverseNotification.Desktop\ReverseNotification.Desktop.exe"/>
      </Extensions>
    </Application>

至此對Desktop Extension的一系列討論告一段落。牽涉的內容較多,很難在一篇文章中解釋清楚,我將之前的連結羅列在下方,供各位參考:
《遷移桌面程式到MS Store(9)——APPX With Desktop Extension》對Desktop Extension做了基礎介紹。
《2020年的UWP(2)——In Process App Service》詳細介紹瞭如何使用AppService。
《2020年的UWP(3)——UWP和desktop extension的簡單互動》介紹了單向的一次性使用場景。
《2020年的UWP(4)——UWP和等待Request的Desktop Extension》background process會有一個較短的生命週期,等待Reqeust執行完成後退出。
《2020年的UWP(5)——UWP和Desktop Extension的雙向互動》通常用在同時展現UWP和WPF介面時使用。
Github:
https://github.com/manupstairs/UWPSamples/tree/master/UWPSamples/DataExchangeUWP/ReverseNotification

&n