创建和使用应用服务

重要

本主题中的代码列表仅限 C#。 对于使用 C++/WinRT 以及 C# 的应用服务示例,请参阅应用服务示例应用

应用服务是可向其他 UWP 应用提供服务的 UWP 应用。 它们与设备上的 Web 服务类似。 应用服务作为后台任务在主机应用中运行,并可向其他应用提供其服务。 例如,应用服务可能会提供其他应用可能使用的条形码扫描仪服务。 应用的企业套件中可能有一个通用的拼写检查应用服务,该服务可供套件中的其他应用使用。 应用服务允许你创建应用可在同一设备上调用的无 UI 服务,从 Windows 10 版本 1607 开始,应用可在远程设备上调用这些服务。

从 Windows 10 版本 1607 开始,可以创建与主机应用在同一进程中运行的应用程序服务。 本文重点介绍如何创建和使用在单独的后台进程中运行的应用服务。 有关在与提供程序相同的进程中运行应用服务的更多详细信息,请参阅 “转换应用服务”以与其主机应用 在同一进程中运行。

创建新的应用服务提供程序项目

在此操作指南中,我们将创建一个解决方案中的所有内容,以便简单起见。

  1. 在 Visual Studio 2015 或更高版本中,创建一个新的 UWP 应用项目,并将其命名为 AppServiceProvider

    1. 依次选择“文件”>“新建”>“项目...”
    2. 在“创建新项目”对话框中,选择“空白应用(通用 Windows) C#”。 这是使应用服务可供其他 UWP 应用使用的应用。
    3. 单击“下一步”,将项目命名为“AppServiceProvider”,为其选择一个位置,然后单击“创建”
  2. 在系统要求为项目选择“目标”和“最低版本”时,请至少选择 10.0.14393。 如果要使用新的 SupportsMultipleInstances 属性,则必须使用 Visual Studio 2017 或 Visual Studio 2019,并且目标为 10.0.15063Windows 10 创意者更新) 或更高版本。

将应用服务扩展添加到 Package.appxmanifest

在“AppServiceProvider”项目中,在文本编辑器中打开 Package.appxmanifest 文件

  1. 在“解决方案资源管理器”中右键单击它
  2. 选择“打开方式”
  3. 选择“XML (文本)编辑器”

<Application> 元素中添加以下 AppService 扩展。 此示例播发服务 com.microsoft.inventory ,并将此应用标识为应用服务提供商。 实际服务将作为后台任务实现。 应用服务项目向其他应用公开该服务。 建议对服务名称使用反向域名样式。

请注意, xmlns:uap4 仅当你面向 Windows SDK 版本 10.0.15063 或更高版本时,命名空间前缀和 uap4:SupportsMultipleInstances 属性才有效。 如果面向较旧的 SDK 版本,则可以安全地删除它们。

注意

对于使用 C++/WinRT 以及 C# 的应用服务示例,请参阅应用服务示例应用

<Package
    ...
    xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
    xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4"
    ...
    <Applications>
        <Application Id="AppServiceProvider.App"
          Executable="$targetnametoken$.exe"
          EntryPoint="AppServiceProvider.App">
          ...
          <Extensions>
            <uap:Extension Category="windows.appService" EntryPoint="MyAppService.Inventory">
              <uap3:AppService Name="com.microsoft.inventory" uap4:SupportsMultipleInstances="true"/>
            </uap:Extension>
          </Extensions>
          ...
        </Application>
    </Applications>

Category 属性将此应用程序识别为应用服务提供程序。

EntryPoint 属性可识别实现服务的命名空间限定类,我们将在下一步实现它。

SupportsMultipleInstances 属性指示每次调用应用服务时,它应在新进程中运行。 这不是必需的,但如果需要该功能,并且面向 10.0.15063 SDK(Windows 10 创意者更新)或更高版本,可以使用此功能。 它还应以 uap4 命名空间为前缀。

