创建和托管应用扩展

本文介绍如何创建 Windows 10 应用扩展并将其托管在应用中。 UWP 应用和 打包的桌面应用均支持应用扩展。

为了演示如何创建应用扩展,本文使用 数学扩展代码示例中的包清单 XML 和代码片段。 此示例是 UWP 应用,但示例中演示的功能也适用于打包的桌面应用。 按照以下说明开始使用示例:

  • 下载并解压缩 数学扩展代码示例
  • 在 Visual Studio 2019 中,打开MathExtensionSample.sln。 将生成类型设置为 x86(生成>Configuration Manager,然后将这两个项目 的平台 更改为 x86 )。
  • 部署解决方案: 生成>部署解决方案

应用扩展简介

在 Windows 10 中,应用扩展提供的功能类似于其他平台上的插件、加载项和附加组件的功能。 Windows 10 周年版(版本 1607 版本 10.0.14393)中引入了应用扩展。

应用扩展是 UWP 应用或打包的桌面应用,这些应用具有扩展声明,允许它们与主机应用共享内容和部署事件。 扩展应用可以提供多个扩展。

由于应用扩展只是 UWP 应用或打包的桌面应用,因此它们也可以是功能齐全的应用、主机扩展,并且无需创建单独的应用包即可向其他应用提供扩展。

创建应用扩展主机时,可以创建一个围绕你的应用开发生态系统的机会,其中其他开发人员可以通过你可能没有预期或拥有资源的方式增强你的应用。 请考虑 Microsoft Office 扩展、Visual Studio 扩展、浏览器扩展等。这些扩展为应用程序创造了更丰富的体验,超出了它们原有的功能。 扩展可以为应用增加价值和寿命。

在高级别上,若要设置应用扩展关系,我们需要:

  1. 将应用声明为扩展主机。
  2. 将应用声明为扩展。
  3. 确定是否以应用服务、后台任务或其他方式实现扩展。
  4. 定义主机及其扩展的通信方式。
  5. 使用主机应用中的 Windows.ApplicationModel.AppExtensions API 访问扩展。

让我们看看如何完成此作,方法是检查 数学扩展代码示例 ,该示例实现了一个假设计算器,你可以使用扩展向该计算器添加新函数。 在 Microsoft Visual Studio 2019 中,从代码示例加载 MathExtensionSample.sln

数学扩展代码示例

将应用声明为扩展主机

应用通过在 Package.appxmanifest 文件中声明 <AppExtensionHost> 元素,将自身标识为应用扩展主机。 请参阅 MathExtensionHost 项目中的 Package.appxmanifest 文件,了解如何执行此作。

在 MathExtensionHost 项目中 Package.appxmanifest

<Package
  ...
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
  IgnorableNamespaces="uap uap3 mp">
  ...
    <Applications>
      <Application Id="App" ... >
        ...
        <Extensions>
            <uap3:Extension Category="windows.appExtensionHost">
                <uap3:AppExtensionHost>
                  <uap3:Name>com.microsoft.mathext</uap3:Name>
                </uap3:AppExtensionHost>
          </uap3:Extension>
        </Extensions>
      </Application>
    </Applications>
    ...
</Package>

注意 xmlns:uap3="http://..."中的 uap3IgnorableNamespaces。 这些都是必要的,因为我们使用的是 uap3 命名空间。

<uap3:Extension Category="windows.appExtensionHost"> 将此应用标识为扩展主机。

中的 <uap3:AppExtensionHost> 元素是 扩展协定 名称。 当扩展指定相同的扩展协定名称时,主机将能够找到它。 根据惯例,我们建议使用应用或发布者名称生成扩展协定名称,以避免与其他扩展协定名称发生潜在冲突。

可以在同一应用中定义多个主机和多个扩展。 在此示例中,我们声明一个主机。 该扩展在另一个应用中定义。

将应用声明为扩展

应用通过在 Package.appxmanifest 文件中声明<uap3:AppExtension>元素,将自身标识为应用扩展。 在 MathExtension 项目中打开 Package.appxmanifest 文件,了解此作的完成方式。

在 MathExtension 项目中的 Package.appxmanifest:

<Package
  ...
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
  IgnorableNamespaces="uap uap3 mp">
  ...
    <Applications>
      <Application Id="App" ... >
        ...
        <Extensions>
          ...
          <uap3:Extension Category="windows.appExtension">
            <uap3:AppExtension Name="com.microsoft.mathext"
                               Id="power"
                               DisplayName="x^y"
                               Description="Exponent"
                               PublicFolder="Public">
              <uap3:Properties>
                <Service>com.microsoft.powservice</Service>
              </uap3:Properties>
              </uap3:AppExtension>
          </uap3:Extension>
        </Extensions>
      </Application>
    </Applications>
    ...
