I was inspired by this article that I saw on the Windows Blog in the past week or so;
to revisit ‘Project Rome’ that I’ve had a look at in these previous posts;
The article is a great read and, having it read it once, I later went back to it to see if I could get hold of the ‘Contoso Music’ app that it talks about but then I realised that ‘Contoso Music’ is being used in the article to talk about concepts rather than being a code sample in itself and so I set off to build my own sample.
For me, the Rome APIs in Windows 10 1607 today enable;
- A developer’s code to discover the user’s device graph that they’ve registered under the same Microsoft Account.
- A developer’s code to do a remote launch of a (custom/standard) URI on a remote system.
- A developer’s code to invoke a remote app service on a remote system.
Naturally, all under the control of the user and under the control of the remote app being invoked.
I wanted to try this out for a scenario that I find I’m often doing where I’ve opened up a set of tabs in my browser and I want to transfer that set of tabs to another device whether that be a PC or a phone or whatever.
I wrote a basic browser in order to try that out and I demo it in the video below;
There’s a few things that I found interesting in build that and so I thought I’d share those here.
The Device Discovery Code is Tiny
Here’s the code that I wrote in order to figure out which remote systems I have available to me. I like how short and neat it can be;
async Task StartRemoteSystemDetectionAsync() { var result = await RemoteSystem.RequestAccessAsync(); if (result == RemoteSystemAccessStatus.Allowed) { this.RemoteSystems = new ObservableCollection<RemoteSystem>(); this.remoteWatcher = RemoteSystem.CreateWatcher(); this.remoteWatcher.RemoteSystemAdded += (s, e) => { this.Dispatch(() => { this.AddRemoteSystem(e.RemoteSystem); }); }; this.remoteWatcher.Start(); } }
I ultimately data-bind the DisplayName, Kind and IsAvailableByProximity properties of the RemoteSystem object into a data template within the ComboBox that sits on the UI and it’s as simple as that.
The Remote App Service Invocation Code is Also Tiny
When it comes time to invoke the remote app service, that’s pretty easy to do as well, mostly relying on AppServiceConnection.OpenRemoteAsync() to do the work for me. In my app, the code looks like this;
internal async void OnTransfer() { var errorString = string.Empty; this.IsTransferring = true; this.TransferStatus = "connecting..."; using (var connection = new AppServiceConnection()) { connection.PackageFamilyName = App.PackageFamilyName; connection.AppServiceName = App.AppServiceName; var remoteRequest = new RemoteSystemConnectionRequest(this.SelectedRemoteSystem); var result = await connection.OpenRemoteAsync(remoteRequest); if (result == AppServiceConnectionStatus.Success) { this.TransferStatus = "Connected, calling..."; var message = new ValueSet(); var content = this.Serialize(); message[App.AppServiceParameterKey] = content; var response = await connection.SendMessageAsync(message); if (response.Status != AppServiceResponseStatus.Success) { this.TransferStatus = $"Failed to call - status was [{response.Status}]"; } this.TransferStatus = "Succeeded"; } else { this.TransferStatus = $"Failed to open connection with status {result}"; } this.transferStatus = "Closing"; } // time for the display of errors etc. to be seen. await Task.Delay(2000); this.IsTransferring = false; } }
Some of that might seem a little opaque without seeing the code for the whole app but it’s essentially;
- Create an AppServiceConnection, filling in the details of the PackageFamlyName and AppServiceName.
- Create a RemoteSystemConnectionRequest to the RemoteSystem that is selected in the UI.
- Make a call to OpenRemoteAsync()
- Create a ValueSet with the serialized list of web page URLs in it.
- Send it over the connection.
- Add a few pieces to update the UI and delay a little so that I can visually see whether it thinks it’s working or not.
and that’s pretty much it although it’s important to flag that the app manifest for the app has to advertise the right app service in the right way via;
<Extensions> <uap:Extension Category="windows.appService"> <uap3:AppService Name="OpenTabsService" SupportsRemoteSystems="true"/> </uap:Extension> </Extensions>
to flag that it’s available for remoting.
Using LeavingBackground and EnteringBackground in my App Class
In this app, I tried the technique of creating my UI when the app goes through its LeavingBackground event and destroying that UI when it hits its EnteringBackground event. I also saved state for the app when it hits EnteringBackground. This was the first time I’d tried this and it worked fine for me but I wasn’t sure whether my choice to destroy the UI at the point where the app moved into the background was a great idea because it might mean that a user who frequently moved the app foreground->background got frustrated with the UI keep rebuilding itself so perhaps I’m being too enthusiastic there.
As an example, my App.EnteringBackground handler does;
async void OnEnteringBackground(object sender, EnteredBackgroundEventArgs e) { // NB: getting rid of the UI here means that if the user opens up // N tabs then when they come back to the app those tabs will still // be there but will all start loading fresh again. That might not // be the best user experience as it loses their position on the // page. if (BrowserViewModel.ForegroundInstance != null) { var deferral = e.GetDeferral(); try { var serialized = BrowserViewModel.ForegroundInstance.Serialize(); await Storage.StoreAsync(serialized); } finally { deferral.Complete(); } } Window.Current.Content = null; this.background = true; }
and so it serializes any state (i.e. the URLs of the web pages) and then it clears the contents of the Window entirely and sets a flag to note that we are now in the background. When I come back from the background I have code that does the reverse;
void OnLeavingBackground(object sender, LeavingBackgroundEventArgs e) { this.EnsureUI(); this.background = false; }
and EnsureUI is pretty much the boilerplate code from a blank UWP template in that it creates a Frame, puts it into the Window and navigates it to a MainPage.
Using the Single Process Execution Model for Background Tasks
This app also uses the single process execution model for its background task implementation, override App.OnBackgroundActivated;
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args) { }
what I found interesting in there is that I can now check the value of my background flag to decide what to do with an incoming app service request as below;
async Task ProcessAppServiceRequestAsync(AppServiceRequestReceivedEventArgs args) { var webPages = args.Request.Message[AppServiceParameterKey] as string; if (!string.IsNullOrEmpty(webPages)) { // What we do now depends on whether we are running in the foreground // or not. if (!this.background) { BrowserViewModel.ForegroundInstance.Dispatch(() => { BrowserViewModel.ForegroundInstance.Deserialize(webPages); }); } else { // Store the pending web pages into our state file. await Storage.StoreAsync(webPages); // Flag that the app should read that state next time it launches // regardless of whether it was terminated or not. Storage.FlagRemoteRequestForNextLaunch(); } } }
where the code is essentially extracting the list of web pages from the incoming App Service message and then it’s deciding based on the background flag whether to simple store that data into a file for the next time the app runs or whether to feed it into a ‘live’ ViewModel such that the UI will actively update.
Using x : Bind
The other thing that I played with quite a bit in this (admittedly simple) bit of code was to use x : Bind for everything. I still find myself forgetting to say Mode=OneWay from time to time but I’m increasingly getting used to it and a few things that I did here were quick/easy wins.
One of those is the simple binding to a boolean property as a visibility as you can see in the code below with the IsClosable property;
in that same snippet you can see that I’m also binding the Click event to a function on the ViewModel called OnClose and I really like that too as it (arguably) avoids me having to create an ICommand implementation. I also did that here on my Pivot control with the SelectionChanged event;
Where this fell down for me slightly was that I wanted to run code at the point where my WebView has loaded and has updated its DocumentTitle property. I ended up bringing in the UWP behaviors/interactivity pieces for this as below;
<WebView x:Name="webView" Grid.Row="1" Source="{x:Bind BrowserUrl,Mode=OneWay}" xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" xmlns:Core="using:Microsoft.Xaml.Interactions.Core"> <Interactivity:Interaction.Behaviors> <Core:EventTriggerBehavior EventName="LoadCompleted"> <Core:ChangePropertyAction PropertyName="Label" TargetObject="{x:Bind}" Value="{Binding ElementName=webView,Path=DocumentTitle}" /> </Core:EventTriggerBehavior> </Interactivity:Interaction.Behaviors> </WebView>
It’s certainly descriptive! but what I really wanted to do was something a little bit like this;
<WebView x:Name="webView" Grid.Row="1" Source="{x:Bind BrowserUrl,Mode=OneWay}" LoadCompleted="{x:Bind OnLoaded, Parameter={Binding DocumentTitle}"/>
which, as far as I know, isn’t quite achievable with the current xBind implementation because the docs state that the function that is bound to an event must;
For events, the target method must not be overloaded and must also:
- Match the signature of the event.
- OR have no parameters.
- OR have the same number of parameters of types that are assignable from the types of the event parameters.
and so I don’t think it’s possible for me to bind this event to a method which takes a string parameter and then somehow pass the DocumentTitle into that method but it’s what I’d have liked to do.
The last thing I found with x : Bind is that there were places where I used converters as below;
<ComboBox.ItemTemplate> <DataTemplate x:DataType="rem:RemoteSystem"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <TextBlock VerticalAlignment="Center" Margin="0,0,2,0"> <Run Text="{x:Bind DisplayName}" /> <Run Text=" " /> <!-- Iknow, I know --> <Run Text="{x:Bind IsAvailableByProximity,Converter={StaticResource cxnTypeConverter}}"/> </TextBlock> <Image Height="24" Grid.Column="1" Source="{x:Bind Kind,Converter={StaticResource deviceImageConverter}}" /> </Grid> </DataTemplate> </ComboBox.ItemTemplate>
In that snippet, I’m using converters to convert the Kind and IsAvailableByProximity properties into different types of objects for display. I couldn’t use a binding to a function on the ViewModel here because I’d been lazy and simply bound a collection of RemoteSystem objects (which I don’t own) as the ItemsSource of the ComboBox. I can’t just add a method to a RemoteSystem class to do my conversions.
I could have;
- Wrapped a ViewModel around the RemoteSystem, enabling me to bind to a function on that ViewModel to do this conversion
- Maybe bound to a static function which did the conversion, passing it the property to convert.
- Gone with a converter.
I went with the 3rd option but it was perhaps just laziness and I should have done the first as I usually would but I did get a little side-tracked thinking about the middle option.
Wrapping Up
I spent maybe 1-2 hours putting this together. I’ve shared the source here if you want to play with it but it’s not intended for anything beyond just experimenting with Rome APIs.
I was inspired by your original set of Project Rome articles and had a play around with this myself. I used it to create an app that launches a phone call on your W10M device from your PC, and also a more general app to share content from one device and “hand off” to another via a Sharing Contract.
My only issue is that sometimes the performance is great, and other times (even under the same conditions) it can take a long time for the remote device to launch the application. Once it does work quickly once it seems to work reliably and quickly multiple times in a row.
Have you noticed any delays or ways to work around them to make it more reliable? Being on a WiFi / Ethernet network does seem to help, but that could just be coincidence.
Dan – thanks for getting in touch. I’ve seen a similar thing and especially where you say “once it does work quickly, it seems to work reliably and multiple times in a row”. I’m not sure what’s going on there but I’ll see if I can dig in and find an answer on it.
hi, i have a question about the discovery for the hub type devices. My supposition is that it is a Surface Hub right? In that case it isn’t in my device graph and should be discoverable locally accepting anonymous connections. I try in my office but it isn’t discoverable. Any clue?
Hi – I’m not sure how this works on a (Surface) Hub, I haven’t tried it there I’m afraid.
Pingback: Szumma #063 – 2016 43. hét | d/fuel