I was writing a little bit of code today on Windows 8 against WinRT and I couldn’t quite come up with a ‘solution’ that I was happy with so I thought I’d share and then maybe someone can pitch in and help me out a little.
The essence of the code was ‘copy a local file to a file selected by the user’. Simple enough. Of course, this is all async with Metro style apps and the WinRT APIs. I was actually doing the work in .NET but let’s say that I was doing it in JavaScript and so it might look something like this;
var wjsStorage = Windows.Storage; var wjsPickers = wjsStorage.Pickers; var picker = new wjsPickers.FileSavePicker(); var pickerPromise = null; var localFilePromise = wjsStorage.ApplicationData.current.localFolder.getFileAsync("img.jpg"); localFilePromise = localFilePromise.then( function (file) { return (file.openReadAsync()); } ); picker.fileTypeChoices.insert("Picture", [".jpg"]); pickerPromise = picker.pickSaveFileAsync().then( function (file) { return(file.openAsync(wjsStorage.FileAccessMode.readWrite)); } ); WinJS.Promise.join( [localFilePromise, pickerPromise] ).done( function (promises) { wjsStorage.Streams.RandomAccessStream.copyAndCloseAsync( promises[0], promises[1]); } );
Now, I’m not claiming there’s not a shorter route to getting that done but it kicks off the async work and brings it all together in a way that I’m happy with.
However, I was working in .NET. Here’s where I started out;
async void Func() { var localFile = await ApplicationData.Current.LocalFolder.GetFileAsync("img.jpg"); var fileSavePicker = new FileSavePicker(); fileSavePicker.FileTypeChoices.Add("Picture", new string[] { ".jpg" }); var userFile = await fileSavePicker.PickSaveFileAsync(); using (Stream localFileStream = await localFile.OpenStreamForReadAsync(), userFileStream = await userFile.OpenStreamForWriteAsync()) { localFileStream.CopyTo(userFileStream); } }
Now, initially it looks a little neater but I realised even when I was writing it that it’s not doing the same thing. The processing is different here in that the JavaScript version has the potential to run some of these steps at the same time;
- Kick off the process of getting the local file.
- Kick off the process of getting a file from the user.
and similarly for the dependent work which is modelled with calls to then(). In contrast, the .NET code will do;
- Open the local img.jpg file.
- Then show the file dialog to the user.
- Then open the local file stream for read.
- Then open the user’s file stream for write.
Now, the first thing to say is that the code isn’t making the very best use of extension methods so I re-worked it to use an extension;
async void Func() { var fileSavePicker = new FileSavePicker(); fileSavePicker.FileTypeChoices.Add("Picture", new string[] { ".jpg" }); var userFile = await fileSavePicker.PickSaveFileAsync(); using (Stream localFileStream = await ApplicationData.Current.LocalFolder.OpenStreamForReadAsync("img.jpg"), userFileStream = await userFile.OpenStreamForWriteAsync()) { localFileStream.CopyTo(userFileStream); } }
but that doesn’t get me to where I want to be. To do that, I need to grab hold of some Tasks and then I can be more selective about when I await them. Getting hold of a Task for my local file stream is easy enough;
Task<Stream> localStreamTask = ApplicationData.Current.LocalFolder.OpenStreamForReadAsync("img.jpg");
but where I struggled a little was in producing a task which wrapped up the dependent actions of;
- Asking the user for a file.
- Opening that file for write to get a file stream.
but only in the sense of how to best combine this with await. I can do it fine if I forget await and go back to Task<T> on its own then I’m fine.
I’ll try and illustrate as best I can where I struggled. Here was my first attempt;
async void Func() { Task<Stream> localStreamTask = ApplicationData.Current.LocalFolder.OpenStreamForReadAsync("img.jpg"); var fileSavePicker = new FileSavePicker(); fileSavePicker.FileTypeChoices.Add("Picture", new string[] { ".jpg" }); Task<Task<Stream>> userStreamTask = fileSavePicker.PickSaveFileAsync().AsTask().ContinueWith( userFile => { return (userFile.Result.OpenStreamForWriteAsync()); } ); await userStreamTask; using (Stream localFileStream = await localStreamTask, userFileStream = await userStreamTask.Result) { localFileStream.CopyTo(userFileStream); } }
and you can perhaps tell where I’m not having much fun – it’s around the ContinueWith() aspect of things. In order to chain together an asynchronous call to PickSaveFileAsync and OpenStreamForWriteAsync it feels like I have to go from a Task<T> to a Task<Task<T>> and it doesn’t feel quite right to me.
Now…maybe I can use an asynchronous lambda and use await in order to await that OpenStreamForWriteAsync() call? As in;
async void Func() { Task<Stream> localStreamTask = ApplicationData.Current.LocalFolder.OpenStreamForReadAsync("img.jpg"); var fileSavePicker = new FileSavePicker(); fileSavePicker.FileTypeChoices.Add("Picture", new string[] { ".jpg" }); Task<Task<Stream>> userStreamTask = fileSavePicker.PickSaveFileAsync().AsTask().ContinueWith( async userFile => { return (await userFile.Result.OpenStreamForWriteAsync()); } ); await userStreamTask; using (Stream localFileStream = await localStreamTask, userFileStream = await userStreamTask.Result) { localFileStream.CopyTo(userFileStream); } }
Nope – it’s the same situation (not surprisingly).
Now, here’s what I settled on which I feel is quite ‘nice’ but I got my “ContinueWith” by a complete different means by moving the 2 dependent pieces of functionality into a separate method and then leaning heavily on await;
async Task<Stream> PickAndOpenFileAsync() { var fileSavePicker = new FileSavePicker(); fileSavePicker.FileTypeChoices.Add("Picture", new string[] { ".jpg" }); StorageFile file = await fileSavePicker.PickSaveFileAsync(); Stream stream = await file.OpenStreamForWriteAsync(); return (stream); } async void Func() { Task<Stream> localStreamTask = ApplicationData.Current.LocalFolder.OpenStreamForReadAsync("img.jpg"); Task<Stream> userStreamTask = PickAndOpenFileAsync(); await Task.WhenAll(localStreamTask, userStreamTask); using (Stream localFileStream = localStreamTask.Result, userFileStream = userStreamTask.Result) { localFileStream.CopyTo(userFileStream); } }
But I feel I’m missing something fairly obvious when it comes to ContinueWith?
What would you do with it? Please add suggestions in the comments section below and I’ll update the post with a better solution 🙂
Update 1: TaskExtensions.Unwrap
Based on the comments below, I’m adding an alternative which makes use of TaskExtensions.Unwrap – this doesn’t feel too bad to me;
async void Func() { Task<Stream> localStreamTask = ApplicationData.Current.LocalFolder.OpenStreamForReadAsync("img.jpg"); var fileSavePicker = new FileSavePicker(); fileSavePicker.FileTypeChoices.Add("Picture", new string[] { ".jpg" }); var userStreamTask = fileSavePicker.PickSaveFileAsync().AsTask().ContinueWith( storageFile => { return (storageFile.Result.OpenStreamForWriteAsync()); } ).Unwrap(); await Task.WhenAll(localStreamTask, userStreamTask); using (Stream localFileStream = localStreamTask.Result, userFileStream = userStreamTask.Result) { localFileStream.CopyTo(userFileStream); } }
Thanks to the commenter who also pointed out Jon Skeet’s blog post around this very topic – the funny thing being that I read that at the time but I wouldn’t have remembered to revisit it without prompting.