共用方式為


Consuming Sensor Data

patterns & practices Developer Center

You Will Learn

  • How to adapt the Accelerometer SDK class and consume the adapted class.
  • How to adapt the Compass SDK class and consume the adapted class.
  • How to adapt the Gyroscope SDK class and consume the adapted class.
  • How to adapt the Motion SDK class and consume the adapted class.
  • How to create a mock of the Accelerometer class and write unit tests that test view model business logic by consuming the mock class.
  • How to create a mock of the Compass class and write unit tests that test view model business logic by consuming the mock class.
  • How to create a mock of the Gyroscope class and write unit tests that test view model business logic by consuming the mock class.
  • How to create a mock of the Motion class and write unit tests that test view model business logic by consuming the mock class.

Applies To

  • Silverlight for Windows Phone OS 7.1
On this page: Download:
The Application | Adapting the Accelerometer SDK Class | Adapting the Compass SDK Class | Adapting the Gyroscope SDK Class | Adapting the Motion SDK Class | Consuming the Adapters | 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 sensor data.

The Application

This sample application receives and displays the raw data from the different sensors in a Windows Phone device. Windows Phone supports multiple sensors that allow applications to determine the orientation and motion of the device. These sensors enable the development of applications where the physical device itself is a means of user input. You can download the code for this sample application at the following link:

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

When the application is launched, the page enables the user to select one of the four sensors supported by the Windows Phone platform. After selecting the sensor, the user can tap the Start button which starts data acquisition from the selected sensor and displays the data in a scrollable form at the bottom of the page. Clicking the Stop button will stop the data acquisition.

Follow link to expand image

Adapting the Accelerometer SDK Class

The Accelerometer class provides access to the device's accelerometer sensor, which will detect the force of gravity along with any forces resulting from the movement of the phone. However, this class is not easily testable. To create a testable version of this class you should adapt the class, and then create a mock for it.

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

public interface IAccelerometer : IDisposable
{
  void Start();
  void Stop();

  AccelerometerSensorReading CurrentValue { get; }
  bool IsDataValid { get; }
  bool IsSupported { get; }
  SensorState State { get; }
  TimeSpan TimeBetweenUpdates { get; set; }

  event EventHandler<SensorReadingEventArgs<AccelerometerSensorReading>> 
    CurrentValueChanged;
}

The IAccelerometer interface is implemented by the AccelerometerAdapter class. The AccelerometerAdapter class adapts the Accelerometer class from the Windows Phone 7.1 SDK. Adapter classes pass parameters and return values to and from the underlying Windows Phone 7.1 SDK class. Any classes that want to consume the Accelerometer class to access the device's accelerometer sensor should consume the AccelerometerAdapter class instead. The following code example shows the AccelerometerAdapter class.

public class AccelerometerAdapter : IAccelerometer
{
  public AccelerometerAdapter()
  {
    WrappedSubject = new Accelerometer();
    AttachToEvents();
  }

  private Accelerometer WrappedSubject { get; set; }

  …

  public AccelerometerSensorReading CurrentValue
  {
    get { return new AccelerometerSensorReading(
      WrappedSubject.CurrentValue); }
  }

  public void Start()
  {
    WrappedSubject.Start();
  }

  public void Stop()
  {
    WrappedSubject.Stop();
  }
  
  …
  
  public event EventHandler
  <SensorReadingEventArgs<AccelerometerSensorReading>> CurrentValueChanged;

  private void AttachToEvents()
  {
    WrappedSubject.CurrentValueChanged += WrappedSubjectCurrentValueChanged;
  }

  void WrappedSubjectCurrentValueChanged(object sender, 
    SensorReadingEventArgs<AccelerometerReading> e)
  {
    CurrentValueChangedRelay(sender, 
      new SensorReadingEventArgs<AccelerometerSensorReading>() 
      { 
        SensorReading = new AccelerometerSensorReading(e.SensorReading) 
      });
  }

  private void CurrentValueChangedRelay(object sender, 
    SensorReadingEventArgs<AccelerometerSensorReading> e)
  {
    var handler = CurrentValueChanged;
    if (handler != null)
    {
      handler(sender, e);
    }
  }
}

The AccelerometerAdapter class constructor simply initializes the WrappedSubject property to be an instance of the Accelerometer class. AccelerometerAdapter class properties and methods then simply get, set, and invoke the equivalent Accelerometer class properties and methods on the WrappedSubject property.

The event implemented by the AccelerometerAdapter class is CurrentValueChanged. This event occurs when new data arrives from the accelerometer sensor, and it exposes the new data through the CurrentValue property. The CurrentValue property is of type AccelerometerReading. However, the properties in the AccelerometerReading class are not settable, which makes the Accelerometer class not testable through mocks. Therefore, the AccelerometerSensorReading class was added to provide an implementation of the AccelerometerReading class where the property setters are protected. To enable testability, the SettableAccelerometerSensorReading class was added, which inherits from the AccelerometerSensorReading class, where the properties are settable. When the CurrentValueChanged event is raised, the WrappedSubjectCurrentValueChanged event handler calls the CurrentValueChangedRelay event handler, passing in a new instance of the AccelerometerSensorReading class. The following code example shows the AccelerometerSensorReading class.

public class AccelerometerSensorReading : ISensorReading
{
  public AccelerometerSensorReading(AccelerometerReading accelerometerReading)
  {
    Acceleration = accelerometerReading.Acceleration;
    Timestamp = accelerometerReading.Timestamp;
  }

  public AccelerometerSensorReading()
  {
  }

  public Vector3 Acceleration { get; protected set; }
  public DateTimeOffset Timestamp { get; protected set; }
}

When a new instance of the AccelerometerSensorReading class is created, the Acceleration and Timestamp properties are simply set to their equivalent properties from the AccelerometerReading class.

