Working with Files- Programming Windows Store Apps with C# (2014)

Programming Windows Store Apps with C# (2014)

Chapter 6. Working with Files

When I was first putting together the structure of this book, I intentionally left out a section on working with files. Whenever a developer faces a new platform, one of the first things that he or she does after “Hello, world” is to try reading or writing files on disk. As a result, you tend to get a lot of community-generated content early on in a platform’s inception that comprises a million and one different rehashes on how to read and write files.

However, upon deeper reflection I decided to include this chapter, not necessarily to show you how to read or write files, but how to work with files within the restrictions imposed by WinRT and the Windows Store app UX. One aspect to this is sandboxing, which prevents abuse of the filesystem and its attendant data (a common activity of malware). Another aspect relates to how all the file access is done in WinRT, and so we have to deal with components that blend operations between WinRT and .NET.

In this chapter I’ll take you through the various ways in which we can drive the filesystem. If you look in the download package for this chapter, you’ll find some solutions/projects in addition to the StreetFoo ones that we have been working with thus far. Some of the examples we build will be “scratch” examples that don’t fit into this greater body of work.

So, let’s get going. First we’ll look at the file picker.

The File Picker

Sandboxed file access control in the world of Windows Store apps is generally limited to forcing one of two modes—you can either show a UI that lets the user explicitly give you a file, or you can programmatically access files within known folders.

In this first section we’re going to look at the file picker, which, essentially, is the traditional “open file” and “save file” dialogs of yesterday rendered in a way that’s touch-centric, but with some additional restrictions. For example, whereas in the old Windows file dialogs you can let the user choose All Files (*.*)—or in fact, the user can just nominate any file desired—you can’t do that in the new pickers. The behavior is locked down to just the file types that you specify.

To separate some of this work from the core StreetFoo work, I’m proposing creating a new project. (The final StreetFoo app only uses files in a very limited way, and I don’t want to confuse that implementation.) If you want to follow along, create a new solution containing a Visual C#—Windows Store—Blank App project. Mine is called FileScratch. In the code downloads for this chapter, you’ll find a separate solution with that project in it.

For a scratch project, there’s no point building out an entire MVVM subsystem, so we’ll just bind event handlers to buttons in an old-school, VB-like manner. To get working, add a button to MainPage with content set to “Open File Picker for JPEG.” Double-click it and add this code (note the need to change the handler to be async).

private async void Button_Click_1(object sender, RoutedEventArgs e)

{

// create...

var dialog = new FileOpenPicker();

// set the types...

dialog.FileTypeFilter.Add(".jpeg");

dialog.FileTypeFilter.Add(".jpg");

// show...

StorageFile file = await dialog.PickSingleFileAsync();

// show...

if (file != null)

await this.ShowAlertAsync(file.Path);

else

await this.ShowAlertAsync("No file chosen.");

}

Click the button and the picker will appear (Figure 6-1 illustrates). You may have already seen these as part of normal use of Windows 8. The key thing to remember is that this UI is all about touch access, not about mouse access.

The FileOpenPicker filtered for .jpg and .jpeg files

Figure 6-1. The FileOpenPicker filtered for .jpg and .jpeg files

This is a good example of where the Windows Store app APIs that relate to the new UX features tend to be very easy to use.

The only problem comes from adjustment of the types of files that you’re trying to use. In the dialog that we’ve just seen, there is no way to change the file types, but as Figure 6-2 shows, there is a file of type .txt in the folder that we were looking at. The .txt file was not displayed, as it wasn’t in the file type filter.

Non-.jpg/.jpeg file shown in my Pictures library

Figure 6-2. Non-.jpg/.jpeg file shown in my Pictures library

This limitation is because of the sandboxing. Windows is trying to control the user experience in a more “full trust” manner and is boxing the user in to produce an experience aligned with Microsoft’s original vision.

NOTE

We’ll see this a lot as we go through this book—whenever you see restrictions imposed by the API design, try not to fight them. You will feel hemmed in and constrained by the limits imposed by the operating system and APIs, but this is just the new world that we’re dealing with.

Back to the code that we just looked at: the PickSingleFileAsync returns a System.Storage.StorageFile instance. This is the base class in the WinRT filesystem API for representing a file. It’s essentially a pointer to a file on the filesystem. We’ll see more about this, along withStorageFolder, later.

NOTE

Predictably, as well as FileOpenPicker, you’ll also find a companion FileSavePicker.

That’s all we need to see in terms of that basic functionality. Next we’ll have a look at file associations.

File Associations

The next feature that I want to take you through is the file associations. Although I suspect this feature will be rarely used, the way it works is interesting and it’s a good demonstration of how complex Windows features are implemented very easily in Windows Store apps.

File associations in Windows allow the user to double-click a file in Windows Explorer and have an application execute and handle it. I suggest that in Windows Store apps this feature is unlikely to get heavy use because in a post-PC world, we tend to think more about apps than files and so it’s unlikely you’re ever going to be firing up Windows Store apps through document associations. (That said, how Windows 8/Windows RT handles files and sharing adds an interesting dimension to document-centricity when compared to the iPad, which has almost zero focus on documents as per its original design objectives.)

File associations are configured in the Declarations tab in the manifest editor in Visual Studio 2012. You can access the Declarations tab by double-clicking the Package.appxmanifest file in Solution Explorer. This opens the manifest editor, and you can select the Declarations tab from there.

