Using the Camera - Programming Windows Store Apps with C# (2014)

Programming Windows Store Apps with C# (2014)

Chapter 11. Using the Camera

In this chapter, we’re going to look at using the camera. While in previous chapters I’ve endeavored to make the examples applicable to both retail and LOB app use, basic use of the camera is more LOB than retail.

The issue with the camera in retail is that just taking a photo is actually very easy. All you need to use is the CameraCaptureUI, and it’ll return a file containing the photo. (You can also use this class to capture video, although we won’t be looking at that in detail in this chapter.)

In LOB applications, or more specifically in field service applications (i.e., people in the field undertake work for the organization), there are some basic use cases for the camera. You typically need to use the camera to capture work that needs to be done (e.g., inspectors visit an estate and proactively look for problems to solve like graffiti or trash), or to capture the state of something before and/or after work has been done (e.g., you take a photo of a broken sink before you fix it, and again after you fix it). A common related use case is to capture a photograph of the premises if the operative gets to the site but cannot gain access.

All of those scenarios follow roughly the same process: take a photo, store it on a device, and send it to the server when you can.

This chapter will mainly center on building out the functionality to create new problem reports, which will include, among other things, capturing a photo and storing on disk, as well as storing our problem report data in the local SQLite database—that is, staging the new report so that it is ready for upload. (We won’t do the actual upload until Chapter 15.)

We’ll also look at how we can resize the image to make storing quantities of images “gentler” in terms of device storage and bandwidth for transmission.

Capturing Photos

In the last chapter, we created ReportPage to create a singleton read-only view of a page. In this chapter, we’re going to create EditReportPage, which will be used to create an editable view of the report. We’ll either pass this a blank, new ReportViewItem instance to create a new item, or pass it an instance created from the database data to edit an existing item.

We do, however, need to think first about how we’re going to get these new problem reports up to the server, although, as mentioned, we’re not actually going to do this until Chapter 15.

At the moment, if we have a report in our SQLite database we know that it will have originated on the server. If we are able to locally create new items, we’ll have some items that are from the server and some items that are not. I’m proposing creating a new field on the report called Statusthat will indicate (among other things) whether the report is on the server or not. This property will use an enumeration that at this point has the values of Unchanged (i.e., the server gave it to us) or New (i.e., we created it locally).

Similarly, if we have changed an item in the local SQLite database that is marked as New we need to be able to indicate that it has been Updated. The final case is that we need to be able to indicate that a local item has been Deleted.

When we get to Chapter 15, we’ll write code that examines this Status property to determine whether we need to send up new or changed items, or delete existing items, depending on the value of that property.

Where this is relevant to this chapter is that we already know that in order to put the photo on the screen on the Reports page, we use the native ID of the item to build up the filename. (Remember, if we have a native ID ABCDEF, the local path on disk where the image gets downloaded is~/LocalState/ReportImages/ABCDEF.jpg.) When we insert a new report, we also need to store the image in ReportImages, but we won’t have an ID to go with it, as we’re currently dependent on the server supplying one. Thus, for items in state New we’ll set the NativeId property to be a new GUID. This gives us a valid, noncolliding value from which to dereference the image. So the process will be:

1. Create a new ReportViewItem and pass it into EditReportPage and EditReportPageViewModel.

2. Do data binding as normal to populate the fields.

3. At user request, we’ll use CameraCaptureUI to get a photo from the camera. This image will be stored on disk as TempState until it’s time to be used. (See Chapter 7 if you need a refresher on how to work with files.)

4. When the user clicks Save, we’ll validate the data. If the item is new, we’ll set its status to New and its native ID to the value returned by Guid.NewGuid(), and then store the image in ReportImages.

5. Ultimately, when we’re able we’ll call up to the server with the report data and the new image.

NOTE

What we haven’t covered here—and actually we won’t do this in the pages here, but the code download for this chapter will support it—is the scenario where the user goes in to edit a report, downloaded from the server, and takes a new photo. If we do this, we’ll flag the fact that this has happened in another property called ImageUpdated in ReportItem.

Let’s get started with building out the UI.

Creating EditReportPage

Structurally, EditReportPage will be very similar to ReportPage, which we built in the last chapter. As it is similar work, I’ll try to work quite quickly through aspects you’ve already seen.

We’re not going to worry about location so much in this chapter, other than to capture the location because we need it to create a valid record. What this means is that we won’t put a map on the screen on the edit page, although in a production app you probably would want to in order to help the user understand that he had indeed captured the correct location. (You can use the Bing Maps control that we used in the last chapter to do this if you need to.)

All we’ll do is call LocationHelper.GetCurrentLocationAsync (which we built in the last chapter) and render the coordinates on the screen.

What we’re looking to build is something like Figure 11-1.

The objective: creating a new report

