Программа Async все еще замораживает пользовательский интерфейс - PullRequest
0 голосов
/ 04 сентября 2018

Здравствуйте, я пишу программу 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. Поэтому я должен видеть их по одному.

UPDATE:

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

Ответы [ 4 ]

0 голосов
/ 07 сентября 2018

Альтернативой, которая вообще избегает 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;
        }
    }
}
0 голосов
/ 05 сентября 2018

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

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];
}
0 голосов
/ 05 сентября 2018

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

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

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

Задача, которая запускает 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));
}
...