All declarations—not just ones related to files—allow you to say what special access your app requires to system features. I’m not going to enumerate them all here, but we’ll see most of these as we work through the book. The one we’re going to use here is the File Type Associations declaration. Add one of these and you’ll see something like Figure 6-3.

Starting to add file type association

Figure 6-3. Starting to add file type association

In Windows, file type associations are stored in the Registry. Whenever your app is deployed, WinRT creates a whole bunch of Registry entries to register your app with the system. If you have file associations listed in your manifest, you’ll get appropriate Registry keys configured that allow for your app to be launched.

Let’s look at how the app is launched now.

Launching the App

Interestingly, you can’t launch the app from the Start screen via the file associations route. The only way to launch an app from the Start screen is to touch its tile. As I’ve hinted at, files are a legacy feature that belongs to the old WIMP metaphor in Windows. Apps are launched from File Explorer in the legacy desktop only. That’s not to say that file associations have no place in Windows Store apps. Consider the situation of “yesteryear,” where lots of apps vie to be the default music player. It’s certainly possible that some of those will be Windows Store apps. The same is true of PDF readers—these would actually benefit from running in a sandboxed environment.

You need to deploy the app to test it, and it’s worth getting used to invoking Build – Deploy to do this. Whenever you have been running the project thus far, the deploy step is done implicitly. In this scenario, we want to explicitly do it.

If you create a file with a type of .foobar anywhere on your device, this will show up in File Explorer as associated with our app. (Use Notepad to do this—we don’t care about the format of these files, only that they exist.) However, it won’t have a usable icon yet, as the default icon created with a Windows Store app is a white icon on a transparent background. Figure 6-4 tries to show this, albeit it’s hard to see in print.

A .foobar file ready for launch

Figure 6-4. A .foobar file ready for launch

Setting the icon is very easy, and you may have already guessed how to do it. In the File Type Association editor (see Figure 6-3), you can assign a logo. Interestingly, this isn’t a multiformat .ico file. I added an icon by creating a 64×64 PNG file. Whatever file you create, put it in the ~/Assetsfolder within the project and then configure it in the File Type Association editor. Run/deploy the project and your icon will appear. Figure 6-5 shows my new icon.

The .foobar file with an associated icon

Figure 6-5. The .foobar file with an associated icon

But what happens when the user opens the file? Let’s find out.

Handling the Launch

If you double-click the file in File Explorer, Windows will either activate your app if it’s in the background, or start your app if it’s not. (An interesting wrinkle about Windows Store apps is that you cannot have two versions of the same app running. We’ll talk more about app lifetime inChapter 14.)

What’s missing is the part that actually responds to the file being opened. In this example we’ll just render the name of the file on the screen. In fact, we can be given multiple files, so we’ll need to handle this eventuality.

We can receive notifications of activation in this way via the OnFileActivated method on the Visual Studio-provided App class. The only wrinkle is that the notification will come into the app directly as opposed to the active view. We’ll need to make sure we can react and display an appropriate view.

In this example we’ll be clunky about this. If the app isn’t showing the view that we want, we’ll navigate to that view and then handle the file dropping. We won’t nuance this by checking that the view is in a state where we’re safe to navigate away without losing data—something that you’d want to do in a production app if you were in the middle of an edit.

To start with, create a new file in your project called FileActivationPage. Use the Basic Page template for this so that you get a caption and a back button; this will help you when experimenting with the behavior. It’s this page that we’ll go to in order to display our files. I should say that a separate page is not required if you want to handle file associations. I’m using a separate page here, as it’s expeditious to our discussion.

In FileActivationPage add this code. The purpose of this code is simply to render the names of any provided list of files into a message.

// Add to FileActivationPage...

internal async Task FilesActivatedAsync(IEnumerable<IStorageItem> files)

{

// build a list of the files...

var builder = new StringBuilder();

foreach (var file in files)

{

if (builder.Length > 0)

builder.Append("\r\n");

builder.Append(file.Path);

}

// show...

await this.ShowAlertAsync(builder.ToString());

}

In the App class, you can now add this code.

// Add method to App...

protected async override void OnFileActivated(FileActivatedEventArgs

args)

{

base.OnFileActivated(args);

// find the page and tell it...

var frame = (Frame)Window.Current.Content;

if (!(frame.Content is FileActivationPage))

frame.Navigate(typeof(FileActivationPage));

// if we did it...

if (frame.Content is FileActivationPage)

await ((FileActivationPage)frame.Content).FilesActivatedAsync

(args.Files);

}

The purpose of this code is to attempt to navigate to our new page. The approach is to find the active window (of which there’s only ever one), find the frame on that window (which convention says has to be held in the Windows instance’s Content property), and then get the content of the frame. This last item will be our page. If that page isn’t the page we want, we’ll attempt to navigate.

Navigation may fail (for example, the page may be busy and refuse navigation away—see my point about having a more nuanced implementation in production); hence, we check to see if it worked before we call through to FilesActivatedAync.

You can try this for yourself now by going into File Manager and double-clicking one or more .foobar files. Figure 6-6 shows possible output if you select multiple files. Make sure that you confirm this works both when the app is running and when it is not. You may also want to check the behavior depending on whether FileActivationPage is the current page or not.

Multiple file activations

Figure 6-6. Multiple file activations

Now we know how to let the user give us a file; let’s see what happens if we try to find files programmatically.

Sandboxed File Access

In both of the examples that we’ve looked at, we’ve been given files to work with through direct user interaction. Whenever this user-driven action happens, WinRT assumes that we have explicit permission to access the files and that they are OK to work with. (You should note, though, that NTFS’s security trumps all here—if Windows doesn’t think you have access to the file, you can’t have it regardless of how it was sourced.)