Adapting the Compass SDK Class

The Compass class provides access to the device's compass sensor, and can be used to determine the angle by which the device is rotated relative to the Earth's magnetic north pole. However, this class is not easily testable. To create a testable version of this class you should adapt the class, and then create a mock for it.

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

public interface ICompass : IDisposable
{
  void Start();
  void Stop();

  CompassSensorReading CurrentValue { get; }
  bool IsDataValid { get; }
  bool IsSupported { get; }
  TimeSpan TimeBetweenUpdates { get; set; }

  event EventHandler<CalibrationEventArgs> Calibrate;
  event EventHandler<SensorReadingEventArgs<CompassSensorReading>> 
    CurrentValueChanged;
}

The ICompass interface is implemented by the CompassAdapter class. The CompassAdapter class adapts the Compass class from the Windows Phone 7.1 SDK. Adapter classes pass parameters and return values to and from the underlying Windows Phone 7.1 SDK class. Any classes that want to consume the Compass class to access the device's compass sensor should consume the CompassAdapter class instead. The following code example shows the CompassAdapter class.

public class CompassAdapter : ICompass
{
  public CompassAdapter()
  {
    WrappedSubject = new Compass();
    AttachToEvents();
  }

  private Compass WrappedSubject { get; set; }

  public CompassSensorReading CurrentValue
  {
    get { return new CompassSensorReading(WrappedSubject.CurrentValue); }
  }

  …

  public void Start()
  {
    WrappedSubject.Start();
  }

  public void Stop()
  {
    WrappedSubject.Stop();
  }

  public void Dispose()
  {
    WrappedSubject.Dispose();
  }

  public event EventHandler<CalibrationEventArgs> Calibrate;
  public event EventHandler
    <SensorReadingEventArgs<CompassSensorReading>> CurrentValueChanged;

  private void AttachToEvents()
  {
    WrappedSubject.Calibrate += WrappedSubjectCalibrate;
    WrappedSubject.CurrentValueChanged += WrappedSubjectCurrentValueChanged;
  }

  void WrappedSubjectCalibrate(object sender, CalibrationEventArgs e)
  {
    CalibrateRelay(sender, e);
  }

  private void CalibrateRelay(object sender, CalibrationEventArgs e)
  {
    var handler = Calibrate;
    if (handler != null)
    {
      handler(sender, e);
    }
  }

  void WrappedSubjectCurrentValueChanged(object sender, 
    SensorReadingEventArgs<CompassReading> e)
  {
    CurrentValueChangedRelay(sender, 
      new SensorReadingEventArgs<CompassSensorReading>() 
      { 
        SensorReading = new CompassSensorReading(e.SensorReading) 
      });
  }

  private void CurrentValueChangedRelay(object sender, 
    SensorReadingEventArgs<CompassSensorReading> e)
  {
    var handler = CurrentValueChanged;
    if (handler != null)
    {
      handler(sender, e);
    }
  }
}

The CompassAdapter class constructor simply initializes the WrappedSubject property to be an instance of the Compass class. CompassAdapter class properties and methods then simply get, set, and invoke the equivalent Compass class properties and methods on the WrappedSubject property.

The two events that are supported by the CompassAdapter class are Calibrate and CurrentValueChanged. The Calibrate event occurs when the operating system detects that the compass needs calibration. The CurrentValueChanged event occurs when new data arrives from the compass sensor, and it exposes the new data through the CurrentValue property. The CurrentValue property is of type CompassReading. However, the properties in the CompassReading class are not settable, which makes the Compass class not testable through mocks. Therefore, the CompassSensorReading class was added to provide an implementation of the CompassReading class where the property setters are protected. To enable testability, the SettableCompassSensorReading class was added, which inherits from the CompassSensorReading class, where the properties are settable. When the CurrentValueChanged event is raised, the WrappedSubjectCurrentValueChanged event handler calls the CurrentValueChangedRelay event handler, passing in a new instance of the CompassSensorReading class. The following code example shows the CompassSensorReading class.

public class CompassSensorReading : ISensorReading
{
  public CompassSensorReading(CompassReading compassReading)
  {
    HeadingAccuracy = compassReading.HeadingAccuracy;
    MagneticHeading = compassReading.MagneticHeading;
    MagnetometerReading = compassReading.MagnetometerReading;
    Timestamp = compassReading.Timestamp;
    TrueHeading = compassReading.TrueHeading;
  }

  public CompassSensorReading()
  {
  }

  public double HeadingAccuracy { get; protected set; }
  public double MagneticHeading { get; protected set; }
  public Vector3 MagnetometerReading { get; protected set; }
  public DateTimeOffset Timestamp { get; protected set; }
  public double TrueHeading { get; protected set; }
}

When a new instance of the CompassSensorReading class is created, the properties are simply set to their equivalent properties from the CompassReading class.

Adapting the Gyroscope SDK Class

The Gyroscope class provides access to the device's gyroscope sensor, and is used to determine the rotational velocity of the device around each axis. However, this class is not easily testable. To create a testable version of this class you should adapt the class, and then create a mock for it.

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

public interface IGyroscope : IDisposable
{
  void Start();
  void Stop();

  GyroscopeSensorReading CurrentValue { get; }
  bool IsDataValid { get; }
  bool IsSupported { get; }
  TimeSpan TimeBetweenUpdates { get; set; }

  event EventHandler<SensorReadingEventArgs<GyroscopeSensorReading>> 
    CurrentValueChanged;
}

The IGyroscope interface is implemented by the GyroscopeAdapter class. The GyroscopeAdapter class adapts the Gyroscope class from the Windows Phone 7.1 SDK. Adapter classes pass parameters and return values to and from the underlying Windows Phone 7.1 SDK class. Any classes that want to consume the Gyroscope class to access the device's gyroscope sensor should consume the GyroscopeAdapter class instead. The following code example shows the GyroscopeAdapter class.