创建应用服务

  1. 应用服务可以作为后台任务实现。 这样,前台应用程序就可以在另一个应用程序中调用应用服务。 若要将应用服务创建为后台任务,请将新的 Windows 运行时组件项目添加到名为 MyAppService 的解决方案(“文件”>“添加”>“新建项目”)。 在“添加新项目”对话框中,依次选择“已安装”>“Visual C#”>“Windows 运行时组件(通用 Windows)”

  2. 在 AppServiceProvider 项目中,向新的 MyAppService 项目添加项目到项目的引用(在“解决方案资源管理器”中,右键单击“AppServiceProvider”项目>“添加”>“引用”>“项目”>“解决方案”,选择“MyAppService”>“确定”)。 此步骤至关重要,因为如果不添加引用,应用服务将不会在运行时连接。

  3. 在 MyAppService 项目中,将以下 using 语句添加到 Class1.cs 的顶部

    using Windows.ApplicationModel.AppService;
    using Windows.ApplicationModel.Background;
    using Windows.Foundation.Collections;
    
  4. 将 Class1.cs 重命名为 Inventory.cs,并将 Class1 的存根区域代码替换为名为 Inventory 的新后台任务类

    public sealed class Inventory : IBackgroundTask
    {
        private BackgroundTaskDeferral backgroundTaskDeferral;
        private AppServiceConnection appServiceconnection;
        private String[] inventoryItems = new string[] { "Robot vacuum", "Chair" };
        private double[] inventoryPrices = new double[] { 129.99, 88.99 };
    
        public void Run(IBackgroundTaskInstance taskInstance)
        {
            // Get a deferral so that the service isn't terminated.
            this.backgroundTaskDeferral = taskInstance.GetDeferral();
    
            // Associate a cancellation handler with the background task.
            taskInstance.Canceled += OnTaskCanceled;
    
            // Retrieve the app service connection and set up a listener for incoming app service requests.
            var details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
            appServiceconnection = details.AppServiceConnection;
            appServiceconnection.RequestReceived += OnRequestReceived;
        }
    
        private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            // This function is called when the app service receives a request.
        }
    
        private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            if (this.backgroundTaskDeferral != null)
            {
                // Complete the service deferral.
                this.backgroundTaskDeferral.Complete();
            }
        }
    }
    

    此类是应用服务将执行其工作的地方。

    创建后台任务时会调用 Run。 由于后台任务在运行完成后终止,因此代码会发出延迟,以便后台任务能够继续处理请求。 作为后台任务实现的应用服务在收到调用后将保持活动状态约 30 秒,除非该时间范围内再次调用该应用服务或延迟。如果在与调用方相同的进程中实现应用服务,则应用服务的生存期与调用方生存期相关联。

    应用服务的生存期取决于调用方:

    • 如果调用方在前台,则应用服务生命周期与调用方的生命周期相同。
    • 如果调用方在后台,则应用服务将获得 30 秒的运行时间。 退出延迟提供另外一次 5 秒。

    取消任务时会调用 OnTaskCanceled。 当出现以下情形时会取消该任务:客户端应用释放 AppServiceConnection,客户端应用暂停,操作系统关闭或处于睡眠状态,或操作系统耗尽用于运行该任务的资源。

编写应用服务的代码

OnRequestReceived 是用于应用服务的代码将前往的位置。 将 MyAppService 的 Inventory.cs 中的存根 OnRequestReceived 替换为本示例中的代码。 此代码获取清单项的索引,并将其连同命令字符串一起传递给服务,以检索指定库存项的名称和价格。 对于你自己的项目,请添加错误处理代码。

private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
    // Get a deferral because we use an awaitable API below to respond to the message
    // and we don't want this call to get canceled while we are waiting.
    var messageDeferral = args.GetDeferral();

    ValueSet message = args.Request.Message;
    ValueSet returnData = new ValueSet();

    string command = message["Command"] as string;
    int? inventoryIndex = message["ID"] as int?;

    if (inventoryIndex.HasValue &&
        inventoryIndex.Value >= 0 &&
        inventoryIndex.Value < inventoryItems.GetLength(0))
    {
        switch (command)
        {
            case "Price":
            {
                returnData.Add("Result", inventoryPrices[inventoryIndex.Value]);
                returnData.Add("Status", "OK");
                break;
            }

            case "Item":
            {
                returnData.Add("Result", inventoryItems[inventoryIndex.Value]);
                returnData.Add("Status", "OK");
                break;
            }

            default:
            {
                returnData.Add("Status", "Fail: unknown command");
                break;
            }
        }
    }
    else
    {
        returnData.Add("Status", "Fail: Index out of range");
    }

    try
    {
        // Return the data to the caller.
        await args.Request.SendResponseAsync(returnData);
    }
    catch (Exception e)
    {
        // Your exception handling code here.
    }
    finally
    {
        // Complete the deferral so that the platform knows that we're done responding to the app service call.
        // Note for error handling: this must be called even if SendResponseAsync() throws an exception.
        messageDeferral.Complete();
    }
}