In this section we’ll look at how we can work with lists of apps programmatically (in other words, how we can automate the operation of the user actually picking a file). For example, we might want to walk all TIFF documents in a certain folder within the user’s Documents folder and OCR (optical character recognition) them.

You can only do programmatic work with folders that are within the sandbox. There are two sets of sandboxed folders: those given to your app for their private use, and those that are shared with other apps on the system.

In terms of your private folders, you get the following, which are accessed from special properties within the API. You can then use the storage APIs on these, which we’ll get to in a moment. You can work with your private folders without specifying special manifest permissions. You can also work with any file type in these folders.

Windows.Storage.ApplicationData.Current.LocalFolder

This is the private data store tied to the current machine (e.g., c:\Users\<User>\AppData\Local\Packages\<id>\LocalState).

Windows.Storage.ApplicationData.Current.RoamingFolder

This is the private data store that is cloud synchronized (e.g., c:\Users\<User>\AppData\Local\Packages\<id>\RoamingState). (We’ll talk about this more later, but for now you should note that this location is not in the Windows user profile roaming folder. This is not the roaming profile functionality that’s been managed by Active Directory for many years. It just happens to have the same name.)

Windows.Storage.ApplicationData.Current.TemporaryFolder

This is where you store temporary files (e.g., c:\Users\<User>\AppData\Local\Packages\<id>\TempState).

Windows.ApplicationModel.Package.Current.InstalledLocation

This is where your app is installed, which if you’re developing will be the AppX folder underneath your Debug or Release folder. You cannot, unsurprisingly, write to this folder.

The following special folders exist in Windows Store apps, each of which is accessed through a special property. However, in order to use them, you need to switch on a specific permission in the manifest. You also need to nominate the file types that you wish to work with. Here’s the list of folders, and the related manifest permission.

Windows.Storage.KnownFolders.DocumentsLibrary

Requires Documents Library Access

Windows.Storage.KnownFolders.MusicLibrary

Requires Music Library

Windows.Storage.KnownFolders.PicturesLibrary

Requires Pictures Library Access

Windows.Storage.KnownFolders.VideosLibrary

Requires Videos Library Access

Windows.Storage.KnownFolders.RemovableDevices

Requires Removable Storage

Windows.Storage.KnownFolders.HomeGroup

Requires at least one of Music Library, Pictures Library Access, or Videos Library Access

Windows.Storage.KnownFolders.MediaServerDevices

Requires at least one of Music Library, Pictures Library Access, or Videos Library Access

NOTE

The inconsistent naming here isn’t down to typographical errors—I’ve replicated how it currently is in Visual Studio 2012, which is inconsistent.

There’s an additional folder called Windows.Storage.DownloadsFolder that allows write-only access to the system downloads folder. We’ll talk about that in a moment.

What we’ll do to demonstrate some of the file APIs is build some code that will copy files from your Pictures library into the private, local folder for your app.

Walking and Copying Pictures

To get a feel for how the file API works, we’ll have a look at actually running through the API. We’ll create a simple function that will copy any pictures with the word graffiti in their name over to our private local data folder. We’ll initially do this without setting appropriate manifest permissions so that you can see it fail the permissions check.

Whenever we access the filesystem in WinRT, we use its native filesystem API, as opposed to the .NET one in System.IO. This is a real shame, as the one in System.IO was fantastically put together, and the WinRT one is arguably not as good. It’s also oddly incomplete, with no easy way to check for file existence and so on. (The official line is that checking for existence and then performing an action invites a race condition scenario, so you’re supposed to check for exceptions. Personally, that argument seems flimsy, as since .NET v1 we’ve been told to design specifically against using exceptions to report ordinary [“nonexceptional”] failures.)

Coming back to my first point at the top of this chapter, I don’t want to belabor the file API usage, as it’s all basic stuff. To that end, you can add a button to your page and wire up this code, which will walk the files in your Pictures folder and copy them over to ~/LocalState. It should be obvious to see what it’s doing.

private async void HandleCopyGrafittiPicturesToLocal(object sender,

RoutedEventArgs e)

{

await CopyGraffitiPicturesAsync(ApplicationData.Current.LocalFolder);

}

private async Task CopyGraffitiPicturesAsync(StorageFolder targetFolder)

{

try

{

// copy...

var files = (await KnownFolders.PicturesLibrary.GetFilesAsync())

.Where(v => v.Name.ToLower().Contains("graffiti")

&& (v.FileType.ToLower() == ".jpg" || v.FileType.ToLower()

== ".jpeg"));

var builder = new StringBuilder();

foreach (var file in files)

{

// get...

var newFile = await file.CopyAsync(targetFolder);

// add...

builder.Append("\r\n");

builder.Append(newFile.Path);

}

// show...

if (builder.Length > 0)

await this.ShowAlertAsync("Copied:\r\n" +

builder.ToString());

else

await this.ShowAlertAsync("No files were found to copy.");

}

catch (Exception ex)

{

this.ShowAlertAsync(ex.ToString());

}

}

If you run this code, you’ll see an error similar to that shown in Figure 6-7. The problem occurs because we don’t have rights to access the PicturesLibrary property in KnownFolders. PicturesLibrary happens to be of type Windows.Storage.StorageFolder, and you should note that if some mechanism actually gave us a StorageFolder that mapped to the same underlying folder, this operation would not have failed. User selection via the FolderPicker, FileOpenPicker, or FileSavePicker always trumps the manifest setting.

