Condividi tramite


Using a Chooser to Take a Photo

patterns & practices Developer Center

You Will Learn

  • How to adapt the CameraCaptureTask SDK class and consume the adapted class.
  • How to adapt the MessageBox SDK class and consume the adapted class.
  • How to create a mock of the CameraCaptureTask class and write unit tests that test view model business logic by consuming the mock class.
  • How to create a mock of the MessageBox 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 CameraCaptureTask SDK Class | Adapting the MessageBox 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 uses a chooser to take a photo.

The Application

This sample application uses a chooser to take a photo and then displays the photo. When the application is launched, the page enables the user to start the camera capture task. This launches a chooser from which the user can take a photo. Once a photo has been taken, it is displayed on the page. Alternatively, if the photo capture process failed, a message box is displayed containing an error message. You can download the code for this sample application at the following link:

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

Note

If your application is camera intensive, you should consider using the PhotoCamera API class which allows you to configure functionality such as image capture, focus, resolution, and flash mode.

Follow link to expand image

Adapting the CameraCaptureTask SDK Class

The CameraCaptureTask class enables an application to launch the Camera application which can be used to take photos from the application. 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 CameraCaptureTask class is not available in the Windows Phone 7.1 SDK. Therefore, the ICameraCaptureTask interface was generated from the CameraCaptureTask class. The following code example shows the ICameraCaptureTask interface, which contains the property, method signature, and event implemented by the CameraCaptureTask SDK class.

public interface ICameraCaptureTask
{
  PhotoResultTaskEventArgs TaskEventArgs { get; set; }
  event EventHandler<PhotoResultTaskEventArgs> Completed;
  void Show(); 
}

The ICameraCaptureTask interface is implemented by the CameraCaptureTaskAdapter class. The CameraCaptureTaskAdapter class adapts the CameraCaptureTask 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 wants to consume the CameraCaptureTask class to take a photo should consume the CameraCaptureTask class instead. The following code example shows the CameraCaptureTaskAdapter class.

public class CameraCaptureTaskAdapter : ICameraCaptureTask
{
  public CameraCaptureTaskAdapter()
  {
    WrappedSubject = new CameraCaptureTask();
    AttachToEvents();
  }

  private CameraCaptureTask WrappedSubject { get; set; }

  public PhotoResultTaskEventArgs TaskEventArgs
  {
    get
    {
      return new PhotoResultTaskEventArgs(WrappedSubject.TaskEventArgs);
    }
    set { WrappedSubject.TaskEventArgs = value.PhotoResult; }
  }

  public event EventHandler<PhotoResultTaskEventArgs> Completed;

  private void AttachToEvents()
  {
    WrappedSubject.Completed += WrappedSubjectCompleted;
  }

  void WrappedSubjectCompleted(object sender, PhotoResult e)
  {
    CompletedRelay(sender, new PhotoResultTaskEventArgs(e));
  }

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

  private void CompletedRelay(object sender, PhotoResultTaskEventArgs e)
  {
    var handler = Completed;
    if (handler != null)
    {
      handler(sender, e);
    }
  }
}

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

The event implemented by the CameraCaptureTaskAdapter class is Completed. This event occurs when a Chooser task is completed, and it exposes the results of the Chooser task through the TaskEventArgs property. The TaskEventArgs property is of type PhotoResult. However, the properties in the PhotoResult class are not settable, which makes the Completed event not testable through mocks. Therefore, the PhotoResultTaskEventArgs class was added that provides an implementation of the PhotoResult class where the property setters are protected. To enable testability the SettablePhotoResultTaskEventArgs class was added, that inherits from the PhotoResultTaskEventArgs class, where the properties are settable. When the Completed event is raised, the WrappedSubjectCompleted event handler calls the CompletedRelay event handler, passing in a new instance of the PhotoResultTaskEventArgs class. The following code example shows the PhotoResultTaskEventArgs class.

public class PhotoResultTaskEventArgs : TaskEventArgs
{
  public PhotoResultTaskEventArgs(PhotoResult photoResult)
  {
    this.PhotoResult = photoResult;

    if (photoResult == null) return;

    ChosenPhoto = photoResult.ChosenPhoto;
    OriginalFileName = photoResult.OriginalFileName;
    Error = photoResult.Error;
  }

  public new Stream ChosenPhoto { get; set; }
  public new string OriginalFileName { get; set; }
  public new Exception Error { get; set; }

