Программа Async по-прежнему замораживает пользовательский интерфейс

Здравствуйте, я пишу программу WPF, которая получает эскизы внутри ThumbnailViewer. Сначала я хочу создать эскизы, а затем асинхронно генерировать изображения для каждого эскиза.

Я не могу включить все, но я думаю, что это актуально

Способ генерации миниатюр.

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;
            });
        }
  }

Когда я запускаю приведенный выше код, он работает так, как будто я никогда не добавляю async / await / task в код. Я что-то пропустил? Опять же, я хочу, чтобы интерфейс оставался открытым, а миниатюрные изображения заполнялись во время работы GetImage. Так что я должен видеть их по одному.

ОБНОВИТЬ:

Спасибо @Peregrine за то, что указал мне правильное направление. Я сделал свой пользовательский интерфейс с пользовательскими элементами управления, используя шаблон MVVM. В своем ответе он использовал его и предложил использовать мою viewModel. Итак, я добавил строковое свойство к моей viewModel и создал асинхронный метод, который зацикливает все миниатюры и установил мое строковое свойство на BitmapImage, а затем привязал свой интерфейс к этому свойству. Таким образом, в любое время он асинхронно обновляет свойство, которое также обновляет пользовательский интерфейс.

4 ответа

Решение

Похоже, вы были введены в заблуждение конструктором BitmapImage, который может принимать URL.

Если эта операция действительно достаточно медленная, чтобы оправдать использование шаблона async-await, то вам лучше разделить ее на две части.

а) Извлечение данных из URL. Это медленная часть - она ​​связана с вводом-выводом и больше всего выиграет от 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);
        }
    }
}

б) Создание растрового объекта. Это должно происходить в главном потоке пользовательского интерфейса, и, поскольку в любом случае он относительно быстрый, использование async-await для этой части не имеет смысла.

Предполагая, что вы следуете шаблону MVVM, у вас не должно быть никаких визуальных элементов в слое ViewModel - вместо этого используйте ImageItemVm для каждого требуемого эскиза

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;
        }
    }
}

Затем вы можете привязать свойство источника элемента управления Image к свойству ImageBytes соответствующего ImageItemVm - WPF автоматически обработает преобразование из байтового массива в растровое изображение.

редактировать

Я неправильно понял исходный вопрос, но принцип все еще применяется. Мой код, вероятно, все еще будет работать, если вы создадите стартовый файл URL:// но я сомневаюсь, что он будет наиболее эффективным.

Чтобы использовать локальный файл изображения, замените вызов GetBytesFromUrlAsync() этим

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;
    }
}

Задача, которая запускает GetImages, практически ничего не делает, кроме Dispatcher.Invoke, то есть более или менее весь ваш код выполняется в потоке пользовательского интерфейса.

Измените его так, чтобы BitmapImage создавался вне потока пользовательского интерфейса, затем заморозьте его, чтобы сделать его доступным для всех потоков:

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);
    }
}

Вы также должны избегать любых async void Метод исключить, когда это обработчик события. Измените его, как показано ниже, и дождитесь его вызова:

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

или просто:

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

Альтернатива, которая вообще избегает async/await модель представления со свойством ImageSource, метод получения которого вызывается асинхронно IsAsync на привязке:

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

с такой моделью вида:

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;
        }
    }
}

Вместо того, чтобы привлекать диспетчера и прыгать туда-сюда, я бы сделал что-то вроде этого:

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;
    }
}

Затем в потоке пользовательского интерфейса вызываем код:

var images = await GetImagesAsync(path, pageCount);
for (int i = 0; i < pageCount; i++)
{
    this._viewModel.Thumbs[i].img.Source = images[i];
}
Другие вопросы по тегам