Windows 10 UWP 19 of N: App Extensibility

  • 941
  • 0
  • UAP
  • 2021-04-30

介紹如何在UWP APP中提升APP的延伸性!

今年的Build介紹更多的UWP特性在2016夏季將會釋出的Anniversary Update On Windows 10!今天要介紹的是App Extensibility 也就是APP的延伸性,很多開發人員都已經開發了很多非常好用及強大的APP!有時候可以不需要自己的APP去時做到這個部分(例如:DirectX,Media extension...等)軟體分成非常多個領域區塊並非每個都能夠精通。簡單來說可以使用別的APP開發好的功能已與串接來延伸APP的能力!

在Anniversary Update的UWP將會支援以下三種App的Extensibility

  1. App Handover (Protocol Launch)
  2. App Service
  3. App Extension ( 新功能)

Protocol handler

第一個早Protocol launcher在Windows Store的時候就可以使用!讓我們來複習一下Protocol的部份吧!

先透過以下API來Query是否有安裝該App以及支援Protocol

var queryResult = await Launcher.QueryUriSupportAsync(new Uri(uriString), LaunchQuerySupportType.Uri, packageFamilyName);

var queryResult = await Launcher.QueryUriSupportAsync(new Uri(uriString), LaunchQuerySupportType.UriForResults, packageFamilyName);

差別是在第二個參數使用的Enum,可以查詢是否API的protocol需要等待有Result!

然後看看Uri laucher在現在UWP使用方式,使用內建Photos的Crop功能做示範

上圖可以看到Photos所支援的URL Protocols。

var inputFile = SharedStorageAccessManager.AddFile(input);
var destinationFile = SharedStorageAccessManager.AddFile(destination);
var options = new LauncherOptions();
options.TargetApplicationPackageFamilyName = "Microsoft.Windows.Photos_8wekyb3d8bbwe";
var parameters = new ValueSet();
parameters.Add("InputToken", inputFile);
parameters.Add("DestinationToken", destinationFile);
parameters.Add("CropWidthPixals", widthPixals);
parameters.Add("CropHeightPixals", heightPixals);
return await Launcher.LaunchUriForResultsAsync(new Uri("microsoft.windows.photos.crop:"), options, parameters);

以上的Code就是可以藉由Photos App裁切圖片,可以帶以上的參數(輸入、輸出的檔案位置、裁切的高度、寬度的pixals值)需要注意的是在Protocol的Handler可以指定PackageFamilyName來確保使用的App是所想要的指定App。

以上就是裁切圖片的執行畫面。


App Service

接者第二個Demo就是使用App Service來與其他App做溝通。

目前App Service需要寫Windows Runtime component的方式(也就是在1607之前的background task的實作方式)

使用到App Service在目前的方式會需要建立一般的Foreground App以及Windows Runtime component的專案然後再由另外一個App呼叫該app service

需要特別注意的是這邊會有個Name以及PackageFamilyName需要被記錄起來!

Name就是如上圖的Name的設定,而PackageFamilyNam可用兩種方式查看

  1. 使用 Package.Current.Id.FamilyName在C#的Runtime時期可以看到。
  2. 在Package.appxmanifest的Packaging的分類可以看到。

在Service provide的Foreground app需要宣告App Service並參考Windows Runtime component設定好Entry point後再Windows runtime component的實作進入Background的Interface(IBackgroundTask)

