介紹如何在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
- App Handover (Protocol Launch)
- App Service
- 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可用兩種方式查看
- 使用 Package.Current.Id.FamilyName在C#的Runtime時期可以看到。
- 在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的實作如下
先建立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
然後在建立另一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
以上就是先設定好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是否移除成功。 |
在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();
});
}
}
然後測試流程大致如下
- 先將extension的app進行打包產生出appx或是appxbundle的package。
- 使用Powershell並輸入add-appxpackage的指令
- add-appxpackage "這邊帶入.appx或是.appxbundle的檔案"
- 若是成功安裝完成可以在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>
接者在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 的新技術拉~