7

I am trying to display a please wait dialog for a long running operation. The problem is since this is single threaded even though I tell the WaitScreen to display it never does. Is there a way I can change the visibility of that screen and make it display immediately? I included the Cursor call as an example. Right after I call this.Cursor, the cursor is updated immediately. This is exactly the behavior I want.

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.Cursor = System.Windows.Input.Cursors.Pen;
  WaitScreen.Visibility = Visibility.Visible;

  // Do something long here
  for (Int32 i = 0; i < 100000000; i++)
  {
    String s = i.ToString();
  }

  WaitScreen.Visibility = Visibility.Collapsed;
  this.Cursor = System.Windows.Input.Cursors.Arrow; 
}

WaitScreen is just a Grid with a Z-index of 99 that I hide and show.

update: I really don't want to use a background worker unless I have to. There are a number of places in the code where this start and stop will occur.

Shaun Bowe
  • 9,840
  • 11
  • 50
  • 71
  • I was the same when I was trying to do it single threaded. Thinking, lets just change the cursor. But it never really worked. – Ray Mar 05 '09 at 21:19

5 Answers5

19

Doing it single threaded really is going to be a pain, and it'll never work as you'd like. The window will eventually go black in WPF, and the program will change to "Not Responding".

I would recommending using a BackgroundWorker to do your long running task.

It's not that complicated. Something like this would work.

private void DoWork(object sender, DoWorkEventArgs e)
{
    //Do the long running process
}

private void WorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    //Hide your wait dialog
}

private void StartWork()
{
   //Show your wait dialog
   BackgroundWorker worker = new BackgroundWorker();
   worker.DoWork += DoWork;
   worker.RunWorkerCompleted += WorkerCompleted;
   worker.RunWorkerAsync();
}

You can then look at the ProgressChanged event to display a progress if you like (remember to set WorkerReportsProgress to true). You can also pass a parameter to RunWorkerAsync if your DoWork methods needs an object (available in e.Argument).

This really is the simplest way, rather than trying to do it singled threaded.

Ray
  • 45,695
  • 27
  • 126
  • 169
  • Thanks for the answer. I found a way to do it on the UI thread (See answer). In a perfect world BackgroundWorker is definitely the way to go. I just don't have the luxury of changing that much code at this time. – Shaun Bowe Mar 05 '09 at 21:23
17

I found a way! Thanks to this thread.

public static void ForceUIToUpdate()
{
  DispatcherFrame frame = new DispatcherFrame();

  Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, new DispatcherOperationCallback(delegate(object parameter)
  {
    frame.Continue = false;
    return null;
  }), null);

  Dispatcher.PushFrame(frame);
}

That function needs to be called right before the long running operation. That will then Force the UI thread to update.

Shaun Bowe
  • 9,840
  • 11
  • 50
  • 71
4

Check out my comprehensive research of this very delicate topic. If there's nothing you can do to improve the actual performance, you have the following options to display a waiting message:

Option #1 Execute a code to display a waiting message synchronously in the same method which does the real task. Just put this line before a lengthy process:

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => { /* Your code to display a waiting message */ }));

It'll process pending messages on the main dispatcher thread at the end of the Invoke().

Note: Reason for selecting Application.Current.Dispatcher but Dispatcher.CurrentDispatcher is explained here.

Option #2 Display a “Wait” screen and update UI (process pending messages).

To do it WinForms developers executed Application.DoEvents method. WPF offers two alternatives to achieve similar results:

Option #2.1 With using DispatcherFrame class.

Check a bit bulky example from MSDN:

[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public void DoEvents()
{
    DispatcherFrame frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
    Dispatcher.PushFrame(frame);
}

public object ExitFrame(object f)
{
    ((DispatcherFrame)f).Continue = false;
    return null;
}

Option #2.2 Invoke an empty Action

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, (Action)(() => { }));

See discussions which one (2.1 or 2.2) is better here. IMHO option #1 is still better than #2.

Option #3 Display a waiting message in a separate window.

It comes in handy when you display not a simple waiting message, but an animation. Rendering a loading animation at the same time that we are waiting for another long rendering operation to complete is a problem. Basically, we need two rendering threads. You can't have multiple rendering threads in a single window, but you can put your loading animation in a new window with its own rendering thread and make it look like it's not a separate window.

