Reputation: 776
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.
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.
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.
To solve my problem I created 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
<ProgressBar tesWpfAppMvvm:ProgressBarAttachedBehavior.ValueAsync="{Binding Progress}"/>
And my ViewModel code
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:
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
Upvotes: 2
Views: 7501
Reputation: 11595
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.
NOTE:
When the UI thread awakes from its sleep, the application will become unresponsive as a result of the UI intensive process executing.
Once the UI intensive process has completed, your application will become responsive again.
The following is a code example:
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); });
Upvotes: 0
Reputation: 6475
Why not just use Application.Current.Dispatcher.Invoke()
from the view-model?
Please, take a look at this sample:
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());
}
}
}
and the 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>
Upvotes: 2
Reputation: 1288
Can you check this thread on 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.
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);));
Upvotes: 0