上一篇我們討論了UWP和Desktop Extension間的雙向通訊,適用於Desktop Extension中存在使用者互動的場景。本篇我們討論最後一種情況,與前者不同的是,Desktop Extension和UWP保持相同的生命週期,同時規避AppServiceConnection可能被Windows回收的限制,在任意時刻能夠反向通知UWP的場景。
首先回顧之前總結的四個場景分類:
- 執行後立即退出
- 等待request,處理完後退出
- 一或多個request/response週期
- 與UWP相同生命週期,且保持由Desktop Extension發起通知的能力
在長生命週期Desktop Extension向UWP的反向通知場景中,有以下特徵:
- 通知發起方是Desktop Extension
- 通過request傳遞引數
- 不關心返回結果
- 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