Tuesday 13 November 2012

Be very careful saving data in Windows 8

Windows 8 presents a number of new challenges for developers.

My challenge today is how can I save what the user is doing, and not lose it.

Best practice for Windows 8 apps suggests you save early, and often.  From the time a user closes your app you have about 10 seconds until it’s definitely not running.  For those of you who aren’t totally up to speed on the details of the Windows Store Application Lifecycle I suggest you have a look here first.

Now, the problem is in that article, on the section on App Close

App close

Generally, users don't need to close apps, they can let Windows manage them. However, users can choose to close an app using the close gesture or by pressing Alt+F4. You can't include any UI in your app to enable the user to close your app, or it won't pass the Store certification process.

Given the above, you can see that there is no way to be notified of a close event, however your OnSuspending event will still fire, after about 5 seconds.

So, assuming you busily began to save when you’re app received the VisibilityChanged event, you have maybe 5 seconds left to save the remainder of your state when you get to OnSuspending.

I say maybe, because, if you’re really unlucky, you’ll find that the user restarted your application from the start menu, after beginning your OnSuspending code.  In that case, your currently running thread will be terminated and your app will restart.  The file you were in the middle of saving could now be in any state.

If you don’t believe me, you can read the details here

Saving the application data to file in the Application.OnSuspending method does not work?

In a nutshell, this quote from the bottom of the thread sums up the problem
"To summarize, if the user closes the app and then immediately restarts before the first instance has finished closing then the first instance is forcibly terminated so the user runs in a clean state."

So, how do we build an application to demonstrate the problem?  First, in App.xaml.cs, wire up the VisibilityChanged event when you create your root frame.

Window.Current.VisibilityChanged += Current_VisibilityChanged;
this.Suspending += OnSuspending;

Next, implement the event handler like this and add a couple of members.


Task currentSaveTask;
void Current_VisibilityChanged(object sender, WIndows.UI.Core.VisibilityChangedEventArgs e)
{
if (!e.Visible && currentSaveTask == null)
{
CurrentSaveTask = SaveOurData();
}
}
async Task RenameFile(string tempFile, string newName)
{
StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;
StorageFile sf = await localFolder.GetFileAsync(tempFile);
await sf.RenameAsync(newName, NameCollisionOption.ReplaceExisting);
}

async Task WriteData(string filename, string content)
{
StorageFolder localFolder = Windows.Storage.ApplicationData.Current.LocalFolder;
// on my machine, this is C:\Users\Tony\AppData\Local\Packages\2da24013-8fd9-49f2-8067-778dece884d6_6j3fz8tfj8x4m\LocalState

StorageFile sf = await localFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);

using (Stream s = await sf.OpenStreamForWriteAsync())
using (TextWriter output = new StreamWriter(s))
{
output.WriteLine(content);
}
}

async Task BeginSaving()
{
await WriteData("startup.txt", "We started saving");
}

async Task ContinueSaving()
{
await WriteData("temporary.txt", "The data file is incomplete");
}

async Task FinishSaving()
{
await WriteData("temporary.txt", "The data file was written successfully");
}

async Task SaveOurData()
{
await BeginSaving(); // show some progress to the user.
await Task.Delay(5500); // simulate extra work
await ContinueSaving();
await Task.Delay(4000); // simulate still more work.
await FinishSaving();
await RenameFile("temporary.txt", "final.txt");
currentSaveTask = null; // remove reference to our pending save
}


This writes a file in to our local storage, named startup.txt, and then delays long enough to guarantee we’re in the Suspend code, it then creates a file temporary.txt, delays a little longer, and finally writes the finished version of the temporary.txt file.


Finally it proceeds to rename the file to final.txt, and cleans up the handle to the outstanding task.


Now implement our OnSuspending handler like this:


 


async private void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
if (currentSaveTask != null) await currentSaveTask;
// you may want to use Task.WhenAll here to rendezvous multiple tasks...
// Task[] OutstandingSave = new Task[] { currentSaveTask };
//if (OutstandingSave[0] != null)
//{
// await Task.WhenAll(OutstandingSave);
//}
deferral.Complete();
}


This code waits for the currentSaveTask we kicked off if it exists.





Testing


To test this, do the following.


Fire up your app, and and open a window to wherever your App’s StorageFolder.LocalFolder resolves to.


Now press F4 on the app, to close it.


You should see a startup.txt file appear, followed after about 5.5 seconds a temporary.txt file appear. When you see that appear, quickly press the windows key, and click to restart your app.


You’ll notice that the remainder of the code never executes, and when you examine temporary.txt, it will contain “The data file is incomplete”.


Takeaway


If you were saving directly to a file (in XML format) and hadn’t yet completed by the time you relaunched, your file would be incomplete or corrupt.  This method allows you to check that the temporary.txt file does not exist at startup.  If it does exist, you can be pretty certain that final.txt file is incomplete.  Of course, temporary.txt file may be incomplete as well, so you’ll need to code in an additional step to allow you to check that it too is complete, (possibly by renaming to another intermediate file, temporaryfinal.txt?)

1 comment:

  1. While playing around with this today, it's interesting to note that when you move a Windows Store App from one monitor to another, you may move through the NotVisible state.

    ReplyDelete