Failure to access the PicturesLibrary property

Figure 6-7. Failure to access the PicturesLibrary property

You can change the manifest setting by double-clicking the Package.appxmanifest file in Solution Explorer and changing to the Capabilities tab. Once there, select Pictures Library Access. This is shown in Figure 6-8.

Enabling Pictures Library Access

Figure 6-8. Enabling Pictures Library Access

Rerun the code and you’ll get a successful result, as shown in Figure 6-9.

A successful file copy operation

Figure 6-9. A successful file copy operation

This code works on the assumption that appropriately named files exist in the root of your standard Windows pictures library. You obviously may have to create some in order to see this operation work.

That’s all I want to show you on the basic API. In the next section, I’ll give a quick mention of the special DownloadsFolder property, and then we’ll go on to talk about roaming files.

Roaming Files

Now we’re going to take a look at one of the neatest features in Windows Store app development: roaming data. In roaming data, certain data that’s stored on one local machine is automatically propagated to any other device that the user operates.

Unless joined to the domain, Windows 8 and Windows RT devices have the option of being bound to a Microsoft account. (This process makes the device “Microsoft Account Connected.”) When in this mode, apps are able to write data into a special roaming folder. Other devices on the same Microsoft account will receive synchronized copies of this data. In order for this to work, the device has to be “trusted.” You may have noticed that when you installed Windows 8, you received an email asking for you to trust the machine. Those emails are part of this process.

NOTE

These same rules apply to Roaming Settings. I’ll talk about that a little more toward the end of this section.

ROAMING DATA VERSUS ROAMING PROFILES

There’s likely to be some confusion with regard to the Roaming Profiles feature that’s been in Windows since the very early days of Active Directory. This is not that feature. This is an entirely different feature that does something similar but has—basically—the same name (not a fantastic move by Microsoft there). Floating might have been better, but that’s by the by. In short, just ignore anything related to Active Directory in this discussion.

There are some limitations. You have only a very small quota of data that can be used—you can and should ask WinRT to tell you how much your quota is (because it’s not a fixed limit), but at the time of writing apps have 100KB of quota space. (When I say the space is not fixed, I mean that you should not rely on the amount, as Microsoft has the capability to change it either globally or in different scenarios going forward.) This 100KB limit is so small that you will need to carefully consider what file protocol you use. XML is particularly inefficient when it comes to space, for example. Alternatively, you may wish to compress the data.

The need for the device to be connected to a Microsoft account implies that this won’t work for domain-connected devices; however, the documentation also states that you can use Group Policy to turn off this feature at a device level, which hints that it will work for domain-connected devices. But the fact that you can turn this off regardless tells us that this is not a mechanism that should be relied upon.

This sort of roaming feature is specifically designed to move around small amounts of data that is “nice to have”—for example, state information or preference information. It’s impractical to make it work as a synchronization mechanism for sometimes-offline devices. (This last point is further militated against by the fact that you can’t control when the synchronization happens.) We’ll talk more about apps that synchronize changes with a server in Chapter 14.

All that said, I do want to take you through this feature, as it is pretty clever and can be helpful in certain cases.

Multiple Devices

To test that your roaming data implementation works, you will need multiple devices. Using the simulator and your development machine won’t work, as these are technically the same machine. (The simulator is just a remote desktop view into the machine it’s running on.) If you don’t have two devices, you’ll have to follow along mentally rather than practically.

First we’ll look at using the Remote Debugging tools. These allow Visual Studio to deploy, run, and receive debugger telemetry from remote devices. Remote Debugging has been a feature of Visual Studio since the very first editions (i.e., it’s not a special Windows Store app thing).

Setting Up the Remote Debugging Client

These are the same steps that you’ll need to follow if you want to debug your apps on Windows RT. Visual Studio won’t run on Windows RT, so this will be the only way to access debugging capability if you’re troubleshooting software running on Windows RT.

To reiterate the requirements for the roaming data part: both devices must be on the same Microsoft account, and both must be trusted.

The easiest way to get the tools is to visit the Microsoft Downloads site and search for Remote Tools. This should yield installation packages for ARM, x86, and x64 machines. Install the one that matches the OS of your target.

Once you’ve installed the package, run the debugging client from the Start screen. When prompted, accept the firewall changes that it needs to make.

You can now connect to the machine from Visual Studio. On the toolbar, drop down the target selector and choose Remote Machine. Figure 6-10 illustrates the option, and Figure 6-11 illustrates selection of the remote device.

Selecting the Remote Machine debugging option

Figure 6-10. Selecting the Remote Machine debugging option

Choosing the device to use as the debugging target

Figure 6-11. Choosing the device to use as the debugging target

With the remote device selected, run the solution from within Visual Studio to confirm that it operates as you expect. Now that we know that we can join the two halves of the problem together, we’ll look at building our synchronization code.

NOTE

If you need to change the target device, open the Properties window on the project that you’re looking to debug, and select the Debug pane.

Syncing Files

In an earlier section I called out example paths of folders used by apps to store their private data. I gave the path referred to by ApplicationData.Current.RoamingFolder as being of the following format:

c:\Users\<User>\AppData\Local\Packages\<id>\RoamingState

All we have to do to make this work is write data into that ~/RoamingState folder. Windows will then do the rest. In this example, we’ll drop a small text file into the folder on one device and then wait to see if it comes through on the other. It doesn’t matter whether you create the file on your Visual Studio machine or the remote device, although if you want to try out the remote debugging firsthand, using the remote device to create the file would seem sensible.