请注意,OnRequestReceived 为 async,因为我们在此示例中对 SendResponseAsync 进行了可等待的方法调用

将使用延迟,以便该服务可以在 OnRequestReceived 处理程序中使用 async 方法。 它可以确保对 OnRequestReceived 的调用不会在完成处理消息之前结束SendResponseAsync 将结果发送到调用方。 SendResponseAsync 不指示调用完成。 延迟完成时会发信号给 SendMessageAsync,以表明 OnRequestReceived 已完成。 对 SendResponseAsync 的调用包装在 try/finally 块中,因为即使 SendResponseAsync 引发异常,也必须完成延迟

应用服务使用 ValueSet 对象交换信息。 可以传递的数据大小仅受系统资源的限制。 ValueSet没有预定义的密钥。 必须确定将用于定义应用服务的协议的键值。 调用方必须考虑到该协议。 在此示例中,我们选择了一个名为的键,该键 Command 具有一个值,该值指示我们是否希望应用服务提供库存项的名称或其价格。 清单名称的索引存储在密钥下 ID 。 返回值存储在密钥下 Result

AppServiceClosedStatus 枚举将返回到调用方,以指示对应用服务的调用是成功还是失败。 例如,如果 OS 中止服务终结点,因为它的资源已超出,因此对应用服务的调用可能会失败。 可以通过 ValueSet 返回其他错误信息。 在此示例中,我们使用名为 Status 的键将更详细的错误信息返回给调用方。

对 SendResponseAsync 的调用ValueSet 返回到调用方。

部署服务应用并获取包系列名称

必须先部署应用服务提供程序,才可以通过客户端调用它。 可以通过在 Visual Studio 中选择“生成”>“部署解决方案”来部署它

还需要应用服务提供程序的包系列名称,以便调用它。 可以通过在设计器视图中打开 AppServiceProvider 项目的 Package.appxmanifest 文件来获取它(在“解决方案资源管理器”中双击它)。 选择“打包”选项卡,复制“包系列名称”旁边的值,并暂时将它粘贴到某个位置(例如记事本)

编写客户端以调用应用服务

  1. 使用 文件 > 添加新 > 项目将新的空白 Windows 通用应用项目添加到解决方案。 在“添加新项目”对话框中,依次选择“已安装”>“Visual C#”>“空白应用(通用 Windows)”,然后将它命名为“ClientApp”

  2. 在 ClientApp 项目中,将以下 using 语句添加到 MainPage.xaml.cs 的顶部

    using Windows.ApplicationModel.AppService;
    
  3. 将名为“textBox”的文本框和一个按钮添加到 MainPage.xaml

  4. 为名为“button_Click”的按钮添加按钮单击处理程序,并将关键字 async 添加到按钮处理程序的签名

  5. 将按钮单击处理程序的存根替换为以下代码。 请务必包含 inventoryService 字段声明。

    private AppServiceConnection inventoryService;
    
    private async void button_Click(object sender, RoutedEventArgs e)
    {
       // Add the connection.
       if (this.inventoryService == null)
       {
           this.inventoryService = new AppServiceConnection();
    
           // Here, we use the app service name defined in the app service 
           // provider's Package.appxmanifest file in the <Extension> section.
           this.inventoryService.AppServiceName = "com.microsoft.inventory";
    
           // Use Windows.ApplicationModel.Package.Current.Id.FamilyName 
           // within the app service provider to get this value.
           this.inventoryService.PackageFamilyName = "Replace with the package family name";
    
           var status = await this.inventoryService.OpenAsync();
    
           if (status != AppServiceConnectionStatus.Success)
           {
               textBox.Text= "Failed to connect";
               this.inventoryService = null;
               return;
           }
       }
    
       // Call the service.
       int idx = int.Parse(textBox.Text);
       var message = new ValueSet();
       message.Add("Command", "Item");
       message.Add("ID", idx);
       AppServiceResponse response = await this.inventoryService.SendMessageAsync(message);
       string result = "";
    
       if (response.Status == AppServiceResponseStatus.Success)
       {
           // Get the data  that the service sent to us.
           if (response.Message["Status"] as string == "OK")
           {
               result = response.Message["Result"] as string;
           }
       }
    
       message.Clear();
       message.Add("Command", "Price");
       message.Add("ID", idx);
       response = await this.inventoryService.SendMessageAsync(message);
    
       if (response.Status == AppServiceResponseStatus.Success)
       {
           // Get the data that the service sent to us.
           if (response.Message["Status"] as string == "OK")
           {
               result += " : Price = " + response.Message["Result"] as string;
           }
       }
    
       textBox.Text = result;
    }
    

    将行this.inventoryService.PackageFamilyName = "Replace with the package family name";中的包系列名称替换为在部署服务应用中获取AppServiceProvider 项目的包系列名称,并获取包系列名称

    注意

    请确保粘贴字符串文本,而不是将其放在变量中。 如果使用变量,它将不起作用。

    代码首先与应用服务建立连接。 连接将保持打开状态,直到释放 this.inventoryService。 应用服务名必须与添加到 AppServiceProvider 项目的 Package.appxmanifest 文件的 AppService 元素的 Name 属性相匹配。 在此示例中,它是 <uap3:AppService Name="com.microsoft.inventory"/>

    创建了名为 messageValueSet,以指定想要发送到应用服务的命令。 示例应用服务需要一个命令来指示要采取的两项操作中的哪一项。 我们从客户端应用中的文本框获取索引,然后通过 Item 命令调用该服务,以获取项目描述。 然后,我们使用命令进行调用 Price 以获取项目的价格。 按钮文本设置为结果。

    由于 AppServiceResponseStatus 仅指示操作系统是否能够将调用连接到应用服务,因此我们检查Status从应用服务收到的 ValueSet 中的密钥,以确保它能够满足请求。

  6. 将 ClientApp 项目设置为启动项目(在“解决方案资源管理器”>“设置为启动项目”中右键单击它),并运行该解决方案。 在文本框中输入数字 1,然后单击按钮。 应从服务中获取“Chair : Price = 88.99”。

    显示椅子价格的示例应用=88.99