public class GyroscopeAdapter : IGyroscope
{
  public GyroscopeAdapter()
  {
    WrappedSubject = new Gyroscope();
    AttachToEvents();
  }

  private Gyroscope WrappedSubject { get; set; }

  public GyroscopeSensorReading CurrentValue
  {
    get { return new GyroscopeSensorReading(WrappedSubject.CurrentValue); }
  }

  …

  public void Start()
  {
    WrappedSubject.Start();
  }

  public void Stop()
  {
    WrappedSubject.Stop();
  }

  …

  public event EventHandler<SensorReadingEventArgs<GyroscopeSensorReading>> 
    CurrentValueChanged;

  private void AttachToEvents()
  {
    WrappedSubject.CurrentValueChanged += WrappedSubjectCurrentValueChanged;
  }

  void WrappedSubjectCurrentValueChanged(object sender, 
    SensorReadingEventArgs<GyroscopeReading> e)
  {
    CurrentValueChangedRelay(sender, 
      new SensorReadingEventArgs<GyroscopeSensorReading>() 
      { 
        SensorReading = new GyroscopeSensorReading(e.SensorReading) 
      });
  }

  private void CurrentValueChangedRelay(object sender, 
    SensorReadingEventArgs<GyroscopeSensorReading> e)
  {
    var handler = CurrentValueChanged;
    if (handler != null)
    {
      handler(sender, e);
    }
  }
}

The GyroscopeAdapter class constructor simply initializes the WrappedSubject property to be an instance of the Gyroscope class. GyroscopeAdapter class properties and methods then simply get, set, and invoke the equivalent Gyroscope class properties and methods on the WrappedSubject property.

The event that is supported by the GyroscopeAdapter class is CurrentValueChanged. The CurrentValueChanged event occurs when new data arrives from the gyroscope sensor, and it exposes the new data through the CurrentValue property. The CurrentValue property is of type GyroscopeReading. However, the properties in the GyroscopeReading class are not settable, which makes the Gyroscope class not testable through mocks. Therefore, the GyroscopeSensorReading class was added to provide an implementation of the GyroscopeReading class where the property setters are protected. To enable testability, the SettableGyroscopeSensorReading class was added, which inherits from the GyroscopeSensorReading class, where the properties are settable. When the CurrentValueChanged event is raised, the WrappedSubjectCurrentValueChanged event handler calls the CurrentValueChangedRelay event handler, passing in a new instance of the GyroscopeSensorReading class. The following code example shows the GyroscopeSensorReading class.

public class GyroscopeSensorReading : ISensorReading
{
  public GyroscopeSensorReading(GyroscopeReading gyroscopeReading)
  {
    RotationRate = gyroscopeReading.RotationRate;
    Timestamp = gyroscopeReading.Timestamp;
  }

  public GyroscopeReading()
  {
  }

  public Vector3 RotationRate { get; protected set; }
  public DateTimeOffset Timestamp { get; protected set; }
}

When a new instance of the GyroscopeSensorReading class is created, the RotationRate and DateTimeOffset properties are simply set to their equivalent properties from the GyroscopeReading class.

Adapting the Motion SDK Class

The Motion class provides the simplest way to obtain information about the device's orientation and motion by combining and processing data from all the sensors. However, this class is not easily testable. To create a testable version of this class you should adapt the class, and then create a mock for it.

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

public interface IMotion : IDisposable
{
  void Start();
  void Stop();

  MotionSensorReading CurrentValue { get; }
  bool IsDataValid { get; }
  bool IsSupported { get; }
  TimeSpan TimeBetweenUpdates { get; set; }

  event EventHandler<CalibrationEventArgs> Calibrate;
  event EventHandler<SensorReadingEventArgs<MotionSensorReading>> 
    CurrentValueChanged;
}

The IMotion interface is implemented by the MotionAdapter class. The MotionAdapter class adapts the Motion class from the Windows Phone 7.1 SDK. Adapter classes pass parameters and return values to and from the underlying Windows Phone 7.1 SDK class. Any classes that want to consume the Motion class to access the device's orientation and motion data should consume the MotionAdapter class instead. The following code example shows the MotionAdapter class.

public class MotionAdapter : IMotion
{
  public MotionAdapter()
  {
    WrappedSubject = new Motion();
    AttachToEvents();
  }

  private Motion WrappedSubject { get; set; }

  public MotionSensorReading CurrentValue
  {
    get { return new MotionSensorReading(WrappedSubject.CurrentValue); }
  }

  …

  public void Start()
  {
    WrappedSubject.Start();
  }

  public void Stop()
  {
    WrappedSubject.Stop();
  }

  …

  public event EventHandler<CalibrationEventArgs> Calibrate;
  public event EventHandler<SensorReadingEventArgs<MotionSensorReading>> 
    CurrentValueChanged;

  private void AttachToEvents()
  {
    WrappedSubject.Calibrate += WrappedSubjectCalibrate;
    WrappedSubject.CurrentValueChanged += WrappedSubjectCurrentValueChanged;
  }

  void WrappedSubjectCalibrate(object sender, CalibrationEventArgs e)
  {
    CalibrateRelay(sender, e);
  }

  private void CalibrateRelay(object sender, CalibrationEventArgs e)
  {
    var handler = Calibrate;
    if (handler != null)
    {
      handler(sender, e);
    }
  }

  void WrappedSubjectCurrentValueChanged(object sender, 
    SensorReadingEventArgs<MotionReading> e)
  {
    CurrentValueChangedRelay(sender, 
      new SensorReadingEventArgs<MotionSensorReading>() 
      { 
        SensorReading = new MotionSensorReading(e.SensorReading) 
      });
  }