Here’s the code to create a file in the roaming folder—the intention here is to wire up a button that calls this code. Again, this is a simple example, so I’m just going to present it on the assumption that it’s obvious.

private async void HandleCreateFileInRoamingFolder(object sender,

RoutedEventArgs e)

{

// create...

var file = await ApplicationData.Current.RoamingFolder.

CreateFileAsync(Guid.NewGuid().ToString()

+ ".txt");

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

{

using(var writer = new StreamWriter(stream))

writer.WriteLine(string.Format("Hello, world. ({0})",

DateTime.Now));

}

// ok...

await this.ShowAlertAsync(string.Format("Created file '{0}'.

Quota: {1}KB", file.Path,

ApplicationData.Current.RoamingStorageQuota));

}

There’s one thing that is important to catch here. In .NET you don’t need to wrap the StreamWriter reference in a using statement. In WinRT, if you don’t wrap it in a using statement, the data won’t be flushed out of the writer before the stream is closed, and your data won’t be written. Of course, we should always wrap everything that implements IDisposable, but this caught me out so it may well catch you out too.

You’ll also notice that in the message that’s rendered when the file is written, I’m including the RoamingStorageQuota value. I’ve included this so that you know where it is. The upshot of going over quota is that no data will get synced for your app. I’d suggest, then, that if you have to keep track of and manage your storage within this quota, you’re using this feature inappropriately.

If you run this code on the remote device, a file will be created that will eventually be synced up to the cloud and then down onto other devices where the app is installed. If you want to force the sync to run, lock the device, as this triggers all pending sync operations to flush. (It may still take a few minutes, however.)

Roaming Settings

In Chapter 3 where we looked at SQLite, we used a local database to store system settings. WinRT has a built-in way of sharing a bucket of settings via the ApplicationData.Current.RemoteSettings member.

I chose not to use this in Chapter 3 because of the limitations outlined in this chapter (i.e., a required Microsoft account and the fact that domain administrators can just turn off this feature wholesale).

I said, perhaps enigmatically, at the top of this section that you might want to roll your own roaming implementation rather than using the one provided in the Windows Store app APIs. I’m not going to go through how you might do this in this book, but it’s relatively easy. All you have to do is keep settings in SQLite as we first saw in Chapter 3. When they change, these settings just have to float up to your own cloud. When your apps activate, one thing they can then do is download the settings from the cloud. It’s not rocket science, but it does involve more lifting than using the built-in features.

We’ll talk more about background cloud synchronization in Chapter 14.

Using Files with StreetFoo

Now that we’ve looked at the basics of the filesystem APIs, we can turn our attention back to the StreetFoo app. Philosophically, I’m a great believer in making examples in books as “real world” as possible. The examples that we have looked at thus far have explored some of the edges of the API, but as software engineers we know that it’s only by fighting weird edge cases that we actually learn how things work. It was easier to show you those examples in isolation than to confuse the issue by adding spurious features to StreetFoo.

The objective of this section is to download photos from the StreetFoo server and display them on the Reports grid. These photos are obviously reasonably large, so fetching them on demand makes sense. Storing them on the filesystem as opposed to in the database also makes sense, as SQLite isn’t particularly adept at storing large pieces of BLOB data.

We’ll also make some improvements to our view-model story. At the moment, what we show on the view is a straight-through projection of what’s in the database. What we’ll do in this section is create a special ReportViewItem class that will hold both the core data in ReportItem, along with image data.

In summary, then:

1. We’ll create a new service proxy called GetReportImageServiceProxy. This will take the native ID of the report and return a JPEG image as a base-64-encoded string.

2. We’ll create a new ReportViewItem class and refactor our ReportsPageViewModel class to use instances of these as opposed to the ReportItem database entity.

3. We’ll create a new ReportImageCacheManager class that will, as the name implies, manage getting the report’s images cached onto the local disk.

Let’s press on.

Getting Report Images

The simplest thing to do first is create our service proxy that will get an image from the server. The server has a set of sample images, and will return these when presented with a report’s native ID.

As we’ve built the service proxies many times, I’ll just present the basic code. First, here’s the result object that will contain either an array of bytes containing JPEG data, or error information:

public class GetReportImageResult : ErrorBucket

{

public byte[] ImageBytes { get; private set; }

internal GetReportImageResult(byte[] bs)

{

this.ImageBytes = bs;

}

internal GetReportImageResult(ErrorBucket errors)

: base(errors)

{

}

}

Next, our service proxy interface:

public interface IGetReportImageServiceProxy : IServiceProxy

{

Task<GetReportImageResult> GetReportImageAsync(string nativeId);

}

The last class we have to create is the GetReportImageServiceProxy itself. The image will be returned as a base-64 string. We’ll convert that and store it as bytes in the result. Here’s the code:

public class GetReportImageServiceProxy : ServiceProxy,

IGetReportImageServiceProxy

{

public GetReportImageServiceProxy()

: base("GetReportImage")

{

}

public async Task<GetReportImageResult> GetReportImageAsync

(string nativeId)

{

var input = new JsonObject();

input.Add("nativeId", nativeId);

var executeResult = await this.Execute(input);

// did it work?

if (!(executeResult.HasErrors))

{

// get the reports...

var asString = executeResult.Output.GetNamedString("image");

// bytes...

var bs = Convert.FromBase64String(asString);

return new GetReportImageResult(bs);

}

else

return new GetReportImageResult(executeResult);

}

}

