阻止MVVM中的UI线程

时间:2013-12-30 05:20:04

标签: c# wpf multithreading mvvm

我有一个关于MVVM实践的基本问题,不建议在viewmodel属性的setter中调用模型,该属性绑定到UI控件。

public int Woo
{
   get
    {
       return _Woo;
    }
    set
    {
      _Woo=model.SetWoo(value);
       NotifyPropertyChange("Woo");
    }
}

相反,人们建议在模型中设置属性并向viewmodel发送事件以使用新值刷新其属性并最终刷新UI控件。因为如果模型的方法花费大量时间,上述代码将阻止UI控件。

我的问题是,即使我从模型中发送事件,UI线程仍然被阻塞,直到执行其订阅的委托完成。那么这两种方法有什么区别,哪一种是正确的?

4 个答案:

答案 0 :(得分:4)

我总是使用上面的方法来阻止UI线程。

public int Woo
{
    get
    {
       _Woo;
    }
    set
    {
       BackgroundWorker backgroundWorker = new BackgroundWorker();
       //DoWork is ran on separate thread and does not block UI
       backgroundWorker.DoWork += (sender, arguments) =>
       {
           arguments.Result = model.GetWoo(); //Store data in worker result
       }
       //This method runs definitely on UI
       backgroundWorker.RunWorkerCompleted += (sender, arguments) =>
       {
           //Get stored result and assign to UI-bound stuff
           _Woo = arguments.Result; 
           NotifyPropertyChange("Woo");
       }      
       backgroundWorker.RunWorkerAsync(); 
   }
}

答案 1 :(得分:2)

MVVM关注的是关注点的分离,它没有描述线程如何在事件和事件处理方面起作用。在UI线程中执行代码是不可避免的,诀窍是只在UI线程中执行快速操作,如果某些事情要花费超过50ms,那么作为程序员/开发人员,你有责任剥离一个新线程(或线程抽象,例如Task)来执行任务并在操作完成时通知UI线程。

答案 2 :(得分:1)

理想情况下,您希望在与UI线程不同的线程上尽可能多地进行重要操作。然后,您可以使用某种策略重新连接到UI线程,以应用繁重的结果。

我发现最简单的方法是使用带有延续的Task,使用2个不同的TaskScheduler。我认为有一种更简单的方法可以使用async / await,但是我对它不熟悉,无法对任何权限进行评论。

使用您的示例:

private int? _Woo;
public int? Woo
{
    get
    {
        if (!_Woo.HasValue)
        {
            var task = Task.Factory.StartNew<int>(model.GetWoo, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default());
            task.ContinueWith(a => { _Woo = a.Result; }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());
        }

        return _Woo;
    }
    set
    {

        _Woo = value;
        NotifyPropertyChange("Woo");
    }
}

请注意,使用Default TaskScheduler调度繁重的任务(意味着它将在从ThreadPool获取的线程上执行),同时使用当前同步上下文TaskScheduler调度延续(意味着它将在最初调用的任何线程上执行)获得)。

通常我会将2个TaskSchedulers(Background和Foreground / UI)注入ViewModel,以便我可以控制单元测试的调度策略,但我刚刚在上面的例子中引用了Default和FromCurrentSynchronizationContext。

(旁注:我不完全确定你的示例代码试图完成什么,如果有的话。我把它打开,以便它现在基本上是一个异步Get。用户界面将要求的价值Woo,它将使用Task触发异步刷新并立即返回其当前的NULL值。一旦GetWoo完成,它将在View Model上设置Woo的值,这将触发Property Changed事件(在正确的线程上) ),这将导致UI在屏幕上更新Woo的值。我认为它显示了无论如何都要从UI线程中解除升级的概念。)

答案 3 :(得分:1)

如果要更新UI,可以使用BackgroundWorker。像下面的例子。

var worker = new BackgroundWorker();
        worker.DoWork += (o, ea) =>
        {

            Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
               () =>
               {
                 //you code for update UI

               }));

        };
        worker.RunWorkerCompleted += (o, ea) =>
        {

        };
        worker.RunWorkerAsync();