更新MVVM中主线程的进度条

时间:2013-06-08 13:08:16

标签: c# wpf multithreading mvvm dispatcher

在我的应用程序中,我执行了长时间的操作,并且我想显示操作的进度。在长时间的操作中,我使用第三方DLL。不幸的是,dll不支持来自非主线程的调用。所以我不能用另一个线程来启动我的进程。

我找到了一种如何使用Dispather更新主线程中的进度条的方法。起初我编写了一个简单的WPF应用程序,并在代码隐藏中编写了简单的方法。

private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
    for (int i = 0; i <= 100; i++)
    {
        Dispatcher.Invoke(DispatcherPriority.Loaded,
                            (Action)(() =>
                                {
                                    pb.Value = i;
                                }));
        Thread.Sleep(10);
    }
}

此代码工作正常。我在窗口看到了进展。 但是我使用MVVM的问题,所以我不能使用这种方法。

为了解决我的问题,我创建了AttachedProperty

internal class ProgressBarAttachedBehavior
{
public static readonly DependencyProperty ValueAsyncProperty =
    DependencyProperty.RegisterAttached("ValueAsync", 
        typeof (double), 
        typeof (ProgressBarAttachedBehavior), 

        new UIPropertyMetadata(default(double), ValueAsyncChanged));

private static void ValueAsyncChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    var pb =
        d as ProgressBar;

    if (pb == null)
    {
        return;
    }          

    var dispatcher =
        d.Dispatcher;

    //if (dispatcher == null || dispatcher.CheckAccess())
    //{
    //    pb.Value = (double) e.NewValue;
    //}
    //else
    {

        DispatcherFrame frame = 
            new DispatcherFrame(true);

        var dispatcherOperation = dispatcher.BeginInvoke(DispatcherPriority.Background,
                            new Action(() =>
                                {
                                    pb.Value = (double)e.NewValue;
                                    frame.Continue = false;
                                })
                            );

        Dispatcher.PushFrame(frame);                
    }                        
}



public static void SetValueAsync(ProgressBar progressBar, double value)   
{
    progressBar.SetValue(ValueAsyncProperty, value);
}

public static double GetValueAsync(ProgressBar progressBar)
{
    return (double)progressBar.GetValue(ValueAsyncProperty);
}

在XAML中我写了

<ProgressBar                   tesWpfAppMvvm:ProgressBarAttachedBehavior.ValueAsync="{Binding Progress}"/>

我的ViewModel代码

class Workspace1ViewModel : WorkspaceViewModel
{
private ICommand _startCommand;
private double _progress;

public ICommand StartCommand
{
    get
    {
        if (_startCommand == null)
        {
            _startCommand =
                new RelayCommand(Start);

        }

        return _startCommand;
    }
}

private void Start()
{
    for (int i = 0; i <= 100; i++)
    {
        Progress = i;
        Thread.Sleep(20);
    }
}

public double Progress
{
    get
    {
        return _progress;
    }
    set
    {                
        _progress = value;
        RaisePropertyChanged(() => Progress);                
    }

}
}

代码工作正常。长进程在主线程中运行,我在窗口中看到进度。

但问题是,当我将Active ViewModel更改为其他模型时,我收到错误:

Cannot perform this operation while dispatcher processing is suspended.

我尝试到处寻找解决方案但不能。解决方案在任何地方都在单独的线程中运行日志进程。

请告诉我我的错误在哪里以及如何解决我的问题。

您可以下载演示项目以重现问题here

3 个答案:

答案 0 :(得分:2)

为什么不直接使用视图模型中的Application.Current.Dispatcher.Invoke()

请看一下这个样本:

<强> MainViewModel.cs

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication4
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged
                                                            = delegate { };

        private int mCounter;

        public int Counter
        {
            get { return mCounter; }
            set
            {
                mCounter = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Counter"));
            }
        }

        /// <summary>
        /// Supposed to be run from the background thread
        /// </summary>
        public void Start()
        {
            for(int i = 0; i <= 100; i++)
            {
                if(Application.Current == null)
                {
                    //do not try to update UI if the main window was closed
                    break;
                }

                Application.Current.Dispatcher.Invoke(
                        DispatcherPriority.Background,
                        (Action)(() =>
                        {
                            // Long running operation in main thread
                            // with low priority to prevent UI freeze
                            Thread.Sleep(100);
                            Counter = i;
                        }));
            }
        }
    }
}

<强> MainWindow.xaml.cs

using System.Threading.Tasks;
using System.Windows;

namespace WpfApplication4
{
    public partial class MainWindow : Window
    {
        private MainViewModel mainViewModel;
        public MainWindow()
        {
            InitializeComponent();
            Loaded += (sender, args) => StartOperation();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            StartOperation();
        }

        /// <summary>
        /// Start the long running operation in the background.
        /// </summary>
        private void StartOperation()
        {
            DataContext = mainViewModel = new MainViewModel();
            Task.Factory.StartNew(() => mainViewModel.Start());
        }
    }
}

MainWindow.xaml

<Window x:Class="WpfApplication4.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ProgressBar Height="20" Width="200" Value="{Binding Counter}" />
        <Button Content="Change View model" Height="23" Margin="0,100,0,0"
                HorizontalAlignment="Center"
                Click="Button_Click" />
    </Grid>
</Window>

答案 1 :(得分:0)

您可以查看同一例外的this thread吗?根据文档,您可以使用另一个委托包装“ValueAsyncChanged”事件处理程序,并使用Dispatcher.BeginInvoke方法调用“ValueAsyncChanged”。似乎WPF引擎在忙于加载时不允许执行PushFrame调用。

    public static readonly DependencyProperty ValueAsyncProperty =
    DependencyProperty.RegisterAttached("ValueAsync", 
        typeof (double), 
        typeof (ProgressBarAttachedBehavior), 

        new UIPropertyMetadata(default(double),
(o, e) => 
Dispatcher.BeginInvoke( 
new DependencyPropertyChangedEventHandler( ValueAsyncChanged), o, e);));

答案 2 :(得分:0)

这里有你想要的黑客: 设置进度条可见性,然后为UI线程提供足够的时间来更新其状态。

注:

当UI线程从睡眠状态唤醒时,由于UI密集型进程的执行,应用程序将无法响应。

完成UI密集型流程后,您的应用程序将再次响应。

以下是代码示例:

<强> XAML:

<telerik:RadProgressBar x:Name="progress"  
Visibility="{Binding ProgressVisibility, Mode=OneWay}" IsIndeterminate="True"  

<强>视图模型:

const int MINIMUM_UI_WAIT_REQUIRED = 2;

ProgressVisibility = Visibility.Visible;
await Task.Factory.StartNew(() => { Thread.Sleep(MINIMUM_UI_WAIT_REQUIRED); });