  private void CurrentValueChangedRelay(object sender, 
    SensorReadingEventArgs<MotionSensorReading> e)
  {
    var handler = CurrentValueChanged;
    if (handler != null)
    {
      handler(sender, e);
    }
  }
}

The MotionAdapter class constructor simply initializes the WrappedSubject property to be an instance of the Motion class. MotionAdapter class properties and methods then simply get, set, and invoke the equivalent Motion class properties and methods on the WrappedSubject property.

The two events that are supported by the MotionAdapter class are Calibrate and CurrentValueChanged. The Calibrate event occurs when the operating system detects that the compass needs calibration. The CurrentValueChanged event occurs when new data arrives from the sensors, and it exposes the new data through the CurrentValue property. The CurrentValue property is of type MotionReading. However, the properties in the MotionReading class are not settable, which makes the Motion class not testable through mocks. Therefore, the MotionSensorReading class was added to provide an implementation of the MotionReading class where the property setters are protected. To enable testability, the SettableMotionSensorReading class was added, which inherits from the MotionSensorReading class, where the properties are settable. When the CurrentValueChanged event is raised, the WrappedSubjectCurrentValueChanged event handler calls the CurrentValueChangedRelay event handler, passing in a new instance of the MotionSensorReading class. The following code example shows the MotionSensorReading class.

public class MotionSensorReading : ISensorReading
{
  public MotionSensorReading(MotionReading motionReading)
  {
    Attitude = motionReading.Attitude;
    DeviceAcceleration = motionReading.DeviceAcceleration;
    DeviceRotationRate = motionReading.DeviceRotationRate;
    Gravity = motionReading.Gravity;
    Timestamp = motionReading.Timestamp;
  }

  public MotionSensorReading()
  {
  }

  public AttitudeReading Attitude { get; protected set; }
  public Vector3 DeviceAcceleration { get; protected set; }
  public Vector3 DeviceRotationRate { get; protected set; }
  public Vector3 Gravity { get; protected set; }
  public DateTimeOffset Timestamp { get; protected set; }
}

When a new instance of the MotionSensorReading class is created, the properties are simply set to their equivalent properties from the MotionReading class.

Consuming the Adapters

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

<Grid x:Name="LayoutRoot" Background="Transparent">
  …
  <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <StackPanel Orientation="Vertical" VerticalAlignment="Top" … >         
      <RadioButton Content="Accelerometer" IsChecked="True" 
        IsEnabled="{Binding StartEnabled}" Click="AccelerometerButton_Click"/>
      <RadioButton Content="Compass" IsEnabled="{Binding StartEnabled}" 
        Click="CompassButton_Click"/>
      <RadioButton Content="Gyroscope" IsEnabled="{Binding StartEnabled}" 
        Click="GyroscopeButton_Click"/>
      <RadioButton Content="Motion" IsEnabled="{Binding StartEnabled}" 
        Click="MotionButton_Click"/>
    </StackPanel>
    <StackPanel Orientation="Vertical" VerticalAlignment="Top" … >
      <Button Content="Start" IsEnabled="{Binding StartEnabled}" 
        Click="StartButton_Click" />
      <Button Content="Stop" IsEnabled="{Binding StopEnabled}" 
        Click="StopButton_Click" Margin="0,1,0,0"/>
    </StackPanel>
    <TextBlock … Text="Current Value" VerticalAlignment="Bottom"/>
    <ScrollViewer Height="192" Margin="12,0,12,32" … >
      <TextBlock TextWrapping="Wrap" Text="{Binding ValueLog}"/>
    </ScrollViewer>
  </Grid>
</Grid>

The RadioButtons call event handlers when their Click event is raised. The event handlers simply call the SelectSensor method in the MainPageViewModel class, passing the view model a parameter that represents the particular sensor. Each RadioButtons IsEnabled property binds to the StartEnabled property of the MainPageViewModel class. This disables the RadioButtons when data is being acquired from any sensor.

The MainPage view also contains two Buttons, which Start and Stop the chosen sensor. Both Buttons call event handlers when their Click event is raised, and the event handlers simply call theappropriatemethod (StartSensor or StopSensor) of the MainPageViewModel class. Each Button also binds to a Boolean value in the MainPageViewModel class which controls whether the Button is enabled or not. In addition, the MainPage view also contains a ScrollViewer, which contains a TextBlock that displays data returned from the chosen sensor. It does this by binding to the ValueLog property of the MainPageViewModel class.

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

private MainPageViewModel mainPageViewModel;

public MainPageViewModel MainPageViewModel
{
  get
  {
    return new MainPageViewModel(new AccelerometerAdapter(),
      new CompassAdapter(),
      new GyroscopeAdapter(),
      new MotionAdapter());
  }
}

The MainPageViewModel property returns a new instance of the MainPageViewModel class, and passes new instances of the AccelerometerAdapter, CompassAdapter, GyroscopeAdapter, and MotionAdapter classes into the MainPageViewModel constructor.

The MainPageViewModel class uses an instance of the IAccelerometer interface to get data from the accelerometer, an instance of the ICompass interface to get data from the compass, an instance of the IGyroscope interface to get data from the gyroscope, and an instance of the IMotion interface to potentially get data from all three sensors. The following code example shows how the constructor receives these instances from the ViewModelLocator class.

public class MainPageViewModel : INotifyPropertyChanged
{
  private IAccelerometer accelerometer;
  private ICompass compass;
  private IGyroscope gyroscope;
  private IMotion motion;
  …

