异步程序仍然冻结UI

时间:2018-09-04 19:34:58

标签: wpf asynchronous async-await task

您好,我正在编写一个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;
            });
        }
  }

当我运行上面的代码时,它的工作就像从未将异步/等待/任务添加到代码中一样。我想念什么吗?再次,我要让ui保持打开状态,并在GetImage运行时填充缩略图。所以我应该一次见到他们。

更新:

感谢@Peregrine向我指出正确的方向。我使用MVVM模式使用自定义用户控件制作了UI。他在回答中使用了它,并建议我使用我的viewModel。所以我要做的是将一个字符串属性添加到viewModel中,并创建了一个异步方法,该方法循环遍历所有缩略图,并将字符串属性设置为BitmapImage,并将用户界面数据绑定到该属性。因此,无论何时它会异步更新UI也会更新的属性。

4 个答案:

答案 0 :(得分:1)

运行GetImages的任务除了Dispatcher.Invoke几乎不执行任何操作,即您的所有代码或多或少都在UI线程中运行。

对其进行更改,以便在UI线程之外创建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));
}

答案 1 :(得分:1)

一种完全避免使用async / await的替代方法是使用ImageSource属性的视图模型,通过在Binding上指定IsAsync可以异步调用其getter:

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

答案 2 :(得分:0)

我不会像调度员那样来回跳动,而是这样做:

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

然后,在UI线程上调用代码:

var images = await GetImagesAsync(path, pageCount);
for (int i = 0; i < pageCount; i++)
{
    this._viewModel.Thumbs[i].img.Source = images[i];
}

答案 3 :(得分:0)

似乎您被可能占用网址的BitmapImage构造函数误导了。

如果此操作确实足够慢,无法使用async-await模式进行验证,那么最好将其分为两部分。

a)从网址中获取数据。这是最慢的部分-受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);
        }
    }
}

b)创建位图对象。这需要在主UI线程上发生,并且由于它相对较快,因此对于此部分使用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控件的source属性绑定到相应ImageItemVm的ImageBytes属性-WPF将自动处理从字节数组到位图图像的转换。

修改

我误解了原始问题,但原理仍然适用。如果您创建一个以file://开头的网址,我的代码可能仍然可以工作,但我怀疑它会是最有效的。

要使用本地图像文件,请用此替换对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;
    }
}