  public PhotoResult PhotoResult { get; private set; }
}

When a new instance of the PhotoResultTaskEventArgs class is created, the ChosenPhoto, OriginalFileName, and Error properties are simply set to their equivalent properties from the PhotoResult class.

Adapting the MessageBox SDK Class

The MessageBox class displays a message to the user and optionally prompts for a response. However, this class is not easily testable because it contains some static members. To create a testable version of this class you should adapt the class, and then create a mock for it.

The interface for the MessageBox class is not available in the Windows Phone 7.1 SDK. Therefore, the IMessageBox interface was generated from the MessageBox SDK class. The following code example shows the IMessageBox interface, which contains two method signatures.

public interface IMessageBox
{
  MessageBoxResult Show(string messageBoxText);
  MessageBoxResult Show(string messageBoxText, string caption, 
    MessageBoxButton button);
}

The IMessageBox interface is implemented by the MessageBoxAdapter class. The MessageBoxAdapter class adapts the MessageBox 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 MessageBox class to display a message to the user should consume the MessageBoxAdapter class instead. The following code example shows the MessageBoxAdapter class.

public class MessageBoxAdapter : IMessageBox
{
  public MessageBoxResult Show(string message)
  {
    return MessageBox.Show(message);
  }

  public MessageBoxResult Show(string messageBoxText, string caption, 
    MessageBoxButton button)
  {
    return MessageBox.Show(messageBoxText, caption, button);
  }
}

The MessageBoxAdapter class implements two Show methods. The first Show method displays a message box that contains the specified text and an OK button. The second Show method displays a message box that contains the specified text, title bar caption, and response buttons.

Consuming the Adapters

The following code example shows the MainPage view, which comprises an Image and a Button.

<Grid x:Name="LayoutRoot" Background="Transparent">
  …

  <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <StackPanel Orientation="Vertical" … >
      <Image Width="320" Height="240" Source="{Binding Picture}" 
        Margin="0,0,0,12" />
      <Button Content="Start Camera Capture Task" Click="TaskButton_Click"/>
    </StackPanel>
  </Grid>
</Grid>

The Button calls an event handler when its Click event is raised. The event handler simply calls the CapturePicture method in the MainPageViewModel class. The Image displays the captured picture by binding to the Picture property in 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.

public MainPageViewModel MainPageViewModel
{
  get
  {
    return new MainPageViewModel(new CameraCaptureTaskAdapter(), 
      new MessageBoxAdapter());
  }
}

The MainPageViewModel property returns a new instance of the MainPageViewModel class, and passes in new instances of the CameraCaptureTaskAdapter and MessageBoxAdapter classes into the MainPageViewModel constructor.

The MainPageViewModel class uses an instance of the ICameraCaptureTask interface to take a photo, and an instance of the IMessageBoxAdapter interface to display an error message to the user if an error occurs during the capture process. The following code example shows how the constructor receives these instances from the ViewModelLocator class.

private readonly ICameraCaptureTask task;
private readonly IMessageBox messageBox;
…

public MainPageViewModel(ICameraCaptureTask cameraCaptureTask, 
  IMessageBox messageBox)
{
  this.messageBox = messageBox;
  this.task = cameraCaptureTask;

  this.task.Completed += task_Completed;
}

The constructor sets the messageBox field to the instance of the IMessageBox it receives from the ViewModelLocator class—in this case an instance of the MessageBoxAdapter class. It then sets the task field to the instance of the ICameraCaptureTask it receives from the ViewModelLocator class—in this case an instance of the CameraCaptureTaskAdapter class. It also registers an event handler method to the instance of the CameraCaptureTaskAdapterCompleted event.

The following code example shows the CapturePicture method and the event handler for the Completed event.

void task_Completed(object sender, PhotoResultTaskEventArgs e)
{
  this.Capturing = false;

  if (e.ChosenPhoto != null)
  {
    //photo taken
    DisplayPhoto(e.ChosenPhoto);
  }
  else if (e.Error != null && !string.IsNullOrEmpty(e.Error.Message))
  {
    //error thrown
    this.messageBox.Show(e.Error.Message);
  }
}

public void CapturePicture()
{
  if (!this.Capturing)
  {
    this.task.Show();
    this.Capturing = true;
  }
}