如果应用服务调用失败,请检查 ClientApp 项目中的以下内容

  1. 验证分配给库存服务连接的包系列名称是否匹配 AppServiceProvider 应用的包系列名称。 请查看 button_Click 中 this.inventoryService.PackageFamilyName = "..."; 的行
  2. 在 button_Click 中,验证分配给库存服务连接的应用服务名称是否匹配 AppServiceProvider 的 Package.appxmanifest 文件中的应用服务名称。 请参阅: this.inventoryService.AppServiceName = "com.microsoft.inventory";.
  3. 确保已部署 AppServiceProvider 应用。 (在“解决方案资源管理器”中,右键单击解决方案,然后选择“部署解决方案”)。

调试应用服务

  1. 确保在调试之前部署解决方案,因为必须先部署应用服务提供商应用,然后才能调用服务。 (在 Visual Studio 中,依次单击“生成”>“部署解决方案”)。
  2. 在“解决方案资源管理器”中,右键单击 AppServiceProvider 项目,然后选择“属性”。 在“调试”选项卡上,将“开始操作”更改为“不启动,但在开始时调试我的代码”。 (请注意,如果使用C++从 将启动应用程序更改为“否”的“调试”选项卡。
  3. 在 MyAppService 项目的 Inventory.cs 文件中,在 OnRequestReceived 中设置断点
  4. 将 AppServiceProvider 项目设置为启动项目,并按 F5
  5. 从“开始”菜单(而不是从 Visual Studio 中)启动 ClientApp
  6. 在文本框中输入数字 1,然后按按钮。 调试器将在应用服务中的断点上停止应用服务调用。

调试客户端

  1. 按照前面的步骤中的说明调试调用应用服务的客户端。
  2. 从“开始”菜单启动 ClientApp
  3. 将调试程序附加到 ClientApp.exe 进程(而不是 ApplicationFrameHost.exe 进程)。 (在 Visual Studio 中,依次选择“调试”>“附加到进程...”。)
  4. 在 ClientApp 项目的 button_Click 中设置断点
  5. 在 ClientApp 的文本框中输入数字 1 并单击按钮时,将立刻命中客户端和应用服务中的断点

常规应用服务故障排除

如果在尝试连接到应用服务后遇到 AppUnavailable 状态,请检查以下各项

  • 确保部署应用服务提供商项目和应用服务项目。 两者都需要在运行客户端之前进行部署,因为否则客户端将没有任何要连接到的任何内容。 可以使用生成>部署解决方案从 Visual Studio 进行部署。
  • 在解决方案资源管理器中,确保应用服务提供程序项目具有对项目的项目间引用以实现应用服务
  • 验证 <Extensions> 项及其子元素是否已添加至属于应用服务提供程序项目的 Package.appxmanifest 文件,如上文中将应用服务扩展添加到 Package.appxmanifest 所述
  • 确保调用应用服务提供程序的客户端中的 AppServiceConnection.AppServiceName 字符串与应用服务提供程序项目的 Package.appxmanifest 文件中指定的 <uap3:AppService Name="..." /> 匹配
  • 确保 AppServiceConnection.PackageFamilyName 与应用服务提供程序组件的包系列名称匹配,如上文中将应用服务扩展添加到 Package.appxmanifest 所述
  • 对于进程外应用服务,如本示例中的服务,请验证应用服务提供程序项目 Package.appxmanifest 文件的 <uap:Extension ...> 元素中指定的 EntryPoint 是否与在应用服务项目中实现 IBackgroundTask 的公共类的命名空间和类名称匹配

调试疑难解答

如果调试器未在应用服务提供商或应用服务项目中的断点处停止,请检查以下各项:

  • 确保部署应用服务提供商项目和应用服务项目。 两者都需要在运行客户端之前进行部署。 可以使用生成>部署解决方案从 Visual Studio 部署它们。
  • 确保将想要调试的项目设置为启动项目,并且将该项目的调试属性设置为按 F5 时不运行该项目。 右键单击项目,然后单击“属性”,然后单击“调试”(或C++中的调试)。 在 C# 中,将 “开始”操作 更改为 “不启动”,但在启动代码时调试代码。 在C++中,将启动应用程序设置为“否”。

注解

此示例介绍了如何创建作为后台任务运行的应用服务,并从另一个应用调用它。 需要注意的重要事项如下:

  • 创建用于托管应用服务的后台任务。
  • windows.appService 扩展添加到应用服务提供程序的 Package.appxmanifest 文件
  • 获取应用服务提供程序的包系列名称,以便可以从客户端应用程序连接到该服务提供程序。
  • 将应用服务提供程序项目中的项目到项目引用添加到应用服务项目。
  • 使用 Windows.ApplicationModel.AppService.AppServiceConnection 调用服务。

MyAppService 的完整代码

using System;
using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.Foundation.Collections;

namespace MyAppService
{
    public sealed class Inventory : IBackgroundTask
    {
        private BackgroundTaskDeferral backgroundTaskDeferral;
        private AppServiceConnection appServiceconnection;
        private String[] inventoryItems = new string[] { "Robot vacuum", "Chair" };
        private double[] inventoryPrices = new double[] { 129.99, 88.99 };

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            // Get a deferral so that the service isn't terminated.
            this.backgroundTaskDeferral = taskInstance.GetDeferral();

            // Associate a cancellation handler with the background task.
            taskInstance.Canceled += OnTaskCanceled;

            // Retrieve the app service connection and set up a listener for incoming app service requests.
            var details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
            appServiceconnection = details.AppServiceConnection;
            appServiceconnection.RequestReceived += OnRequestReceived;
        }

        private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            // Get a deferral because we use an awaitable API below to respond to the message
            // and we don't want this call to get canceled while we are waiting.
            var messageDeferral = args.GetDeferral();

            ValueSet message = args.Request.Message;
            ValueSet returnData = new ValueSet();

            string command = message["Command"] as string;
            int? inventoryIndex = message["ID"] as int?;

            if (inventoryIndex.HasValue &&
                 inventoryIndex.Value >= 0 &&
                 inventoryIndex.Value < inventoryItems.GetLength(0))
            {
                switch (command)
                {
                    case "Price":
                        {
                            returnData.Add("Result", inventoryPrices[inventoryIndex.Value]);
                            returnData.Add("Status", "OK");
                            break;
                        }

                    case "Item":
                        {
                            returnData.Add("Result", inventoryItems[inventoryIndex.Value]);
                            returnData.Add("Status", "OK");
                            break;
                        }

                    default:
                        {
                            returnData.Add("Status", "Fail: unknown command");
                            break;
                        }
                }
            }
            else
            {
                returnData.Add("Status", "Fail: Index out of range");
            }

            // Return the data to the caller.
            await args.Request.SendResponseAsync(returnData);

            // Complete the deferral so that the platform knows that we're done responding to the app service call.
            // Note for error handling: this must be called even if SendResponseAsync() throws an exception.
            messageDeferral.Complete();
        }


        private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
        {
            if (this.backgroundTaskDeferral != null)
            {
                // Complete the service deferral.
                this.backgroundTaskDeferral.Complete();
            }
        }
    }
}