  public MainPageViewModel(IAccelerometer accelerometer, 
    ICompass compass, IGyroscope gyroscope, IMotion motion)
  {
    this.accelerometer = accelerometer;
    this.accelerometer.CurrentValueChanged += accelerometer_CurrentValueChanged;
    this.compass = compass;
    this.compass.CurrentValueChanged += compass_CurrentValueChanged;
    this.gyroscope = gyroscope;
    this.gyroscope.CurrentValueChanged += gyroscope_CurrentValueChanged;
    this.motion = motion;
    this.motion.CurrentValueChanged += motion_CurrentValueChanged;
  }
  …
}

The constructor sets the accelerometer field to the instance of IAccelerometer it receives from the ViewModelLocator class, in this case an instance of the AccelerometerAdapter class. It also registers an event handler method to the accelerometer's CurrentValueChanged event. It then sets the compass field to the instance of ICompass it receives from the ViewModelLocator class, in this case an instance of the CompassAdapter class. It also registers an event handler method to the compass's CurrentValueChanged event. It then sets the gyroscope field to the instance of IGyroscope it receives from the ViewModelLocator class, in this case an instance of the GyroscopeAdapter class. It also registers an event handler method to the gyroscope's CurrentValueChanged event. It then sets the motion field to the instance of IMotion it receives from the ViewModelLocator class, in this case an instance of the MotionAdapter class. Finally, it registers an event handler method to the motion APIs CurrentValueChanged event.

The following code example shows the StartSensor and StopSensor methods that are invoked from event handler methods in the view's code-behind.

public void StartSensor()
{
  bool startSucceeded = false;
  
  …

  startedSensorIndex = selectedSensorIndex;
  switch (startedSensorIndex)
  {
    case 0:
      if (accelerometer.IsSupported)
      {
       startSucceeded = true;
       accelerometer.Start();
      }
      break;
    case 1:
      if (compass.IsSupported)
      {
        startSucceeded = true;
        compass.Start();
      }                
      break;
    case 2:
      if (gyroscope.IsSupported)
      {
        startSucceeded = true;
        gyroscope.Start();
      }
      break;
    case 3:
      if (motion.IsSupported)
      {
        startSucceeded = true;
        motion.Start();
      }
      break;
  }
  …
}

public void StopSensor()
{
  switch (startedSensorIndex)
  {
    case 0:
      accelerometer.Stop();
      break;
    case 1:
      compass.Stop();
      break;
    case 2:
      gyroscope.Stop();
      break;
    case 3:
      motion.Stop();
      break;
  }
  …
}

The StartSensor method simply sets the startSucceeded flag to true and then calls the Start method on the chosen sensor, which starts data acquisition from the sensor. The startSucceeded flag is then used to update the view's UI. The StopSensor method simply calls the Stop method on the chosen sensor, which stops data acquisition from the sensor.

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 adapter and facade classes contained in the Microsoft.Practices.Phone.Adapters project. These mocks were developed for general-purpose use, 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 MockAccelerometer class.

public class MockAccelerometer : IAccelerometer
{
  public bool StartCalled { get; set; }
  public Action StartTestCallback { get; set; }
  public bool StopCalled { get; set; }
  public Action StopTestCallback { get; set; }

  public void Start()
  {
    StartCalled = true;
    if (StartTestCallback != null)
    {
      StartTestCallback();
    }
  }

  public void Stop()
  {
    StopCalled = true;
    if (StopTestCallback != null)
    {
      StopTestCallback();
    }
  }

  public AccelerometerSensorReading CurrentValue { get; set; }

  …

  public event EventHandler<SensorReadingEventArgs<AccelerometerSensorReading>> 
    CurrentValueChanged;

  public void ChangeCurrentValue(Vector3 acceleration, DateTimeOffset timestamp)
  {
    var accelerometerSensorReading = new SettableAccelerometerSensorReading();
    accelerometerSensorReading.SetAcceleration(acceleration);
    accelerometerSensorReading.SetTimestamp(timestamp); 
    CurrentValue = accelerometerSensorReading;

    var handler = CurrentValueChanged;
    if (handler != null)
      handler(null, new SensorReadingEventArgs<AccelerometerSensorReading>() 
      { 
        SensorReading = CurrentValue 
      });
  }
  …
}

The MockAccelerometer class implements the IAccelerometer interface and is an example of how a general-purpose mock can be given behavior using delegates. The StartTestCallback and StopTestCallback properties can be set with a delegate or a lambda expression so that the calls to the Start and Stop methods can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock. The Start and Stop methods also set to true the StartCalled and StopCalled properties, respectively. These properties can be queried by unit tests to discover whether the Start and Stop methods have been called.

Unit tests that use the MockAccelerometer class may also need to use the SettableAccelerometerSensorReading class. This class inherits from the AccelerometerSensorReading class and makes its properties settable. The following code example shows the SettableAccelerometerSensorReading class.

public class SettableAccelerometerSensorReading : AccelerometerSensorReading
{
  public SettableAccelerometerSensorReading(
    AccelerometerReading accelerometerReading)
  {
    Acceleration = accelerometerReading.Acceleration;
    Timestamp = accelerometerReading.Timestamp;
  }

  public SettableAccelerometerSensorReading()
  {
  }

  public void SetAcceleration(Vector3 acceleration)
  {
    this.Acceleration = acceleration;
  }