The CapturePicture method is invoked from the TaskButton_Click event handler in the view. Provided that the capturing process is not already underway, the method calls the Show method to show the camera application and sets the Capturing property to true. Once a photo has been captured, the task_Completed method is called to handle the Completed event being raised. This method sets the Capturing property to false, and if the ChosenPhoto property of the instance of the PhotoResultTaskEventArgs class is not null, it means that a photo has been successfully taken and it will be displayed using the DisplayPhoto method. If the ChosenPhoto property is null, it's most likely that an error has occurred. Therefore, if the Error property of the instance of the PhotoResultTaskEventArgs class is not null, and the Error property contains a Message, the error message is displayed to the user in a message box.

The following code example shows the DisplayPhoto method.

public void DisplayPhoto(Stream chosenPhoto)
{
  try
  {
    var imageSource = PictureDecoder.DecodeJpeg(chosenPhoto);
    this.Picture = imageSource;
  }
  catch (Exception ex)
  {
    messageBox.Show(ex.Message);
  }
}

The DisplayPhoto method uses the DecodeJpeg method of the PictureDecoder class to decode the JPEG data from the chosenPhoto stream to a WriteableBitmap and stores the decoded data in the Picture property. This is wrapped in a try/catch block so that if the operation fails, a message box will be displayed to the user containing details of the error that occurred.

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 MockCameraCaptureTask class.

public class MockCameraCaptureTask : ICameraCaptureTask
{
  public PhotoResultTaskEventArgs TaskEventArgs { get; set; }

  public Action ShowTestCallback { get; set; } 

  public event EventHandler<PhotoResultTaskEventArgs> Completed;
  public void Show()
  {
    if (TaskEventArgs == null)
    {
      throw new InvalidOperationException(
        "Must set TaskEventArgs prior to calling Show.");
    }

    if (ShowTestCallback != null) ShowTestCallback();

    var handler = Completed;
    if (handler != null)
      handler(null, TaskEventArgs);
  }
}

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

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

public class SettablePhotoResultTaskEventArgs : PhotoResultTaskEventArgs
{
  public SettablePhotoResultTaskEventArgs(PhotoResult photoResult) 
    : base(photoResult)
  {
  }

  public SettablePhotoResultTaskEventArgs() : base(null)
  {
  }

  public void SetChosenPhoto(Stream chosenPhoto)
  {
    this.ChosenPhoto = chosenPhoto;
  }

  public void SetOriginalFileName(string originalFileName)
  {
    this.OriginalFileName = originalFileName;
  }

  public void SetError(Exception error)
  {
    this.Error = error;
  }
}

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 ViewModelGetsPictureFromCameraTask test method, which demonstrates the testing of asynchronous functionality, and which follows the standard arrange, act, assert pattern. The unit test validates that the MainPageViewModel class can use the camera application to capture a photo and display it. 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 ViewModelGetsPictureFromCameraTask()
{
  var mockCameraCaptureTask = new MockCameraCaptureTask();
  WriteableBitmap picture = null;
  var imageUri = new Uri("/TestableCamera.Tests;component/Images/block.jpg",
    UriKind.Relative);
  var src = new BitmapImage();
  src.SetSource(Application.GetResourceStream(imageUri).Stream);
  picture = new WriteableBitmap(src);

  var sri = Application.GetResourceStream(imageUri);
  var photoResult = new SettablePhotoResultTaskEventArgs(new PhotoResult());
  photoResult.SetChosenPhoto(sri.Stream);

  mockCameraCaptureTask.TaskEventArgs = photoResult; 

  var target = new MainPageViewModel(mockCameraCaptureTask, 
    new MockMessageBox());
  Assert.IsNull(target.Picture);

  target.PropertyChanged += (s, e) =>
  {
    if (e.PropertyName != "Picture") return;

    Assert.IsNotNull(target.Picture);
    Assert.IsTrue(IntArraysMatch(target.Picture.Pixels, picture.Pixels));

    EnqueueTestComplete();
  };

  target.CapturePicture();
}

