Condividi tramite


Consuming Location Data

patterns & practices Developer Center

You Will Learn

  • How to adapt the GeoCoordinateWatcher SDK class.
  • How to consume the adapted GeoCoordinateWatcher class.
  • How to create a mock of the GeoCoordinateWatcher class.
  • How to write unit tests that test view model business logic, by consuming the adapted GeoCoordinateWatcher class.

Applies To

  • Silverlight for Windows Phone OS 7.1
On this page: Download:
The Application | Adapting the GeoCoordinateWatcher SDK Class | Consuming the Adapter | Unit Testing the Application | Summary Download code samples

The purpose of this sample application is to demonstrate how to build a testable Windows Phone application that consumes location data.

The Application

This sample application captures the location of the Windows Phone device and a target location, and calculates the distance in meters between the two locations. You can download the code for this sample application at the following link:

Hh830875.CABCB44D03D1B3D9C8B19DBE980FC99E(en-us,PandP.10).png

When the application is launched, the current latitude and longitude coordinates are shown. Tapping the Save button sets the target position to use the latitude and longitude of the current position, and causes the distance between the current and target positions to be calculated.

Follow link to expand image

The Windows Phone device can either gather location data using the device's built-in Assisted GPS (aGPS) receiver, or you can simulate location data using the location sensor simulator in the emulator. It's the responsibility of the phone to determine the optimal way to obtain the phone's location: using the available data from the aGPS receiver, using cellular triangulation, or using Wi-Fi data.

In production applications, you must obtain the user's consent before collecting and using location data. You should also make sure that your application can continue to function if the user doesn't give their consent to use the phone's location.

Adapting the GeoCoordinateWatcher SDK Class

The GeoCoordinateWatcher class supplies location data that is based on latitude and longitude coordinates. However, this class is not easily testable as it is tied to the Windows Phone hardware. To create a testable version of this class you should create a mock for it.

The interface for the GeoCoordinateWatcher class is not available in the Windows Phone 7.1 SDK. Therefore, the IGeoCoordinateWatcher interface was generated from the GeoCoordinateWatcher SDK class. The following code example shows the IGeoCoordinateWatcher interface, which contains the properties, events, and method signatures implemented by the GeoCoordinateWatcher SDK class.

public interface IGeoCoordinateWatcher : IGeoPositionWatcher<GeoCoordinate>, 
  IDisposable
{
  GeoPositionAccuracy DesiredAccuracy { get; }
  double MovementThreshold { get; set; }
  GeoPositionPermission Permission { get; }
}

The IGeoCoordinateWatcher interface is implemented by the GeoCoordinateWatcherAdapter class. The GeoCoordinateWatcherAdapter class inherits from the GeoCoordinateWatcher class and provides no additional functionality. Any classes that want to consume the GeoCoordinateWatcher class to obtain a location should consume the GeoCoordinateWatcherAdapter class instead. The following code example shows the GeoCoordinateWatcherAdapter class.

public class GeoCoordinateWatcherAdapter : GeoCoordinateWatcher, 
  IGeoCoordinateWatcher
{
  public GeoCoordinateWatcherAdapter() : base()
  {

  }

  public GeoCoordinateWatcherAdapter(GeoPositionAccuracy desiredAccuracy)
    : base (desiredAccuracy)
  {

  }
}

The GeoCoordinateWatcherAdapter class constructor simply calls the constructor of the base GeoCoordinateWatcher SDK class.

Consuming the Adapter

The following code example shows the MainPage view, which comprises a series of TextBlocks and a Button.

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
  <Grid>
    …
    <TextBlock Text="Current Position:" />
    <TextBlock Text="Latitude" Grid.Row="1" />
    <TextBlock Text="Longtitude" Grid.Row="2" />
    <TextBlock Text="{Binding CurrentLatitude, StringFormat=\{0:#.######\}}" 
      Grid.Row="1" Grid.Column="1" />
    <TextBlock Text="{Binding CurrentLongtitude, StringFormat=\{0:#.######\}}" 
      Grid.Row="2" Grid.Column="1" />
    <TextBlock Text="Distance" Grid.Row="3" />
    <TextBlock Text="{Binding Distance, StringFormat=\{0:#\}}" 
      Grid.Row="3" Grid.Column="1" />
    <TextBlock Text="Use Current as Target" Grid.Row="4" />
    <Button Content="Save" Grid.Row="4" Grid.Column="1" Click="Button_Click" />
    <TextBlock Text="Target Position:" Grid.Row="6"/>
    <TextBlock Text="Latitude" Grid.Row="7" />
    <TextBlock Text="Longtitude" Grid.Row="8" />
    <TextBlock Text="{Binding TargetLatitude, StringFormat=\{0:#.######\}}" 
      Grid.Row="7" Grid.Column="1" />
    <TextBlock Text="{Binding TargetLongtitude, StringFormat=\{0:#.######\}}" 
      Grid.Row="8" Grid.Column="1" />
  </Grid>         