Figure 11-1. The objective: creating a new report

Building EditReportPageViewModel and Its View-Model

Our new view-model interface will extend IViewModelSingleton<ReportViewItem> in the same way that we saw in the last chapter. In addition, we’ll have two commands—one for handling taking a photo, and one for updating the location. We also need to expose an image that we can render on the page. This requires loading the image into a BitmapImage instance.

To make our lives easier, we’ll add a property that indicates whether the item is new or not. (We’ll use this when we want to display a different caption when we are in new or edit mode.)

Here’s the definition of IEditReportPageViewModel:

public interface IEditReportPageViewModel : IViewModelSingleton

<ReportViewItem>

{

ICommand TakePhotoCommand { get; }

ICommand CaptureLocationCommand { get; }

// are we new?

bool IsNew { get; }

// image presentation...

BitmapImage Image { get; }

bool HasImage { get; }

}

Going over the XAML side, we’ll need a new EditReportPage created, and we’ll need to create some styles for the app bar buttons. On the app bar we’ll need options for Save and Cancel, as well as Take Photo and Capture Location. We already have a style for the Save button (Visual Studio creates it for us), so we only need to create the other three. We can use icons defined in the Segoe UI Symbol font. Here are the styles to add to StandardStyles.xaml:

<!-- Add styles to StandardStyles.xaml -->

<Style x:Key="SaveAppBarButtonStyle" TargetType="Button" BasedOn=

"{StaticResource AppBarButtonStyle}">

<Setter Property="AutomationProperties.AutomationId" Value=

"SaveAppBarButton"/>

<Setter Property="AutomationProperties.Name" Value="Save"/>

<Setter Property="Content" Value=""/>

</Style>

<Style x:Key="CancelAppBarButtonStyle" TargetType="Button" BasedOn=

"{StaticResource AppBarButtonStyle}">

<Setter Property="AutomationProperties.AutomationId" Value=

"CancelAppBarButton"/>

<Setter Property="AutomationProperties.Name" Value="Cancel"/>

<Setter Property="Content" Value=""/>

</Style>

<Style x:Key="TakePhotoAppBarButtonStyle" TargetType="Button" BasedOn=

"{StaticResource AppBarButtonStyle}">

<Setter Property="AutomationProperties.AutomationId" Value=

"TakePhotoAppBarButton"/>

<Setter Property="AutomationProperties.Name" Value="Photo"/>

<Setter Property="Content" Value=""/>

</Style>

<Style x:Key="CaptureLocationAppBarButtonStyle" TargetType="Button"

BasedOn="{StaticResource AppBarButtonStyle}">

<Setter Property="AutomationProperties.AutomationId" Value=

"CaptureLocationAppBarButton"/>

<Setter Property="AutomationProperties.Name" Value="Capture Location"/>

<Setter Property="Content" Value=""/>

</Style>

We can now put the app bar on the page. Because the app bar is integral to the operation of the form, and because the user can’t actually do anything with his editing until he presses Save, we can make the app bar “sticky” and open it from the start. We can do this in markup on the page. Here’s the markup for the app bar:

<!-- Add markup to EditReportPage.xaml -->

<Page.BottomAppBar>

<AppBar IsSticky="true" IsOpen="true">

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="auto"/>

<ColumnDefinition Width="50*"/>

</Grid.ColumnDefinitions>

<StackPanel Orientation="Horizontal">

<Button Style="{StaticResource TakePhotoAppBarButtonStyle}"

Command="{Binding TakePhotoCommand}" />

<Button Style="{StaticResource

CaptureLocationAppBarButtonStyle}" Command="{Binding CaptureLocationCommand}" />

</StackPanel>

<StackPanel HorizontalAlignment="Right" Orientation="Horizontal"

Grid.Column="1" Margin="0,1,5,-1">

<Button Style="{StaticResource SaveAppBarButtonStyle}"

Command="{Binding SaveCommand}" />

<Button Style="{StaticResource CancelAppBarButtonStyle}"

Command="{Binding CancelCommand}" />

</StackPanel>

</Grid>

</AppBar>

</Page.BottomAppBar>

I’ve chosen to split the items onto the left and right to create a logical separation of their function. It’s convention to put controls relating to save/close/cancel on the right. A side effect of splitting them in this way across the whole width of the device is that a user holding the device and working the screen with her thumbs can access both sets of buttons equally well. (Besides, as discussed in Chapter 5 this is actually in Microsoft’s user interface guidelines for Windows Store apps.)

For the caption, we’re going to have two TextBlock labels that will indicate whether the view is in create mode or edit mode. I’ve done it like this for localization and separation of concerns. When we get to localization in Chapter 15, you’ll see the advantage there—the XAML subsystem is able to swap out the localized string on the UI surface with slightly less lifting than if we did it in the view-model programmatically. This segues nicely into the point that if we did it in the view-model we’re slightly muddying our concerns, in that it’s better not to have the view-model making presentation decisions if we can help it.