Download WpfLoadingOverlay.zip from this github (it was a sample from article "WPF Responsiveness: Asynchronous Loading Animations During Rendering", but I can't find it on the Web anymore) or have a look at the main idea below:

public partial class LoadingOverlayWindow : Window
{
    /// <summary>
    ///     Launches a loading window in its own UI thread and positions it over <c>overlayedElement</c>.
    /// </summary>
    /// <param name="overlayedElement"> An element for overlaying by the waiting form/message </param>
    /// <returns> A reference to the created window </returns>
    public static LoadingOverlayWindow CreateAsync(FrameworkElement overlayedElement)
    {
        // Get the coordinates where the loading overlay should be shown
        var locationFromScreen = overlayedElement.PointToScreen(new Point(0, 0));

        // Launch window in its own thread with a specific size and position
        var windowThread = new Thread(() =>
            {
                var window = new LoadingOverlayWindow
                    {
                        Left = locationFromScreen.X,
                        Top = locationFromScreen.Y,
                        Width = overlayedElement.ActualWidth,
                        Height = overlayedElement.ActualHeight
                    };
                window.Show();
                window.Closed += window.OnWindowClosed;
                Dispatcher.Run();
            });
        windowThread.SetApartmentState(ApartmentState.STA);
        windowThread.Start();

        // Wait until the new thread has created the window
        while (windowLauncher.Window == null) {}

        // The window has been created, so return a reference to it
        return windowLauncher.Window;
    }

    public LoadingOverlayWindow()
    {
        InitializeComponent();
    }

    private void OnWindowClosed(object sender, EventArgs args)
    {
        Dispatcher.InvokeShutdown();
    }
}
Community
  • 1
  • 1
Alex Klaus
  • 8,168
  • 8
  • 71
  • 87
3

Another option is to write your long-running routine as a function that returns IEnumerable<double> to indicate progress, and just say:

yield return 30;

That would indicate 30% of the way through, for example. You can then use a WPF timer to execute it in the "background" as a co-operative coroutine.

It's described in some detail here, with sample code.

Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
0

Short Answer

Simplifying the previous answers, you could just create a task like this, with VERY LITTLE change in your code.

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.Cursor = System.Windows.Input.Cursors.Pen;
  WaitScreen.Visibility = Visibility.Visible;

  Task.Factory.StartnNew(()=>{
      // Do something long here
      for (Int32 i = 0; i < 100000000; i++)
      {
        String s = i.ToString();
      }
  }).ContinueWith(()=>{
      WaitScreen.Visibility = Visibility.Collapsed;
      this.Cursor = System.Windows.Input.Cursors.Arrow; 
  }, TaskScheduler.FromCurrentSynchronizationContext());
}

Scalable Answer

If you want it to be more scalable, you could create a RunLongTask(Action action) method:

private void RunLongTask(Action action)
{
  IsBusy = true;

  Task.Factory.StartnNew(action).ContinueWith(()=>{
      IsBusy = false;
  }, TaskScheduler.FromCurrentSynchronizationContext());
}

Where you can bind IsBusy to your window control properties like some IsEnabled or Visibility property. (For the visibility, you need a converter, which I'll add at the end of the answer)

<Grid Name="OverlayGrid" Visibility={Binding IsBusy, Converter={local:BoolVisibilityCollapseConverter}}">...</Grid>
<!-- where local is defined at the Window element, referring to the namespace where you created the converter code -->

You then use it like:

RunLongTask(SomeParameterlessMethodName);

or

RunLongTask(()=>{
    //long
    //long
    //long
});

The converters used

using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

[ValueConversion(typeof(bool), typeof(Visibility))]
class BoolVisibilityCollapseConverter : MarkupExtension, IValueConverter
{
    //convert from source to target
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool v = (bool)value;
        if (v)
            return Visibility.Visible;
        else
            return Visibility.Collapsed;
    }

    //convert from target to source
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new InvalidOperationException("BoolVisibilityHideConverter is intended to be bound one way from source to target");
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

[ValueConversion(typeof(bool), typeof(Visibility))]
class InverseBoolVisibilityCollapseConverter : MarkupExtension, IValueConverter
{
    //convert from source to target
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool v = !(bool)value;
        if (v)
            return Visibility.Visible;
        else
            return Visibility.Collapsed;
    }

    //convert from target to source
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new InvalidOperationException("InverseBoolVisibilityCollapseConverter is intended to be bound one way from source to target");
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}
Daniel Möller
  • 84,878
  • 18
  • 192
  • 214