在 Windows 10 中微軟為 UWP 引入了 App Service (即應用服務)這一新特性用以提供應用間交互功能。提供 App Service 的應用能夠接收來自其它應用傳入的參數進行處理后返回數據。
創建應用服務
要使應用支持提供 App Service 非常簡單。只需正確配置應用的清單文件后添加服務相關的代碼即可。
配置應用清單文件
- 打開項目中的
Package.appxmanifest
文件。 - 切換到
Declarations
選項卡。 - 在左側
Available Declarations
中選擇App Service
,點擊Add
將其添加到Supported Declarations
列表。 - 然后在右側設置該 App Service 相關屬性。
一般情況下,需要設置的是 Name
和 Entry point
兩項。Name
是應用所提供的服務名稱,Entry point
即入口點,是實現了應用服務功能的后台任務類。
也可以手動編輯 Package.appxmanifest
文件,添加 App Service 相關配置:
- 打開項目中的
Package.appxmanifest
文件,按下F7
切換到代碼編輯模式。 - 在清單文件中找到
<Applications></Applications>
節點,一般情況下,該節點只包含一個<Application/>
節點。(也可以包含多個,也就是在一個應用包中封入多個應用,但這需要額外權限,一般開發者無權這么做。) - 在
<Application></Application>
節點中繼續找到<Extensions></Extensions>
節點(如果沒有則手動添加)。 - 在
<Extensions></Extensions>
中添加以下代碼:
<uap:Extension Category="windows.appService" EntryPoint="入口點"> <uap:AppService Name="服務名稱" /> </uap:Extension>
添加應用服務代碼
- 在當前 UWP 解決方案中添加一個 Windows 運行時組件(Windows Universal) 項目。注意,項目類型務必是 Windows 運行時組件 (也就是 Windows Runtime Component)而不是類庫。
- 在 UWP 解決方案的主項目中添加對新建的 Windows 運行時組件項目的引用。
- 在新建項目中的 Class1.cs 文件中添加以下代碼:
using Windows.ApplicationModel.AppService; using Windows.ApplicationModel.Background; using Windows.Foundation.Collections;
- 在 Class1.cs 文件中編寫實現
IBackgroundTask
接口的后台任務類,假設我們要提供一個生成 GUID 的 App Service:
public sealed class GUIDProviderTask : IBackgroundTask { private BackgroundTaskDeferral backgroundTaskDeferral; private AppServiceConnection appServiceconnection; public void Run(IBackgroundTaskInstance taskInstance) { this.backgroundTaskDeferral = taskInstance.GetDeferral(); taskInstance.Canceled += OnTaskCanceled; var details = taskInstance.TriggerDetails as AppServiceTriggerDetails; appServiceconnection = details.AppServiceConnection; appServiceconnection.RequestReceived += OnRequestReceived; } private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) { //獲取 deferral var requestDeferral = args.GetDeferral(); try { //讀取服務接收到的消息 var message = args.Request.Message; var result = new ValueSet(); //檢測消息中是否包含約定的內容 if (message.ContainsKey("requestguid")) { //在響應結果中填入生成的 GUID result.Add("guid", GenerateGUID()); } //服務發回響應結果 await args.Request.SendResponseAsync(result); } catch (Exception ex) { var result = new ValueSet(); //將異常寫入響應結果 result.Add("exception", ex); //服務發回響應 await args.Request.SendResponseAsync(result); } finally { //通過 deferral 通知系統服務已經完成響應 requestDeferral.Complete(); } } private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { if (this.backgroundTaskDeferral != null) { // 通知服務 deferral 完成。 this.backgroundTaskDeferral.Complete(); } } private string GenerateGUID() { return Guid.NewGuid().ToString(); } }
以上代碼就是實現一個返回 GUID 的 App Service 的完整代碼。Run()
在后台任務創建時被調用。因為后台任務會在 Run
完成后被結束,所以在代碼中需要獲取 deferral 以保證后台任務保持活躍以便響應發送到服務的請求。
OnTaskCanceled()
在后台任務取消時被調用。導致后台任務取消的因素很多樣,有可能是客戶端應用釋放了 AppServiceConnection
;客戶端應用掛起了;操作系統關閉或進入睡眠,或者操作系統沒有足夠的資源運行后台任務。
在 Run
方法中,通過 taskInstance
獲得 AppServiceTriggerDetails
。AppServiceTriggerDetails 表示 App Service 觸發器的詳情。該類包含三個屬性可用於獲取服務名稱、調用者包名和服務連接,即 AppServiceConnecton
。代碼中通過 AppServiceTriggerDetails
獲得當前對服務調用建立的 AppServiceConnecton
。
AppServiceConnection
代碼中實現 App Service 的核心就是 AppServiceConnection。 AppServiceConnection
類表示到一個 App Service 端點的連接。
該類包含兩種事件和四種方法:
事件
-
RequestReceived 該事件在服務接收到請求時觸發。
-
ServiceClosed該事件在服務關閉時觸發。
方法
-
Close 關閉到服務端點的連接。該方法在 C++/Javascript 中使用。
-
Dispose 關閉到到服務端點的連接。該方法在 C#/VB 中使用。
-
OpenAsync 打開到服務端點的連接。
-
SendMessageAsync 向服務另一端點發送消息。
屬性
AppServiceConnection
類還擁有以下兩個屬性:
-
AppServiceName 獲取或設置要連接的服務的名稱。
-
PackageFamilyName 獲取或設置包含服務的應用包的 PFN (Package family name)。
獲得 AppServiceConnection
之后就可以偵聽 RequestReceived
來對服務請求進行響應處理了。
在上述示例代碼中,服務接收請求,檢查請求消息中是否包含預先約定的鍵 "requestguid",如果包含該鍵,則生成一個 GUID,並作為響應結果發回調用者。
由於將響應結果發回調用者的方法 SendResponseAsync 是一個異步方法,所以事件處理方法 OnRequestedReceived
帶有 async 關鍵字。
而為了能夠在事件處理方法 OnRequestedReceived
中正常使用異步方法,需要首先獲取一個 deferral。deferral 確保了對 OnRequestedReceived
的調用不會在請求消息處理完成之前結束。需要注意的是,SendResponseAsync
發回響應與對 OnRequestedReceived
的調用完成無關,SendResponseAsync
不負責通知 OnRequestedReceived
的完成,而是 deferral 負責通知客戶端的 SendMessageAsync
方法已經處理完事件響應 OnRequestedReceived
。( SendMessageAsync
是客戶端用於發送請求的方法,下文詳述。)
App Service 使用 ValueSet 來交換信息。通過 ValueSet
可傳遞的數據大小受系統資源限制決定。在傳遞的 ValueSet
中沒有預定義的鍵值,開發者需自行約定服務所需的鍵值協議。客戶端調用者必須遵守這一協議構造 ValueSet
。
服務端端點關閉時,服務會返回一個 AppServiceClosedStatus 枚舉指示對服務的調用是否成功。AppServiceClosedStatus
可能返回四種結果:完成、取消、系統資源不足和未知情況。服務端可以在響應結果的 ValueSet
中附加詳細的狀態信息,例如異常信息一並發回給客戶端調用者。
最后,對 SendResponseAsync
方法的調用將響應結果發回客戶端調用者。
隱藏提供服務的應用
假設你開發了一個專門提供某種服務的應用,只希望它的后台服務提供功能,不希望主程序出現在開始菜單的所有程序當中,只需簡單修改應用的清單文件即可:
- 在解決方案瀏覽器中右鍵單擊
Package.appxmanifest
選擇查看代碼 (View Code) - 在打開的清單文件中找到
<Applications>
節點中的<Application>
節點,再找到<uap:VisualElements>
節點。 - 在
<uap:VisualElements>
節點中加入屬性:
AppListEntry = "none"
使用應用服務
要讓客戶端應用能夠使用由服務提供方應用提供的 App Service,需要首先知道服務提供方應用的 PFN 包名(Package Family Name)。
獲取服務提供應用包名
MSDN 文檔上列舉了兩種獲取 PFN 包名的方法:
-
從服務提供應用項目內(例如,從
App.xaml.cs
中的public App()
)調用Windows.ApplicationModel.Package.Current.Id.FamilyName
,可以通過輸出到 Visual Studio 的輸出窗口進行觀察記錄,或展示在應用界面上等。 -
部署解決方案(
生成
>部署解決方案
)並記下輸出窗口中的完整程序包名稱(查看
>輸出
)。 注意部署時輸出的是完整包名,必須從輸出窗口中的字符串中去除平台信息,以獲取 PFN 包名。 例如,如果輸出窗口中呈現的完整程序包名稱為9fe3058b-3de0-4e05-bea7-84a06f0ee4f0_1.0.0.0_x86__yd7nk54bq29ra
,需去除其中的1.0.0.0_x86__
,留下9fe3058b-3de0-4e05-bea7-84a06f0ee4f0_yd7nk54bq29ra
作為所需的 PFN 包名。
還有一種簡單的推薦方法:
- 在解決方案瀏覽器中雙擊
Package.appxmanifest
文件打開清單文件設計器,切換到Packaging
選項卡,查看Package family name
一欄中的值。
編寫調用服務的客戶端應用
創建一個新的 Windows 通用應用項目作為客戶端調用者應用, 打開需要調用 App Service 的代碼文件,在代碼頂部加入以下引用的命名空間:
using Windows.ApplicationModel.AppService;
假設我們的客戶端應用需要通過 App Service 獲得一個新的 GUID 並將其顯示在文本框上,我們現在應用的頁面上添加一個按鈕和一個文本框,並將文本框命名為 textBox
。
在頁面的后台代碼中聲明一個 AppServiceConnection
:
private AppServiceConnection guidService;
然后為按鈕的單擊事件添加以下代碼:
private async void button_Click(object sender, RoutedEventArgs e) { if (guidService == null) { guidService = new AppServiceConnection(); guidService.AppServiceName = "GuidProviderService"; guidService.PackageFamilyName = "0e37a0ad-6f9f-41f6-ac5f-ac93c00b9474_21qyshkbc51y2"; var status = await this.guidService.OpenAsync(); if (status != AppServiceConnectionStatus.Success) { button.Content = "Failed to connect"; return; } } var message = new ValueSet(); message.Add("requestguid", null); AppServiceResponse response = await this.guidService.SendMessageAsync(message); if (response.Status == AppServiceResponseStatus.Success) { if (response.Message != null) textBox.Text = (string) response.Message["guid"]; } }
以上代碼很簡單,首先檢查 AppServiceConnection
實例是否存在,不存在則創建一個新的實例。創建時需要指定目標服務的名稱和提供服務應用的包名,這兩個值和上文在服務提供應用的清單文件中指定的一致。
創建好 AppServiceConnection
的實例,並設置好服務名稱和包名屬性后,調用 OpenAsync()
方法開啟到服務的連接。該方法返回一個 AppServiceConnectionStatus 枚舉指示打開連接的結果。
連接成功建立后,創建一個 ValueSet
並在其中填入一個名為 requestguid
的鍵,該鍵名遵守上文服務中定義的協議規則。由於本示例中服務僅對鍵名進行檢查,鍵值無需填寫。
之后調用 SendMessageAsync()
方法將之前創建的 ValueSet
的作為參數發送到服務,並等待響應。注意該方法是個異步方法,需要攜帶 await 關鍵字進行等待,所以按鈕的單擊事件處理代碼也要添加 async 關鍵字。
服務端發回的響應狀態可以通過 Status
屬性進行檢測,如果枚舉值為 AppServiceResponseStatus.Success
則表示成功響應。接下來只需檢查響應消息是否包含鍵guid
,如果有則讀取其鍵值即可。
如果所有代碼編寫無誤,部署以上兩個應用,並運行客戶端應用,則當點擊按鈕時,客戶端會正確地從服務端獲得一個 GUID 並顯示在文本框上。
調試
要調試客戶端應用很簡單,只需像一般調試 UWP 應用一樣直接在 Visual Studio 中啟動調試即可。或從開始菜單啟動客戶端應用,再通過 Visual Studio 附加調試器到啟動的客戶端進程。(注意進程列表中可能會出現兩個窗口標題為客戶端應用名稱的進程,需選擇應用自身的進程,而非 ApplicationFrameHost.exe
,該進程是 UWP 應用的視圖框架宿主進程。)
應用服務的調試要稍微麻煩一些,步驟如下:
- 確保整個解決方案都已經生成並部署(即服務提供應用和客戶端調用者應用都已生成部署)。
- 打開服務提供應用項目(注意,是應用的項目,不是后台任務的項目)的項目屬性設置,切換到調試(Debug)選項卡,在
啟動動作(Start action)
中勾選 不啟動應用,但在應用啟動時開始調試(Do not launch, but debug my code when it starts)。 - 右鍵單擊服務提供應用項目(注意,是應用的項目,不是后台任務的項目),選擇設置為啟動項目(Set as StartUp Project)。
- 在后台進程的服務代碼中設端點。
- 在當前 Visual Studio 窗口中按下
F5
啟動調試,此時應用應該不會啟動,調試也不會立即啟動,而是等待來自外部對服務的請求激活后台任務后才開始調試。 - 從開始菜單啟動客戶端調用者應用,點擊按鈕觸發對服務的調用,此時 Visual Studio 會開始進行調試,並停在第 4 步中設置的斷點處。
總結
雖然目前看來 UWP 提供的應用間交互能力相對較弱,但借助 App Service 還是能實現很多場景下的應用的。比如服務為其它應用提供一個 token 用於訪問公用資源、服務生成一個一次性的安全 URL 等等。
本博客為個人博客備份用,本文原文地址:http://validvoid.net/uwp-app-service/