0
votes

I'm trying to use MVVM for a Universal Windows project but the interfaces for Storage File complains a lot about using async. The following code sometimes works:

    public object Convert(object value, Type targetType, object parameter, string language)
    {
        var storageFile = value as StorageFile;
        return GetImageAsync(storageFile).Result;
    }

    private static async Task<ImageSource> GetImageAsync(StorageFile storageFile)
    {
        var bitmapImage = new BitmapImage();
        var stream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask().ConfigureAwait(false);
        bitmapImage.SetSource(stream);
        return bitmapImage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        return null;
    }
}

Until I select a new image to load, then I get the error "{"The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))"}"

So I tried changing it to use the CoreDispatcher per another thread:

public class FileToImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        var storageFile = value as StorageFile;
        Task<ImageSource> image = null;
        Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            image = GetImageAsync(storageFile);
            image.RunSynchronously();
        });

        return image.Result;
    }

    private static async Task<ImageSource> GetImageAsync(StorageFile storageFile)
    {
        var bitmapImage = new BitmapImage();
        var stream = await storageFile.OpenAsync(FileAccessMode.Read).AsTask().ConfigureAwait(false);
        bitmapImage.SetSource(stream);
        return bitmapImage;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        return null;
    }
}

NullReferenceException on bitmapimage. This makes absolute sense to me of course - the async dispatcher cedes control to the parent process, image has not been assigned, null reference exception. But I don't know what the right way is!

1
A value converter should not at all return a UIElement. Why do you think you need somethíng like this?Clemens
I wanted to get potentially large image files and display them in an app, providing pixel/photography information about them, so I didn't want to freeze the UI waiting for the file to load.C Bauer
Still, the converter should return an ImageSource (e.g. a BitmapImage), not an Image control.Clemens
Not sure if this approach can work at all. As far as I know, Bindings in UWP are always synchronous, hence a binding converter is always called synchronously, i.e. there is no await on some async method. There is however built-in asynchronous downloading (see the DownloadProgress and ImageOpened events in class BitmapImage), but this may be limited to remote files (i.e. http URIs).Clemens
For a binding converter, yes. I am thinking about how an IValueConverter for StorageFile to ImageSource could look like, but without success. StorageFile only provides an async API, which can't be used in an IValueConverter. StorageFile is probably not a good choice for the type of a view model property that represents an image file.Clemens

1 Answers

0
votes

Thank you Clemens for your comments on OP, which made me realize that I was pattern-obsessing and focused on using something I liked instead of doing the right design.

The source that was causing the issue is this:

    private async void GetFile()
    {
        var filePicker = new FileOpenPicker();
        filePicker.FileTypeFilter.Add(".jpg");
        filePicker.FileTypeFilter.Add(".png");
        filePicker.FileTypeFilter.Add(".gif");
        filePicker.FileTypeFilter.Add(".bmp");
        filePicker.ViewMode = PickerViewMode.Thumbnail;
        filePicker.SuggestedStartLocation = PickerLocationId.Desktop;
        filePicker.CommitButtonText = "Open";
        CurrentFile = await filePicker.PickSingleFileAsync(); //Bad code used CurrentFile set and NotifyPropertyChanged to start up the value converter code from OP

        //New, obvious better code            
        CurrentImage = await GetImageSource(CurrentFile);

        var statistics = new ImageStatistics();

        Logger.Log("Metadata start");
        var data = statistics.GetMetaData(CurrentFile);

        Logger.Log("Color Counts start");
        var colorCounts = statistics.GetColorCounts(CurrentFile);

        var filterer = new ColorFilterer();
        Logger.Log("Color Counts await start");
        var filteredColors = filterer.GetTopColors(await colorCounts, 10);
        Logger.Log("Color Counts await end");

        Logger.Log("Metadata await start");
        var metaData = await data;
        Logger.Log("Metadata await end");

        Make = metaData[SystemProperty.CameraManufacturer];
        Model = metaData[SystemProperty.CameraModel];
        ExposureTime = string.Format("1/{0}",1/Convert.ToDouble(metaData[SystemProperty.ExposureTime]));
        ISOSpeed = metaData[SystemProperty.ISOSpeed];
        FStop = string.Format("f/{0}", metaData[SystemProperty.FStop]);
        ExposureBias = metaData[SystemProperty.ExposureBias];

        TopColors = filteredColors.Select(pair => new ColorStatistics { Color = pair.Key, Count = pair.Value }).ToList();
    }

So I just continue performing the operations that I wanted on the image after its been selected. There's still a lot to fix here, especially since I'm blocking UI while performing work and not delegating these operations by subscribing these other UI components to the property, but it's a start, and no more exceptions!

Note that not included here, the value converter has been removed from the main application layer.