</Grid>

The Button calls an event handler when its Click event is raised. The event handler simply calls the SaveCurrentPosition method in the MainPageViewModel class. The MainPage view also contains a number of TextBlock controls that are bound to properties on the MainPageViewModel class.

The ViewModelLocator class connects the view to the view model by settings the view's DataContext to the value of the MainPageViewModel property in the ViewModelLocator class. The following code example shows the MainPageViewModel property.

public MainPageViewModel MainPageViewModel 
{ 
  get { return new MainPageViewModel(
    new GeoCoordinateWatcherAdapter(GeoPositionAccuracy.High)); } 
}

The MainPageViewModel property returns a new instance of the MainPageViewModel class, and passes a new instance of the GeoCoordinateWatcherAdapter class into the MainPageViewModel constructor. The GeoPositionAccuracy.High enumeration specifies that the GeoCoordinateWatcherAdapter class will produce highly accurate location data.

Higher accuracy in location data leads to higher power consumption.

The MainPageViewModel class uses an instance of the IGeoCoordinateWatcher interface to retrieve the current location from the phone. The following code example shows how the constructor receives this IGeoCoordinateWatcher instance from the ViewModelLocator class.

private IGeoCoordinateWatcher geoCoordinateWatcher;
…

public MainPageViewModel(IGeoCoordinateWatcher geoCoordinateWatcher)
{
  this.geoCoordinateWatcher = geoCoordinateWatcher;
  this.geoCoordinateWatcher.PositionChanged += 
    geoCoordinateWatcher_PositionChanged;
  this.geoCoordinateWatcher.Start();
}

The constructor sets the geoCoordinateWatcher field to the instance of the IGeoCoordinateWatcher it receives from the ViewModelLocator class—in this case an instance of the GeoCoordinateWatcherAdapter class. Initializing the class in this way enables view model testability, as a mock implementation of the GeoCoordinateWatcherAdapter class can be passed into the constructor. The constructor then registers an event handler method to the PositionChanged event. The Start method is then called to initiate the acquisition of location data. The following code example shows the PositionChanged event handler method in the MainPageViewModel class.

void geoCoordinateWatcher_PositionChanged(object sender, 
  GeoPositionChangedEventArgs<GeoCoordinate> e)
{
  if (geoCoordinateWatcher.Status == GeoPositionStatus.Ready)
  {
    this.CurrentPosition = e.Position;
  }
}

If the Status property of the IGeoCoordinateWatcher instance is equal to GeoPositionStatus.Ready, the CurrentPosition property is set to the new position.

The following code example shows the UpdateDistance method in the MainPageViewModel class. This method is called when the CurrentPosition property is updated.

private void UpdateDistance()
{
  if (TargetPosition != null)
  {
    Distance = CurrentPosition.Location.GetDistanceTo(TargetPosition.Location);
  }
}

The UpdateDistance method uses the GeoCoordinate.GetDistanceTo method to calculate the distance in meters between the current position and the target position, and stores the result in the Distance property.

The MainPageViewModel implements the INotifyPropertyChanged interface, thus using change notification to notify the view when a property in the view model changes.

Unit Testing the Application

The Microsoft.Practices.Phone.Testing project contains mock implementations of the Windows Phone 7.1 SDK classes contained in the Microsoft.Practices.Phone.Adapters project. These mocks were developed to be general purpose with many of them having properties that accept delegates. Delegate accepting properties enable the execution of any desired behavior necessary for the unit test.

The following code example shows the MockGeoCoordinateWatcher class.

public class MockGeoCoordinateWatcher : IGeoCoordinateWatcher
{
  public MockGeoCoordinateWatcher()
  {
    TryStartTestCallback = (b1, t) => true;
    StopTestCallback = () => { };

    this.DesiredAccuracy = GeoPositionAccuracy.Default;
    this.MovementThreshold = 0.0;
    this.Permission = GeoPositionPermission.Granted;
    this.Status = GeoPositionStatus.NoData;
  }

