Windows 8.0/8.1 have the ability to roam an app’s data for a user across their devices in the form of settings values (i.e. simple types stored in dictionaries) and files.
This is written up on MSDN here;
with one of the the key things being that the OS limits the amount of data that it’s prepared to roam for an app (see RoamingStorageQuota) and simply turns off roaming if that limit is exceeded.
The other key things are that the roaming system is a simple “last write wins” type system rather than a complicated sync engine and that the settings are dictionaries of simple types that can be nested (to 32 levels deep) with each setting being a maximum of 8KB and each dictionary being a maximum of 64KB.
Synchronisation is done in a fairly non-deterministic way but there’s one bunch of settings called “HighPriority” which is sync’d within 1 minute if possible but that’s restricted to 8KB of data.
The “simple types” part of settings feels quite restrictive but you can always serialize an object graph into a string if that helps in getting it into a settings value (subject to the size limits).
I thought I’d experiment with making a simple universal app with one setting value which roamed across Windows/Phone. Here’s the steps I went through;
Step 1 – Make an App
Clearly, I’m going to have to make an app so I made a new, blank universal app project in Visual Studio 2013 Update 2 RC;
I’m not going to need much in the way of UI that differs on Windows/Phone so I deleted the MainPage.xaml/.cs from the Windows project and then moved the one from the Windows Phone project into the Shared project such that my UI and the “code behind” is all in the shared project;
I’m going to display some image so I reached into PowerPoint, searched for “animals” and found some royalty free images I can use;
I saved each of these into a new Images folder within my Shared project naming them Pic0 to Pic4.png.
Then I wrote a “ViewModel” which could be used to present these images and handle their selection changing;
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Windows.ApplicationModel; namespace SyncIt { class BindableBase : INotifyPropertyChanged { protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null) { if (object.Equals(storage, value)) return false; storage = value; this.OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged( [CallerMemberName] string propertyName = null) { var eventHandler = this.PropertyChanged; if (eventHandler != null) { eventHandler(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; } class PictureViewModel : BindableBase { public string SelectedValue { get { return (this._selectedValue); } set { base.SetProperty(ref this._selectedValue, value); } } string _selectedValue; public IEnumerable<string> Images { get { if (this.images == null) { GetImagesAsync(); } return (images); } } async Task GetImagesAsync() { // might want to cache this list perhaps. var folder = await Package.Current.InstalledLocation.GetFolderAsync("Images"); var files = await folder.GetFilesAsync(); this.images = files .Where(f => System.IO.Path.GetExtension(f.Path) == @".png") .Select(f => f.Path) .ToList(); this.OnPropertyChanged("Images"); } IEnumerable<string> images; } }
and then wrote a little XAML to display that;
<Page x:Class="SyncIt.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SyncIt" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Page.DataContext> <local:PictureViewModel /> </Page.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Image Source="{Binding SelectedValue}" Stretch="Uniform" Margin="5,5,5,15"/> <ListView Grid.Row="1" ItemsSource="{Binding Images}" SelectedValue="{Binding SelectedValue,Mode=TwoWay}" Margin="5,0,5,5" HorizontalAlignment="Center"> <ListView.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </ListView.ItemsPanel> <ListView.ItemTemplate> <DataTemplate> <Image Width="48" Height="48" Stretch="Uniform" Source="{Binding}" /> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </Page>
and ran it up on Windows/Phone at the same time and that works “fine”;
but, clearly, there’s no link between the two applications because that “SelectedValue” isn’t being stored anywhere other than in memory.
It’s not persisted anywhere across invocations of the app and it certainly isn’t shared between Windows & phone.
I need to persist it and in a way that’s shared between the two apps.
As it turns out, the 2 systems produce different values for the file paths that I’m binding to here which will also cause me a minor hiccup in a moment…
Step 2 – Associate the Apps with the Stores & Link Them
In order for the cloud infrastructure to be able to handle roaming settings between the two applications, I think they need to be associated with the Stores. This doesn’t mean that you have to ship the apps, you can always experiment and then delete the applications afterwards.
If I take the Windows Store application and associate it with the Store;
and then register an application name for it (not sure where this emulator picture came from below);
then on the packaging page of the Windows 8.1 manifest I see;
if I then repeat the process with the Windows Phone 8.1 application and pick the same mtaultySyncIt application name from the list as I’m running through the dialog and I then look in the Windows Phone 8.1 manifest I see;
the two applications have the same “Package family name” at the Store which (I think) links them. I now need to write some code to test that out.
Step 3 – A Common Microsoft Account Across Windows/Phone
If you’re logging in to your Windows 8.1 device and your Windows Phone 8.1 device with the same Microsoft Account then you’re in business from the point of view of synchronising roaming settings/files.
In my case, I’m debugging on my laptop which has an associated Microsoft Account but I’m also debugging on the Phone emulator which means that things won’t work unless I go into the emulator’s settings and tell it to use the same Microsoft Account. If I do that, things seem to work ok but this might be an argument for simply debugging on my Phone as that shares the same Microsoft Account so things would be simpler to set up.
Step 4 – Using Roaming Settings Rather than a Local Value
The first thing I realised while experimenting with this was that I am trying to persist a path to an image as a shared setting. The problem was that the path that I was building up on Windows 8.1 wasn’t a path that Windows Phone 8.1 was happy with so I needed to change my ViewModel such that the paths were going to be consistent (and, as it happens, just shorter and nicer) so I changed my ViewModel so that it returned image paths more of the form Images/pic0.png;
class PictureViewModel : SettingsBindableBase { public string SelectedValue { get { return (base.GetSettingsProperty<string>( "UsersImage", SettingsScope.Roaming)); } set { base.SetSettingsProperty<string>( "UsersImage", value, SettingsScope.Roaming); } } public IEnumerable<string> Images { get { if (this.images == null) { GetImagesAsync(); } return (images); } } async Task GetImagesAsync() { // might want to cache this list perhaps. var folder = await Package.Current.InstalledLocation.GetFolderAsync("Images"); var files = await folder.GetFilesAsync(); this.images = files .Where(f => System.IO.Path.GetExtension(f.Path) == @".png") .Select(f => string.Format("Images/{0}", f.Name)) .ToList(); this.OnPropertyChanged("Images"); } IEnumerable<string> images; }
I also changed the base class of the ViewModel to a new class I experimented with called SettingsBindableBase. I’m not so sure whether I like what I’ve done here but I added a GetSettingsProperty and SetSettingsProperty to that class and you can see them being used from the SelectedValue property above and what that usage is meant to mean is that these values are now stored in RoamingSettings under a key of “UsersImage”.
Those Get/SetSettingProperty methods will also support a “dotted notation” – e.g. “Preferences.Colours.Foreground” as a way of navigating into a particular container of application settings. They also support using Local/Roaming settings for the containers.
The code for that base class is as below;
class SettingsBindableBase : BindableBase { public enum SettingsScope { Local, Roaming } static SettingsBindableBase() { globalTrackedObjects = new ConcurrentQueue<WeakReference<SettingsBindableBase>>(); ApplicationData.Current.DataChanged += OnRoamingSettingsChanged; } static void OnRoamingSettingsChanged(ApplicationData sender, object args) { List<SettingsBindableBase> liveObjects = new List<SettingsBindableBase>(); WeakReference<SettingsBindableBase> reference; while (globalTrackedObjects.TryDequeue(out reference)) { SettingsBindableBase entry; if (reference.TryGetTarget(out entry)) { liveObjects.Add(entry); } } foreach (var entry in liveObjects) { globalTrackedObjects.Enqueue(new WeakReference<SettingsBindableBase>(entry)); } foreach (var entry in liveObjects) { entry.NotifyAllRoamedPropertiesChanged(); } } static ApplicationDataContainer ContainerFromName(string settingName, out string key, SettingsScope scope, bool create = false) { string[] pieces = settingName.Split('.'); key = pieces[pieces.Length - 1]; ApplicationDataContainer container = scope == SettingsScope.Local ? ApplicationData.Current.LocalSettings : ApplicationData.Current.RoamingSettings; for (int i = 0; i < pieces.Length - 1; i++) { if (container.Containers.ContainsKey(pieces[i])) { container = container.Containers[pieces[i]]; } else if (create) { container = container.CreateContainer(pieces[i], ApplicationDataCreateDisposition.Always); } else { container = null; break; } } return (container); } protected T GetSettingsProperty<T>( string settingName, SettingsScope scope = SettingsScope.Local, [CallerMemberName] string propertyName = null) { T t = default(T); string key; ApplicationDataContainer container = ContainerFromName(settingName, out key, scope); if (container != null) { object o; if (container.Values.TryGetValue(key, out o)) { // might blow up here... t = (T)o; if (scope == SettingsScope.Roaming) { this.TrackRoamedPropertyAccess(propertyName); } } } return (t); } protected void SetSettingsProperty<T>(string settingName, T value, SettingsScope scope = SettingsScope.Local, [CallerMemberName] string propertyName = null) { string key; ApplicationDataContainer container = ContainerFromName(settingName, out key, scope, true); container.Values[key] = value; this.OnPropertyChanged(propertyName); } void TrackRoamedPropertyAccess(string propertyName) { if (this.syncContext == null) { this.syncContext = SynchronizationContext.Current; } if (this.trackedRoamedProperties == null) { this.TrackRoamedObject(); this.trackedRoamedProperties = new List<string>(); } if (!this.trackedRoamedProperties.Contains(propertyName)) { this.trackedRoamedProperties.Add(propertyName); } } void TrackRoamedObject() { globalTrackedObjects.Enqueue(new WeakReference<SettingsBindableBase>(this)); } void NotifyAllRoamedPropertiesChanged() { this.syncContext.Post( _ => { if (this.trackedRoamedProperties != null) { foreach (var property in this.trackedRoamedProperties) { base.OnPropertyChanged(property); } } }, null); } static ConcurrentQueue<WeakReference<SettingsBindableBase>> globalTrackedObjects; List<string> trackedRoamedProperties; SynchronizationContext syncContext; }
and I was kind of feeling ok about this class until it came to the point where I wanted to see if I could make sure that whenever the ApplicationData.Current.DataChanged event fired (i.e. when roaming settings have been updated by a synchronisation event) any properties that were “backed” by those settings values correctly reported via INotifyPropertyChanged that their value had changed.
I have something kind of working right now but I’m not sure I feel so good about it just yet.
What the code above is trying to do is to ensure that if an object it has been asked for a value for a property called Foo which is backed by a roaming application setting then whenever all the settings are changed by a sync event then that object will fire a PropertyChanged for that property Foo.
The problem in doing that is that the ApplicationData.Current.DataChanged event is just a global event. There’s no notion (as far as I know) of being aware of what has changed.
So, the code above is trying to do its own lazy book-keeping in the sense that it keeps a weak-reference to any object that has “handed out” a property value backed by a roamed property setting and also a reference to the name of the property that was backed by that setting. Then, whenever the roaming data changes, it attempts to find any of those objects that are still “alive” and asks them to fire property changed notifications for any of those properties.
Because the “DataChanged” event comes in on a background thread and because it’s possible that the static list of weak references that I maintain here could (at least on Windows 8.1) be used by multiple UI threads I get into having to think a little about thread-safety and SynchronizationContexts and it all gets a bit out of hand and that’s why I’m not sure that I’m pleased with where it ended up.
But, for the purposes of seeing if I can synchronize a simple setting value across Windows/Phone 8.1 apps it more than serves its purpose and I can try things out using these classes to debug on my local device (Windows 8.1) and on the emulator (Windows Phone 8.1).
Step 5 – Trying it Out
With all that setup, I can run the 2 apps side-by-side on my PC – one on the emulator, one locally;
and then if I change the Windows 8.1 value (on the left) to be the chicken;
and then lock/unlock my PC (to kick the sync engine into life) then after a few seconds I see chickens!
and if I change the Windows Phone value to be the monkey;
then I seem to wait an awfully long time before the Windows 8.1 app displays any change.
I’m not sure how to “kick start” the sync process from the emulator here so I tried debugging on my Phone rather than the emulator and I still didn’t manage to see a situation where a change made on the Phone showed up on Windows 8.1 (but, again, the reverse situation worked fine on the Phone just like it did on the emulator).
I’ll work on this one a little more and will update the post when I figure why changes aren’t seeming to be sync’d in both directions.