The test method first creates an instance of the MockCameraCaptureTask class and then creates a WriteableBitmap object that it sets to a sample image in the application package. The TaskEventArgs property of the instance of the MockCameraCaptureTask class is then set to a new instance of the SettablePhotoResultTaskEventArgs class. The test uses the SetChosenPhoto method of the SettablePhotoResultTaskEventArgs class to set the ChosenPhoto property of the SettablePhotoResultTaskEventArgs instance to the sample image in the application package. An instance of the MainPageViewModel class is then created, passing in the instance of the MockCameraCaptureTask class, and a new instance of the MockMessageBox class. The unit test then listens to the MainPageViewModelPropertyChanged event, looking for a change to the Picture property. In the event handler for the PropertyChanged event, the picture is validated by comparing the pixels of the source and target images. The CapturePicture method call on the instance of the MainPageViewModel triggers the code under test. The CapturePicture method calls the Show method on the instance of the MockCameraCaptureTask class and then listens for the Completed event. Once the event is raised and a photo is returned, the Picture property of the instance of the MainPageViewModel is set. The unit test validates this behavior, but must wait for the MockCameraCaptureTask Completed event to be raised, and the MainPageViewModel PropertyChanged event to be raised, in order to validate the results.

The following code example shows the MockMessageBox class.

public class MockMessageBox : IMessageBox
{
  public string MessageBoxText { get; set; }
  public MessageBoxResult MessageBoxResult { get; set; }
  public bool ShowCalled { get; set; }

  public MessageBoxResult Show(string messageBoxText)
  {
    ShowCalled = true;
    MessageBoxText = messageBoxText;
    return MessageBoxResult;
  }

  public MessageBoxResult Show(string messageBoxText, string caption, 
    MessageBoxButton button)
  {
    ShowCalled = true;
    MessageBoxText = messageBoxText;
    return MessageBoxResult;
  }
}

The MockMessageBox class implements the IMessageBox interface, with the pattern being that the Show methods set the ShowCalled property to true to indicate that a Show method has been called. The ShowCalled property can then be queried from outside 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 SaveShowsMessageBoxWhenIsoStoreThrows test method, which demonstrates testing asynchronous functionality, and which follows the standard arrange, act, assert pattern. The unit test validates that a message box will be displayed by the MainPageViewModel class if the camera returns an error. 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 ViewModelShowsErrorMessageIfCameraTaskReturnsError()
{
  var mockCameraCaptureTask = new MockCameraCaptureTask();
  var mockMessageBox = new MockMessageBox();

  var photoResult = new SettablePhotoResultTaskEventArgs();
  photoResult.SetError(new Exception("test exception message"));
  mockCameraCaptureTask.TaskEventArgs = photoResult; 

  var target = new MainPageViewModel(mockCameraCaptureTask, mockMessageBox);

  target.PropertyChanged += (s, e) =>
  {
    if (e.PropertyName != "Capturing" && target.Capturing != false) return;

    Assert.AreEqual("test exception message", mockMessageBox.MessageBoxText);

    EnqueueTestComplete();
  };

  target.CapturePicture();
}

The test method first creates an instance of the MockCameraCaptureTask class, and an instance of the MockMessageBox class. The TaskEventArgs property of the instance of the MockCameraCaptureTask class is then set to a new instance of the SettablePhotoResultTaskEventArgs class, with the Error property being set to an Exception that contains a message. An instance of the MainPageViewModel class is then created, passing in the instances of the MockCameraCaptureTask and MockMessageBox classes. The unit test then listens to the MainPageViewModel PropertyChanged event, looking for the instance of the MockMessageBox to display an error message. In the event handler for the PropertyChanged event, the MessageBoxText property of the instance of the MockMessageBoxAdapter class is compared to the original message that was placed in the Error property of the instance of the SettablePhotoResultTaskEventArgs class. The CapturePicture method call on the instance of the MainPageViewModel triggers the code under test. The CapturePicture method calls the Show method on the instance of the MockCameraCaptureTask class and then listens for the Completed event. Once the event is raised, the Show method of the instance of the MockMessageBox is called, which sets its MessageBoxText property. The unit test validates this behavior, but must wait for the MockCameraCaptureTask Completed and MainPageViewModel PropertyChanged events to be raised in order to validate the results.

Summary

This sample application has demonstrated how to build a testable Windows Phone application that uses a chooser to take a photo. The CameraCaptureTaskAdapter class adapts the CameraCaptureTask SDK class, and the MessageBoxAdapter class adapts the MessageBox SDK class. The application then consumes the adapted classes. The MockCameraCaptureTask and MockMessageBox classes are used by unit tests to test business logic contained in the view model class.

Previous Topic | Home

Last built: February 10, 2012