</Package>

再次注意 xmlns:uap3="http://..." 行,以及 uap3中的 IgnorableNamespaces。 这是必要的,因为我们使用的是 uap3 命名空间。

<uap3:Extension Category="windows.appExtension"> 将此应用标识为扩展。

属性的含义如下:<uap3:AppExtension>

特征 DESCRIPTION 必选
名称 这是扩展协定名称。 当它匹配主机中声明的 名称 时,该主机将能够找到此扩展。 ✔️
ID 唯一标识此扩展。 由于可能存在使用同一扩展合同名称的多个扩展(例如一个画图应用程序支持多个扩展),因此可以使用 ID 来区分它们。 应用扩展主机可以使用 ID 推断有关扩展类型的内容。 例如,可以将一个扩展设计为桌面,另一个扩展用于移动设备,ID 是区分器。 还可以为此使用下面讨论的 Properties 元素。 ✔️
显示名称 可以通过宿主应用向用户展示扩展。 可以从 新的资源管理系统ms-resource:TokenName)查询,并且可以使用它进行本地化。 本地化内容是从应用扩展包加载的,而不是主机应用。
说明 可用于通过主机应用向用户描述扩展。 可以从 新的资源管理系统ms-resource:TokenName)查询,并且可以使用它进行本地化。 本地化内容是从应用扩展包加载的,而不是主机应用。
PublicFolder 与包根目录相关的文件夹名称,在此文件夹中可以与扩展宿主共享内容。 按照约定,名称为“Public”,但你可以使用与扩展中的文件夹匹配的任何名称。 ✔️

<uap3:Properties> 是一个可选元素,其中包含主机可以在运行时读取的自定义元数据。 在代码示例中,扩展作为应用服务实现,因此主机需要一种方法来获取该应用服务的名称,以便它可以调用它。 应用服务的名称在 <服务> 元素中定义,该元素是我们所定义的(我们可以随意命名)。 代码示例中的主机在运行时查找此属性,以了解应用服务的名称。

决定如何实现扩展。

关于应用扩展的 Build 2016 会话演示如何使用主机和扩展之间共享的公用文件夹。 在此示例中,该扩展由存储在主机调用的公用文件夹中的 JavaScript 文件实现。 这种方法的优点是轻量级,不需要编译,并且可以支持创建默认登陆页面,该页面提供扩展说明和主机应用的Microsoft应用商店页面的链接。 有关详细信息,请参阅 Build 2016 应用扩展代码示例 。 具体而言,请参阅 ExtensibilitySample 项目中的 InvokeLoad() 项目和 ExtensionManager.cs。

在此示例中,我们将使用应用服务实现扩展。 应用服务具有以下优势:

  • 如果扩展崩溃,它不会关闭主机应用,因为主机应用在其自己的进程中运行。
  • 可以使用所选语言来实现服务。 它不必匹配用于实现主机应用的语言。
  • 应用服务有权访问其自己的应用容器,其功能可能与主机具有的功能不同。
  • 服务中的数据与主机应用之间存在隔离。

主机应用程序服务代码

下面是调用扩展的应用服务的主机代码:

MathExtensionHost 项目中的 ExtensionManager.cs

public async Task<double> Invoke(ValueSet message)
{
    if (Loaded)
    {
        try
        {
            // make the app service call
            using (var connection = new AppServiceConnection())
            {
                // service name is defined in appxmanifest properties
                connection.AppServiceName = _serviceName;
                // package Family Name is provided by the extension
                connection.PackageFamilyName = AppExtension.Package.Id.FamilyName;

                // open the app service connection
                AppServiceConnectionStatus status = await connection.OpenAsync();
                if (status != AppServiceConnectionStatus.Success)
                {
                    Debug.WriteLine("Failed App Service Connection");
                }
                else
                {
                    // Call the app service
                    AppServiceResponse response = await connection.SendMessageAsync(message);
                    if (response.Status == AppServiceResponseStatus.Success)
                    {
                        ValueSet answer = response.Message as ValueSet;
                        if (answer.ContainsKey("Result")) // When our app service returns "Result", it means it succeeded
                        {
                            return (double)answer["Result"];
                        }
                    }
                }
            }
        }
        catch (Exception)
        {
             Debug.WriteLine("Calling the App Service failed");
        }
    }
    return double.NaN; // indicates an error from the app service
}

