创建和托管应用扩展

本文向你介绍如何创建 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 办公室扩展、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://..." uap3 IgnorableNamespaces。 这些都是必要的,因为我们使用的是 uap3 命名空间。

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

“Name元素<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://..." 行,以及 IgnorableNamespaces 中存在的 uap3。 这是必要的,因为我们使用的是 uap3 命名空间。

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

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

属性 说明 必需
Name 这是扩展协定名称。 当它与主机中声明的 Name 匹配时,该主机能够找到此扩展。
ID 唯一地标识此扩展。 由于可能有多个扩展使用相同的扩展协定名称(想象一个支持多个扩展的绘图应用),因此可以使用 ID 来区分它们。 应用扩展主机可以使用 ID 来推断有关扩展类型的信息。 例如,可以有一个为桌面设计的扩展,以及另一个为移动设备设计的扩展,其中 ID 是区分因素。 还可以为此使用下面讨论的 Properties 元素。
DisplayName 可在主机应用中用于向用户标识扩展。 它可以从新的资源管理系统 (ms-resource:TokenName) 中查询,并可使用该系统进行本地化。 本地化内容是从应用扩展包加载的,而不是从主机应用。
描述 可在主机应用中用于向用户描述扩展。 它可以从新的资源管理系统 (ms-resource:TokenName) 中查询,并可使用该系统进行本地化。 本地化内容是从应用扩展包加载的,而不是从主机应用。
PublicFolder 相对于包根目录的文件夹名称,可在其中与扩展主机共享内容。 按照惯例,名称为“Public”,但可以使用与扩展中的文件夹匹配的任何名称。

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

决定如何实现扩展。

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

Update() 在 MathExtensionHost 项目内的 ExtensionManager.cs 中

...
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;

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

应用服务使用 ValueSet 交换信息。 作为主机的作者,你需要想出一个协议来与灵活扩展通信。 在代码示例中,这意味着考虑将来可能需要 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 中提取所需的值。 如果它可以执行计算,则将结果放在返回给主机的 ValueSet 中名为 Result 的键下。 回想一下,根据定义此主机及其扩展通信方式的协议,结果密钥的存在将指示成功;否则失败。

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 函数在系统上查找与主机具有相同扩展协定名称的应用扩展:

mathExtensionHost 项目中的ExtensionManager.cs。

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

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

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

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

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

提供 ExtensionManager 集合 Extension 实例,以便扩展、其名称、说明和徽标可以绑定到 UI。 ExtensionsTab 页绑定到此集合,并提供用于启用/禁用扩展以及删除扩展的 UI。

“扩展”选项卡示例 UI

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

卸载 UI

调试应用扩展和主机

通常,扩展主机和扩展不属于同一解决方案。 在这种情况下,若要调试主机和扩展,请执行以下操作:

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

现在,你将能够在主机和扩展中命中断点。 如果开始调试扩展应用本身,你将看到应用的空白窗口。 如果你不希望看到空白窗口,则可以更改扩展项目的调试设置,从而不启动应用,但改为在启动时调试应用(右键单击扩展项目,选择“属性”>“调试”> 选择“不启动,但在启动时调试代码”)。你仍然需要开始调试 (F5) 扩展项目,但它将等到主机激活扩展,然后将命中扩展中的断点。

调试代码示例

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

  1. 确保 MathExtensionHost 是启动项目(右键单击 MathExtensionHost 项目,单击“ 设置为启动项目”)。
  2. MathExtensionHost 项目中的 ExtensionManager.cs 中放置断点Invoke
  3. F5 运行 MathExtensionHost 项目。
  4. MathExtension 项目中的 App.xaml.cs中放置断点OnAppServiceRequestReceived
  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 访问和管理扩展。