  public void SetTimestamp(DateTimeOffset timestamp)
  {
    this.Timestamp = timestamp;
  }
}

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 CurrentValueAddedToValueLog 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 acquire data from the accelerometer sensor and add it to the ValueLog property. 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 CurrentValueAddedToValueLog()
{
  var accelerometer = new MockAccelerometer() { IsSupported = true };
  var acceleration = new Vector3(0, 0, 0);
  var timestamp = new DateTimeOffset(DateTime.Now);
  var accelerationSensorReading = new SettableAccelerometerSensorReading();
  accelerationSensorReading.SetAcceleration(acceleration);
  accelerationSensorReading.SetTimestamp(timestamp);
  var target = new MainPageViewModel(accelerometer, new MockCompass(),
    new MockGyroscope(), new MockMotion());

  target.SelectSensor(0);
  target.StartSensor();
  target.PropertyChanged += (s, e) =>
  {
    if (e.PropertyName == "ValueLog")
    {
      Assert.AreEqual(target.LastLogItem, 
        accelerationSensorReading.Acceleration.ToString());
      EnqueueTestComplete();
    }
  };
  accelerometer.ChangeCurrentValue(acceleration, timestamp);
}

The test method first creates an instance of the MockAccelerometer class, and sets its IsSupported property to true. The IsSupported property represents whether the device on which the application is running supports the accelerometer sensor. An instance of the SettableAccelerometerSensorReading class is then created, with the Acceleration property being set to an empty three-dimensional vector and the Timestamp property being set to the current date and time. An instance of the MainPageViewModel class is then created, passing the instance of the MockAccelerometer class, and new instances of the MockCompass, MockGyroscope, and MockMotion classes, into the MainPageViewModel constructor. The SelectSensor method of the MainPageViewModel class is then called in order to ensure that the accelerometer is the selected sensor for when the StartSensor method is called. The StartSensor method of the MainPageViewModel is then called, which calls the Start method on the instance of the MockAccelerometer class. The unit test then listens for the MainPageViewModel PropertyChanged event, looking for a change to the ValueLog property. In the event handler for the PropertyChanged event, the LastLogItem property, which contains the last item added to the ValueLog, is validated by comparing it to the value of the Acceleration property from the instance of the SettableAccelerometerSensorReading class. The ChangeCurrentValue method call on the instance of the MockAccelerometer class triggers the code under test by raising the event handler for the CurrentValueChanged event. Once the event is raised, both the LastLogItem and ValueLog properties in the instance of the MainPageViewModel class are updated. The unit test validates this behavior, but must wait for the MockAccelerometer CurrentValueChanged event and the MainPageViewModel PropertyChanged event to be raised in order to validate the result.

The following code example shows the MockCompass class.

public class MockCompass : ICompass
{
  public bool StartCalled { get; set; }
  public Action StartTestCallback { get; set; }
  public bool StopCalled { get; set; }
  public Action StopTestCallback { get; set; }

  public void Start()
  {
    StartCalled = true;
    if (StartTestCallback != null)
    {
      StartTestCallback();
    }
  }

  public void Stop()
  {
    StopCalled = true;
    if (StopTestCallback != null)
    {
      StopTestCallback();
    }
  }

  public CompassSensorReading CurrentValue { get; set; }

  …

  public event EventHandler<CalibrationEventArgs> Calibrate;

  public void RaiseCalibrate()
  {
    var handler = Calibrate;
    if (handler != null)
      handler(null, new CalibrationEventArgs());
  }

  public event EventHandler<SensorReadingEventArgs<CompassSensorReading>>   
    CurrentValueChanged;

  public void ChangeCurrentValue(double headingAccuracy, double magneticHeading, 
    Vector3 magnetometerReading, DateTimeOffset timestamp, double trueHeading)
  {
    var compassSensorReading = new SettableCompassSensorReading();
    compassSensorReading.SetHeadingAccuracy(headingAccuracy);
    compassSensorReading.SetMagneticHeading(magneticHeading);
    compassSensorReading.SetMagnetometerReading(magnetometerReading);
    compassSensorReading.SetTimestamp(timestamp);
    compassSensorReading.SetTrueHeading(trueHeading);

    CurrentValue = compassSensorReading;
    var handler = CurrentValueChanged;
    if (handler != null)
      handler(null, new SensorReadingEventArgs<CompassSensorReading>() 
      { 
        SensorReading = CurrentValue 
      });
  }
  …
}

The MockCompass class implements the ICompass interface and is an example of how a general-purpose mock can be given behavior using delegates. The StartTestCallback and StopTestCallback properties can be set with a delegate or a lambda expression so that the calls to the Start and Stop methods can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock. The Start and Stop methods also set to true the StartCalled and StopCalled properties, respectively. These properties can be queried by unit tests to discover whether the Start and Stop methods have been called.

Unit tests that use the MockCompass class may also need to use the SettableCompassSensorReading class. This class inherits from the CompassSensorReading class and makes its properties settable. The following code example shows the SettableCompassSensorReading class.

public class SettableCompassSensorReading : CompassSensorReading
{
  public SettableCompassSensorReading(CompassReading compassReading)
  {
    HeadingAccuracy = compassReading.HeadingAccuracy;
    MagneticHeading = compassReading.MagneticHeading;
    MagnetometerReading = compassReading.MagnetometerReading;
    Timestamp = compassReading.Timestamp;
    TrueHeading = compassReading.TrueHeading;
  }

  public SettableCompassSensorReading()
  {
  }

  public void SetHeadingAccuracy(double headingAccuracy)
  {
    this.HeadingAccuracy = headingAccuracy;
  }

  public void SetMagneticHeading(double magneticHeading)
  {
    this.MagneticHeading = magneticHeading;
  }

  public void SetMagnetometerReading(Vector3 magnetometerReading)
  {
    this.MagnetometerReading = magnetometerReading;
  }

  public void SetTimestamp(DateTimeOffset timestamp)
  {
    this.Timestamp = timestamp;
  }

  public void SetTrueHeading(double trueHeading)
  {
    this.TrueHeading = trueHeading;
  }
}

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 UserNotifiedIfSensorNotSupported test method, which follows the standard arrange, act, assert pattern. The unit test validates that the MainPageViewModel class can determine if the compass sensor is supported on the device on which the application is running.

