Создание и использование расширений для UWP-приложений с помощью App Services

в 12:53, , рубрики: .net, C#, Universal Windows Platform, uwp, расширения

image

В недавнем Anniversary Update появилась такая замечательная вещь, как App Extensions. К сожалению, на данный момент из документации по ней есть только одно видео и пара GitHub-репозиториев. Но я смог собрать всю нужную информацию по использованию этой возможности, и сейчас расскажу, как можно написать расширяемое приложение.

И да, вам понадобится SDK версии не ниже 14393.

Как это будет работать

У нас будет одно host-приложение, к которому будут подключаться расширения. Каждое расширение будет содержать сервис (App Service), с помощью которого приложение будет взаимодействовать с расширением.

Немного о сервисах

В UWP-приложении вы не можете просто взять, и подключить динамическую библиотеку на лету. Это сделано в целях безопасности. Вместо этого, вы можете общаться с другим приложением с помощью простых типов данных (их список очень ограничен). Это приложение должно объявить о том, что у него есть сервис, с которым можно общаться, и это объявление пишется в манифесте приложения (Package.appxmanifest). Всё общение происходит при помощи подключения к сервису, отправки ему сообщений, и получения ответа. И сообщения, и ответы передаются с помощью ValueSet (по сути это просто Dictionary<string, object>), и, как уже говорилось ранее, в качестве значений там могут быть только простейшие типы данных (числа, строки, массивы).

Итак, приступаем.

Создание host-приложения

Для удобства все проекты будут размещены в одном решении. Открываем Visual Studio и создаем пустое UWP-приложение с минимальной версией 14393. Я назову его Host.

image

Теперь нам нужно подредактировать манифест. Открываем Package.appxmanifest в режиме кода, и для начала находим <Package, добавляем новый namespace: xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" и дописываем uap3 в IgnorableNamespaces. В результате должно получиться что-то вроде этого:

<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" 
  IgnorableNamespaces="uap mp uap3">

Дальше ищем <Application> и внутрь него добавляем следующее:

<Extensions>
  <uap3:Extension Category="windows.appExtensionHost">
    <uap3:AppExtensionHost>
      <uap3:Name>com.extensions.myhost</uap3:Name>
    </uap3:AppExtensionHost>
  </uap3:Extension>
</Extensions>

Тут мы объявляем новый хост для расширений. Именно по этому имени расширения будут подключаться, а мы будем их искать. Студия может начать ругаться, ничего страшного в этом нет.

Результат

<?xml version="1.0" encoding="utf-8"?>

<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" 
  IgnorableNamespaces="uap mp uap3">

  <Identity
    Name="9df790c4-956b-400b-8710-08a834e39c5a"
    Publisher="CN=acede"
    Version="1.0.0.0" />

  <mp:PhoneIdentity PhoneProductId="9df790c4-956b-400b-8710-08a834e39c5a" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>

  <Properties>
    <DisplayName>Host</DisplayName>
    <PublisherDisplayName>acede</PublisherDisplayName>
    <Logo>AssetsStoreLogo.png</Logo>
  </Properties>

  <Dependencies>
    <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
  </Dependencies>

  <Resources>
    <Resource Language="x-generate"/>
  </Resources>

  <Applications>
    <Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="Host.App">
      <Extensions>
        <uap3:Extension Category="windows.appExtensionHost">
          <uap3:AppExtensionHost>
            <uap3:Name>com.extensions.myhost</uap3:Name>
          </uap3:AppExtensionHost>
        </uap3:Extension>
      <uap:VisualElements
        DisplayName="Host"
        Square150x150Logo="AssetsSquare150x150Logo.png"
        Square44x44Logo="AssetsSquare44x44Logo.png"
        Description="Host"
        BackgroundColor="transparent">
        <uap:DefaultTile Wide310x150Logo="AssetsWide310x150Logo.png"/>
        <uap:SplashScreen Image="AssetsSplashScreen.png" />
      </uap:VisualElements>
    </Application>
  </Applications>

  <Capabilities>
    <Capability Name="internetClient" />
  </Capabilities>
</Package>

Теперь напишем код для поиска расширений. В этом примере я не буду делать UI, архитектуру и т.п., а просто сделаю все в одном классе. В реальном приложении так, разумеется, делать не стоит, но тут ради упрощения можно.

Идем в MainPage.xaml.cs, удаляем всё и пишем следующее:

