MVVM Wait Cursor how to set the.wait cursor during invocation of a command?
Asked Answered
G

9

33

Scenario:

  • User clicks a button on the View
  • This invokes a command on the ViewModel, DoProcessing

How, and where does the Wait cursor get set, considering the responsibilitues of View and ViewModel?

Just to be clear, I am just looking to change the DEFAULT cursor to an hourglass while the command is running. When the command completes, the cursor mut change back to an arrow. (It is a synchronous operation I am looking for, and I want the UI to block).

I have created an IsBusy property on the ViewModel. How do I ensure that the Application's mouse pointer changes?

Glop answered 12/4, 2012 at 6:52 Comment(0)
H
37

I am using it successfully in my application:

/// <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, System.Windows.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();
        }
    }
}

This has been taken from here. Courtsey huttelihut.

You need to call the SetBusyState method every time you think you are going to perform any time consuming operation. e.g.

...
UIServices.SetBusyState();
DoProcessing();
...

This will automatically change your cursor to wait cursor when the application is busy and back to normal when idle.

Histogenesis answered 12/4, 2012 at 11:46 Comment(5)
How would you use it with a progressbar? (ex: modernui progress bar)Grecism
@Grecism - Not sure how that works. Can try to help you if u can post a link of a sample app and describe how you want it to work.Histogenesis
When I get home later today, I'll post that question, but what've been reading I have to bind IsBusy to Visibility...but later I'll try and ask again :) Thanks in advanceGrecism
<ProgressBar Minimum="0" Maximum="1" Height="16" IsIndeterminate="True" Margin="0,0,0,16" /> <--supposing you have this, how would you bind isbusy to visibility (for example)?Grecism
I am not sure why people are downvoting this answer. I got 6 downvotes today without any comments, and that too after a year and a half after posting? At least leave a comment for downvoting this so that I can improve it.Histogenesis
L
14

A very simple method is to simply bind to the 'Cursor' property of the window (or any other control). For example:

XAML:

<Window
    x:Class="Example.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     Cursor="{Binding Cursor}" />

ViewModel Cursor Property (Using Apex.MVVM):

    private NotifyingProperty cursor = new NotifyingProperty("Cursor", typeof(System.Windows.Input.Cursor), System.Windows.Input.Cursors.Arrow);
    public System.Windows.Input.Cursor Cursor
    {
        get { return (System.Windows.Input.Cursor)GetValue(cursor); }
        set { SetValue(cursor, value); }
    }

Then simply change the cursor in your view when needed...

    public void DoSomethingLongCommand()
    {
        Cursor = System.Windows.Input.Cursors.Wait;

        ... some long process ...

        Cursor = System.Windows.Input.Cursors.Arrow;
    }
Luxemburg answered 13/2, 2014 at 15:22 Comment(2)
I think you need to change NotifyingProperty to DependencyPropertyLeaguer
Note, I am using the Apex library (apex.codeplex.com). I am defining the Cursor as a property in my ViewModel. I make it a NotifyingProperty so it can alert the View when it is changed. No need for a DependencyProperty here.Luxemburg
B
9

You want to have a bool property in viewmodel.

    private bool _IsBusy;
    public bool IsBusy 
    {
        get { return _IsBusy; }
        set 
        {
            _IsBusy = value;
            NotifyPropertyChanged("IsBusy");
        }
    }

Now you want to set the window style to bind to it.

<Window.Style>
    <Style TargetType="Window">
        <Setter Property="ForceCursor" Value="True"/>
        <Style.Triggers>
            <DataTrigger Binding="{Binding IsBusy}" Value="True">
                <Setter Property="Cursor" Value="Wait"/>
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Window.Style>

Now whenever a command is being executed and your view model is is busy, it would just set the IsBusy flag and reset it when done. The Window will automatically display the wait cursor and restore the original cursor when done.

You can write the command handler function in view model something like this:

    private void MyCommandExectute(object obj) // this responds to Button execute
    {
        try
        {
            IsBusy = true;

            CallTheFunctionThatTakesLongTime_Here();
        }
        finally
        {
            IsBusy = false;
        }
    }
Botheration answered 17/6, 2020 at 22:33 Comment(4)
Did you test this? I tried this approach, but apparently, the UI is only updated after MyCommandExectute is complete, so nothing really happensShutdown
@Shutdown Yes I did and it works beautifully and this is very neat solution that you can plug in your base class and reuse.Botheration
Thanks for the hint, I added it to the base class and it seems to work fine - but only as long as CallTheFunctionThatTakesLongTime_Here() is async.Shutdown
This is great because the VM does not have to reference Mouse so the VM does not need windows in the target framework.Quittance
B
2

Command is handled on the view model, so the reasonable decission would be to do folowing:

1) Create a busy indicator service and inject it into the view model (this will allow you to replace the cursor logic with some nasty animation easily)

2) In the command handler call the busy indicator service to notify the user