[TestMethod]
public void UserNotifiedIfSensorNotSupported()
{
  var msg = "compass not supported";

  var compass = new MockCompass();
  compass.IsSupported = false;

  var target = new MainPageViewModel(new MockAccelerometer(), compass,
    new MockGyroscope(), new MockMotion());

  target.SelectSensor(1);
  target.StartSensor();

  Assert.AreEqual(target.LastLogItem, msg);

  Assert.IsTrue(target.StartEnabled);
  Assert.IsFalse(target.StopEnabled);
}

The test method creates an instance of the MockCompass class, and sets its IsSupported property to false. The IsSupported property represents whether the device on which the application is running supports the compass sensor. An instance of the MainPageViewModel class is then created, passing a new instance of the MockAccelerometer class, the instance of the MockCompass class, and new instances of the MockGyroscope and MockMotion classes, into the MainPageViewModel constructor. The SelectSensor method of the MainPageViewModel class is then called, in order to ensure that the compass is the selected sensor for when the StartSensor method is called. The StartSensor method of the MainPageViewModel is then called, which calls the Start method on the instance of the MockCompass class. Finally the unit test validates that the LastLogItem property of the instance of the MainPageViewModel class contains the message that the compass isn't supported, that the StartEnabled property of the instance of the MainPageViewModel class is true, and that the StopEnabled property of the instance of the MainPageViewModel class is false.

The following code example shows the MockGyroscope class.

public class MockGyroscope : IGyroscope
{
  public bool StartCalled { get; set; }
  public Action StartTestCallback { get; set; }
  public bool StopCalled { get; set; }
  public Action StopTestCallback { get; set; }

  public void Start()
  {
    StartCalled = true;
    if (StartTestCallback != null)
    {
      StartTestCallback();
    }
  }

  public void Stop()
  {
    StopCalled = true;
    if (StopTestCallback != null)
    {
      StopTestCallback();
    }
  }

  public GyroscopeSensorReading CurrentValue { get; set; }

  …

  public event EventHandler<SensorReadingEventArgs<GyroscopeSensorReading>> 
    CurrentValueChanged;

  public void ChangeCurrentValue(Vector3 rotationRate, DateTimeOffset timestamp)
  {
    var gyroscopeSensorReading = new SettableGyroscopeSensorReading();
    gyroscopeSensorReading.SetRotationRate(rotationRate);

    gyroscopeSensorReading.SetTimestamp(timestamp);

    CurrentValue = gyroscopeSensorReading;

    var handler = CurrentValueChanged;
    if (handler != null)
      handler(null, new SensorReadingEventArgs<GyroscopeSensorReading>() 
      { 
        SensorReading = CurrentValue 
      });
  }
  …
}

The MockGyroscope class implements the IGyroscope interface and is an example of how a general purpose mock can be given behavior using delegates. The StartTestCallback and StopTestCallback properties can be set with a delegate or a lambda expression so that the calls to the Start and Stop methods can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock. The Start and Stop methods also set to true the StartCalled and StopCalled properties, respectively. These properties can be queried by unit tests to discover whether the Start and Stop methods have been called.

Unit tests that use the MockGyroscope class may also need to use the SettableGyroscopeSensorReading class. This class inherits from the GyroscopeSensorReading class and makes its properties settable. The following code example shows the SettableGyroscopeSensorReading class.

public class SettableGyroscopeSensorReading : GyroscopeSensorReading
{
  public SettableGyroscopeSensorReading(GyroscopeReading gyroscopeReading)
  {
    RotationRate = gyroscopeReading.RotationRate;
    Timestamp = gyroscopeReading.Timestamp;
  }

  public SettableGyroscopeSensorReading()
  {
  }
  
  public void SetRotationRate(Vector3 rotationRate)
  {
    this.RotationRate = rotationRate;
  }

  public void SetTimestamp(DateTimeOffset timeStamp)
  {
    this.Timestamp = timeStamp;
  }
}

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 UIIsEnabledWhenSensorStopped test method, which follows the standard arrange, act, assert pattern. The unit test validates that when the MainPageViewModel class has stopped a sensor from acquiring data, the properties that determine whether the UI is enabled or disabled are correctly set.

[TestMethod]
public void UIIsEnabledWhenSensorStopped()
{
  var gyroscope = new MockGyroscope();
  gyroscope.IsSupported = true;

  var target = new MainPageViewModel(new MockAccelerometer(), new MockCompass(),
    gyroscope, new MockMotion());

  gyroscope.CurrentValueChanged += (s, e) =>
  {
    target.StopSensor();
    Assert.IsTrue(target.StartEnabled);
    Assert.IsFalse(target.StopEnabled);
  };

  target.SelectSensor(2);
  target.StartSensor();
  gyroscope.ChangeCurrentValue(new Vector3(0,1,0), 
    new DateTimeOffset(DateTime.Now));
}

The test method first creates an instance of the MockGyroscope class, and sets its IsSupported property to true. The IsSupported property represents whether the device on which the application is running supports the gyroscope sensor. An instance of the MainPageViewModel class is then created, passing new instances of the MockAccelerometer and MockCompass classes, the instance of the MockGyroscope class, and a new instance of the MockMotion class, into the MainPageViewModel constructor. The unit test then listens to the MockGyroscope CurrentValueChanged event. In the event handler for the CurrentValueChanged event, the StopSensor method of the instance of the MainPageViewModel class is called, then the test validates that the StartEnabled property of the instance of the MainPageViewModel class is true, and that the StopEnabled property of the instance of the MainPageViewModel class is false. The SelectSensor method of the MainPageViewModel class is then called in order to ensure that the gyroscope is the selected sensor for when the StartSensor method is called. The StartSensor method of the MainPageViewModel is then called, which calls the Start method on the instance of the MockCompass class. The ChangeCurrentValue method call on the instance of the MockGyroscope class triggers the code under test by raising the event handler for the CurrentValueChanged event. The unit test validates that the StartEnabled and StopEnabled properties of the instance of the MainPageViewModel are correctly set, but must wait for the MockGyroscope CurrentValueChanged event to be raised in order to validate the result.

