WP7 App Settings Persistence
The final part of my series of blog posts covering the code tips and tricks I shared at UK Tech.Days 2011, is about providing users with an improved experience when they are forced to re-install your application, whether that be due to them migrating to a new phone, or a bug forcing them to uninstall it.
So what's the problem? Well when a user reinstalls your app they have to start from scratch, any settings/customisations they make to your app are lost and this barrier to re-entry can sometimes force users to simply go elsewhere. In an ideal world a user should be presented with the option to restore their settings/customisations to you app when they load it for the first time after the uninstall/on the new device. Obviously any settings stored in Isolated Storage are lost so if you want to provide this experience you will need to save these settings in the cloud.
The process is quite simple, when the app is running a 'backup' of the user's settings are sent to the cloud along with the User ID. Whenever the application loads it checks the isolated storage for the settings, meaning that an app can have fast and offline loading, but if no settings are found locally then a request for the settings matching the current User ID is sent to the cloud. If settings for that user are found in the cloud then they are returned and the application should prompt the user to ask if they want these settings restored. A key point here is to ask the user at this point for 2 reasons, firstly the user may have intentionally uninstalled the application so that they can start from scratch, or the user's settings may be causing the bug which forced them to uninstall it to start fresh. Thanks to the simplicity of these steps this code can easily be added to any existing application without any major refactors of code.
At this point many people will say that while this process sounds simple the 'scary' part is the use of the world 'cloud', as cloud services can be complex and expensive which often doesn't make sense for a small phone application. However I will demonstrate below that implementing a simple WCF Web Service like this is very simple and is lightweight enough to run on the lowest cost Azure instance (or even share an instance with other cloud services you may be running). For more information on the cost of Azure hosting take a look here, or if you are a startup I'd highly recommend looking into Microsoft's BizSpark scheme in which you can get lots of Azure usage (and may other benefits and Microsoft software) for free!
Implementation - Client Side
The implementation of this consists of 2 parts, the client side and the server/cloud side. All code snippets are taken from our TechDays Sample app and are available here, in this sample the only user settings we have is the list of 'followed' sessions, but you should be able to see how this can be extended to include whatever settings your app needs.
First, in the background function where we load our settings from Isolated Storage and then apply them, we need to add in the extra logic to fire off a simple Web Service call if no settings are loaded. Then once the Web Service call returns we need to respond appropriatley as explained above. The code to do this is shown below:
void BackgroundLoadFavouriteSessions(object sender, DoWorkEventArgs e) { var favIDs = LoadFavouriteSessionsFromFile(); if (favIDs == null) { // We failed when loading from file so check the cloud store CloudUserDataServiceClient client = new CloudUserDataServiceClient(); client.GetStoredTechDaysUserDataCompleted += client_GetStoredTechDaysUserDataCompleted; client.GetStoredTechDaysUserDataAsync(HelperFuncs.UserID); } else { // Loaded from file successfully so use this data SetFavourites(GetSessionListFromIDs(favIDs)); } } void client_GetStoredTechDaysUserDataCompleted(object sender, GetStoredTechDaysUserDataCompletedEventArgs e) { if ((e.Error == null) && (e.Result != null) && (!String.IsNullOrEmpty(e.Result.FavouriteIDs))) { if (MessageBox.Show("Would you like to restore your settings for this app?", "No settings found", MessageBoxButton.OKCancel) == MessageBoxResult.OK) { var favouriteSessionList = GetSessionListFromIDString(e.Result.FavouriteIDs); SetFavourites(favouriteSessionList); } } }
Also at some other point while the application is running we want to send the app's current settings off to the Web Service, like so:
private void SaveFavouritesToCloud(string favIDString) { TechDaysUserData saveData = new TechDaysUserData() { UserID = HelperFuncs.UserID, FavouriteIDs = favIDString }; CloudUserDataServiceClient client = new CloudUserDataServiceClient(); client.StoreTechDaysUserDataAsync(saveData); }
Both these code blocks are fairly standard Web Service calls, the only potentially unknown thing here is how to retrieve the user's ID. This is shown in the following block of code:
private static string cachedUserID = null; public static string UserID { get { if (cachedUserID == null) { string anid = UserExtendedProperties.GetValue("ANID") as string; if (anid != null) { cachedUserID = anid.Substring(2, 32); } else { cachedUserID = "EMULATOR_USER"; } } return cachedUserID; } }
This is just a wrapper around the Microsoft API to retrieve the User Extended Property but we add some caching here to improve performance, and strip away part of the string to provide an anonymous User ID. More information on this is available via the MSDN page. That is all the client side code that you will need to add into your existing application, now onto the server side.
Implementation - Cloud Side
If you have ever used Azure Table Storage before the following code should look very simple, if you haven't then I'd suggest having a quick read up on it as I won't go into great detail about it here. The same also applies here for WCF Web Services.
The first thing we want to do on the Azure/Cloud side is create the structures for holding our User Settings, the code below describes the Azure Table Storage Table and Entity that will be used to hold this data:
public class TechDaysDataEntryContext : TableServiceContext { public static string TableName = "TechDaysDataEntrys"; public TechDaysDataEntryContext(CloudStorageAccount account) : base(account.TableEndpoint.ToString(), account.Credentials) { } public IQueryable<TechDaysDataEntry> TechDaysDataEntrys { get { return this.CreateQuery<TechDaysDataEntry>(TableName); } } public void AddTechDaysDataEntry(TechDaysDataEntry newEntry) { this.AddObject(TableName, newEntry); this.SaveChanges(); } } public class TechDaysDataEntry : TableServiceEntity { public string UserID { get { return PartitionKey; } set { RowKey = value; PartitionKey = value; } } public string FavouriteIDs { get; set; } public TechDaysDataEntry() { } public TechDaysDataEntry(string UserID, string inFavouriteIDs) { PartitionKey = UserID; RowKey = UserID; FavouriteIDs = inFavouriteIDs; } }
That is the Azure Table Storage code out of the way, the next thing we need to do is write the WCF Web Service to provide access to/from the storage. The code for those two functions is below:
public TechDaysUserData GetStoredTechDaysUserData(string userID) { var storageAccount = GetStorageAccount(); var tddeContext = new TechDaysDataEntryContext(storageAccount); tddeContext.IgnoreResourceNotFoundException = true; var result = (from entry in tddeContext.TechDaysDataEntrys where entry.PartitionKey == userID && entry.RowKey == userID select entry).ToList(); if (result.Count == 1) { var entry = result.First(); var userData = new TechDaysUserData() { UserID = entry.UserID, FavouriteIDs = entry.FavouriteIDs }; return userData; } return null; } public void StoreTechDaysUserData(TechDaysUserData userData) { var storageAccount = GetStorageAccount(); var tddeContext = new TechDaysDataEntryContext(storageAccount); tddeContext.IgnoreResourceNotFoundException = true; var result = (from entry in tddeContext.TechDaysDataEntrys where entry.PartitionKey == userData.UserID && entry.RowKey == userData.UserID select entry).ToList(); if (result.Count == 0) { var entry = new TechDaysDataEntry(userData.UserID, userData.FavouriteIDs); tddeContext.AddTechDaysDataEntry(entry); } else { var entry = result.First(); entry.FavouriteIDs = userData.FavouriteIDs; tddeContext.UpdateObject(entry); tddeContext.SaveChanges(); } }
Then the final bit of code is a very simple class used for passing data between our Application and the Web Service:
[DataContract] public class TechDaysUserData { [DataMember] public string UserID{ get; set; } [DataMember] public string FavouriteIDs { get; set; } }
And that's it, everything you need to implement this feature. You will likely want to expand the settings that are saved, as well as consider your privacy policy around storing User's data, etc. but I'll leave that as an exercise for you.
This post was also a guest post over at the VerySoftware Blog
Monday, July 18, 2011 at 9:30AM
Reader Comments (1)
So cute! I already like you on FB and also get your posts on Google Reader. :) mmumdr mmumdr - supra foot.