Getting geographic locations on Windows Phone 7 and MVVM

by Geert 4. April 2011 01:05

We heard a lot about Windows Phone 7. We also heard a lot about MVVM. But lately I was wondering: how to get the current geographical location in Windows Phone 7 using MVVM? And even better, how to simulate it when you don’t have a Windows Phone device (like me for example)?

During the development of Catel, I write more and more service that are useful for WPF and Silverlight, but this one is very specific to Windows Phone 7. Until now, Google tells me there are no such services available yet! Now the question is: am I the only one that wants to:

  1. Write clean MVVM where getting the current location via regular classes is not clean?
  2. Does not understand MVVM correctly and completely misses the point?

For my own safety, I’d like to go for option 1. If you think option 2 matches better, I strongly suggest you to skip the rest of this article.

History

I am one of the developers of Catel, a great open-source MVVM framework for WPF, Silverlight and Windows Phone 7. It takes care of separation of concerns (SoC) in MVVM using services. There are services like IMessageService (show message boxes), INavigationService (navigate between views), and much more. Each service also provides a unit test version so you don’t have to mock

Writing the service

The service on itself is not very complex. As usual, I like to start by providing the full interface. Whether you like to read the interface definition or not is up to you. The most important aspects will be covered later in this article.

   1: /// <summary>
   2: /// Interface that supports retrieving the current location.
   3: /// </summary>
   4: public interface ILocationService
   5: {
   6:     /// <summary>
   7:     /// Occurs when the current location has changed.
   8:     /// </summary>
   9:     event EventHandler<LocationChangedEventArgs> LocationChanged;
  10:  
  11:     /// <summary>
  12:     /// Gets the current location represented as <see cref="ILocation"/>. If no location is available, <c>null</c> will be returned.
  13:     /// </summary>
  14:     /// <value>The current location.</value>
  15:     /// <remarks>
  16:     /// This is convenience property that internally calls <see cref="GetCurrentLocation"/>.
  17:     /// <para />
  18:     /// Note that the services inside Catel do not support <see cref="INotifyPropertyChanged"/>, thus you cannot 
  19:     /// subscribe to changes of this property. Instead, subscribe to the <see cref="LocationChanged"/> event.
  20:     /// </remarks>
  21:     ILocation CurrentLocation { get; }
  22:  
  23:     /// <summary>
  24:     /// Gets the current location.
  25:     /// </summary>
  26:     /// <returns>
  27:     /// The current location represented as <see cref="ILocation"/>. If no location is available, <c>null</c> will be returned.
  28:     /// </returns>
  29:     ILocation GetCurrentLocation();
  30:  
  31:     /// <summary>
  32:     /// Starts the location service so it's retrieving data.
  33:     /// </summary>
  34:     void Start();
  35:  
  36:     /// <summary>
  37:     /// Stops the location service so it's no longer retrieving data.
  38:     /// </summary>
  39:     void Stop();
  40: }

Using the service

The usage of the service is really, really simple. In the Initialize method of your view-model, get the service and start it:

   1: protected override void Initialize()
   2: {
   3:     if (AvailableMapSources.Count > 0)
   4:     {
   5:         CurrentMap = AvailableMapSources[0];
   6:     }
   7:  
   8:     var locationService = GetService<ILocationService>();
   9:     locationService.LocationChanged += OnCurrentLocationChanged;
  10:     locationService.Start();
  11: }

The service provides a LocationChanged event where you should subscribe to. In the handler, we update the MapCenter property on the view-model:

   1: private void OnCurrentLocationChanged(object sender, LocationChangedEventArgs e)
   2: {
   3:     // Only update if there is a new location, otherwise assume that the user wants to see the last position
   4:     if (e.Location != null)
   5:     {
   6:         MapCenter = new GeoCoordinate(e.Location.Latitude, e.Location.Longitude, e.Location.Altitude);
   7:     }
   8: }

 

Don’t forget to clean up your mess, stop the service in the Close method:

   1: protected override void Close()
   2: {
   3:     var locationService = GetService<ILocationService>();
   4:     locationService.LocationChanged -= OnCurrentLocationChanged;
   5:     locationService.Stop();
   6:  
   7:     base.Close();
   8: }

Unit-test or simulate locations

Great, we have a location service. Very neat and clean, but you don’t have a WP7 device connected during unit tests, or maybe you don’t have a WP7 device at all. No problem, Catel to the rescue! As always, we implemented a test service that allows you to enter expected locations where the user is at a specific moment in time so you can either test or simulate locations. To use the test-version of the ILocationService, you need to instruct the IoC container to use the test version instead of the real one:

   1: IoC.IoCProvider.Instance.RegisterType<ILocationService, MVVM.Services.Test.LocationService>();