这是用于调用应用服务的典型代码。 有关如何实现和调用应用服务的详细信息,请参阅 如何创建和使用应用服务

需要注意的一点是如何确定要调用的应用服务的名称。 由于主机没有有关扩展实现的信息,因此该扩展需要提供其应用服务的名称。 在代码示例中,该扩展在文件中的 <uap3:Properties> 元素内声明了应用服务的名称:

在 MathExtension 项目中 Package.appxmanifest

    ...
    <uap3:Extension Category="windows.appExtension">
      <uap3:AppExtension ...>
        <uap3:Properties>
          <Service>com.microsoft.powservice</Service>
        </uap3:Properties>
        </uap3:AppExtension>
    </uap3:Extension>

可以在<uap3:Properties>元素中定义自己的 XML。 在这种情况下,我们定义应用服务的名称,使得主机在调用扩展时可以调用它。

当主机加载扩展时,类似代码将从扩展的 Package.appxmanifest 中定义的属性中提取服务的名称:

在 ExtensionManager.cs 中的 mathExtensionHost 项目中 Update()

...
var properties = await ext.GetExtensionPropertiesAsync() as PropertySet;

...
#region Update Properties
// update app service information
_serviceName = null;
if (_properties != null)
{
   if (_properties.ContainsKey("Service"))
   {
       PropertySet serviceProperty = _properties["Service"] as PropertySet;
       this._serviceName = serviceProperty["#text"].ToString();
   }
}
#endregion

主机可以使用存储在_serviceName中的应用服务名称来调用应用服务。

调用应用服务还需要包含应用服务的包的系列名称。 幸运的是,应用扩展 API 提供了这一信息,该信息在行:connection.PackageFamilyName = AppExtension.Package.Id.FamilyName; 中获得。

定义主机和扩展的通信方式

应用服务使用 值集 来交换信息。 作为主机程序的作者,你需要设计一个灵活的协议来与扩展通信。 在代码示例中,这意味着考虑将来可能需要 1、2 或更多参数的扩展。

对于此示例,参数的协议是一个 ValueSet,其中包含名为“Arg”加上参数编号的键值对,例如Arg1Arg2。 主机传递 ValueSet 中的所有参数,扩展使用它所需的参数。 如果扩展能够计算出结果,则主机期望扩展返回的 ValueSet 中有一个名为 Result 的键,其中包含计算值。 如果该键不存在,主机假定扩展无法完成计算。

扩展应用服务代码

在代码示例中,扩展的应用服务未作为后台任务实现。 相反,它使用单个进程应用服务模型,其中应用服务与托管它的扩展应用在同一进程中运行。 这仍然是与主机应用不同的过程,提供进程分离的好处,同时通过避免扩展过程与实现应用服务的后台进程之间的跨进程通信来获得一些性能优势。 请查看,将应用服务转换为与其主机应用程序在同一进程中运行,以了解应用服务在作为后台任务运行与在同一进程中运行之间的差异。

系统在应用服务激活时执行OnBackgroundActivate()。 该代码设置事件处理程序来处理实际应用服务调用(OnAppServiceRequestReceived()),并处理管家事件,例如获取处理取消或关闭事件的延迟对象。

在 MathExtension 项目中的 App.xaml.cs。

protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
    base.OnBackgroundActivated(args);

    if ( _appServiceInitialized == false ) // Only need to setup the handlers once
    {
        _appServiceInitialized = true;

        IBackgroundTaskInstance taskInstance = args.TaskInstance;
        taskInstance.Canceled += OnAppServicesCanceled;

        AppServiceTriggerDetails appService = taskInstance.TriggerDetails as AppServiceTriggerDetails;
        _appServiceDeferral = taskInstance.GetDeferral();
        _appServiceConnection = appService.AppServiceConnection;
        _appServiceConnection.RequestReceived += OnAppServiceRequestReceived;
        _appServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed;
    }
}

执行扩展工作的代码位于 OnAppServiceRequestReceived(). 调用应用服务以执行计算时调用此函数。 它从 ValueSet 中提取所需的值。 如果它可以执行计算,将结果放置在名为 Result的键下,并将其放入返回给主机的 ValueSet 中。 回想一下,根据定义此主机及其扩展通信方式的协议,存在 结果 密钥将指示成功;否则为失败。

在 MathExtension 项目中的 App.xaml.cs。

