异步程序仍然冻结UI [英] Async Program still freezing up the UI

查看:59
本文介绍了异步程序仍然冻结UI的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

您好,我正在编写一个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添加到代码中一样工作.我想念什么吗?再次,我要让ui保持打开状态,并在GetImage运行时填充缩略图.所以我应该一次见到他们.

更新:

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

解决方案

看来您被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;
    }
}

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.

解决方案

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

这篇关于异步程序仍然冻结UI的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