using System;
using System.Collections.Generic;
using Windows.ApplicationModel.AppExtensions;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace Host
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            InitializeComponent();
            Loaded += OnLoaded;
        }

        private async void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            AppExtensionCatalog catalog = AppExtensionCatalog.Open("com.extensions.myhost");
            var extensions = new List<AppExtension>(await catalog.FindAllAsync());
        }
    }
}

Давайте разберемся с тем, что происходит в методе OnLoaded. Для начала, нам нужно открыть каталог расширений, иcпользуя AppExtensionCatalog.Open. В качестве аргумента мы ему передаем имя хоста, которое ранее указали в манифесте. После этого, мы получаем все расширения в каталоге. Так как у нас нет пользовательского интерфейса, есть смысл поставить в конце метода breakpoint. Уже можно запустить приложение, вы увидите, что расширений у нас нет (что логично). Так давайте напишем первое!

Создание расширения

В качестве расширения создадим простенький калькулятор с 4 операциями и 2 операндами. Опять создаем пустое UWP-приложение (именно приложение), называем его Calculator, идем в манифест и добавляем неймспейс (как в host-приложении). Теперь снова ищем <Application>, но код добавляем уже другой:

<Extensions>
  <uap3:Extension Category="windows.appExtension">
    <uap3:AppExtension
      Name="com.extensions.myhost"
      PublicFolder="Public"
      Id="Calculator"
      DisplayName="Calculator"
      Description="This is a calculator">
      <uap3:Properties>
        <Service>com.mycalculator.service</Service>
      </uap3:Properties>
    </uap3:AppExtension>
  </uap3:Extension>
</Extensions>

Рассмотрим эту декларацию чуть подробнее. Для начала, мы объявляем расширение приложения. У этого объявления есть несколько параметров:

  • Name — имя хоста расширений (то самое из манифеста)
  • PublicFolder — публичная папка, к которой у хоста есть доступ. В данном примере она не используется, но знать о ней стоит.
  • Id — уникальный id расширения
  • DisplayName — имя расширения
  • Description — описание

Дальше идет такая вещь, как Properties. Тут вы можете объявлять свои специфичные параметры. В данном случае, мы объявляем имя нашего сервиса (о нем совсем скоро).
Каркас расширения готов, можно протестировать: выбираем Buld > Deploy Solution, запускаем Host, и видим наше расширение! Магия. Давайте теперь заставим его что-нибудь делать, не время отдыхать!

Создание сервиса

Мы вынесем сервис в отдельный проект, т.к. размещение его в том же проекте, что и приложение расширения, требует дополнительных модификаций кода. Создаем новый проект, только на этот раз нам нужен Windows Runtime Component (Class Library к ОЧЕНЬ большому сожалению не подходит). Удаляем ненужный нам Class1 и создаем нужный нам класс Service. Его мы напишем пошагово.

  1. Добавляем нужные using'и:

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

  2. Реализуем интерфейс IBackgroundTask в Service:
    У нас появится пустой метод Run вроде такого
    public void Run(IBackgroundTaskInstance taskInstance) { }
    Пояснение: любой сервис — это фоновая задача. Но фоновые задачи применяются не только для сервисов. Подробнее можете прочитать, к примеру, на MSDN

  3. Создаем поле для deferral:
    private BackgroundTaskDeferral _deferral;
    Он нужен, чтобы наша задача внезапно не завершилась.

  4. Добавляем следующий код в Run:

    _deferral = taskInstance.GetDeferral();
    taskInstance.Canceled += TaskInstanceOnCanceled;
    var serviceDetails = (AppServiceTriggerDetails) taskInstance.TriggerDetails;
    AppServiceConnection connection = serviceDetails.AppServiceConnection;
    connection.ServiceClosed += ConnectionOnServiceClosed;
    connection.RequestReceived += ConnectionOnRequestReceived;

    Итак, сначала мы присваиваем нашему deferral'у deferral фоновой задачи. Далее мы добавляем обработчик события отмены задачи. Это нужно, чтобы мы смогли освободить наш deferral и позволить задаче завершиться. Затем, мы получаем информацию, связанную непосредственно с нашим сервисом, и регистрируем два обработчика. ServiceClosed вызывается, когда источник вызова задачи уничтожает объект вызова. В RequestRecieved будет происходить вся работа по обработке запроса.

  5. Создаем обработчики для двух событий, связанных с освобождением deferral'а:

    private void ConnectionOnServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
    {
        _deferral?.Complete();
    }

    private void TaskInstanceOnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
    {
        _deferral?.Complete();
    }

  6. Напишем метод для выполнения расчетов

    private double Execute(string op, double left, double right)
    {
        switch (op)
        {
            case "+":
                return left + right;
            case "-":
                return left - right;
            case "*":
                return left*right;
            case "/":
                return left/right;
            default:
                return left;
        }
    }

    Тут ничего сверхъестественного, метод принимает оператор и два операнда, возвращает результат вычислений.

  7. Самая мякотка. Пишем обработчик для RequestRecieved:

    private async void ConnectionOnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
    {
        var deferral = args.GetDeferral(); // Получаем defferal, но уже для запроса
        AppServiceRequest request = args.Request; // Непосредственно запрос
    
        var op = request.Message["Operator"].ToString(); // Вытаскиваем
        var left = (double) request.Message["Left"];    // параметры
        var right = (double) request.Message["Right"];  // из запроса
    
        var result = Execute(op, left, right); // Получаем результат вычислений
        await request.SendResponseAsync(new ValueSet // Отправляем результат обратно
        {
            ["Result"] = result
        });
    
        deferral.Complete(); // Освобождаем deferral
    }