private async void OnAppServiceRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
    // Get a deferral because we use an awaitable API below (SendResponseAsync()) to respond to the message
    // and we don't want this call to get cancelled while we are waiting.
    AppServiceDeferral messageDeferral = args.GetDeferral();
    ValueSet message = args.Request.Message;
    ValueSet returnMessage = new ValueSet();

    double? arg1 = Convert.ToDouble(message["arg1"]);
    double? arg2 = Convert.ToDouble(message["arg2"]);
    if (arg1.HasValue && arg2.HasValue)
    {
        returnMessage.Add("Result", Math.Pow(arg1.Value, arg2.Value)); // For this sample, the presence of a "Result" key will mean the call succeeded
    }

    await args.Request.SendResponseAsync(returnMessage);
    messageDeferral.Complete();
}

管理扩展

现在,我们已经了解如何实现主机与其扩展之间的关系,让我们看看主机如何查找系统上安装的扩展,并响应添加和删除包含扩展的包。

Microsoft应用商店以包的形式提供扩展。 AppExtensionCatalog 查找包含与主机扩展协定名称匹配的扩展的已安装包,并提供在安装或删除与主机相关的应用扩展包时触发的事件。

在代码示例中,ExtensionManager类(定义在 MathExtensionHost 项目的ExtensionManager.cs中)封装了用于加载扩展及响应扩展包安装和卸载的逻辑。

ExtensionManager 构造函数使用 AppExtensionCatalog 查找系统中具有与主机相同扩展协定名称的应用扩展:

ExtensionManager.cs 在 MathExtensionHost 项目中。

public ExtensionManager(string extensionContractName)
{
   // catalog & contract
   ExtensionContractName = extensionContractName;
   _catalog = AppExtensionCatalog.Open(ExtensionContractName);
   ...
}

安装扩展包时,ExtensionManager 会收集包中扩展的信息,这些扩展的扩展协定名称与主机相同。 安装可能表示更新,在这种情况下,受影响的扩展的信息会更新。 卸载扩展包后,会 ExtensionManager 删除有关受影响扩展的信息,以便用户知道哪些扩展不再可用。

Extension为代码示例创建了类(在 MathExtensionHost 项目中的 ExtensionManager.cs中定义),用于访问扩展的 ID、说明、徽标和特定于应用的信息,例如用户是否启用了扩展。

假设扩展已加载(见Load()ExtensionManager.cs),这意味着包状态良好,并且我们获取了其 ID、徽标、说明和公用文件夹(本示例中未使用它),只是为了说明如何获取它。 扩展包本身未加载。

卸载的概念用于跟踪不应再向用户显示哪些扩展。

ExtensionManager 提供了一个 Extension 实例的集合,以便扩展、其名称、描述和徽标可与用户界面数据绑定。 ExtensionsTab 页绑定到此集合,并提供用于启用/禁用扩展以及删除扩展的 UI。

“扩展”选项卡示例 UI

删除扩展后,系统会提示用户验证是否要卸载包含扩展的包(可能包含其他扩展)。 如果用户同意,则会卸载该包,并从 ExtensionManager 主机应用可用的扩展列表中删除卸载的包中的扩展。

卸载 UI

调试应用扩展和主机

通常,扩展宿主和扩展程序并非同一解决方案的一部分。 在这种情况下,若要调试主机和扩展,请执行以下操作:

  1. 在 Visual Studio 的一个实例中加载主机项目。
  2. 在另一个 Visual Studio 实例中加载扩展。
  3. 在调试器中启动主机应用。
  4. 在调试器中启动扩展应用。 如果您想部署扩展而不是调试它,以测试主机的包安装事件,请改用 生成 > 部署解决方案

现在,你将能够在主程序和扩展中设置断点。 如果开始调试扩展应用本身,你将看到应用的空白窗口。 如果不想看到空白窗口,可以将扩展项目的调试设置更改为不启动应用程序,而是在应用程序启动后进行调试(右键单击扩展项目,属性>调试>,选择 不启动,但在启动时调试我的代码)。您仍需要启动扩展项目的调试(F5),但调试器会等待,直到主机激活扩展,然后命中扩展中的断点。

调试代码示例

