Async Program still freezing up the UI

2019-01-29 12:18发布

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.

4条回答
倾城 Initia
2楼-- · 2019-01-29 13:05

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));
}
查看更多
我命由我不由天
3楼-- · 2019-01-29 13:08

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;
    }
}
查看更多
Fickle 薄情
4楼-- · 2019-01-29 13:08

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;
        }
    }
}
查看更多
闹够了就滚
5楼-- · 2019-01-29 13:23

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];
}
查看更多
登录 后发表回答