I was a bit hasty in my earlier post when I called the SaveFileDialog inadequate.
One complaint I was going to make was simply a matter of my ignorance. It had to do with the dialog thinking that it was not initiated by the user.
In particular, it was some of our Caliburn magic obscuring this. However, it’s powerful white magic that does more good than harm. :-)
The problem is this, the SaveFileDialog can only be shown as a result of a user’s action, such as clicking a button. (I’m sure this is driven by security concerns that aren’t entirely clear to me.)
If your code follows this sequence:
- user clicks a button
- the event handler initiates a web request
- the callback for the web request tries to show a SaveFileDialog
- the world ends (not with a bang)
Then you will get the “Dialogs must be user-initiated” exception, because Silverlight doesn’t trace the web request callback back to the button click.
Deep Magic
In regards to the Caliburn magic that obscures this, I had a method that looked something like this:
public IEnumerable<IResult> DownloadFile()
{
var result = new Result();
var query = new GetBinaryContent(_data.ContentId);
yield return new ProcessQuery(query, result);
if (result.HasErrors)
{
yield return Show.Errors(result);
yield break;
}
var saveFileResult = new SaveFile();
yield return saveFileResult;
if (saveFileResult.FileStream == null) yield break;
var content = result.FindResponse(query).Data;
using (var writer = new BinaryWriter(saveFileResult.FileStream))
{
writer.Write(content);
writer.Flush();
}
}
This method lives on the ViewModel, and _data is part of that model. It contains the metadata for a file (name, type, last updated) as well as an id for fetching the actual binary content.
First, it processing a query, which is actually an asynchronous web request. If there are errors then it displays them, otherwise it opens a save file dialog. If the users provides a file, then the binary content is written out. The problem here, that is not obvious, is that the save dialog is being opened as a result of the web request and not directly from the user’s click. If I reorder the code in the method, so that the save dialog is shown before processing the query then the exception goes away. In other words, the code below does not throw an exception.
public IEnumerable<IResult> DownloadFile()
{
//open the dialog _before_ the web request
var saveFileResult = new SaveFile();
yield return saveFileResult;
if (saveFileResult.FileStream == null) yield break;
var result = new Result();
var query = new GetBinaryContent(_data.ContentId);
yield return new ProcessQuery(query, result);
if (result.HasErrors)
{
yield return Show.Errors(result);
yield break;
}
var content = result.FindResponse(query).Data;
using (var writer = new BinaryWriter(saveFileResult.FileStream))
{
writer.Write(content);
writer.Flush();
}
}
N.B. All of these implementations of IResult are specific to our project and are not a core part of Caliburn. However, Rob is creating similar (and improved) versions of a few in the samples.
Posted
10-06-2009 5:20 PM
by
Christopher Bennage