В недавнем Anniversary Update появилась такая замечательная вещь, как App Extensions. К сожалению, на данный момент из документации по ней есть только одно видео и пара GitHub-репозиториев. Но я смог собрать всю нужную информацию по использованию этой возможности, и сейчас расскажу, как можно написать расширяемое приложение.
И да, вам понадобится SDK версии не ниже 14393.
Как это будет работать
У нас будет одно host-приложение, к которому будут подключаться расширения. Каждое расширение будет содержать сервис (App Service), с помощью которого приложение будет взаимодействовать с расширением.
Немного о сервисах
В UWP-приложении вы не можете просто взять, и подключить динамическую библиотеку на лету. Это сделано в целях безопасности. Вместо этого, вы можете общаться с другим приложением с помощью простых типов данных (их список очень ограничен). Это приложение должно объявить о том, что у него есть сервис, с которым можно общаться, и это объявление пишется в манифесте приложения (Package.appxmanifest). Всё общение происходит при помощи подключения к сервису, отправки ему сообщений, и получения ответа. И сообщения, и ответы передаются с помощью ValueSet
(по сути это просто Dictionary<string, object>
), и, как уже говорилось ранее, в качестве значений там могут быть только простейшие типы данных (числа, строки, массивы).
Итак, приступаем.
Создание host-приложения
Для удобства все проекты будут размещены в одном решении. Открываем Visual Studio и создаем пустое UWP-приложение с минимальной версией 14393. Я назову его Host.
Теперь нам нужно подредактировать манифест. Открываем 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>Assets\StoreLogo.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="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png"
Description="Host"
BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.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. Его мы напишем пошагово.
Добавляем нужные using'и:
using System; using Windows.ApplicationModel.AppService; using Windows.ApplicationModel.Background; using Windows.Foundation.Collections;
Реализуем интерфейс IBackgroundTask в Service:
У нас появится пустой метод Run вроде такого
public void Run(IBackgroundTaskInstance taskInstance) { }
Пояснение: любой сервис — это фоновая задача. Но фоновые задачи применяются не только для сервисов. Подробнее можете прочитать, к примеру, на MSDN
Создаем поле для deferral:
private BackgroundTaskDeferral _deferral;
Он нужен, чтобы наша задача внезапно не завершилась.
Добавляем следующий код в 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 вызывается, когда источник вызова задачи уничтожает объект вызова. В RequestReceived будет происходить вся работа по обработке запроса.
Создаем обработчики для двух событий, связанных с освобождением deferral'а:
private void ConnectionOnServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args) { _deferral?.Complete(); }
private void TaskInstanceOnCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason) { _deferral?.Complete(); }
Напишем метод для выполнения расчетов
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; } }
Тут ничего сверхъестественного, метод принимает оператор и два операнда, возвращает результат вычислений.
Самая мякотка. Пишем обработчик для RequestReceived:
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.
И, если вы все сделали правильно, у вас должно получиться так:
Если получилось, поздравляю — вы написали свое полноценное (относительно) модульное приложение на UWP! (А если нет, то пишите в комментариях)
Дополнительно
У AppExtensionCatalog есть несколько событий, используя которые вы сможете наблюдать за состояниями расширений.
Вот их список:
- PackageInstalled
- PackageStatusChanged
- PackageUninstalling
- PackageUpdated
- PackageUpdating
- Вы, возможно, захотите проверять подпись расширений. В этом вам поможет AppExtension.Package.SignatureKind
dmitry_dvm
Отличное нововведение, думаю как это можно применить у себя. Очень радует развитие uwp. Спасибо за статью.