public sealed class ServiceEntry : IBackgroundTask
    {
        private BackgroundTaskDeferral _deferral;
        private AppServiceConnection _serviceConn;
        private string[] inventoryItems = new string[] { };

        public void Run(IBackgroundTaskInstance taskInstance)
        {
            _deferral = taskInstance.GetDeferral();
            taskInstance.Canceled += TaskInstance_Canceled;
            var detail = taskInstance.TriggerDetails as AppServiceTriggerDetails;
            _serviceConn = detail.AppServiceConnection;
            _serviceConn.RequestReceived += _serviceConn_RequestReceived;
        }

        private async void _serviceConn_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
        {
            var deferral = args.GetDeferral();
            var valueSet = args.Request.Message;
            var resultSet = new ValueSet();
            var action = valueSet["Action"] as string;
            if (index.HasValue && index.GetValueOrDefault() > 0)
            {
                switch (action)
                {
                    case "PlanA":
                        resultSet.Add("Result", "This is the result of plan A");
                        resultSet.Add("A", "Wtf");
                        break;
                    case "PlanB":
                        resultSet.Add("Result", "This is the result of plan B");
                        resultSet.Add("B", "Sleepy all the day");
                        break;
                    default:
                        resultSet.Add("Result", "This is the result of default");
                        resultSet.Add("Default", "Just empty here");
                        break;
                }
            }
            await args.Request.SendResponseAsync(resultSet);
            deferral?.Complete();
        }

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

IBackground 只需要實作Run的部分,但在Run的Method內部所帶入的taskInstance取的Deferral來做非同步的操作!如果taskInstance的TriggerDetails是AppServiceTriggerDetails就表示該task Instance是由appservice的trigger觸發。

然後在取得App Service的Connection的物件來達到建立App Service的傳輸通道並註冊RequestReceived的事件來做些AppService的邏輯處理!

然後需要先把Provide App Service的App deploy到環境上!接者在來準備寫呼叫App Service的App。

以下的Code就是呼叫App service得方式。

private AppServiceConnection _serviceConn;
private async Task CreateServiceAsync()
{
  _serviceConn = new AppServiceConnection();
  _serviceConn.AppServiceName = "com.Microsoft.Inventory";
  _serviceConn.PackageFamilyName = "2cdcbd07-4b0b-452f-bb24-5e044d14c13b_7yyh6ng4vr50r";
  var status = await _serviceConn.OpenAsync();
  switch (status)
  {
    case AppServiceConnectionStatus.Success:
         await ServiceConnectionHandlerAsync();
    break;
    case AppServiceConnectionStatus.AppNotInstalled:
         break;
    case AppServiceConnectionStatus.AppServiceUnavailable:
         break;
    case AppServiceConnectionStatus.AppUnavailable:
         break;
    case AppServiceConnectionStatus.Unknown:
         break;
    default:
         break;
  }
}

建立AppServiceConnection的物件,然後帶入Service 的Name以及PackageFamilyName然後再OpenAsync就可以得到是否該Connection建立成功!

接者以下Code則是傳入測試資料

private async Task ServiceConnectionHandlerAsync()
        {
            var message = new ValueSet();
            message.Add("Action", "PlanA");
            message.Add("ID", new Random().Next(0, 10));
            var response = await _serviceConn.SendMessageAsync(message);
            switch (response.Status)
            {
                case AppServiceResponseStatus.Success:
                    break;
                case AppServiceResponseStatus.ResourceLimitsExceeded:
                    break;
                case AppServiceResponseStatus.Failure:
                    break;
                case AppServiceResponseStatus.Unknown:
                    break;
                default:
                    break;
            }
            var content = string.Format($"{response.Message["Result"]}\n{response.Message["A"]}");
            var dialog = new MessageDialog(content);
            await dialog.ShowAsync();
        }

特別的部分是在傳送資料的時候使用ValueSet的物件!這是在Windows Runtime component來代表KeyValuePair的用法。

以下為APP執行結果


App Extension

最後要介紹的是Extension啦! 在Anniversary Update將會正式推出的API。該API最佳的範例就是Microsoft Edge的Extension!之前使用Insider Program並在Fast ring的user可以搶先體驗到Extension的方便、而正式版本的Edge要開啟Load extension的功能需要在Tab上輸入

about:flags

進入開發模式中,啟用(Enable extension developer features)

然後重開Edge這樣就可以在Edge的Extension選單中看到 Load extension的Button了。

關於Edge的Extension要如何實作請參考Edge的開發論壇。

需要注意的是Extension也是一個App,這點與Edge的Extension是不同的!

回歸正題,要如何開發並且載入extension的實作如下

先建立Blank app並且設定target version是到14393的SDK版本(anniversary update)

先是把Package.appxmanifest使用XML的編輯器開啟,加入以下Xml code

<?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 uap3 mp">

  <Applications>
      <Extensions>
        <uap3:Extension Category="windows.appExtensionHost">
          <uap3:AppExtensionHost>
            <uap3:Name>ext.sample.app1host</uap3:Name>
          </uap3:AppExtensionHost>
        </uap3:Extension>
      </Extensions>
    </Application>
  </Applications>
</Package>

與原先的XML有點落差,重要的部分是加入xmlns的uap3的reference。然後再Applications裡面的Extensions加入uap3:Extension並且類別選哲維appExtensionHost!接者再放入uap3:AppExtensionHost的node並且給予Name

這邊的name將會是開啟extension host和extension的溝通名稱,但不能使用大寫的英文字母!

然後在建立另一blank app也是如上步驟,但package.appxmanifest的code需要做些微調!調整大致如下

<?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 uap3 mp">

  <Applications>
      <Extensions>
        <uap3:Extension Category="windows.appExtension">
          <uap3:AppExtension Id="baseid" Name="ext.sample.app1host" Description="extension sample" DisplayName="HelloExtension" PublicFolder="ExtensionPublic">
            <uap3:Properties>
              <TextFileName>Extension.html</TextFileName>
            </uap3:Properties>
          </uap3:AppExtension>
        </uap3:Extension>
      </Extensions>
  </Applications>
</Package>

在AppExtension的Node可以設定 id, name, description, display name, publicfolder,在node裡面可以再加入Properties的Node然後Properties的裡面可以在個別自訂需要放入的node。

詳細可以參考MSDN連結-> https://msdn.microsoft.com/en-us/library/windows/apps/dn934720.aspx

需要注意是Name以及PublicFolder的設定,Name如上述所說是extension host和extension互相溝通的欄位。PublicFolder則是能夠讓extension host的app能夠reference到extension的StorageFolder!在UWP的儲存方式一樣是採用Isolated storage的方式,個別的app都有各自的儲存空間。要能夠使用一些特定的folder的方式會在之後的文章介紹!然後uap3:Properties的node則是可以宣告一些附加屬性,等等再寫C#的時候會說明。

以上就是先設定好host和extension的package.appxmanifest檔案宣告。

然後在host裡面我建立了ExtensionManager的class,然後code大致如下。

public class ExtensionManager
    {
        private static ExtensionManager instance = null;
        public static ExtensionManager Instance
        {
            get
            {
                if (instance == null)
                    instance = new ExtensionManager();
                return instance;
            }
        }

        private static ObservableCollection<Extension> appExtensions;
        public static ObservableCollection<Extension> AppExtensions
        {
            get { return appExtensions; }
        }

        private AppExtensionCatalog catalog;
        private CoreDispatcher dispatcher;
        private string extensionContract;

        public void Initialize(string contract)
        {
            if (!Windows.Foundation.Metadata.ApiInformation.IsTypePresent("Windows.ApplicationModel.AppExtensions.AppExtensionCatalog"))
                throw new PlatformNotSupportedException("Extension feature must be 1607");
            if (string.IsNullOrEmpty(contract) || string.IsNullOrWhiteSpace(contract))
                throw new ArgumentException("Extension contract should not be emptu, null or whitespace");

            appExtensions = new ObservableCollection<Extension>();
            extensionContract = contract;
            catalog = AppExtensionCatalog.Open(extensionContract);
            dispatcher = CoreWindow.GetForCurrentThread().Dispatcher;

            catalog.PackageInstalled += Catalog_PackageInstalled;
            catalog.PackageUpdated += Catalog_PackageUpdated;
            catalog.PackageUninstalling += Catalog_PackageUninstalling;
            catalog.PackageUpdating += Catalog_PackageUpdating;
            catalog.PackageStatusChanged += Catalog_PackageStatusChanged;

            FindAllExtensions();
        }

        private async void Catalog_PackageInstalled(AppExtensionCatalog sender, AppExtensionPackageInstalledEventArgs args)
        {

        }

        private void Catalog_PackageUpdated(AppExtensionCatalog sender, AppExtensionPackageUpdatedEventArgs args)
        {

        }

        private void Catalog_PackageUninstalling(AppExtensionCatalog sender, AppExtensionPackageUninstallingEventArgs args)
        {

        }

        private void Catalog_PackageUpdating(AppExtensionCatalog sender, AppExtensionPackageUpdatingEventArgs args)
        {

        }

        private void Catalog_PackageStatusChanged(AppExtensionCatalog sender, AppExtensionPackageStatusChangedEventArgs args)
        {

        }
    }

我使用Singleton的設計模式來實作該Manager,然後加入基本的防呆功能。在1607才加入的extension的code是在 Windows.ApplicationModel.AppExtensions.AppExtensionCatalog的namespace下。所以在Initialize的Method加入ApiInformation的方式判斷並且判斷contract是否有正確的值(contract就是在package.appxmanifest設定的Name)

然後實際的Extension的Manager是AppExtensionCatalog這個class,該class有如下的Event

Event 說明 Remark
PackageInstalled 當Extension安裝成功的事件  
PackageStatusChanged 當Extension狀態改變的事件 當Extension安裝、移除、更新都會觸發。
PackageUninstalling 當Extension正在移除的事件  
PackageUpdated 當Extension更新完成的事件  
PackageUpdating 當Extension正在更新的事件  

然後有如下的Method

Method 說明 Remark
Open(System.String appExtensionName) 開啟extension的manager  
FindAllAsync 找到屬於該appExtensionName的所有extension實體 取得IReadOnlyList<AppExtension>
RequestRemovePackageAsync(System.String packageFullName) 嘗試移除package,並帶入extension的full package name 取得Boolean 知道該extension是否移除成功。

MSDN官方連結-> https://msdn.microsoft.com/en-us/library/windows/apps/windows.applicationmodel.appextensions.appextensioncatalog.aspx?f=255&MSPPError=-2147217396

在Initialize中有個FindAllExtension的Code如下

private async void FindAllExtensions()
{
  var extensions = await catalog.FindAllAsync();
  foreach (var ext in extensions)
  {
            var extId = ext.AppInfo.AppUserModelId + "!@" + ext.Id;
            if (ext.Package.Status.VerifyIsOK())
            {
                switch (ext.Package.SignatureKind)
                {
                    case PackageSignatureKind.Developer:
                        break;
                    case PackageSignatureKind.Enterprise:
                        break;
                    case PackageSignatureKind.None:
                        break;
                    case PackageSignatureKind.Store:
                        break;
                    case PackageSignatureKind.System:
                        break;
                }
                var extension = new Extension(extId, ext);
                await extension.CreateExtensionIconAsync();
                appExtensions.Add(extension);
            }
  }
}

先是取得所有的extension然後可以再檢查extension的狀態是否正常,然後可以再針對extension的signature判斷是否要將該extension載入。

AppExtension有以下的屬性

Property 說明 Remark
AppInfo AppInfo Extension的app資訊  
String Description 在manifest設定的description  
String DisplayName 在manifest設定的displayname  
String Id 在manifest設定的id  
Package Package Extension的app封裝package資訊  

AppExtension有以下方法

Method 說明 Remark
GetExtensionPropertiesAsync 取得在設定在manifest的Properties的Node下的所有屬性 這邊會丟出IPropertySet的物件可以Query每筆屬性的Key value,若是沒有設定Properties呼叫該方法會丟出exception!
GetPublicFolderAsync 取得設定在manifest的PublicFolder的資料夾 取得"公開"資料夾

這邊我自己延伸寫了Extension的class,有個CreateExtenionIconAsync的方法就是將extension的app logo產生。Code如下

public async Task CreateExtensionIconAsync()
        {
            var logoRef = extensionInstance.AppInfo.DisplayInfo.GetLogo(new Windows.Foundation.Size(1, 1));
            using (var logoStream = await logoRef.OpenReadAsync())
            {
                ExtensionIconImage = new BitmapImage();
                ExtensionIconImage.SetSource(logoStream);
            }
        }

直接從extension的AppInfo中的DisplayInfo GetLogo可以把該Extension的logo image的randomaccessstream reference抓出來並轉換成BitmapImage。

接者修改一下在Extenion host的MainPage.xaml如下

<Page
    x:Class="ExtensionHost.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ExtensionHost"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <ListView x:Name="extList" SelectionMode="None" IsItemClickEnabled="False">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="*"/>
                        </Grid.ColumnDefinitions>
                        <CheckBox Grid.Column="0" IsChecked="{Binding IsLoad, Mode=TwoWay}" Command="{Binding CheckCommand}"/>
                        <Image  Grid.Column="1" Source="{Binding ExtensionIconImage}"/>
                        <TextBlock Grid.Column="2">
                            <Run Text="{Binding ExtensionId}"/>
                            <LineBreak/>
                            <Run Text="{Binding PackageName}"/>
                        </TextBlock>
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>

    <Page.BottomAppBar>
        <CommandBar>
            <CommandBar.PrimaryCommands>
                <AppBarButton Click="AppBarButton_Click"/>
            </CommandBar.PrimaryCommands>
        </CommandBar>
    </Page.BottomAppBar>
</Page>

然後在MainPage.xaml.cs的部分修改如下

public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            ExtensionManager.Instance.Initialize("ext.sample.app1host");
            extList.ItemsSource = ExtensionManager.AppExtensions;
        }

        private async void AppBarButton_Click(object sender, RoutedEventArgs e)
        {
            await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
             {
                 ExtensionManager.Instance.LoadExtension();
             });
        }
    }

