在MVVM中,如何防止BackgroundWorker冻结UI?

时间:2016-11-14 22:09:01

标签: c# wpf mvvm data-binding backgroundworker

首先,我在SO和网络上看到过很多类似的问题。这些似乎都没有解决我的特定问题。

我有一个简单的BackgroundWorker,其工作是逐行读取文件并报告进度以指示它有多远。文件中总共有65,553行,因此BackgroundWorker尽快完成对我来说非常重要。

由于MVVM是基于separation of concerns (SoC)构建的,并且视图和视图模型是分离的,因此BackgroundWorker更新View绑定到的View-Model上的属性。我的设置与另一个问题上的Kent Boorgaart's answer非常相似。

在高压力场景中,BackgroundWorker需要大量CPU而不会休眠,UI线程会缺乏资源,无法更新已通过INotifyPropertyChanged通知的任何绑定属性。但是,如果BackgroundWorker睡觉,那么作业将无法尽快完成。

如何确保View在尊重MVVM的同时接收进度更新,同时不限制作业?

在View-Model中,BackgroundWorker的设置如下。 Start()函数由RelayCommandMVVM-Light的一部分)调用。

public void Start(string memoryFile)
{
    this.memoryFile = memoryFile;
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += Worker_DoWork;
    worker.ProgressChanged += Worker_ProgressChanged;
    worker.WorkerReportsProgress = true;
    worker.RunWorkerAsync();
}

以下是实际工作的代码:

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker bw = (BackgroundWorker)sender;
    IsAnalyzing = true;
    bw.ReportProgress(0, new ProgressState("Processing..."));

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

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

        lineIndex++;
        if (lineIndex % 150 == 0)
        {
            //Uncomment to give UI thread some time.
            //However, this will throttle the job.
            //Thread.Sleep(5);
        }
    }
    bw.ReportProgress(100, new ProgressState("Done."));

    Thread.Sleep(1000);
    IsAnalyzing = false;
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    Progress = e.ProgressPercentage;
    if (e.UserState != null)
    {
        Task = ((ProgressState)e.UserState).Task;
    }
}

在上面的代码中,以下属性用于View和View-Model之间的绑定,每个属性都会触发INotifyPropertyChanged.PropertyChanged个事件:

  • Progress
  • Task
  • IsAnalyzing

<小时/>

编辑:

在跟进Stephen Cleary和Filip Cordas的过程中,我尝试使用Task.Run()有无ObservableProgress

我已经简化了后台任务来迭代数字而不是文件行。

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

    for (int i = 0; i < 2000000000; i += 1000000)
    {
        int percent = (int)(((double)i / 2000000000) * 100.0d);
        progress.Report(new ProgressState(percent, String.Format("Processing ({0}%)", percent)));
        Thread.Sleep(5);
    }
    progress.Report(new ProgressState(100, "Done."));

    Thread.Sleep(1000);
    IsAnalyzing = false;
}

现在,我以一种或两种方式启动任务(使用或不使用ObservableProgress):

public void Start(string memoryFile)
{
    this.memoryFile = memoryFile;

    /* TODO: Uncomment this section to use ObservableProgress instead.
     ObservableProgress.CreateAsync<ProgressState>(progress => System.Threading.Tasks.Task.Run(() => DoWork(progress)))
    .Sample(TimeSpan.FromMilliseconds(50))
    .ObserveOn(Application.Current.Dispatcher)
    .Subscribe(p =>
    {
        Progress = p.ProgressPercentage;
        Task = p.Task;
    });*/

    // TODO: Comment this section to use ObservableProgress instead.
    var progress = new Progress<ProgressState>();
    progress.ProgressChanged += (s, p) =>
    {
        Progress = p.ProgressPercentage;
        Task = p.Task;
    };
    System.Threading.Tasks.Task.Run(() => DoWork(progress));
}

ObservableProgress.cs

public static class ObservableProgress
{
    public static IObservable<T> CreateAsync<T>(Func<IProgress<T>, Task> action)
    {
        return Observable.Create<T>(async obs =>
        {
            await action(new Progress<T>(obs.OnNext));
            obs.OnCompleted();

            return Disposable.Empty;
        });
    }
}

在这两种情况下(有或没有ObservableProgress)我发现我仍然需要使用Thread.Sleep(5)来限制后台作业。否则UI会冻结。

<小时/>

编辑2:

我对工作线程内的报告进行了一些小修改:

for (int i = 0; i < 2000000000; i += 10) //Notice this loop iterates a lot more.
{
    int percent = (int)(((double)i / 2000000000) * 100.0d);
    //Thread.Sleep(5); //NOT Throttling anymore.
    if (i % 1000000 == 0)
    {
        progress.Report(new ProgressState(percent, String.Format("Processing ({0}%)", percent)));
    }
}

通过此修改,UI不再锁定,并且更改正在正常传播。为什么会这样?

2 个答案:

答案 0 :(得分:5)

  

在高压力情况下,BackgroundWorker需要大量CPU而不休眠,UI线程缺乏并且无法更新通过INotifyPropertyChanged通知的任何绑定属性。但是,如果BackgroundWorker休眠,则作业将无法尽快完成。

拥有后台线程使用CPU不会干扰UI线程。我怀疑实际正在发生的是后台线程正在向UI线程发送进度更新太快,并且UI线程根本无法跟上。 (这最终看起来像一个完整的&#34;冻结&#34;因为Win32消息的优先级排序方式。)

  

如何确保View在尊重MVVM的同时接收进度更新,同时不限制作业?

相当简单:限制进度更新。或者更具体地说,示例

首先,我建议使用Task.RunIProgress<T>的Filip方法;这是BackgroundWorker的现代等价物(更多信息on my blog)。

其次,为了对进度更新进行采样,您应该使用允许您根据时间进行采样的IProgress<T>实现(即不要使用{{ 1}})。具有基于时间的逻辑的异步序列? Rx是明智的选择。 Lee Campbell有一个great implementation,我有一个lesser one

示例,使用Lee Campbell的Progress<T>

ObservableProgress

答案 1 :(得分:3)

您为什么使用BackgroundWorker?这是一个包含任务的简单进度实现,如果访问PropertyChanged调用

,它将不会阻止UI线程
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);
                }
            });
        }

关于主题的更多信息https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/用于异步编程