One of the things that has been missing from the UWP and the Windows Store has been the ability for one application to extend another one and the Anniversary Update makes this possible.
Perhaps the most obvious way in which this shows up is in the Edge browser which now has the ability to support extensions and I’d heartily recommend watching the (developer) session around Edge extensions here from the Edge Summit as hosted on Channel 9.
but that’s an Edge specific story whereas extensions are something that many app developers might want to make use of.
Before getting to extensions, it’s worth remembering that on the UWP it’s already possible for one application to ‘invoke’ another via a few different mechanisms.
Off the top of my head those might include.
Associations and Launching
- File associations – i.e. one application asks the system to launch a ‘file’ and the system invokes another app to do that.
- Protocol handlers – i.e. one application asks the system to launch a particular URI and the system invokes another app to do that with good examples of schemes being http, mailto, bingmaps, etc.
Launching for Results
It’s also possible for an application to ask another app to do something on its behalf which involves returning some kind of results and displaying some kind of UI;
and this revolves around the App that is the launcher passing data across to the launchee which the system then displays over the top of the launcher until the user has finished with it at which point the system can return any outgoing data from the launchee back to the launcher.
Data is transferred using the ValueSet dictionary and there are possibilities for exchanging more complex data by passing tokens to files through a ValueSet by making use of the SharedStorageAccessManager where either side of the transfer can use the AddFile method to get a token for a file that the other side of the transfer can then get hold of by using the RedeemTokenForFileAsync method.
What does have to be considered here is the ‘protocol’ for this communication as the launcher and launchee apps have to have enough knowledge of each other a priori in order to be able to communicate – the developer of the launcher has to know what to put into their ValueSet and what to expect back.
Application Services
If none of those mechanisms are enough then an application can invoke an ‘app service’ offered by another where that service is a particular type of background task that is known to the calling application. The detail on that is here;
and I remember making a basic demo of this (where one app provides a photo search service to another) back in October of last year;
Example of an App Providing a Photo Search Service to Another App
Once again, the data that flows back and forth here between the consuming app and the providing app is modelled in ValueSets and, once again, that can include files.
The calling app still needs to know quite a bit about the app that it wants to call in order to set up an AppServiceConnection and that includes;
- The app service name.
- The package family name of the app that the service lives in.
- The ‘protocol’ of data that is to flow across the app service boundary in terms of the ValueSet of parameters and return values that get exchanged.
It’s also worth noting that when you ‘plug in’ to Cortana, you do it by writing an app service and if you ever look at writing background code for the Microsoft Band you’ll find that you’re writing an app service there too so Microsoft 1st party apps are making use of this app service infrastructure.
Sharing Files
One last but sometimes forgotten possibility for two apps from the same publisher to share data with each other is via the ‘PublisherCacheFolder’ which is part of the ApplicationData class and is documented here;
and can be useful for a ‘suite’ of apps that come from the same publisher that don’t need to use some of the more complex mechanisms described previously.
Extensions
Some of this landscape of existing mechanisms via which one application can ‘talk’ to or ‘invoke’ another is explored in this //Build session;
and I think the various bits that I described as ‘launching’ above are slotted into the section named ‘App Handover’ and then there’s a section on ‘App Services’ and the coverage is good with some good demos so it’s well worth watching if you’ve not been through those bits before.
Around the 25m mark the video transitions into talking about the new app extensions support that comes with the Windows 10 Anniversary Update and from what I’ve seen so far this provides a fairly generic and lightweight mechanism whereby one app can contribute to another I wanted to try out a ‘hello world’ experiment with this and the remainder of this post is a write-up of where I got to.
The App Providing the Extension/s Declares Them In Its Manifest
The schema which defines how this is done is specified on the web here;
and it’s worth noting that the Category here needs to be set to “windows.appExtension” and that the attributes Name, Id, PublicFolder, DisplayName are all mandatory on the AppExtension element and, at the time of writing, Visual Studio does not seem to have UI for filling out these details in a manifest.
For my example, I declared a manifest which looked like this offering up two extensions named (imaginatively) “MyExtension” and “MyOtherExtension”;
<Extensions> <uap3:Extension xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" Category="windows.appExtension"> <uap3:AppExtension Name="MyExtension" Id="MyId" PublicFolder="MyPublicFolder" DisplayName="My Display Name"> <uap3:Properties> <Property1>Value1</Property1> <Property2>Value2</Property2> </uap3:Properties> </uap3:AppExtension> </uap3:Extension> <uap3:Extension xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" Category="windows.appExtension"> <uap3:AppExtension Name="MyOtherExtension" Id="MyOtherId" PublicFolder="MyOtherPublicFolder" DisplayName="My Other Display Name"> </uap3:AppExtension> </uap3:Extension> </Extensions>
and I made 2 empty folders within my project as below;
The App Consuming the Extension/s Declares Them In Its Manifest
I added an app to consume these extensions to my solution and changed its manifest according to the schema specified here;
and my specific example looks like this;
<Extensions> <uap3:Extension xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" Category="windows.appExtensionHost"> <uap3:AppExtensionHost> <uap3:Name>MyExtension</uap3:Name> <uap3:Name>MyOtherExtension</uap3:Name> </uap3:AppExtensionHost> </uap3:Extension> </Extensions>
The App Consuming the Extensions Opens Catalogs By Name
In my consuming app, I wrote a little code that looked like this (all called from a simple UI button);
async void OnButtonClick(object sender, RoutedEventArgs e) { await this.DisplayMessageAsync("Click ok to search for your extensions"); var myExtensionCatalog = AppExtensionCatalog.Open("MyExtension"); var myOtherExtensionCatalog = AppExtensionCatalog.Open("MyOtherExtension"); var myExtensions = await myExtensionCatalog.FindAllAsync(); var myOtherExtensions = await myOtherExtensionCatalog.FindAllAsync(); await this.DisplayMessageAsync( $"Found {myExtensions.Count} of extension and {myOtherExtensions.Count} other extensions"); await this.DisplayMessageAsync( "Now, deploy the extension app package and then click ok"); myExtensions = await myExtensionCatalog.FindAllAsync(); myOtherExtensions = await myOtherExtensionCatalog.FindAllAsync(); await this.DisplayMessageAsync( $"Found {myExtensions.Count} of extension and {myOtherExtensions.Count} other extensions"); } async Task DisplayMessageAsync(string message) { var dialog = new MessageDialog(message, "demo"); await dialog.ShowAsync(); }
Sure enough – initially my code finds 0 extensions of the two types that I’m interested in (“MyExtension” and “MyOtherExtension”) and then when I deploy the package containing the extensions it later on finds 1 of each type.
It’s worth pointing out that an app that’s consuming extensions can (and probably should!) be notified if those packages which contain the extensions change while the app is running and there are events for all of this hanging off the AppExtensionCatalog class. Specifically;
For some of those, it seems fairly obvious as to what your code would do in a particular case but the PackageStatusChanged event is interesting to me as it provides the detail of the Package and then the Status property can be used to interrogate lots of properties around whether it is available or, if not, why it’s not available. See PackageStatus for the range of options there including the VerifyIsOK method.
Catalogs Contain Extensions
Once the consuming app has a catalog open, it can enumerate all the extensions within it. In my example, this is easy;
foreach (var extension in myExtensions.Union(myOtherExtensions)) { Debug.WriteLine( $"{extension.Id}, {extension.DisplayName}, {extension.AppInfo.PackageFamilyName}" + $"{extension.Description}, {extension.Package.InstalledDate}"); }
and that produces the output;
MyId, My Display Name, 735f2546-33ca-4221-8164-97272bc34fb9_54fjpjf3c8m40, 8/2/2016 3:04:04 PM +00:00
MyOtherId, My Other Display Name, 735f2546-33ca-4221-8164-97272bc34fb9_54fjpjf3c8m40, 8/2/2016 3:04:04 PM +00:00
and we can use the Package property to get hold of things like the Logo for that package if we wanted to display it. Naturally, my two extensions here come from the same package and so their PackageFamilyName and InstalledDate properties are going to be the same.
Extensions Have Properties
I can easily enumerate the properties that I specified on my extension in the manifest. If I add in this additional code to the loop above;
foreach (var extension in myExtensions.Union(myOtherExtensions)) { Debug.WriteLine( $"{extension.Id}, {extension.DisplayName}, {extension.AppInfo.PackageFamilyName}" + $"{extension.Description}, {extension.Package.InstalledDate}"); try { var properties = await extension.GetExtensionPropertiesAsync(); foreach (var property in properties) { var value = ((PropertySet)property.Value)["#text"]; Debug.WriteLine($"PROPERTY {property.Key}, {value}"); } } catch (ArgumentException) { Debug.WriteLine($"Failed to access properties of {extension.DisplayName}"); } }
then for the extension named “MyExtension” I get the expected output whereas for the extension named “MyOtherExtension” I find that the call to GetExtensionPropertiesAsync() throws an ArgumentException. I’m unsure whether this is a ‘preview thing’ at the time of writing or whether there’s a better way to avoid that exception but my output looks as below;
MyId, My Display Name, 735f2546-33ca-4221-8164-97272bc34fb9_54fjpjf3c8m40, 8/2/2016 3:04:04 PM +00:00
PROPERTY Property2, Value2
PROPERTY Property1, Value1
MyOtherId, My Other Display Name, 735f2546-33ca-4221-8164-97272bc34fb9_54fjpjf3c8m40, 8/2/2016 3:04:04 PM +00:00
Failed to access properties of My Other Display Name
and so I could use this mechanism to grab whatever metadata has been made available about an extension and then use it to make decisions around where that extension is useful.
Extensions Have Public Folders
So far, we’ve managed to get to an extension and grab some metadata about it but we’ve not really been able to do any extending as such but each extension has to have a PublicFolder property and so we can use a method on the AppExtension class itself to get hold of that folder and then grab content from it.
To that end, I added a simple text file to each of the public folders that I’d set of on my extension project;
and then I updated my manifest such that each extension had a property which specified its TextFileName;
<Extensions> <uap3:Extension xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" Category="windows.appExtension"> <uap3:AppExtension Name="MyExtension" Id="MyId" PublicFolder="MyPublicFolder" DisplayName="My Display Name"> <uap3:Properties> <TextFileName>foo.txt</TextFileName> </uap3:Properties> </uap3:AppExtension> </uap3:Extension> <uap3:Extension xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" Category="windows.appExtension"> <uap3:AppExtension Name="MyOtherExtension" Id="MyOtherId" PublicFolder="MyOtherPublicFolder" DisplayName="My Other Display Name"> <uap3:Properties> <TextFileName>test.txt</TextFileName> </uap3:Properties> </uap3:AppExtension> </uap3:Extension> </Extensions>
and then I added a TextBlock named txtContent to the consuming app’s UI and wrote some code to try and populate it as/when extensions show up on the system and as/when they get uninstalled or changed.
using System; using System.Collections.Generic; using System.Threading.Tasks; using Windows.ApplicationModel.AppExtensions; using Windows.Foundation.Collections; using Windows.Storage; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } protected async override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); this.extensionContentMap = new Dictionary<string, string>(); foreach (var extensionName in extensionNames) { var catalog = AppExtensionCatalog.Open(extensionName); catalog.PackageInstalled += OnPackageInstalled; catalog.PackageUninstalling += OnPackageUninstalling; catalog.PackageUpdated += OnPackageUpdated; catalog.PackageStatusChanged += OnPackageStatusChanged; await this.AddContentFromCatalogExtensionsAsync(catalog); } await this.PopulateTextBlockFromStringMapAsync(); } async Task AddContentFromCatalogExtensionsAsync(AppExtensionCatalog catalog) { var extensions = await catalog.FindAllAsync(); foreach (var extension in extensions) { if (extension.Package.Status.VerifyIsOK()) { var extensionProperties = await extension.GetExtensionPropertiesAsync(); var fileName = ((PropertySet)extensionProperties[propertyName])["#text"] as string; var storageFolder = await extension.GetPublicFolderAsync(); var file = await storageFolder.GetFileAsync(fileName); var contents = await FileIO.ReadTextAsync(file); var formattedContents = $"[Text from extension named '{extension.DisplayName}' is displayed below]" + $"{Environment.NewLine}{contents}{Environment.NewLine}"; this.extensionContentMap[extension.Id] = formattedContents; } } } async Task PopulateTextBlockFromStringMapAsync() { await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { this.txtContent.Text = string.Empty; foreach (var catalogContents in this.extensionContentMap) { this.txtContent.Text += catalogContents.Value; } } ); } async void OnPackageInstalled(AppExtensionCatalog sender, AppExtensionPackageInstalledEventArgs args) { await this.AddContentFromCatalogExtensionsAsync(sender); await this.PopulateTextBlockFromStringMapAsync(); } async void OnPackageStatusChanged(AppExtensionCatalog sender, AppExtensionPackageStatusChangedEventArgs args) { if (args.Package.Status.VerifyIsOK()) { await this.AddContentFromCatalogExtensionsAsync(sender); } else { await this.RemoveExtensionsForCatalogAsync(sender); } await this.PopulateTextBlockFromStringMapAsync(); } async void OnPackageUpdated(AppExtensionCatalog sender, AppExtensionPackageUpdatedEventArgs args) { await this.AddContentFromCatalogExtensionsAsync(sender); await this.PopulateTextBlockFromStringMapAsync(); } async Task RemoveExtensionsForCatalogAsync(AppExtensionCatalog catalog) { var extensions = await catalog.FindAllAsync(); foreach (var extension in extensions) { this.extensionContentMap.Remove(extension.Id); } } async void OnPackageUninstalling(AppExtensionCatalog sender, AppExtensionPackageUninstallingEventArgs args) { await this.RemoveExtensionsForCatalogAsync(sender); await this.PopulateTextBlockFromStringMapAsync(); } Dictionary<string, string> extensionContentMap; readonly string[] extensionNames = { "MyExtension", "MyOtherExtension" }; readonly string propertyName = "TextFileName"; }
and that seemed to work reasonably well in the sense that when I deployed the app providing the extension my consuming app updated to read the text from the files that were being supplied and when I uninstalled the extension app my consuming app UI cleared itself out.
So…so far, so good and this would work for scenarios where an app wanted to extend itself with content that came from pure files like;
- Additional dictionaries for spell-checking.
- Additional images or videos.
- Additional pieces of HTML or JavaScript.
- Additional binary files containing the layout of levels for a game or similar.
and so on but it doesn’t let an extending app add any additional code to the app that it’s extending.
Back to App Services
It’s clear that this infrastructure can be used to improve the situation around app services in the sense that if an app is to be extended by other apps offering up an app service then this model allows the app to;
- Know as/when extensions arrive and leave the system.
- Get hold of the package family name for those other apps as they arrive on the system rather than somehow knowing it up-front at build time.
- Potentially to use the property metadata around extensions to avoid having to know it up-front at build time although this really just shifts the problem from hard-coding an app service name to hard-coding a property name used to look up the app service name
But I think this is still better than the app service situation today and there’s a demo of doing just this in the //Build video linked from the image above. I’d like to write my own just to try it out and I’ll perhaps do that in a separate post.
I’d also like to think about whether this infrastructure gives any scope for dynamically loading code into the app being extended whether that is by loading up compiled code (e.g. an assembly) or whether it be by dynamically trying to compile code. I’m not sure on either of those yet so I’ll return to them too at a later point.
Pingback: Windows 10 Anniversary Update, App Extensions and App Services – Mike Taulty
Pingback: Szumma #051 – 2016 31. hét | d/fuel