Here’s the caption that can show one of a pair of labels depending on the value of IsNew:

<!-- Modify markup in EditReportPage.xaml -->

<!-- Back button and page title -->

<Grid>

<Grid.ColumnDefinitions>

<ColumnDefinition Width="Auto"/>

<ColumnDefinition Width="*"/>

</Grid.ColumnDefinitions>

<Button x:Name="backButton" Click="GoBack" IsEnabled="{Binding Frame

.CanGoBack, ElementName=pageRoot}" Style="{StaticResource BackButtonStyle}"/>

<TextBlock Grid.Column="1" Text="New Report" Style="{StaticResource

PageHeaderTextStyle}" Visibility="{Binding IsNew, Converter={StaticResource

BooleanToVisibilityConverter}}"/>

<TextBlock Grid.Column="1" Text="Edit Report" Style="{StaticResource

PageHeaderTextStyle}" Visibility="{Binding IsNew, Converter={StaticResource

BooleanToVisibilityNegationConverter}}"/>

</Grid>

We saw the form itself in Figure 11-1. What we didn’t see was that where the image sits on the design surface, we will actually have two controls. We’ll show a TextBlock control if the user has not specified an image, or we’ll show an Image control if she has. We’ll use the value of theHasImage property on the view-model to choose which one to display.

Here’s the markup for that:

<!-- Add markup to EditReportPage.xaml -->

<Grid Grid.Row="1">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="120"/>

<ColumnDefinition Width="800"/>

</Grid.ColumnDefinitions>

<StackPanel Grid.Column="1">

<TextBlock Text="Title"></TextBlock>

<TextBox Text="{Binding Item.Title, Mode=TwoWay}"></TextBox>

<TextBlock Text="Description"></TextBlock>

<TextBox Text="{Binding Item.Description, Mode=TwoWay}">

</TextBox>

<TextBlock Text="Picture"></TextBlock>

<!-- the placeholder -->

<Border Width="480" Height="360" BorderThickness="2" BorderBrush=

"White" HorizontalAlignment="Left"

Visibility="{Binding HasImage, Converter={StaticResource

BooleanToVisibilityNegationConverter}}">

<TextBlock HorizontalAlignment="Center" VerticalAlignment=

"Center">(No picture)</TextBlock>

</Border>

<!-- the image -->

<Image Width="480" Height="360" HorizontalAlignment="Left"

Source="{Binding Image}"

Visibility="{Binding HasImage, Converter={StaticResource

BooleanToVisibilityConverter}}">

</Image>

<TextBlock Text="Location"></TextBlock>

<TextBlock Text="{Binding Item.LocationNarrative}"></TextBlock>

</StackPanel>

</Grid>

The only thing that we haven’t discussed is LocationNarrative. This just needs to return the latitude/longitude pair separated by a comma for display. We can add this now. While we’re there, we also need a method to set the coordinates internally within a ReportViewItem instance from an IMappablePoint instance. (We’ll see this in action later.) However, when the coordinates change, we also need to signal that the narrative has changed. We can do this by overriding OnPropertyChanged and listening for changes to the Latitude and Longitude properties. You’ll note that this method is inefficient—we raise two signals that LocationNarrative has changed per pair of coordinates. An alternative approach—and actually a better approach if you want a cleaner class design—is to build a custom converter class that you can bind to instead of creating this separate property. Although we’re not going to use a custom converter class, I wanted to show you this alternative approach that you could use as a shortcut.

Here’s the code:

// Add method and property to ReportViewItem...

internal void SetLocation(IMappablePoint point)

{

this.InnerItem.SetLocation(point);

// update...

this.OnPropertyChanged("LocationNarrative");

}

public string LocationNarrative

{

get

{

if (this.Latitude != 0 && this.Longitude != 0)

return string.Format("{0:n5},{1:n5}", this.Latitude,

this.Longitude);

else

return string.Empty;

}

}

However, the ReportItem doesn’t have SetLocation defined. Here’s that change:

// Add method to ReportItem...

internal void SetLocation(IMappablePoint point)

{

this.Latitude = point.Latitude;

this.Longitude = point.Longitude;

}

Another little thing to do: we need to set up the view-model on the EditReportPage code. Here’s that change:

// Modify EditReportPage...

public sealed partial class EditReportPage : StreetFooPage

