【问题标题】:Update progress bar in main thread in MVVM更新MVVM主线程中的进度条
【发布时间】:2013-06-08 13:08:16
【问题描述】:

在我的应用程序中,我执行了一个长时间的操作,我想显示操作的进度。在长时间操作中,我使用 3rd-party 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

【问题讨论】:

  • 维克多,如果你的问题仍然适用,你介意考虑我的回答吗?

标签: c# wpf multithreading mvvm dispatcher


【解决方案1】:

为什么不直接使用视图模型中的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>

【讨论】:

    【解决方案2】:

    你能检查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);));
    

    【讨论】:

      【解决方案3】:

      以下是您想要的 hack: 设置进度条的可见性,然后为 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); });
      

      【讨论】:

        猜你喜欢
        • 2015-07-30
        • 1970-01-01
        • 2020-10-31
        • 1970-01-01
        • 2015-04-18
        • 1970-01-01
        • 2012-05-22
        • 2017-08-04
        • 1970-01-01
        相关资源
        最近更新 更多