  public Action<bool> StartTestCallback { get; set; }
  public Action StopTestCallback { get; set; }
  public Func<bool, TimeSpan, bool> TryStartTestCallback { get; set; }

  public void Start()
  {
    Start(false);
  }

  public void Start(bool suppressPermissionPrompt)
  {
    if (StartTestCallback != null)
    {
      StartTestCallback(suppressPermissionPrompt);                
    }
    else
    {
      //Default behavior
      ChangeStatus(GeoPositionStatus.Ready);
    }
  }

  public bool TryStart(bool suppressPermissionPrompt, TimeSpan timeout)
  {
    return TryStartTestCallback(suppressPermissionPrompt, timeout);
  }

  public void Stop()
  {
    StopTestCallback();
  }

  …

  public GeoPosition<GeoCoordinate> Position { get; set; }

  public GeoPositionStatus Status { get; set; }
   
  …
  
  public event EventHandler<GeoPositionChangedEventArgs<GeoCoordinate>> 
    PositionChanged;
  …
  
  public void ChangePosition(GeoCoordinate geoCoordinate, 
    DateTimeOffset dateTimeOffset)
  {
    Position = new GeoPosition<GeoCoordinate>(dateTimeOffset, geoCoordinate);

    var handler = PositionChanged;
    if (handler != null)
      handler(null, new GeoPositionChangedEventArgs<GeoCoordinate>(Position));
  }
  …
}

The MockGeoCoordinateWatcher class implements the IGeoCoordinateWatcher interface and is an example of how a general-purpose mock can be given behavior using delegates. The StartTestCallback property can be set with a delegate or a lambda expression so that a call to the Start method can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock.

Unit tests can then be written that use the mock to test aspects of the view model business logic. The following code example shows the ListensToGeoCoorWatcherAndStoresUpdatedPosition test method, which demonstrates testing asynchronous functionality, and which follows the standard arrange, act, assert pattern. The unit test validates that the MainPageViewModel class can detect when the location has changed, and store the new latitude and longitude. The Asynchronous method attribute allows the test to run until an unhandled exception is thrown, or the EnqueueTestComplete method is called.

[TestMethod, Asynchronous]
public void ListensToGeoCoorWatcherAndStoresUpdatedPosition()
{
  var geoCoordinateWatcher = new MockGeoCoordinateWatcher();
  var target = new MainPageViewModel(geoCoordinateWatcher);
  target.PropertyChanged += (s, e) =>
  {
    if (e.PropertyName == "CurrentPosition")
    {
      Assert.AreEqual(12, target.CurrentPosition.Location.Latitude);
      Assert.AreEqual(34, target.CurrentPosition.Location.Longitude);
      EnqueueTestComplete();
    }
  };

  geoCoordinateWatcher.Status = GeoPositionStatus.Ready;
  geoCoordinateWatcher.ChangePosition(
    new GeoCoordinate(12,34), DateTimeOffset.Now);
}

The test first creates an instance of the MockGeoCoordinateWatcher class, and an instance of the MainPageViewModel class, which uses the instance of the MockGeoCoordinateWatcher class. The test listens to the view model's PropertyChanged event looking for a change to the CurrentPosition property.

The ChangePosition method call on the MockGeoCoordinateWatcher instance triggers the code under test. The ChangePosition method causes the PositionChanged event handler method in the instance of the MainPageViewModel class to be called. Once this event occurs, the CurrentPosition property in the view model is set. In turn, this triggers the PropertyChanged event handler method in the test method. In the event handler for the PropertyChanged event, the current position is validated by comparing the latitude and longitude to the values passed into the ChangePosition method. The unit test validates this behavior but must wait for the MainPageViewModelPropertyChanged event to be fired in order to validate the results.

Summary

This sample application has demonstrated how to build a testable Windows Phone application that consumes location data. The GeoCoordinateWatcherAdapter class adapts the GeoCoordinateWatcher SDK class, and the application consumes the GeoCoordinateWatcherAdapter class. A MockGeoCoordinateWatcher class is used by unit tests to test business logic contained in the view model.

Next Topic | Previous Topic | Home

Last built: February 10, 2012