在 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 等等。