然後測試流程大致如下

  1. 先將extension的app進行打包產生出appx或是appxbundle的package。
  2. 使用Powershell並輸入add-appxpackage的指令
    1. add-appxpackage "這邊帶入.appx或是.appxbundle的檔案"
  3. 若是成功安裝完成可以在app list中找到該extension的app。

流程圖片如下

這樣就把Extension初步的載入到Host中了,接下來是如何載入extension的code。

重點!需要知道的是UWP無法動態把String轉換成C#的程式碼!!(UWP本身採用AOT非JIT,關於UWP compilation的細節以及該平台特性可以到Windows Dev Center參閱)

所以這邊測試使用的是javascript的方式載入到webview並且invoke該script(小弟不會javascript... 所以用簡單的javascript做demo)

所以稍微修改extension的專案結構,如下圖

這邊加入了ExtensionPublic的Folder並且加入的Extension.html,該html的code如下

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <script type="text/javascript">
        function Plus(parameter1, parameter2) {
            var message = parameter1 + parameter2;
            window.external.notify(message);
        }
        function Message() {
            window.external.notify("Hello");
        }
    </script>
</body>
</html>
在UWP的webview中若是要抓到html的javascript的事件回傳需要再javascript使用window.external.notify的語法才行!然後在C#的webview需要regist ScriptNotify的事件

