wpf 更新 MVVM 主线程中的进度条

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16999824/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 08:58:14  来源:igfitidea点击:

Update progress bar in main thread in MVVM

c#wpfmultithreadingmvvmdispatcher

提问by Victor Chekalin

In my application I perform a long operation and I want to show a progress of the operation. In long operation I use 3rd-party dll. Unfortunately that dll doesn't support calls from non-main thread. So I cannot use another thread to start my process.

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

I found a way how to update progress bar in the main thread using Dispather. At first I wrote a simple WPF application and wrote simple method in code-behind.

我找到了一种如何使用 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);
    }
}

This code works fine. I see the progress in my window. But the problem I use MVVM, so I cannot use this method.

这段代码工作正常。我在窗口中看到了进度。但是问题我用的是MVVM,所以不能用这个方法。

To solve my problem I created AttachedProperty

为了解决我的问题,我创建了 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);
}

In XAML I wrote

在 XAML 我写

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

And my ViewModel code

还有我的 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);                
    }

}
}

The code works fine. Long process is run in the main-thread and I see the progress in the window.

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

But the issue, that when I change my Active ViewModel to another model, I get error:

但问题是,当我将 Active ViewModel 更改为另一个模型时,出现错误:

Cannot perform this operation while dispatcher processing is suspended.

I tried find the solution everywhere but couldn't. Everywhere the solution is run log process in separate thread.

我试图到处找到解决方案,但找不到。解决方案无处不在,在单独的线程中运行日志进程。

Please tell me where is my mistake and how to solve my issue.

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

You may download demo project to reproduce the issue here

您可以在此处下载演示项目以重现该问题

回答by Sevenate

Why not just use Application.Current.Dispatcher.Invoke()from the view-model?

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

Please, take a look at this sample:

请看一下这个示例:

MainViewModel.cs

主视图模型.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

主窗口.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());
        }
    }
}

and the MainWindow.xaml

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>

回答by Scott Nimrod

Here's a hack for what you want: Set the progressbar visibility and then provide just enough time for the UI thread to update its state.

这是您想要的一个技巧:设置进度条可见性,然后为 UI 线程提供足够的时间来更新其状态。

NOTE:

笔记:

When the UI thread awakes from its sleep, the application will become unresponsive as a result of the UI intensive process executing.

当 UI 线程从睡眠中唤醒时,由于执行 UI 密集型进程,应用程序将变得无响应。

Once the UI intensive process has completed, your application will become responsive again.

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

The following is a code example:

下面是一个代码示例:

XAML:

XAML:

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

ViewModel:

视图模型:

const int MINIMUM_UI_WAIT_REQUIRED = 2;

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

回答by RockWorld

Can you check this threadon same exception? As per the document, you can wrap "ValueAsyncChanged" event handler with another delegate and call "ValueAsyncChanged" with Dispatcher.BeginInvoke method. It seems that WPF engine doesn't allow to execute your PushFrame call while it is busy doing the loading.

你能在同一个异常上检查这个线程吗?根据文档,您可以使用另一个委托包装“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);));