WP7 Code: Managing Application State

 

Visual Studio and the Windows Phone Developer Tools make building software for the Windows Phone similar to building desktop or browser applications. However, beyond these similarities crafting a phone application differs fundamentally from building an application aimed at a device with a keyboard, large screen, running on AC power, and with reliable network connectivity (to name just a few differences). Mobile device users are less likely to tolerate applications that demand too much of their attention, such as taking too much time to display information or to respond to actions. Consequently developers rely on techniques that make their applications provide a good experience.

This post shows how to extend the GeoFencing application to save and restore application state. Doing so in applications that need time to initialize can lower their launch time, one of the first thing their users will notice. The application state for the GeoFencing application comprises the perimeter’s center and radius. Preserving them across invocations allows the application to display information before acquiring a location fix (which, depending on the environment, could take a while). In addition to improving the perceived responsiveness storing the information about the perimeter also offers a better experience, allowing users to leave the application and then restart it without dropping the data.

Start by adding to the project a class representing the saved perimeter data (make sure you reference the System.Runtime.Serialization.dll which holds the namespace with the same name):

using System.Device.Location;
using System.Runtime.Serialization;

namespace GeoFencing
{
    [DataContract]
    public class PersistentRecord
    {
        [DataMember]
        public GeoCoordinate PerimeterCenter { get; set; }
        [DataMember]
        public double PerimeterRadius { get; set; }
    }
}

For this example I chose simple XML serialization—probably not a good choice if you’re dealing with lots of data and/or blobs.

Add a static property to the App class for holding the perimeter data. Next extend the Application_Activated, Application_Deactivated, and Application_Closing default implementations (template method) to load from/save to the PhoneApplicationService’s dictionary (to be picked up by a tombstoned application instance), or save it to the application’s isolated storage (to be picked up by a new application instance). The SavePersistentData method does all the work involving isolated storage.

// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
}

// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
    if (PhoneApplicationService.Current.State.ContainsKey("GeoFencing"))
    {
        State = (PersistentRecord)PhoneApplicationService.Current.State["GeoFencing"];
    }
}

// Code to execute when the application is deactivated (sent to background)
// This code will not execute when the application is closing
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
    if (State != null)
    {
        PhoneApplicationService.Current.State["GeoFencing"] = State;
        SavePersistentData();
    }
}

// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
    if (State != null)
    {
        SavePersistentData();
    }
}

public static PersistentRecord State { get; set; }

private static void SavePersistentData()
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        using (var stream = store.CreateFile("center.dat"))
        {
            var serializer = new DataContractSerializer(typeof(PersistentRecord));
            serializer.WriteObject(stream, State);
        }
    }
}

Note that the default implementation of Application_Launching stays the same (do nothing). Application_Launching is on the critical path for startup. Consequently file I/O +deserialization (or anything else that has the potential of slowing down launch times) doesn’t belong there. An asynchronous invocation (hint: Rx) from the MainPage ctor is a better alternative. First here’s the method that reads the saved information from isolated storage (App class). While it won’t win any robustness awards (that’s left as an exercise for the reader) it does illustrate the point.

public static bool TryReadState(out PersistentRecord savedState)
{
    using (var store = IsolatedStorageFile.GetUserStoreForApplication())
    {
        IsolatedStorageFileStream stream = null;
        savedState = default(PersistentRecord);
        try
        {
            if (store.FileExists("center.dat"))
            {
                using (stream = store.OpenFile("center.dat", System.IO.FileMode.Open))
                {
                    var serializer = new DataContractSerializer(typeof(PersistentRecord));
                    savedState = (PersistentRecord)serializer.ReadObject(stream);
                }
                return (true);
            }

        }
        catch (System.IO.IOException)
        {
            // swallow
        }
        catch (System.IO.IsolatedStorage.IsolatedStorageException)
        {
            // swallow
        }
        catch (SerializationException)
        {
            store.DeleteFile("center.dat");
        }
        return false;
    }
}

The final updates to the MainPage.xaml.cs enable it to read and use the saved perimeter information. The following Rx query (placed after InitializeComponent) surfaces the saved state as an IObservable<PersistentRecord>. The Start method invokes TryReadState asynchronously ,and ObserveOnDispatcher brings the events into the main (UI) thread. As there is at most one persistent record Take(1) drops everything else after that.

var savedStates = Observable.Start(() =>
{
    if (App.State != null)
    {
        return App.State;
    }
    else
    {
        PersistentRecord savedState;
        var isOk = App.TryReadState(out savedState);
        return isOk ? savedState : default(PersistentRecord);
    }
}).Where(e => e != null).ObserveOnDispatcher().Take(1);

The next change entails merging the stream corresponding to the radius position with the restored value provided by the App instance (either from the PhoneApplicationService or isolated storage):

var radii = this.slider1.GetValueChangedEventStream().Select(e => e.NewValue).StartWith(50)
                         .Merge(savedStates.Select(s => s.PerimeterRadius));

When either the perimeter’s center or radius change (via user input) a new PersistentRecord instance is set on the App object. The CombineLatest combinator provides an elegant solution for this:

centers.CombineLatest(radii, (l, r) => new { Center = l, Radius = r }).Subscribe(crs =>
    {
        App.State = new PersistentRecord { PerimeterCenter = crs.Center
                                         , PerimeterRadius = crs.Radius
                                         };
    });

The subsequent UI updates and distance computations (Rx queries) must use either the saved value or, upon the arrival of the first reading, the values from the geolocation subsystem. The TakeUntil combinator passes through the saved information until the arrival of the first center setting, extracted via the Select combinator :

var savedOrAcquiredCenters = centers.Publish(cs => cs.Merge(savedStates.Select(s => s.PerimeterCenter).TakeUntil(cs)));

The code for MainPage.xaml.cs follows. Building and running the application requires the XAML for the UI, as well as the App. xaml.cs and the PersistentRecord class.

public partial class MainPage : PhoneApplicationPage
{
    private GeoCoordinateWatcher gcw;
    public GeoCoordinateWatcher Gcw
    {
        get
        {
            if (gcw == null)
                gcw = new GeoCoordinateWatcher { MovementThreshold = 0.0 };
            return gcw;
        }
    }

    // Constructor
    public MainPage()
    {
        InitializeComponent();                            

        geoFenceButton.IsEnabled = false;
        slider1.IsEnabled = false;

        var savedStates = Observable.Start(() =>
        {
            if (App.State != null)
            {
                return App.State;
            }
            else
            {
                PersistentRecord savedState;
                var isOk = App.TryReadState(out savedState);
                return isOk ? savedState : default(PersistentRecord);
            }
        }).Where(e => e != null).ObserveOnDispatcher().Take(1);

        var positionUpdates = from sc in Gcw.GetStatusChangedEventStream()
                              where sc.Status == GeoPositionStatus.Ready
                              from pos in Gcw.GetPositionChangedEventStream()
                              let location = pos.Position.Location
                              where (!Double.IsNaN(location.Latitude) && !Double.IsNaN(location.Longitude) && location.HorizontalAccuracy <= 100.0)
                              select location;

        positionUpdates.Take(1).Subscribe(_ => geoFenceButton.IsEnabled = true);

        positionUpdates.Subscribe(l =>
        {
            this.hereTextBlock.Text = string.Format( "{0:###.00000000N;###.00000000S;0}, {1:###.00000000E;###.00000000W;0}"
                                                   , l.Latitude
                                                   , l.Longitude
                                                   );
        });

        var clicks = this.geoFenceButton.GetClickEventStream();

        clicks.Take(1).Subscribe(_ =>
            {
                this.slider1.IsEnabled = true;
            });

        var centers = positionUpdates.Publish(updates => from position in updates
                                                         from click in clicks.Take(1).TakeUntil(updates)
                                                         select position);

        var radii = this.slider1.GetValueChangedEventStream().Select(e => e.NewValue).StartWith(50)
                        .Merge(savedStates.Select(s => s.PerimeterRadius));

        centers.CombineLatest(radii, (l, r) => new { Center = l, Radius = r }).Subscribe(crs =>
            {
                App.State = new PersistentRecord { PerimeterCenter = crs.Center
                                                 , PerimeterRadius = crs.Radius
                                                 };
            });

        var savedOrAcquiredCenters = centers.Publish(cs => cs.Merge(savedStates.Select(s => s.PerimeterCenter).TakeUntil(cs)));

        savedOrAcquiredCenters.Subscribe(center =>
            {
                centerTextBlock.Text = string.Format("{0:###.00000000N;###.00000000S;0}, {1:###.00000000E;###.00000000W;0}", center.Latitude, center.Longitude);
            }
            );

        radii.Take(2).Subscribe(r => this.slider1.Value = r);

        radii.Subscribe(radius =>
            {
                this.fenceTextBlock.Text = string.Format("Perimeter radius {0:####.00}m",radius);
            });

        var distancesFromCenter = positionUpdates.CombineLatest(savedOrAcquiredCenters, (p, c) => p.GetDistanceTo(c)).DistinctUntilChanged();

        var distancesToFence = distancesFromCenter.CombineLatest(this.slider1.GetValueChangedEventStream(), (d, radius) => radius.NewValue - d);
        distancesToFence.Subscribe(distanceToFence =>
            {
                var msg = string.Empty;
                if (distanceToFence >= 0)
                {
                    msg = string.Format("{0:####.00}m from the fence", this.slider1.Value - distanceToFence);
                    distanceTextBlock.Foreground = new SolidColorBrush(Colors.Green);
                }
                else
                {
                     msg = string.Format("{0:####.00}m outside the fenced perimeter", Math.Abs(distanceToFence));
                     distanceTextBlock.Foreground = new SolidColorBrush(Colors.Red);
                }
                this.distanceTextBlock.Text = msg;
            });
    }

In summary, this post has shown:

  • How to tap into the application’s lifecycle via the methods exposed by the App class
  • How to use Observable.Start to asynchronously execute an Action, and bring the results to the UI thread with ObserveOnDispatcher
  • How to leverage Rx to combine events from several event streams