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

image


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


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


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

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


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


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


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


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

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


image


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



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



  
    
      com.extensions.myhost
    
  

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


Результат




  

  

  
    Host
    acede
    Assets\StoreLogo.png
  

  
    
  

  
    
  

  
    
      
        
          
            com.extensions.myhost
          
        
      
        
        
      
    
  

  
    
  

Теперь напишем код для поиска расширений. В этом примере я не буду делать 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(await catalog.FindAllAsync());
        }
    }
}

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


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

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



  
    
      
        com.mycalculator.service
      
    
  

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


  • 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 и пишем туда следующее:



  

Тут мы объявляем название сервиса и класс, в котором он реализован.
Добавляем 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 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

Комментарии (0)

© Habrahabr.ru