接者在extension的class中加入LoadAsync的方法如下

public async Task LoadAsync()
        {
            try
            {
                var folder = await extensionInstance.GetPublicFolderAsync();
                var fileName = @"Extension.html";
                var extensionFile = await folder.GetFileAsync(fileName);
                var htmlString = await Windows.Storage.FileIO.ReadTextAsync(extensionFile);
                webview.NavigateToString(htmlString);
                await Task.Delay(1000);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

需要留意的是使用了GetPublicFolderAsync就是能夠跳脫UWP的app isolated storage限制去存取extension所設定的Public folder!然後把該html載入到webview當中!

然後再呼叫InvokeScriptAsync

private async Task InvokeScriptAsync(string scriptName, IEnumerable<string> arguments)
        {
            try
            {
                await webview.InvokeScriptAsync(scriptName, arguments);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }
        }

接者在webview的ScriptNotify的是件單純顯示輸出

private void Webview_ScriptNotify(object sender, NotifyEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine(e.Value);
        }

最後來看看輸出畫面八

這邊可以看到在Visual Studio的Output畫面有顯示1,2的字串相加以及Hello的文字訊息。


最後來總結一下在UWP中app提升延伸性的方式有三種,個別差異大致如下

使用Protocol算是Fire and forget的方式!如上面所Demo的是使用Photo的裁切的功能,裁切好的的結果(圖片)將會儲存到共享的儲存空間。

而使用App Service則是可以讓App互相溝通但是會受限在回傳資訊的資料型別,因App service是在background的方式進行溝通而且使用的是Runtime的API set。

最後在1607出現的Extension則是可以獨立執行且附加功能擴充App的方式。

 

 

 

***以上Code以及說明都有可能隨著Windows 10 的版本以及Visual Studio 2015版本有所調整!***

參考資料 MSDN, App Extensibility B808

下次再分享Windows 10 的新技術拉~