Мы написали наш сервис, самое время его использовать!

Соединяем все воедино

Для начала, нам нужно объявить наш сервис. Идем в манифест нашего расширения, заходим в Extensions и пишем туда следующее:

<uap:Extension Category="windows.appService" EntryPoint="CalculatorService.Service">
  <uap:AppService Name="com.mycalculator.service" />
</uap:Extension>

Тут мы объявляем название сервиса и класс, в котором он реализован.
Добавляем reference на CalculatorService в Calculator

Теперь нам нужно из хоста соединиться с нашим сервисом. Возвращаемся в MainPage.xaml.cs и добавляем код в наш супер-метод:

var calculator = extensions[0];
var serviceName = await GetServiceName(calculator);
var packageFamilyName = calculator.Package.Id.FamilyName;
await UseService(serviceName, packageFamilyName);

Тут мы получаем имя сервиса и имя семейства пакетов (и то и то понадобится для подключения к сервису) из данных расширения.

Метод GetServiceName:

private async Task<string> GetServiceName(AppExtension calculator)
{
    IPropertySet properties = await calculator.GetExtensionPropertiesAsync();
    PropertySet serviceProperty = (PropertySet) properties["Service"];
    return serviceProperty["#text"].ToString();
}

Здесь мы извлекаем указанное нами ранее в манифесте расширения имя сервиса, используя некое подобие работы с XML.

Теперь напишем последний метод, который наконец-то начнёт делать что-то конкретное:

private async Task UseService(string serviceName, string packageFamilyName)
{
    var connection = new AppServiceConnection
    {
        AppServiceName = serviceName,
        PackageFamilyName = packageFamilyName
    }; // Параметры подключения

    var message = new ValueSet
    {
        ["Operator"] = "+",
        ["Left"] = 2D,
        ["Right"] = 2D
    }; // Параметры для передачи

    var status = await connection.OpenAsync(); // Открываем подключение
    using (connection)
    {
        if (status != AppServiceConnectionStatus.Success) // Проверяем статус
        {
            return;
        }

       var response = await connection.SendMessageAsync(message); // Отправляем сообщение и ждем ответа
       if (response.Status == AppServiceResponseStatus.Success)
       {
           var result = (double) response.Message["Result"]; // Получаем результат
       }
    }
}

Не забудьте про Build > Deploy solution.
И, если вы все сделали правильно, у вас должно получиться так:
image

Если получилось, поздравляю — вы написали свое полноценное (относительно) модульное приложение на UWP! (А если нет, то пишите в комментариях)

Дополнительно

  1. У AppExtensionCatalog есть несколько событий, используя которые вы сможете наблюдать за состояниями расширений.
    Вот их список:

    • PackageInstalled
    • PackageStatusChanged
    • PackageUninstalling
    • PackageUpdated
    • PackageUpdating

  2. Вы, возможно, захотите проверять подпись расширений. В этом вам поможет AppExtension.Package.SignatureKind

Автор: MrNovVad

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js