The code below shows me walking through my own street:

   1: /// <summary>
   2: /// Initializes the demo route for test purposes.
   3: /// <para />
   4: /// Calling this method will register the test version of the <see cref="ILocationService"/>.
   5: /// </summary>
   6: private void InitializeDemoRoute()
   7: {
   8:     // This is a demo app, register test version of the service
   9:     // In normal situations, you would not directly cast a service to a specific type in your view-model,
  10:     // only in unit tests to set the expected locations. However, since we simply want to show the power
  11:     // of IoC in combination with the location service, we register the service here and directly retrieve
  12:     // it to simulate a user walking through a street
  13:     IoC.IoCProvider.Instance.RegisterType<ILocationService, MVVM.Services.Test.LocationService>();
  14:     var testLocationService = (MVVM.Services.Test.LocationService)GetService<ILocationService>();
  15:  
  16:     TimeSpan timeSpan = new TimeSpan(0, 0, 0, 0, 500);
  17:  
  18:     // First one is longer because maps need to initialize
  19:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38772d, 5.56484d), new TimeSpan(0, 0, 0, 5)));
  20:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38771d, 5.56484d), timeSpan));
  21:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38770d, 5.56484d), timeSpan));
  22:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38769d, 5.56483d), timeSpan));
  23:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38768d, 5.56483d), timeSpan));
  24:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38767d, 5.56483d), timeSpan));
  25:  
  26:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38766d, 5.56482d), timeSpan));
  27:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38765d, 5.56482d), timeSpan));
  28:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38764d, 5.56482d), timeSpan));
  29:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38763d, 5.56481d), timeSpan));
  30:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38762d, 5.56481d), timeSpan));
  31:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38761d, 5.56481d), timeSpan));
  32:  
  33:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38760d, 5.56480d), timeSpan));
  34:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38759d, 5.56480d), timeSpan));
  35:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38758d, 5.56480d), timeSpan));
  36:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38757d, 5.56479d), timeSpan));
  37:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38756d, 5.56479d), timeSpan));
  38:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38755d, 5.56479d), timeSpan));
  39:  
  40:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38754d, 5.56478d), timeSpan));
  41:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38753d, 5.56478d), timeSpan));
  42:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38752d, 5.56478d), timeSpan));
  43:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38751d, 5.56477d), timeSpan));
  44:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38750d, 5.56477d), timeSpan));
  45:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38749d, 5.56477d), timeSpan));
  46:  
  47:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38748d, 5.56476d), timeSpan));
  48:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38747d, 5.56476d), timeSpan));
  49:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38746d, 5.56476d), timeSpan));
  50:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38745d, 5.56475d), timeSpan));
  51:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38744d, 5.56475d), timeSpan));
  52:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38743d, 5.56475d), timeSpan));
  53:  
  54:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38742d, 5.56474d), timeSpan));
  55:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38741d, 5.56474d), timeSpan));
  56:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38740d, 5.56474d), timeSpan));
  57:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38739d, 5.56473d), timeSpan));
  58:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38738d, 5.56473d), timeSpan));
  59:     testLocationService.ExpectedLocations.Enqueue(new LocationTestData(new Location(51.38737d, 5.56473d), timeSpan));
  60: }

You might see a lot of coordinates, but that’s just the path I would walk meter by meter when I walk through my street. You can customize both the locations and the timespan between the updates.

If you don’t want updates at all, just use the overload of the LocationTestData class without a timespan and manually call the testLocationService.ProceedToNextLocation() method.

Culprits

I was using this service inside a demo application of Catel. It includes a bing maps control that binds the Center property to a view-model GeoCoordinate property on the view-model. Unfortunately, the Center property of the Map control is not a dependency property (must be a very wise men who decided this)… Therefore, we needed a workaround that if the MapCenter property on the view model changes, the map gets updated by hand (a method). To create this workaround, I used the ControlToViewModelMapping attribute that ships with Catel. It’s a really great feature of Catel to watch view-model changes from a control, window or page to a dependency property. Below is the definition of the dependency property on the control:

   1: [ControlToViewModel(MappingType = ControlViewModelModelMappingType.ViewModelToControl)]
   2: public GeoCoordinate MapCenter
   3: {
   4:     get { return (GeoCoordinate)GetValue(MapCenterProperty); }
   5:     set { SetValue(MapCenterProperty, value); }
   6: }
   7:  
   8: // Using a DependencyProperty as the backing store for MapCenter.  This enables animation, styling, binding, etc...
   9: public static readonly DependencyProperty MapCenterProperty = DependencyProperty.Register("MapCenter", typeof(GeoCoordinate), 
  10:     typeof(MainPage), new PropertyMetadata(null, (sender, e) => ((MainPage)sender).UpdateMapCenter()));
  11:  
  12: private void UpdateMapCenter()
  13: {
  14:     map.SetView(ViewModel.MapCenter, ViewModel.ZoomLevel);
  15: }

It’s just a dependency property with a change callback that we subscribe to. Inside the property change callback, we update the map using the SetView method. It would all be so, so much easier if the Center property of the Map control would be a dependency property so we could simply bind it.

I am really sorry for this convenience. If you hate it, just go to Microsoft and tell them.

Example video

Below is an example video demonstrating the simulation of walking down a street using expected locations:

Example code

You might be at the stage where you thing: wow, might cool, I want that source code! No problem, just download the latest source code of Catel and have fun!

http://catel.codeplex.com

Tags: , ,

C# | Catel | MVVM | Windows Phone 7

Comments are closed

About the Author

Geert van Horrik is a independent freelance software developer since January 1st, 2007. Since then he was been working on several projects from C++ to C# (WPF, ASP.NET, etc). Currently he loves to write his software using WPF (or Silverlight if WPF isn't an option).

Lately, Geert is spending a lot of time on Catel, a free open-source MVVM Framework for WPF and Silverlight. Actually, it's more than "just" an MVVM Framework, it's a complete application library!