分类: 嵌入式
2010-11-26 10:48:07
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 . 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 ’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
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 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 combinator passes through the saved information until the arrival of the first center setting, extracted via the 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: