31

I have a WPF application using the MVVM pattern that sometimes have to show a waitcursor when it is busy doing something the user has to wait for. Thanks to a combination of answers on this page: display Hourglass when application is busy, I have a solution that almost works (although it is not really MVVM in spirit). Whenever I do something time-consuming in my viewmodels I do this:

using (UiServices.ShowWaitCursor())
{
.. do time-consuming logic
this.SomeData = somedata;
}

(ShowWaitCursor() returns a IDisposable that shows the waitcursor until it is being disposed of) The last line in my example is where I set some property. This property is bound in my XAML, e.g. like this:

<ItemsControl ItemsSource="{Binding SomeData}" /> 

However, since this could be a long list of objects and sometimes with complex datatemplates, etc. the actual binding and rendering sometime takes a considerable amount of time. Since this binding takes places outside of my using statement the waitcursor will go away before the actual wait is over for the user.

So my question is how to do a waitcursor in a WPF MVVM application that takes databinding into account?

Community
  • 1
  • 1
Torben Junker Kjær
  • 4,042
  • 4
  • 34
  • 33

4 Answers4

89

Isak's answer did not work for me, because it did not solve the problem of how to act when the actual wait is over for the user. I ended up doing this: Everytime I start doing something timeconsuming, I call a helper-method. This helper method changes the cursor and then creates a DispatcherTimer that will be called when the application is idle. When it is called it sets the mousecursor back:

/// <summary>
///   Contains helper methods for UI, so far just one for showing a waitcursor
/// </summary>
public static class UiServices
{

     /// <summary>
     ///   A value indicating whether the UI is currently busy
     /// </summary>
     private static bool IsBusy;

     /// <summary>
     /// Sets the busystate as busy.
     /// </summary>
     public static void SetBusyState()
     {
          SetBusyState(true);
     }

     /// <summary>
     /// Sets the busystate to busy or not busy.
     /// </summary>
     /// <param name="busy">if set to <c>true</c> the application is now busy.</param>
     private static void SetBusyState(bool busy)
     {
          if (busy != IsBusy)
          {
               IsBusy = busy;
               Mouse.OverrideCursor = busy ? Cursors.Wait : null;

               if (IsBusy)
               {
                   new DispatcherTimer(TimeSpan.FromSeconds(0), DispatcherPriority.ApplicationIdle, dispatcherTimer_Tick, Application.Current.Dispatcher);
               }
          }
     }

     /// <summary>
     /// Handles the Tick event of the dispatcherTimer control.
     /// </summary>
     /// <param name="sender">The source of the event.</param>
     /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
     private static void dispatcherTimer_Tick(object sender, EventArgs e)
     {
          var dispatcherTimer = sender as DispatcherTimer;
          if (dispatcherTimer != null)
          {
              SetBusyState(false);
              dispatcherTimer.Stop();
          }
     }
}
Nathan
  • 1,080
  • 7
  • 16
Torben Junker Kjær
  • 4,042
  • 4
  • 34
  • 33
  • Great implementation! This works perfect for me when the time consumption is happening in a third-party UI binding operation; when I don't really know when it is done. Thank you! – Sheldon Warkentin Nov 02 '11 at 15:38
  • I would change this to use a count rather than a bool. That way, multiple places can say they need the wait cursor and it will remain until they've all relinquished it. – Jeff Yates Aug 27 '12 at 21:39
  • 1
    Jeff - nothing relinquishes it here. The cursor is reset when the application becomes idle. If ten lines of code indicate that the wait cursor should be displayed before the app becomes idle, it doesn't matter; they will all have finished by the time it is reset (obviously this solution assumes synchronous UI thread operations). – Stephen Drew Sep 14 '12 at 13:39
  • 2
    @Steve: Of course! I didn't even see the dispatcher priority. Makes sense. Nice. I've used this in my code but I changed the class to `WaitCursor` and `SetBusyState` to `Show` so that my code is a little more self-documenting: `WaitCursor.Show()`. – Jeff Yates Sep 21 '12 at 21:50
  • +1, very slick. An advantage that should not do unnoticed: Can be called from anywhere, not just from the controls – Filip Nov 04 '12 at 10:54
  • 1
    You can't really use this directly with MVVM, since UiServices is not a static class and so it can't implement an interface. This causes a problem when trying to unit test the ViewModel. However I managed to work around it by renaming the class UiServicesStatic and nesting it inside a class called UiServices which implemented the IUiServices class. This interface has one method - void SetBusyState(); – openshac Jan 28 '14 at 14:27
  • I know this is old, but as a minor enhancement, I've changed `Application.Current.Dispatcher` to `Dispatcher.CurrentDispatcher`. That ensures a new dispatcher is created even if there is no current application running and allows to use it outside the application context (just in case you use several applications on different threads and need to set a wait cursor during application changes). – Jcl Nov 16 '14 at 06:10
  • 1
    The new DispatcherTimer instance is not assigned to a field or variable. How does it get garbage collected? Is this a potential memory leak? – Robin May 29 '16 at 01:43
9

So I didn't like using OverrideCursor because I had multiple windows and I wanted the ones that were not currently executing something to have the normal arrow cursor.

Here is my solution:

<Window.Style>
    <Style TargetType="Window">
        <Style.Triggers>
            <DataTrigger Binding="{Binding IsBusy}" Value="True">
                <Setter Property="Cursor" Value="Wait" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Window.Style>
<Grid>
    <Grid.Style>
        <Style TargetType="Grid">
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsBusy}" Value="True">
                    <Setter Property="IsHitTestVisible" Value="False" /> <!-- Ensures wait cursor is active everywhere in the window -->
                    <Setter Property="IsEnabled" Value="False" /> <!-- Makes everything appear disabled -->
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Grid.Style>
    <!-- Window controls go here -->
</Grid>
Jas Laferriere
  • 804
  • 10
  • 12
7

What I've done in the past is to define boolean properties in the viewmodel that indicates that a lengthy calculation is in progress. For instance IsBusy which is set to true when working and false when idle.

Then in the view I bind to this and display a progress bar or spinner or similar while this property is true. I've personally never set the cursor using this approach but I don't see why it wouldn't be possible.

If you want even more control and a simple boolean isn't enough, you can use the VisualStateManager which you drive from your viewmodel. With this approach you can in detail specify how the UI should look depending on the state of the viewmodel.

Isak Savo
  • 34,957
  • 11
  • 60
  • 92
1

In addition to Isak Savo's contribution, you might want to have a look at Brian Keating's blog for a working sample.

ЯegDwight
  • 24,821
  • 10
  • 45
  • 52
Izmoto
  • 1,939
  • 2
  • 17
  • 21