0

Hello I'm writing a WPF program that gets has thumbnails inside a ThumbnailViewer. I want to generate the Thumbnails first, then asynchronously generate the images for each thumbnail.

I can't include everything but I think this is whats relevant

Method to generate the thumbnails.

public async void GenerateThumbnails()
{
   // In short there is 120 thumbnails I will load.
   string path = @"C:\....\...\...png";
   int pageCount = 120;

   SetThumbnails(path, pageCount);
   await Task.Run(() => GetImages(path, pageCount);
 }

 SetThumbnails(string path, int pageCount)
 {
    for(int i = 1; i <= pageCount; i ++)
    {
        // Sets the pageNumber of the current thumbnail
        var thumb = new Thumbnail(i.ToString());
        // Add the current thumb to my thumbs which is 
        // binded to the ui
        this._viewModel.thumbs.Add(thumb);
    }
  }

  GetImages(string path, int pageCount)
  {
       for(int i = 1; i <= pageCount; i ++)
       {
            Dispatcher.Invoke(() =>
            {
                var uri = new Uri(path);
                var bitmap = new BitmapImage(uri);
                this._viewModel.Thumbs[i - 1].img.Source = bitmap;
            });
        }
  }

When I run the code above it works just as if I never add async/await/task to the code. Am I missing something? Again What I want is for the ui to stay open and the thumbnail images get populated as the GetImage runs. So I should see them one at a time.

UPDATE:

Thanks to @Peregrine for pointing me in the right direction. I made my UI with custom user controls using the MVVM pattern. In his answer he used it and suggested that I use my viewModel. So what I did is I add a string property to my viewModel and made an async method that loop though all the thumbnails and set my string property to the BitmapImage and databound my UI to that property. So anytime it would asynchronously update the property the UI would also update.

Jacob Loncar
  • 107
  • 1
  • 2
  • 11

4 Answers4

1

The Task that runs GetImages does virtually nothing but Dispatcher.Invoke, i.e. more or less all your code runs in the UI thread.

Change it so that the BitmapImage is created outside the UI thread, then freeze it to make it cross-thread accessible:

private void GetImages(string path, int pageCount)
{
    for (int i = 0; i < pageCount; i++)
    {
        var bitmap = new BitmapImage();
        bitmap.BeginInit();
        bitmap.CacheOption = BitmapCacheOption.OnLoad;
        bitmap.UriSource = new Uri(path);
        bitmap.EndInit();
        bitmap.Freeze();

        Dispatcher.Invoke(() => this._viewModel.Thumbs[i].img.Source = bitmap);
    }
}

You should also avoid any async void method, excpet when it is an event handler. Change it as shown below, and await it when you call it:

public async Task GenerateThumbnails()
{
    ...
    await Task.Run(() => GetImages(path, pageCount));
}

or just:

public Task GenerateThumbnails()
{
    ...
    return Task.Run(() => GetImages(path, pageCount));
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
1

An alternative that altogether avoids async/await is a view model with an ImageSource property whose getter is called asynchronously by specifying IsAsync on the Binding:

<Image Source="{Binding Image, IsAsync=True}"/>

with a view model like this:

public class ThumbnailViewModel
{
    public ThumbnailViewModel(string path)
    {
        Path = path;
    }

    public string Path { get; }

    private BitmapImage îmage;

    public BitmapImage Image
    {
        get
        {
            if (îmage == null)
            {
                îmage = new BitmapImage();
                îmage.BeginInit();
                îmage.CacheOption = BitmapCacheOption.OnLoad;
                îmage.UriSource = new Uri(Path);
                îmage.EndInit();
                îmage.Freeze();
            }

            return îmage;
        }
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
0

Rather than involving the the dispatcher and jumping back and forth, I'd do something like this:

private Task<BitmapImage[]> GetImagesAsync(string path, int pageCount)
{
    return Task.Run(() =>
    {
        var images = new BitmapImage[pageCount];
        for (int i = 0; i < pageCount; i++)
        {
            var bitmap = new BitmapImage();
            bitmap.BeginInit();
            bitmap.CacheOption = BitmapCacheOption.OnLoad;
            bitmap.UriSource = new Uri(path);
            bitmap.EndInit();
            bitmap.Freeze();
            images[i] = bitmap;
        }
        return images;
    }
}

Then, on the UI thread calling code:

var images = await GetImagesAsync(path, pageCount);
for (int i = 0; i < pageCount; i++)
{
    this._viewModel.Thumbs[i].img.Source = images[i];
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
Paulo Morgado
  • 14,111
  • 3
  • 31
  • 59
  • 1
    Removed `async` from GetImagesAsync, because it doesn't `await` anything. Besides that, a major drawback of this approach is that no image is shown before all images are loaded. Assigning thumbnail image Sources via Dispatcher calls during the image loading process is far more "smooth". – Clemens Sep 05 '18 at 05:30
  • For that, you cna use `Progress* or Rx. – Paulo Morgado Sep 05 '18 at 06:40
  • Sure you can, but that's not in your answer. – Clemens Sep 05 '18 at 06:54
0

It looks as though you've been mislead by the constructor of BitmapImage that can take a Url.

If this operation really is slow enough to justify using the async-await pattern, then you would be much better off dividing it into two sections.

a) Fetching the data from the url. This is the slow part - it's IO bound, and would benefit most from async-await.

public static class MyIOAsync
{
    public static async Task<byte[]> GetBytesFromUrlAsync(string url)
    {
        using (var httpClient = new HttpClient())
        {
            return await httpClient
                       .GetByteArrayAsync(url)
                       .ConfigureAwait(false);
        }
    }
}

b) Creating the bitmap object. This needs to happen on the main UI thread, and as it's relatively quick anyway, there's no gain in using async-await for this part.

Assuming that you're following the MVVM pattern, you shouldn't have any visual elements in the ViewModel layer - instead use a ImageItemVm for each thumbnail required

public class ImageItemVm : ViewModelBase
{
    public ThumbnailItemVm(string url)
    {
        Url = url;
    }

    public string Url { get; }

    private bool _fetchingBytes;

    private byte[] _imageBytes;

    public byte[] ImageBytes
    {
        get
        {
            if (_imageBytes != null || _fetchingBytes)
                return _imageBytes;

            // refresh ImageBytes once the data fetching task has completed OK
            Action<Task<byte[]>> continuation = async task =>
                {
                    _imageBytes = await task;
                    RaisePropertyChanged(nameof(ImageBytes));
                };

            // no need for await here as the continuations will handle everything
            MyIOAsync.GetBytesFromUrlAsync(Url)
                .ContinueWith(continuation, 
                              TaskContinuationOptions.OnlyOnRanToCompletion)
                .ContinueWith(_ => _fetchingBytes = false) 
                .ConfigureAwait(false);

            return null;
        }
    }
}

You can then bind the source property of an Image control to the ImageBytes property of the corresponding ImageItemVm - WPF will automatically handle the conversion from byte array to a bitmap image.

Edit

I misread the original question, but the principle still applies. My code would probably still work if you made a url starting file:// but I doubt it would be the most efficient.

To use a local image file, replace the call to GetBytesFromUrlAsync() with this

public static async Task<byte[]> ReadBytesFromFileAsync(string fileName)
{
    using (var file = new FileStream(fileName, 
                                     FileMode.Open, 
                                     FileAccess.Read, 
                                     FileShare.Read, 
                                     4096, 
                                     useAsync: true))
    {
        var bytes = new byte[file.Length];

        await file.ReadAsync(bytes, 0, (int)file.Length)
                  .ConfigureAwait(false);

        return bytes;
    }
}
Peregrine
  • 4,287
  • 3
  • 17
  • 34
  • It's not from a website, the image comes from a folder on a database – Jacob Loncar Sep 05 '18 at 17:10
  • @JacobLoncar Another simple way of asynchronously loading BitmapSources from files is shown in the `async Load` method here: https://stackoverflow.com/a/43124089/1136211 – Clemens Sep 05 '18 at 18:48
  • A note on correctness, *"b) Creating the bitmap object. This needs to happen on the main UI thread"* is not actually true. Any ImageSource, e.g. a BitmapImage can well be created on a non-UI thread. You only have to make sure that it gets frozen before you use it in the UI thread, e.g. assign it to the Source property of an Image element. – Clemens Sep 05 '18 at 20:48
  • @Peregrine, thanks for pointing that I should use my viewModel. I didn't exactly follow the code here, but enough to the point where it fixed my issue. Thanks again. – Jacob Loncar Sep 06 '18 at 16:06
  • @JacobLoncar The key tenet of MVVM is a clear separation between the user interface and data (and the means of accessing it). Take a look at my blog http://peregrinesview.uk/ for my take on it. It's a debatable point whether you consider a BitmapImage as a UI object. Clemens is technically correct that they can be generated in the ViewModel without threading issues, although I prefer not to do things that way. – Peregrine Sep 06 '18 at 19:51
  • @JacobLoncar Note however that if you need to take as much load as possible from the UI thread, it still makes much sense to create them in a background thread. With the solution shown here the whole image decoding process, i.e. the conversion from a byte array that contains e.g. a JPEG frame to an actual bitmap, still runs in the UI thread – Clemens Sep 06 '18 at 21:46
  • @JacobLoncar When you actually have a view with Image elements that are bound to thumbnail view model items, you could greatly simplify your code by avoiding async/await altogether, Just set the Binding to `IsAsync`, as shown in my other answer. – Clemens Sep 07 '18 at 04:20