I might be wrong, but it looks like you are trying to do some heavy calculations or I/O on UI thread. I highly recommend you to perform work on thread pool in this case. You can use Task and TaskFactory to easily wrap work with ThreadPool

Boole answered 12/4, 2012 at 7:42 Comment(0)
D
1

There is a great Session(at 50:58) by Laurent Bugnion online (Creator of MVVM Light). There's also an deepDive session available (alternatively here(at 24:47)).

In at least one of them he live codes a busy Indicator using a is BusyProperty.

Doublecheck answered 12/4, 2012 at 7:47 Comment(2)
Agree IsBusy property on ViewModel.Yawl
How does this get displayed on the main window, as opposed to just on the view corresponding to the viewmodel i.e. the view is a child of the maon shell.Glop
O
1

The ViewModel should only decide whether it is busy, and the decision about what cursor to use, or whether to use some other technique such as a progress bar should be left up to the View.

And on the other hand, handling it with code-behind in the View is not so desirable either, because the ideal is that Views should not have code-behind.

Therefore I chose to make a class that can be used in the View XAML to specify that the cursor should be change to Wait when the ViewModel is busy. Using UWP + Prism the class definition is:

public class CursorBusy : FrameworkElement
{
    private static CoreCursor _arrow = new CoreCursor(CoreCursorType.Arrow, 0);
    private static CoreCursor _wait = new CoreCursor(CoreCursorType.Wait, 0);

    public static readonly DependencyProperty IsWaitCursorProperty =
        DependencyProperty.Register(
            "IsWaitCursor",
            typeof(bool),
            typeof(CursorBusy),
            new PropertyMetadata(false, OnIsWaitCursorChanged)
    );

    public bool IsWaitCursor
    {
        get { return (bool)GetValue(IsWaitCursorProperty); } 
        set { SetValue(IsWaitCursorProperty, value); }
    }

    private static void OnIsWaitCursorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        CursorBusy cb = (CursorBusy)d;
        Window.Current.CoreWindow.PointerCursor = (bool)e.NewValue ? _wait : _arrow;
    }
}

And the way to use it is:

<mvvm:SessionStateAwarePage
    x:Class="Orsa.Views.ImportPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mvvm="using:Prism.Windows.Mvvm"
    xmlns:local="using:Orsa"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mvvm:ViewModelLocator.AutoWireViewModel="True"
    mc:Ignorable="d"
    Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid>
    <Grid.RowDefinitions>
    .
    .
    </Grid.RowDefinitions>
    <local:CursorBusy IsWaitCursor="{Binding IsBusy}"/>
    (other UI Elements)
    .
    .
    </Grid>
</mvvm:SessionStateAwarePage>
Outman answered 9/12, 2018 at 4:5 Comment(0)
Q
0

IMHO that it is perfectly fine for the wait cursor logic to be next to the command in the viewmodel.

As to the best way to do change the cursor, create a IDisposable wrapper that changes the Mouse.OverrideCursor property.

public class StackedCursorOverride : IDisposable
{
    private readonly static Stack<Cursor> CursorStack;

    static StackedCursorOverride()
    {
        CursorStack = new Stack<Cursor>();
    }

    public StackedCursorOverride(Cursor cursor)
    {            
        CursorStack.Push(cursor);
        Mouse.OverrideCursor = cursor;            
    }

    public void Dispose()
    {
        var previousCursor = CursorStack.Pop();
        if (CursorStack.Count == 0)
        {
            Mouse.OverrideCursor = null;
            return;
        }

        // if next cursor is the same as the one we just popped, don't change the override
        if ((CursorStack.Count > 0) && (CursorStack.Peek() != previousCursor))
            Mouse.OverrideCursor = CursorStack.Peek();             
    }
}

Usage:

using (new StackedCursorOverride(Cursors.Wait))
{
     // ...
}

The above is a revised version of the solution that I posted to this question.

Quarto answered 12/4, 2012 at 8:16 Comment(2)
I'm not so sure. A wait cursor is just one implementation of a busy indicator. The ViewModel should not know about View implementation. What if the view wanted to denote busyness in some other way such as an animation or simple text message? A simple IsBusy property allows the view to implement how it wants.Yawl
@GazTheDestroyer: That is true, never thought of it like that.Quarto
T
0
private static void LoadWindow<T>(Window owner) where T : Window, new()
{
    owner.Cursor = Cursors.Wait;
    new T { Owner = owner }.Show();
    owner.Cursor = Cursors.Arrow;
}
Tiaratibbetts answered 22/4, 2016 at 18:34 Comment(0)
C
0

I have successfully used a simple alternative. You can add this boolean to cursor converter to your application, and then bind the Window's cursor to the to the IsBusy property of your view model, using the converter:

    public class BooleanToBusyCursorConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool booleanValue)
            {
                return booleanValue ? Cursors.Wait : Cursors.Arrow;
            }
            return Cursors.Arrow;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
Caesarism answered 19/7, 2024 at 19:2 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.