The following code example shows the MockMotion class.

public class MockMotion : IMotion
{
  public bool StartCalled { get; set; }
  public Action StartTestCallback { get; set; }
  public bool StopCalled { get; set; }
  public Action StopTestCallback { get; set; }

  public void Start()
  {
    StartCalled = true;
    if (StartTestCallback != null)
    {
      StartTestCallback();
    }
  }

  public void Stop()
  {
    StopCalled = true;
    if (StopTestCallback != null)
    {
      StopTestCallback();
      }
  }

  public MotionSensorReading CurrentValue { get; set; }

  …

  public event EventHandler<CalibrationEventArgs> Calibrate;

  public void RaiseCalibrate()
  {
    var handler = Calibrate;
    if (handler != null)
      handler(null, new CalibrationEventArgs());
  }

  public event EventHandler<SensorReadingEventArgs<MotionSensorReading>> 
    CurrentValueChanged;

  public void ChangeCurrentValue(AttitudeReading attitude, Vector3 
    deviceAcceleration, Vector3 deviceRotationRate, Vector3 gravity, 
    DateTimeOffset timestamp)
  {
    var motionSensorReading = new SettableMotionSensorReading();
    motionSensorReading.SetAttitude(attitude);
    motionSensorReading.SetDeviceAcceleration(deviceAcceleration);
    motionSensorReading.SetDeviceRotationRate(deviceRotationRate);
    motionSensorReading.SetGravity(gravity);
    motionSensorReading.SetTimestamp(timestamp);

    CurrentValue = motionSensorReading;
    var handler = CurrentValueChanged;
    if (handler != null)
      handler(null, new SensorReadingEventArgs<MotionSensorReading>() 
    { 
      SensorReading = CurrentValue 
    });
  }
}

The MockMotion class implements the IMotion interface and is an example of how a general-purpose mock can be given behavior using delegates. The StartTestCallback and StopTestCallback properties can be set with a delegate or a lambda expression so that the calls to the Start and Stop methods can execute the delegate. By initializing the mock in this way, unit tests have unlimited test control of the mock. The Start and Stop methods also set the StartCalled and StopCalled properties to true, respectively. These properties can be queried by unit tests to discover whether the Start and Stop methods have been called.

Unit tests that use the MockMotion class may also need to use the SettableMotionSensorReading class. This class inherits from the MotionSensorReading class and makes its properties settable. The following code example shows the SettableMotionSensorReading class.

public class SettableMotionSensorReading : MotionSensorReading
{
  public SettableMotionSensorReading(MotionReading motionReading)
  {
    Attitude = motionReading.Attitude;
    DeviceAcceleration = motionReading.DeviceAcceleration;
    DeviceRotationRate = motionReading.DeviceRotationRate;
    Gravity = motionReading.Gravity;
    Timestamp = motionReading.Timestamp;
  }

  public SettableMotionSensorReading()
  {
  }

  public void SetAttitude(AttitudeReading attitudeReading)
  {
    this.Attitude = attitudeReading;
  }
        
  public void SetDeviceAcceleration(Vector3 deviceAcceleration)
  {
    this.DeviceAcceleration = deviceAcceleration;
  }
        
  public void SetDeviceRotationRate(Vector3 deviceRotationRate)
  {
    this.DeviceRotationRate = deviceRotationRate;
  }

  public void SetGravity(Vector3 gravity)
  {
    this.Gravity = gravity;
  }

  public void SetTimestamp(DateTimeOffset timestamp)
  {
    this.Timestamp = timestamp;
  }
}

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 SensorStartsWhenSelectedAndStartSensorCalled test method, which follows the standard arrange, act, assert pattern. The unit test validates that the MainPageViewModel class can start acquiring data from the motion sensor.

[TestMethod]
public void SensorStartsWhenSelectedAndStartSensorCalled()
{
  var motion = new MockMotion();
  motion.IsSupported = true;

  var target = new MainPageViewModel(new MockAccelerometer(),
    new MockCompass(), new MockGyroscope(), motion);

  target.SelectSensor(3);
  target.StartSensor();

  Assert.IsTrue(motion.StartCalled);
}

The test method first creates an instance of the MockMotion class, and sets its IsSupported property to true. The IsSupported property represents whether the device on which the application is running supports the motion sensor. An instance of the MainPageViewModel class is then created, passing new instances of the MockAccelerometer,MockCompass, and MockGyroscope classes, and the instance of the MockMotion class, into the MainPageViewModel constructor. The SelectSensor method of the MainPageViewModel class is then called, in order to ensure that the motion sensor is the selected sensor for when the StartSensor method is called. The StartSensor method of the MainPageViewModel is then called, which calls the Start method on the instance of the MockMotion class. The Start method sets the StartCalled property to true. Finally, the test method validates that the StartCalled property is true, which indicates that the MainPageViewModel class has started acquiring data from the accelerometer sensor.

Summary

This sample application has demonstrated how to build a testable Windows Phone application that consumes sensor data. The AccelerometerAdapter class adapts the Accelerometer SDK class, the CompassAdapter class adapts the Compass SDK class, the GyroscopeAdapter class adapts the Gyroscope class, and the MotionAdapter class adapts the Motion SDK class. The application then consumes the adapted classes. The MockAccelerometer, MockCompass, MockGyroscope, and MockMotion classes are used by unit tests to test business logic contained in the view model class.

Next Topic | Previous Topic | Home

Last built: February 10, 2012