Remember that in order to use the proxy we need to enlist it in our IoC container, which has to be done manually. Add the mapping for IGetReportImageServiceProxy to GetReportImageServiceProxy to the Start method.

Migrating to ReportViewItem

With that out of the way, we can start looking at the more interesting aspects of this problem.

As we know, the data binding subsystem in XAML works on this idea of listening to changes. In Chapter 4, we first looked at dependency properties—these are the foundation of XAML’s data binding. We have seen already the ObservableCollection (used to hold our list of reports for presentation on the grid), and back in Chapter 1 when we first started looking at data binding we built our ModelItem class, which issued change notifications through INotifyPropertyChanged. When we built ReportItem back in Chapter 3, we also used ModelItem to get that same change notification.

As things stand at the moment, the grid view used on the Reports page just projects ReportItem. In terms of data, against each ReportItem we have to be able to dereference whether an image exists in the local cache or not. We can either update ReportItem and the attendant database schema to store a flag that records image presence, or we can look on disk. Avoiding changing the schema and relying on file existence as the definitive indicator seems more straightforward, at first glance. It also avoids polluting the schema with items that only relate to the client implementation.

This is where some of the advantages of MVVM come in. We can use view-model items that are a combination of data stored in SQLite and on disk. The nonimage data will be held in SQLite, and the flag to indicate whether we have an image and the URL will be determined by querying the disk. To that extent, we’ll build a ReportViewItem.

Then comes the question as to whether we extend ReportItem, or whether we create a new class and encapsulate a ReportItem instance within it. Because we don’t have any control over the creation aspects of the object (they are given to us by sqlite-net as part of the ORM), I’m suggesting encapsulation.

I also think it’s not much of a stretch to risk a moment of YAGNI and build a new base class that supports encapsulation in this way. WrappingModelItem<T> will be a generic type of type ModelItem. One thing this will allow us to do is subscribe to notifications on the encapsulated instance and propagate them up as if they were our own. (We actually won’t use that feature directly in this code, but it’s a worthy illustration, as it shows how we can extend onto and enhance the preexisting notifications without having to do any specific work.)

Here’s the code for WrappingModelItem<T>—although you’ll be able to understand it more fully when we build ReportViewItem immediately after.

public abstract class WrappingModelItem<T> : ModelItem

where T : ModelItem

{

public T InnerItem { get; private set; }

protected WrappingModelItem(T innerItem)

{

this.InnerItem = innerItem;

// subscribe...

this.InnerItem.PropertyChanged += InnerItem_PropertyChanged;

}

void InnerItem_PropertyChanged(object sender,

PropertyChangedEventArgs e)

{

// re-raise this as our own...

this.OnPropertyChanged(e);

}

}

Now we can turn our attention to implementing ReportViewItem. What we want to do here is expose our read-only versions of the relevant ReportItem data, and have a special property called ImageUrl that we’ll set when an image is available. Only exposing our read-only versions of the properties is there for neatness—if we don’t need setters for those properties, why expose them? Finer control of the surface area is another advantage of the MVVM model.

Here’s the code for ReportViewItem:

public class ReportViewItem : WrappingModelItem<ReportItem>

{

internal ReportViewItem(ReportItem item)

: base(item)

{

}

public string NativeId { get { return this.InnerItem.NativeId; } }

public string Title { get { return this.InnerItem.Title; } }

public string Description { get { return this.InnerItem.Description; } }

public string ImageUrl { get { return GetValue<string>(); } set

{ SetValue(value); } }

}

It doesn’t look like much yet, but it will shortly when we add some behavior. But before we do that, let’s refactor our ReportsPageViewModel code to work with ReportViewItem instances rather than ReportItem instances.

Refactoring to ReportViewItem

Refactoring involves changing both the IReportsPageViewModel interface and the mapped ReportsPageViewModel class. Here’s the change for the interface:

public interface IReportsPageViewModel : IViewModel

{

ICommand CreateTestReportsCommand { get; }

ICommand RefreshCommand { get; }

ICommand DumpSelectionCommand { get; }

ICommand SelectionCommand { get; }

ObservableCollection<ReportViewItem> Items

{

get;

}

bool HasSelectedItems

{

get;

}

}

There aren’t many touch points in ReportsPageViewModel that need changing. The property definitions and the constructor need changing, as does the operation of ReloadReportsFromCacheAsync. Here’s the code with the types changed in the constructor (I’ve omitted some code for brevity):

public class ReportsPageViewModel : ViewModel, IReportsPageViewModel

{

public ObservableCollection<ReportViewItem> Items { get; private set; }

private List<ReportViewItem> SelectedItems { get; set; }

public ICommand CreateTestReportsCommand { get; private set; }

public ICommand RefreshCommand { get; private set; }

public ICommand DumpSelectionCommand { get; private set; }

public ICommand SelectionCommand { get; private set; }

public ReportsPageViewModel(IViewModelHost host)

: base(host)

{

// setup...

this.Items = new ObservableCollection<ReportViewItem>();

this.SelectedItems = new List<ReportViewItem>();

// commands...

this.RefreshCommand = new DelegateCommand(async (e) =>

{

// omitted...

});

// update any selection that we were given...

this.SelectionCommand = new DelegateCommand((args) =>

{

// update the selection...

this.SelectedItems.Clear();

foreach (ReportViewItem item in (IEnumerable<object>)args)

this.SelectedItems.Add(item);

// raise...

this.OnPropertyChanged("SelectedItems");

this.OnPropertyChanged("HasSelectedItems");

});

// dump the state...

this.DumpSelectionCommand = new DelegateCommand(async (e) =>

{

// omitted...

});

}

// omitted...

}

