Программа 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];
}