在代码示例中,主机和扩展位于同一解决方案中。 执行以下步骤进行调试:

  1. 确保 MathExtensionHost 是启动项目(右键单击 MathExtensionHost 项目,单击“ 设置为启动项目”)。
  2. Invoke 项目中,将断点设置在 ExtensionManager.cs 的
  3. F5 运行 MathExtensionHost 项目。
  4. OnAppServiceRequestReceived 项目的 App.xaml.cs 中,将断点设置在
  5. 开始调试 MathExtension 项目(右键单击 MathExtension 项目,调试 > 启动新实例),这将部署它并触发主机中的安装程序事件。
  6. MathExtensionHost 应用中,导航到 “计算 ”页,然后单击 x^y 激活扩展。 首先命中 Invoke() 断点,你会看到扩展应用服务调用正在进行。 然后, OnAppServiceRequestReceived() 扩展中的方法命中,可以看到应用服务计算结果并返回结果。

故障排除作为应用服务实现的扩展

如果扩展主机在连接您的扩展的应用服务时遇到问题,请确保 <uap:AppService Name="..."> 属性与您在 <Service> 元素中指定的内容匹配。 如果它们不匹配,则扩展提供的服务名称与实现的应用服务名称不匹配,并且主机将无法激活扩展。

在 MathExtension 项目中的 Package.appxmanifest:

<Extensions>
   <uap:Extension Category="windows.appService">
     <uap:AppService Name="com.microsoft.sqrtservice" />      <!-- This must match the contents of <Service>...</Service> -->
   </uap:Extension>
   <uap3:Extension Category="windows.appExtension">
     <uap3:AppExtension Name="com.microsoft.mathext" Id="sqrt" DisplayName="Sqrt(x)" Description="Square root" PublicFolder="Public">
       <uap3:Properties>
         <Service>com.microsoft.powservice</Service>   <!-- this must match <uap:AppService Name=...> -->
       </uap3:Properties>
     </uap3:AppExtension>
   </uap3:Extension>
</Extensions>   

要测试的基本方案的清单

当您构建一个扩展主机并准备好测试其对扩展的支持效果时,以下是一些基本场景可供尝试:

  • 运行主机,然后部署扩展应用
    • 主机是否会在正在运行时识别新的扩展?
  • 部署扩展应用,然后部署并运行主机。
    • 主机是否识别已有的扩展?
  • 运行主机,然后删除扩展应用。
    • 主机是否正确检测到删除?
  • 运行主机,然后将扩展应用更新到较新版本。
    • 主机是否能正确检测到更改并卸载旧版本的扩展?

要测试的高级方案:

  • 运行主机,将扩展应用移动到可移动媒体,删除媒体
    • 主机是否检测到包状态更改并禁用扩展?
  • 运行主机,然后损坏扩展应用(使其无效、签名方式不同等)
    • 主机是否检测到被篡改的扩展并正确处理?
  • 运行主机,然后部署具有无效内容或属性的扩展应用
    • 主机是否检测到无效内容并正确处理它?

设计注意事项

  • 提供 UI,向用户显示哪些扩展可用,并允许它们启用/禁用它们。 还可以考虑为因包下线等而变得不可用的扩展添加图标。
  • 将用户定向到可获取扩展的位置。 也许你的扩展页面可以提供一个Microsoft应用商店搜索查询,该查询显示可用于应用的扩展列表。
  • 请考虑如何通知用户添加和删除扩展。 可以在安装新扩展时为其创建通知,并邀请用户启用它。 默认情况下应禁用扩展,以便用户处于控制状态。

应用扩展与可选包有何不同

可选包和应用扩展之间的主要区别在于开放生态系统与封闭生态系统,以及依赖包与独立包。

应用扩展参与开放生态系统。 如果你的应用可以托管应用扩展,只要它们符合你从扩展传递/接收信息的方法,任何人都可以为主机编写扩展。 这不同于参与封闭生态系统的可选包,其中发布者决定允许谁创建可用于应用的可选包。

应用扩展是独立的包,可以是独立应用。 它们不能对另一个应用具有部署依赖项。 可选包需要主包,不能在没有主包的情况下运行。

游戏的扩展包是作为可选包的一个理想候选项,因为它紧密绑定于游戏,无法独立运行,并且你可能不希望扩展包由生态系统中的任何开发人员随意创建。

如果同一游戏具有可自定义的 UI 加载项或主题设置,则应用扩展可能是一个不错的选择,因为提供该扩展的应用可以自行运行,并且任何第三方都可以进行。

注解

本主题介绍应用扩展。 要注意的要点是创建主机并将其标记为 Package.appxmanifest 文件、创建扩展并将其标记为 Package.appxmanifest 文件、确定如何实现扩展(例如应用服务、后台任务或其他方式),定义主机如何与扩展通信, 并使用 AppExtensions API 访问和管理扩展。