The change to ReloadReportsFromCacheAsync is more interesting because it’s here that we key into the logic to update the cache. How this will work is that we’ll ask SQLite for our list of ReportItems as usual, but we’ll wrap them up in a ReportViewItem and then pass eachReportViewItem instance over to a manager class that will update the image in the background. We won’t build ReportImageCacheManager immediately—we’ll close off of the refactoring by editing the template XAML first. Here’s the change to ReloadReportsFromCacheAsyncthat will defer over to ReportImageCacheManager:

// Modify method in ReportsPageViewModel...

private async Task ReloadReportsFromCacheAsync()

{

// set up a load operation to populate the collection

// from the cache...

using (this.EnterBusy())

{

var reports = await ReportItem.GetAllFromCacheAsync();

// update the model...

this.Items.Clear();

foreach (ReportItem report in reports)

this.Items.Add(new ReportViewItem(report));

// go through and initialize...

var manager = new ReportImageCacheManager();

foreach (var item in this.Items)

await item.InitializeAsync(manager);

}

}

As mentioned, we need just a quick change to the template XAML and we’re done.

Modifying the grid item template

If you recall, in Chapter 3 we copied the Standard250x250ItemTemplate used on the grid to a new template called ReportItem250x250Template. This template has an Image control, but it’s set up to bind off of the Image property on the view-model. We’ve called the property that refers to an image ImageUrl. (This makes more sense to me, as a property called Image should return an object that represents loaded image data, not a reference to a location on disk.)

Thus we have to change the XAML for ReportItem250x250Template. Here’s the code:

<DataTemplate x:Key="ReportItem250x250Template">

<Grid HorizontalAlignment="Left" Width="250" Height="250">

<Border Background="{StaticResource

ListViewItemPlaceholderBackgroundThemeBrush}">

<Image Source="{Binding ImageUrl}" Stretch="UniformToFill"/>

</Border>

<StackPanel VerticalAlignment="Bottom" Background="{StaticResource

ListViewItemOverlayBackgroundThemeBrush}">

<TextBlock Text="{Binding Title}" Foreground="{StaticResource

ListViewItemOverlayForegroundThemeBrush}"

Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/>

<TextBlock Text="{Binding Description}"

Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}"

Style="{StaticResource CaptionTextStyle}" TextWrapping="NoWrap"

Margin="15,0,15,10"/>

</StackPanel>

</Grid>

</DataTemplate>

That’s all we have to do for now. You can satisfy yourself that the refactoring “took” by running the project, although you won’t see different behavior.

Implementing ReportImageCacheManager

Now we come to the interesting part.

We know that we can store anything we like in the ~/LocalState folder beneath our package folder. (As a reminder, this lives at c:\Users\<User>\AppData\Local\Packages\<PackageId>.) My proposal is that we create a separate folder called ReportImages under LocalState. Files will be named in the format <NativeId>.jpg. Don’t forget that we can’t use RoamingState because the quota allotment will be too small.

As well as seeing some real-world file behavior, this is also where we’ll see some real-world async/await and multithreading behavior. All of the filesystem interaction that we do will run on a background thread, as will any network access.

One other thing we’re going to see here is the ms-appdata URI protocol. This is a really clever part of WinRT and XAML. Like the ms-appx URI that we met in Chapter 4, ms-appdata allows us to reach into the LocalState, RoamingState, and TempState folders. By binding the Imagecontrol in our grid template to an ms-appdata protocol-based URI, we can bind directly to items on the filesystem.

To summarize our objectives for this section:

§ When a new item is readied for the view, we’ll pass it to the image cache manager.

§ The manager will look on disk to see if a matching file already exists. If it does, it will calculate the URL of the image as an ms-appdata URI and set the item’s ImageUrl property. This will rattle through the XAML data binding subsystem and the display will be updated.

§ If a matching file does not exist, we need to go away, download it, and save it to disk. Here, rather than using async/await, we’re going to create tasks directly within the TPL (task parallel library) by explicitly creating and scheduling background tasks. When those tasks are complete, they will update the ImageUrl property with an ms-appdata URI. Again, notifications will be raised and the image will be displayed.

§ If we do have to create tasks directly within the TPL, we’ll need to manage the synchronization context. We’ll dig into that in detail when the time comes.

Let’s get on with that and build the code.

Checking for file existence

When you go through this, it’s likely that you’ll reach the conclusion that working with the filesystem is quite fiddly in WinRT. That’s to be expected—it is fiddly to work with!

We’ll start with the method that returns to use a reference to the folder where we’re going to store the cached files. This is where we start to run into some less good bits of WinRT. As mentioned before, we can’t check to see whether a file or folder exists ahead of time. The mandated method is to capture any exceptions. As a wrinkle to this, if the exception isn’t actually exceptional and you don’t want to do anything with the exception data, it becomes a faff to hide the compiler warnings associated with ignoring the exceptions. To that end, I’ve created a SinkWarning method. This tricks the compiler into believing the reference to ex has been used and hides the warning. Apart from that, the method is straightforward. If the folder does not exist, it’s created. Here’s the code:

// Add members to ReportImageCacheManager...

private const string LocalCacheFolderName = "ReportImages";

private async Task<StorageFolder> GetCacheFolderAsync()

