In MVVM, how can I prevent BackgroundWorker from freezing UI?

淺唱寂寞╮ 提交于 2019-12-04 21:00:57

In high-stress scenarios where the BackgroundWorker demands a lot of CPU without sleeping, the UI thread is starved and not able to update any of the bound properties that have been notified via INotifyPropertyChanged. However, if the BackgroundWorker sleeps, then the job will not finish as fast as possible.

Having a background thread use CPU won't interfere with the UI thread. What I suspect is actually happening is that the background thread is sending progress updates too quickly to the UI thread, and the UI thread is simply unable to keep up. (This ends up looking like a complete "freeze" because of the way Win32 messages are prioritized).

How can I ensure the View receives progress updates while respecting MVVM and while not throttling the job?

Rather simple: throttle the progress updates. Or more specifically, sample them.

First, I recommend using Filip's approach of Task.Run with IProgress<T>; this is the modern equivalent of BackgroundWorker (more info on my blog).

Second, in order to sample the progress updates, you should use an implementation of IProgress<T> that allows you to sample based on time (i.e., don't use Progress<T>). Asynchronous sequences with time-based logic? Rx is the clear choice. Lee Campbell has a great implementation, and I have a lesser one.

Example, using Lee Campbell's ObservableProgress:

private void DoWork(IProgress<ProgressState> progress)
{
  IsAnalyzing = true;
  progress.Report(new ProgressState(0, "Processing..."));

  int count = File.ReadLines(memoryFile).Count();
  StreamReader reader = new StreamReader(memoryFile);
  string line = "";
  int lineIndex = 0;
  while ((line = reader.ReadLine()) != null)
  {
    progress.Report(new ProgressState((int)(((double)lineIndex / count) * 100.0d));

    //Process record... (assume time consuming operation)
    HexRecord record = HexFileUtil.ParseLine(line);

    lineIndex++;
  }
  progress.Report(new ProgressState(100, "Done."));
  IsAnalyzing = false;
}

...

ObservableProgress.CreateAsync<ProgressState>(progress => Task.Run(() => DoWork(progress)))
    .Sample(TimeSpan.FromMilliseconds(250)) // Update UI every 250ms
    .ObserveOn(this) // Apply progress updates on UI thread
    .Subscribe(p =>
    {
      Progress = p.ProgressPercentage;
      Task = p.Task;
    });

Why are you using BackgroundWorker? Here is a simple progress implementation with tasks and it won't block UI thread if you access PropertyChanged invoke

Do = new GalaSoft.MvvmLight.Command.RelayCommand(()=>
            {
                var progress = new Progress<int>();
                progress.ProgressChanged += (s, p) => Progress = p;
                //Run and forget 
                DoWork(progress);
            });
public async Task DoWork(IProgress<int> progress = null)
        {
            await Task.Run(() =>
            {
                for (int i = 1; i < 11; i++)
                {
                    var count = 0;
                    for (int j = 0; j < 10000000; j++)
                    {
                        count += j;
                    }
                    progress.Report(i * 10);
                }
            });
        }

Some more info on the subject https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/ For async programing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!