{

public EditReportPage()

{

this.InitializeComponent();

this.InitializeViewModel();

}

// code omitted...

Let’s now turn our attention back toward completing the implementation of EditReportPageViewModel.

In terms of fields in the class, we’ll need to store the two commands and also store a reference to the image on disk when we capture one from the camera. As mentioned, CameraCaptureUI will create an image on disk in TempState for us to use. Here’s the first part of the view-model:

public class EditReportPageViewModel : ViewModelSingleton<ReportViewItem>,

IEditReportPageViewModel

{

public ICommand TakePhotoCommand {

get { return this.GetValue<ICommand>(); }

private set { this.SetValue(value); } }

public ICommand CaptureLocationCommand {

get { return this.GetValue<ICommand>(); }

private set { this.SetValue(value); } }

// holds the image in TempState that we're displaying...

private IStorageFile TempImageFile { get; set; }

public EditReportPageViewModel(IViewModelHost host)

: base(host)

{

// set up the commands...

this.TakePhotoCommand = new DelegateCommand(async (args) => await

this.CaptureImageAsync());

this.CaptureLocationCommand = new DelegateCommand(async (args)

=> await this.CaptureLocationAsync());

}

}

The first thing we’ll handle is the location code. This will be updated when the view is activated, and from time to time if the user explicitly asks for it to be done via the app bar button. Here’s the code; notice how we’re reusing LocationHelper from Chapter 11:

// Add methods to EditReportPageViewModel...

public override async void Activated(object args)

{

base.Activated(args);

// capture...

await CaptureLocationAsync();

}

private async Task CaptureLocationAsync()

{

var result = await LocationHelper.GetCurrentLocationAsync();

if (result.Code == LocationResultCode.Ok)

this.Item.SetLocation(result.ToMappablePoint());

}

The last thing we’ll do in this section is to add the code that handles changing the caption depending on whether the item is new or not. The IsNew property will have to handle the situation where it’s called before the Item property is initialized. We’ll also flag that the property has changed when the item initialization happens. Here’s the code:

// Add property and method to EditReportViewItem...

public bool IsNew

{

get

{

// we may not have an item yet...

if (this.Item == null)

return true;

else

return this.Item.Id == 0;

}

}

protected override void ItemChanged()

{

base.ItemChanged();

// update the caption...

this.OnPropertyChanged("IsNew");

}

Before we go on to taking the picture, let’s look at adding functionality to the base class to allow us to save and cancel.

Saving and Canceling

One of the points of creating ViewModelSingleton<T> was so that we could add in functionality that’s typical when working with a view of a single item. An example of this sort of functionality is a common approach to canceling, saving, and validating when we’re in the sort of edit mode that we’re in now. Thus, we’ll go ahead and add methods to support those.

This will illustrate an interesting situation with async/await. Let’s say you want to create a Save virtual method on a base class, the signature being protected virtual void Save(). A class that specializes from that base class may need to call async methods from the override of that method. The developer can do that by decorating the method with the async keyword. However, because the method returns void and not Task, it cannot be awaited. Without the await part, you still get asynchrony, but you also get in a total mess because you cannot control the process. You’ll end up “lost in space” insofar as the holistic operation goes.

What we can do, then, is create our overrideable methods as “async friendly” without necessarily needing async functionality. We can do this by making them return a Task instance and then using Task.FromResult<T> to get a task to return. That returned task is effectively faked—it’ll contain a result and appear to the caller that it has been completed already. The important part is that it allows the caller to do an await.

In these methods, we will create a ValidateAsync method that will return an ErrorBucket instance containing problems. That method will be virtual so that the specializing class can do the validation. The SaveAsync method won’t be virtual. It will call ValidateAsync using awaitand then defer to a virtual helper method that will do the actual work.

Here’s the code to add to ViewModelSingleton<T>:

// Add methods to ViewModelSingleton<T>...

protected virtual Task<ErrorBucket> ValidateAsync()

{

// return an empty error bucket (i.e. "success")...

return Task.FromResult<ErrorBucket>(new ErrorBucket());

}

protected async Task<bool> SaveAsync()

{

// validate...

var result = await ValidateAsync();

if (result.HasErrors)

{

await this.Host.ShowAlertAsync(result);

return false;

}

// ok...

await DoSaveAsync();

return true;

}

protected virtual Task DoSaveAsync()

{

return Task.FromResult<bool>(true);

}

Notice that DoSaveAsync returns a value from Task.FromResult<bool>(true). Because the method isn’t marked as async, we have to return something; however, the only implementation on FromResult provided by WinRT requires a type argument. A common trick is to return a Boolean value in this way. It really doesn’t matter what you return as the method is defined as returning a Task instance that doesn’t have type parameters; the caller won’t be expecting a particular type, as the method returns a vanilla Task.

The default operation of CancelAsync will be to call the host and tell it to GoBack. Again, we’ll use the “Boolean task” trick from before, as this method doesn’t actually await anything.

// Add method to ViewModelSingleton<T>...

protected virtual Task CancelAsync()

{

// go back...

this.Host.GoBack();

// return...

return Task.FromResult<bool>(true);

}

We’ll need a way of calling those, and one way to do this is to create standard commands on the view-model that defer to the methods. That’s exactly what we’ll do.

In the IViewModelSingleton interface, we can define the properties for the commands:

public interface IViewModelSingleton<T> : IViewModel

where T : ModelItem

{

ICommand SaveCommand { get; }

ICommand CancelCommand { get; }

T Item { get; }

}

And in the actual class, we can implement them. Here’s the code (I’ve omitted parts of ViewModelSingleton<T> for brevity):

public abstract class ViewModelSingleton<T> : ViewModel,

IViewModelSingleton<T>

where T : ModelItem

{

// save and cancel commands...

public ICommand SaveCommand { get { return this.GetValue<ICommand>(); }

private set { this.SetValue(value); } }

public ICommand CancelCommand { get { return this.GetValue<ICommand>();

} private set { this.SetValue(value); } }

// holds the base item that we're mapped to...

private T _item;

protected ViewModelSingleton(IViewModelHost host)

: base(host)

{

this.SaveCommand = new DelegateCommand(async (args) => await

SaveAsync());

this.CancelCommand = new DelegateCommand(async (args) => await

CancelAsync());

}

// code omitted...

}

We’ll now look at how to display the form and ultimately take a picture.

Adding the New Option

We’ll add a button to the app bar on the Reports page that allows us to create a new item. Oddly, although we are given an app bar style called AddAppBarButtonStyle, which happens to have the caption “Add,” the convention in the built-in Windows 8 apps is that the option to create new items should be labeled “New.” Hence, the first thing to do is create a new style called NewAppBarButtonStyle that will use the same icon:

<!-- Add to StandardStyles.xaml -->

<Style x:Key="NewAppBarButtonStyle" TargetType="Button" BasedOn=

"{StaticResource AppBarButtonStyle}">

<Setter Property="AutomationProperties.AutomationId" Value=

"NewAppBarButton"/>

<Setter Property="AutomationProperties.Name" Value="New"/>

<Setter Property="Content" Value=""/>

</Style>

I won’t go through how to add the button to the app bar, or how to wire up a command, as we’ve done it a few times—take a look at Chapter 5 for the first run-through of this. I also won’t show how to create a command called NewCommand in IReportsPageViewModel. I’ll assume that you can make those changes to the Reports page, although of course you’ll find it properly implemented in the code download.

The actual implementation of that command could take some explanation, though, as we haven’t seen it before. EditReportPageViewModel requires an instance of a ReportViewItem, and we’ll need to create a blank one to use. Here’s the modified constructor ofReportsPageViewModel. I’ve omitted some code for brevity.

public ReportsPageViewModel()

{

// code omitted...

// add...

this.NewCommand = new DelegateCommand((e) => this.Host.ShowView

(typeof(IEditReportPageViewModel), new ReportViewItem(new ReportItem())));

}

Handling Temporary Files

So we know that we can use CameraCaptureUI to take a picture, and we know that picture will get stored on disk in TempState.

One of the problems with TempState is that it’s not automatically cleaned up for us. Should the device’s disk come under pressure, Windows will treat any files that it finds in any TempState folder belonging to any installed Windows 8 apps as fair game for deletion, but it’s certainly possible for us to misuse TempState to the point where we’re not creating a great experience for the user not just for our app, but systemwide. (Our massive temporary state could prevent the user from downloading his email mailbox, for example.)

Personally, I think it would have been better to have Windows manage these folders for us. It would have been easy enough to clear down this folder when the app stopped, and to rig in some sort of proactive cleanup mechanism within the OS. However, such a thing would take precious cycles, especially on Windows RT devices. To that end, we have to manage all of this ourselves.

What we need to do is be careful not to create orphaned temporary files as part of the photo-taking process. For example, if we take a photo and then take another photo, we can safely delete the first one, as it’s been implicitly discarded. These images in the 1280×720 resolution that I was using took up about 150KB each, which may not seem like a lot, but over many months of heavy use could easily clog up the device unnecessarily.

In addition, if the user cancels the edit operation, we also have to be careful to remove any file that we may have taken during the aborted process. Although we’ll talk more about application lifetime in Chapter 15, we aren’t told when our process is unloaded; thus, if we happen to have a temporary image at the time when we’re unloaded from memory, that file will be orphaned. Plus, you can’t rely on a “suspend” notification here, because if you Alt-Tab between apps you’ll get suspended and incorrectly delete any photo that got taken—an image the user may want.

The best approach would actually be to track the temporary filepath in the SQLite database and explicitly look to see if there was one on disk to clean up on application start.

I have, however, ignored this subtlety—it seemed to me to be acceptable to omit it for the sake of not overloading this chapter. I belabor the preceding point so that it’s on your radar for your own production apps.

Changing the Manifest

In order to take pictures, we need to turn on the appropriate capability in the manifest. Specifically, we need to turn on the Webcam capability. Open the manifest editor and do this now. Figure 11-2 illustrates.

The Webcam capability

Figure 11-2. The Webcam capability

Taking Pictures

Back when we built the basic structure of ReportPageViewModel, we rigged the TakePhotoCommand to call a method called CaptureImageAsync. We now need to build this method.

The first thing this method does is to call the WinRT photo capture methods—specifically, it will create a new CameraCaptureUI instance and call CaptureFileAsync. By default, this will return a JPEG file.

We’ll store the file we capture in the TempImageFile property. However, we may already have one of those, so we’ll call a method called CleanupTempImageFileAsync to get rid of any old one that we might have.

In order to put the file on the screen, we need to provide the XAML subsystem with an object that contains it. We’ll create a new BitmapImage instance and load up the image data from the supplied file. We’ll then set the Image property (which ultimately will go through and trigger a data-binding update so that the image displays on the form), and we’ll store the reference to the file.

Here’s the code:

// Add method to EditReportPageViewModel...

private async Task CaptureImageAsync()

{

// get the image...

var ui = new CameraCaptureUI();

var file = await ui.CaptureFileAsync(CameraCaptureUIMode.Photo);

// did we get one?

if (file != null)

{

// do we have an old one to delete...

await CleanupTempImageFileAsync();

// load the image for display...

var newImage = new BitmapImage();

using (var stream = await file.OpenReadAsync())

newImage.SetSource(stream);

// set...

this.Image = newImage;

this.TempImageFile = file;

}

}

By way of supporting members, we’ve already spoken about CleanupTempImageFileAsync. Here’s that method:

// Add method to EditReportPageViewModel...

private async Task CleanupTempImageFileAsync()

{

try

{

if (this.TempImageFile != null)

await this.TempImageFile.DeleteAsync();

}

catch

{

// ignore errors...

}

finally

{

this.TempImageFile = null;

}

}

This method is written so that failed deleted operations are ignored. (This might happen if you have some weird file locking happening on the device.) This is usually a good approach if the main operation is impacted by whether or not the old file was deleted.

The HasImage and Image properties will look like this:

// Add properties to EditReportPageViewModel...

public bool HasImage

{

get

{

return this.Image != null;

}

}

public BitmapImage Image

{

get

{

return this.GetValue<BitmapImage>();

}

set

{

// set...

this.SetValue(value);

// update the flag...

this.OnPropertyChanged("HasImage");

}

}

At this point, the application will run and we can take a photo. However, before we do that, we’ll just override the CancelAsync method, the objective being to clean up any image file that we may have when we quit via that option. Here’s the code:

// Add method to EditReportPageViewModel...

protected override async Task CancelAsync()

{

// remove the temp image...

await this.CleanupTempImageFileAsync();

// base...

await base.CancelAsync();

}

Now you can run the project and access the New page to add an item. If you take a photo, you’ll find it in the TempState folder of your deployed package. (If you need to find that, go to C:\Users\<User>\AppData\Local\Packages and sort by Date Modified. Find the one with the most recent modification date, and it’s likely yours.) Figure 11-3 shows File Explorer displaying the new image.

The captured image in the TempState folder

Figure 11-3. The captured image in the TempState folder

Of course, you’ll actually see the image on the page, as illustrated back in Figure 11-1.

The next thing we need to look at is how we can actually save the image when we’ve finished working with it.

Implementing Save

The objective here is to commit the changes to the underlying ReportItem to the database, and to position the image in the proper place on disk. The image itself will likely be too big, so we’re going to look at resizing it so that it takes up less space on disk and—relevantly—is smaller for network transmission.

NOTE

In this book, I’m only going to take you through creating new items. In the code download, you’ll find that EditReportPageViewModel is also able to update existing items.

Validating and Saving

The first thing that we have to do is validate the data. We built a method called ValidateAsync into ViewModelSingleton<T> to support this. We just need to override it.

In the method, we’ll check the Title and Description fields, we’ll check that we have an image, and we’ll check that we have a location. The location check is a little hacky—all we’ll do is make sure we haven’t got 0,0 as a coordinate. (Seeing as that point on the planet is in the middle of the ocean, it’s probably OK. A better approach would be to track whether the user had actually updated the location.) Here’s the code:

// Add method to EditReportPageViewModel...

protected override Task<ErrorBucket> ValidateAsync()

{

var bucket = new ErrorBucket();

if (string.IsNullOrEmpty(this.Item.Title))

bucket.AddError("Title is required.");

if (string.IsNullOrEmpty(this.Item.Description))

bucket.AddError("Description is required.");

if (!(this.HasImage))

bucket.AddError("An image is required.");

if (this.Item.Latitude == 0 && this.Item.Longitude == 0)

bucket.AddError("A position is required.");

// return...

return Task.FromResult<ErrorBucket>(bucket);

}

Note here that we use the Task.FromResult<T> “trick” to allow us to design the base class to support asynchrony without having asynchrony in the deriving classes.

We’ll build the method to create the ReportItem in the database in a moment, but for now we can override the Save method similarly. As mentioned before, in these pages we’re only going to build the insert functionality. In the download, update is also supported.

// Add method to EditReportPageViewModel...

protected override async Task DoSaveAsync()

{

// save...

if (this.IsNew)

{

// create a new one...

await ReportItem.CreateReportItemAsync(this.Item.Title,

this.Item.Description, this.Item,

this.TempImageFile);

}

else

{

// update an existing one...

throw new InvalidOperationException("Implemented in the

download, not the book...");

}

// cleanup...

await this.CleanupTempImageFileAsync();

// return...

this.Host.GoBack();

}

Note here that we clean up the temporary file. That’s intentional—by design, CreateReportItemAsync (which we’re about to build) won’t assume it owns the file that it’s been given to work with and hence it’s not its responsibility to delete it.

For the save operation itself, we need to create a new ReportItem instance in the database and flag its status so that we know it needs to be transmitted up to the server when we get to that in Chapter 15. To this end, we need to set the Status property to New and set the NativeId to be a newly created GUID. I’ll go through the CreateReportItemAsync method in stages, starting with that:

// Add method to ReportItem...

internal static async Task<ReportItem> CreateReportItemAsync(string

title, string description,

IMappablePoint point, IStorageFile image)

{

var item = new ReportItem()

{

Title = title,

Description = description,

NativeId = Guid.NewGuid().ToString(),

Status = ReportItemStatus.New

};

Once we’ve created the basic item, we can set the Latitude and Longitude fields via the SetLocation call that we built earlier:

item.SetLocation(point);

Then, we can do the actual insert:

// save...

var conn = StreetFooRuntime.GetUserDatabase();

await conn.InsertAsync(item);

With the basic database change made, we can turn our attention to the image. The idea here is that we “stage” the image into the ~/LocalState/ReportImages folder, just as if it had been downloaded from the server. We can do this by making up a filename based on the NativeId that we created before and then copying the supplied image over to that new location.

// stage the image...

if (image != null)

{

// new path...

var manager = new ReportImageCacheManager();

var folder = await manager.GetCacheFolderAsync();

// create...

await image.CopyAsync(folder, item.NativeId + ".jpg");

}

Finally, we can return the item:

// return...

return item;

}

At this point, the operation will work and we can save reports. More importantly, we can go back to the Reports page and actually see our new report, as Figure 11-4 shows.

The new Foobar report on the Reports page

Figure 11-4. The new Foobar report on the Reports page

Resizing Images

The last thing that I want to cover with regard to images is resizing. The default image configuration that’s created on my machine is a 1280×720-pixel, JPEG-formatted image that happens to fit into a file of around 150KB.

Although that’s not a big file, there are a couple of reasons why in LOB scenarios you may wish to control file size. If each operative does 10 visits a day, that’s 31MB of transfer just on image data. Over cellular networks, this can end up as significant, especially if you have a lot of users all working in the same way.

Second—and this is a weirder problem—over time, devices tend to increase their default capture size. Whereas you can deploy some software on day one where the images are 150KB, on a typical new device deployed a couple years hence, it’s not unusual to find the file size doubled. And while processor speed and memory tends to ramp up quickly, network transfer speeds are slower to roll out.

My recommendation is that in all cases you pick the size of image that the business demands and make your app always return that, rather than rely on the vagaries of the device.

This is one area of WinRT where things are quite different in the .NET world. A common way to resize images with .NET was to use GDI+. Specifically, you loaded a bitmap, created a new bitmap, created a device context over that new bitmap, and used GDI+ to scale and render the source into the target. However, that approach was not supported on servers; on the server we were supposed to use classes in the System.Windows.Media.Imaging namespace, such as BitmapEncoder and related classes. These classes worked directly with the image data in a more server-friendly way. Specifically, BitmapEncoder knew how to write out raster file data, and its companion BitmapDecoder knew how to read it.

In Windows 8 apps, you don’t have access to GDI+ at all. Plus, the image manipulation functions have moved to the Windows.Graphics.Imaging namespace. As is often the case when things move from the .NET world into WinRT, their baseline functionality and/or structure also gets changed. So, although we have BitmapEncoder et al. in the new WinRT library, there are various bits missing. If you’re used to using these APIs, your mileage may vary.

We’re going to create a new method called ResizeAndSaveAsAsync in a new class called ImageHelper. This will take an input file, an output file, and a single “target dimension” value. Taking one value may seem odd, but what we’re looking to do is constrain the edge of an image to the largest possible size. A landscape image will end up no more than, say, 640 pixels wide. That same image in portrait orientation will end up no more than 640 pixels high. The aspect ratio will be preserved.

The first thing we need to do is open up the source file. This will be the camera data file in our TempState folder.

We then create a BitmapDecoder. This will interpret the data in the file and tell us metrics about the file (e.g., how wide it is). BitmapDecoder will also surface the pixel data, which is the actual data that makes up the file. (In fact, BitmapDecoder understands files in terms of frames. We only have one frame in our file, which happens to be the picture that we took.)

Next, we open a stream to where we want the resulting image to go. This can be directed to disk (which is what we’re going to do), or to memory (using an InMemoryRandomAccessStream instance). Once we have the stream, we need a BitmapEncoder. You can create these in various ways, but the one we’re going to use is a transcoder, which converts from one format to the other.

Once we have the transcoder, we can specify the new dimensions of the image via the object exposed by the BitmapTransform property. (If we wanted to, we could also crop and rotate the image here.) Once that’s done, we commit the transcoder by calling FlushAsync, and our new file will be written to disk.

I’ll present ResizeAndSaveAsAsync in steps. I’m proposing creating a new class called ImageHelper to host this method. Here’s the code; the first thing we do is open the source stream and get the decoder:

public static class ImageHelper

{

internal static async Task ResizeAndSaveAsAsync

(IStorageFile source, IStorageFile destination, int targetDimension)

{

// open the file...

using(var sourceStream = await source.OpenReadAsync())

{

// step one, get a decoder...

var decoder = await BitmapDecoder.CreateAsync(sourceStream);

Once we’ve done that, we can create the output stream and create the transcoding encoder. This requires the output stream and the source decoder:

// step two, create somewhere to put it...

using(var destinationStream =

await destination.OpenAsync

(FileAccessMode.ReadWrite))

{

// step three, create an encoder...

var encoder = await BitmapEncoder.CreateForTranscodingAsync

(destinationStream, decoder);

The next stage is to configure the transformation. To do this, we need to determine if the image is portrait or landscape and calculate the aspect ratio. Depending on the orientation, we’ll use a different calculation for the final dimension:

// how big is it?

uint width = decoder.PixelWidth;

uint height = decoder.PixelHeight;

decimal ratio = (decimal)width / (decimal)height;

// orientation?

bool portrait = width < height;

// step four, configure it...

if (portrait)

{

encoder.BitmapTransform.ScaledHeight =

(uint)targetDimension;

encoder.BitmapTransform.ScaledWidth =

(uint)((decimal)targetDimension * ratio);

}

else

{

encoder.BitmapTransform.ScaledWidth =

(uint)targetDimension;

encoder.BitmapTransform.ScaledHeight =

(uint)((decimal)targetDimension / ratio);

}

Finally, we can write the image:

// step five, write it...

await encoder.FlushAsync();

}

}

}

}

Before we can test it, we need to change the CreateReportItemAsync method that we wrote earlier to use this “resize and save” method rather than the original code that moved the file. Here’s the change:

internal static async Task<ReportItem> CreateReportItemAsync

(string title, string description,

IMappablePoint point, IStorageFile image)

{

var item = new ReportItem()

{

Title = title,

Description = description,

NativeId = Guid.NewGuid().ToString(),

Status = ReportItemStatus.New

};

item.SetLocation(point);

// save...

var conn = StreetFooRuntime.GetUserDatabase();

await conn.InsertAsync(item);

// stage the image...

if (image != null)

{

// new path...

var manager = new ReportImageCacheManager();

var folder = await manager.GetCacheFolderAsync();

// save it as a file that's no longer than

// 640 pixels on its longest edge...

var newImage = await folder.CreateFileAsync

(item.NativeId + ".jpg");

await ImageHelper.ResizeAndSaveAsAsync(image, newImage, 640);

}

// return...

return item;

}

Run the code now, and you’ll find that the image files are smaller. Open them up, and you’ll notice the dimensions are restricted to 640 pixels along the longest edge.

A WORD ABOUT CAPTURING VIDEO

I’ve limited this chapter’s discussion to basic image capture only—as mentioned, that’s the most common use in LOB apps, and retail app use of this feature is likely to be more advanced.

You can also allow capture video. Obviously, this will eat disk space and battery. The key reason why I didn’t include this is because in field service applications it’s a nightmare dealing with video uploads over cellular service, and as a result it tends to be quite specialized. However, you certainly can do it. If you want to capture video, though, you need to enable the Microphone capability in the manifest.