{

// find...

StorageFolder cacheFolder = null;

try

{

cacheFolder = await ApplicationData.Current.LocalFolder.

GetFolderAsync(LocalCacheFolderName);

}

catch (FileNotFoundException ex)

{

SinkWarning(ex);

}

// did we get one?

if(cacheFolder == null)

cacheFolder = await ApplicationData.Current.LocalFolder.

CreateFolderAsync(LocalCacheFolderName);

// return...

return cacheFolder;

}

private void SinkWarning(FileNotFoundException ex)

{

// no-op - we're just getting rid of compiler warnings...

}

In the modification to ReportsPageViewModel that we built previously, we called into a method called GetLocalImageUrlAsync. The purpose of this method is to return a preexisting cache URL. This method will use two helper methods that we’ll also use in other functions.GetCacheFilename provides the filename used when given a ReportViewItem, and CalculateLocalImageUrl will return an ms-appdata format URI, again given a ReportViewItem. Here’s the code:

// Add methods to ReportImageCacheManager...

private string GetCacheFilename(ReportViewItem viewItem)

{

return viewItem.NativeId + ".jpg";

}

private string CalculateLocalImageUrl(ReportViewItem viewItem)

{

return string.Format("ms-appdata:///local/{0}/{1}.jpg",

LocalCacheFolderName, viewItem.NativeId);

}

internal async Task<string>

GetLocalImageUrlAsync(ReportViewItem viewItem)

{

var cacheFolder = await this.GetCacheFolderAsync();

// build a path based on the native id...

var filename = GetCacheFilename(viewItem);

StorageFile cacheFile = null;

try

{

cacheFile = await cacheFolder.GetFileAsync(filename);

}

catch (FileNotFoundException ex)

{

SinkWarning(ex);

}

// did we get one?

if (cacheFile != null)

{

Debug.WriteLine(string.Format("Cache image for '{0}'

was found locally...", viewItem.NativeId));

return CalculateLocalImageUrl(viewItem);

}

else

{

Debug.WriteLine(string.Format(

"Cache image for '{0}' was not found locally...", viewItem.NativeId));

return null;

}

}

You’ll notice that I’ve put some Debug.WriteLine calls in there. This primarily is to help you see what’s happening with the flow.

Downloading and caching images

Now we can look at the method that actually does the downloading and storing to disk. I’ll do this in parts so that you can follow the flow.

So far we’ve always used the async/await keywords in a very “vanilla” way. What I want to show you now if how you can work with tasks more directly. To load the images, we’ll spin off separate tasks explicitly and use the standard asynchrony features to wait until they are completed and then update the UI.

In reality, you don’t necessarily need to do this. You can just use async/await in the way we have been doing. In the experiments I did when writing this chapter, though, I got a slightly better effect doing it this way.

To run tasks explicitly, you can use the Run static method on the Task class. You can then use the await keyword as you usually would. Once the task has completed, we can set the ImageUri property of the ViewItem, and data binding will take over and display the image.

First we set up the task, passing in an anonymous method to run:

// Add method to ReportImageCacheManager...

internal async void EnqueueImageDownload(ReportViewItem viewItem)

{

Debug.WriteLine(string.Format("Enqueuing download for '{0}'...",

viewItem.NativeId));

// create a new task...

var theUrl = Task.Run<string>(async () =>

{

The first thing the task will do is use GetReportImageServiceProxy to download the image. This will be transferred over the wire as JSON, but returned to us as a byte array containing a JPEG file. We’ll call AssertNoErrors to ensure that we have valid data.

Debug.WriteLine(string.Format("Requesting image for '{0}'...",

viewItem.NativeId));

// load...

var proxy = ServiceProxyFactory.Current.GetHandler

<IGetReportImageServiceProxy>();

var result = await proxy.GetReportImageAsync(viewItem.NativeId);

// check...

result.AssertNoErrors();

If the call is successful, we can write the image to disk.

// create the new file...

var filename = GetCacheFilename(viewItem);

var cacheFolder = await this.GetCacheFolderAsync();

var cacheFile = await cacheFolder.CreateFileAsync(filename,

CreationCollisionOption.ReplaceExisting);

using (var stream = await cacheFile.OpenStreamForWriteAsync())

stream.Write(result.ImageBytes, 0, result.ImageBytes.Length);

Note the use of CreationCollisionOption.ReplaceExisting. This is so that this method works if we’re refreshing a cached version from the server, in which case a file would already exist.

Finally, we can work out what the URL will be given the name of the file and the standard name of the folder. This will be the result of the task. In a moment we’ll build a continuation handler that takes this value and gives it to the ReportViewItem instance.

// get the URL...

string url = this.CalculateLocalImageUrl(viewItem);

Debug.WriteLine(string.Format("Image load for '{0}' finished.",

viewItem.NativeId));

return url;

That completes the work that the anonymous method has to do. By the time we finish the awaited call, we’ll have a URI that we can pass through to the ViewItem.

});

// set it...

viewItem.ImageUri = theUrl;

}

That’s it! If you’ve managed to get everything lined up, when you run this the images will download and appear. Figure 6-12 illustrates.

If the images don’t appear, it’s likely that your exceptions are being masked by the TPL. (Exceptions in background operations don’t crash the main app, so you have to explicitly look for them.) If you look in the Output window in Visual Studio, you may see exceptions being reported. You will get a lot of FileNotFoundExceptions during proper operations caused by the fact that we can’t check for files. If you do see exceptions, go into Debug – Exceptions and check the Thrown option against Common Language Runtime Exceptions. That should help.

The Reports page showing downloaded/cached images

Figure 6-12. The Reports page showing downloaded/cached images