display Hourglass when application is busy

C#Wpf

C# Problem Overview


For a view constructed using WPF, I want to change the mouse cursor to a hourglass when the application is busy and non-responsive.

One solution is to add

 this.Cursor = Cursors.Wait;

to all the places that may cause the UI to become non-responsive. But obviously this is not the best solution. I am wondering what is the best way to achieve this?

Is it possible to achieve this by using Styles or Resources?

C# Solutions


Solution 1 - C#

We did a disposable class that changes the cursor for us when the app is going to take long, it looks like this:

public class WaitCursor : IDisposable
{
	private Cursor _previousCursor;
	
	public WaitCursor()
	{
		_previousCursor = Mouse.OverrideCursor;

		Mouse.OverrideCursor = Cursors.Wait;
	}

	#region IDisposable Members

	public void Dispose()
	{
		Mouse.OverrideCursor = _previousCursor;
	}

	#endregion
}

And we use it like this:

using(new WaitCursor())
{
    // very long task
}

Might not be the greatest design, but it does the trick =)

Solution 2 - C#

I used the answers here to build something that worked better for me. The problem is that when the using block in Carlo's answer finishes, the UI might actually still be busy databinding. There might be lazy-loaded data or events firing as a result of what was done in the block. In my case it sometimes took several seconds from the waitcursor disappeared until the UI was actually ready. I solved it by creating a helper method that sets the waitcursor and also takes care of setting up a timer that will automatically set the cursor back when the UI is ready. I can't be sure that this design will work in all cases, but it worked for me:

    /// <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();
                }
        }
    }

Solution 3 - C#

I am simply doing

Mouse.OverrideCursor = Cursors.Wait;
try {
    // Long lasting stuff ...
} finally {
    Mouse.OverrideCursor = null;
}

According to the documentation of Mouse.OverrideCursor Property

>To clear the override Cursor, set OverrideCursor to null.

The try-finally statement ensures that the default cursor is restored in any case, even when an exception occurs or the try-part is left with return or break (if inside a loop).

Solution 4 - C#

The best way would be to not cause the UI to become non-responsive ever, offloading all of the work to other threads/tasks as appropriate.

Other than that, you're kindof in a catch-22: if you did add a way to detect that the ui is non-responsive, there's no good way to change the cursor, as the place you'd need to do that (the even thread) is non-responsive... You might be able to pinvoke out to standard win32 code to change the cursor for the whole window, though?

Otherwise, you'd have to do it pre-emptively, like your question suggests.

Solution 5 - C#

I personnaly prefer to not see to mouse pointer switching many times from hourglass to arrow. To help prevent that behavior while calling embedded functions that take a while and each try to control the mouse pointer, I use a stack (counter) that I call a LifeTrackerStack. And only when the stack is empty (counter to 0) that I set back the hour glass to an arrow.

I also use MVVM. I also prefer thread safe code.

In my root class of model I declare my LifeTrackerStack that I either populate in childs model classes or use directly from child model classes when I have access to it from them.

My life tracker have 2 states/actions:

  • Alive (counter > 0) => turn Model.IsBusy to true;
  • Done (counter == 0) => turn Model.IsBusy to false;

Then in my view, I manually bind to my Model.IsBusy and do:

void ModelPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
	if (e.PropertyName == "IsBusy")
	{
		if (this._modelViewAnalysis.IsBusy)
		{
			if (Application.Current.Dispatcher.CheckAccess())
			{
				this.Cursor = Cursors.Wait;
			}
			else
			{
				Application.Current.Dispatcher.Invoke(new Action(() => this.Cursor = Cursors.Wait));
			}
		}
		else
		{
			Application.Current.Dispatcher.Invoke(new Action(() => this.Cursor = null));
		}
	}

This is my class LifeTrackerStack:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;

namespace HQ.Util.General
{
    /// <summary>
    /// Usage is to have only one event for a recursive call on many objects
    /// </summary>
    public class LifeTrackerStack
    {
        // ******************************************************************
        protected readonly Action _stackCreationAction;
        protected readonly Action _stackDisposeAction;
        private int _refCount = 0;
        private object _objLock = new object();
        // ******************************************************************
        public LifeTrackerStack(Action stackCreationAction = null, Action stackDisposeAction = null)
        {
            _stackCreationAction = stackCreationAction;
            _stackDisposeAction = stackDisposeAction;
        }

        // ******************************************************************
        /// <summary>
        /// Return a new LifeTracker to be used in a 'using' block in order to ensure reliability
        /// </summary>
        /// <returns></returns>
        public LifeTracker GetNewLifeTracker()
        {
            LifeTracker lifeTracker = new LifeTracker(AddRef, RemoveRef);

            return lifeTracker;
        }

		// ******************************************************************
	    public int Count
	    {
			get { return _refCount; }
	    }
	
		// ******************************************************************
		public void Reset()
		{
			lock (_objLock)
			{
				_refCount = 0;
				if (_stackDisposeAction != null)
				{
					_stackDisposeAction();
				}
			}
		}
		
		// ******************************************************************
        private void AddRef()
        {
            lock (_objLock)
            {
                if (_refCount == 0)
                {
					if (_stackCreationAction != null)
					{
						_stackCreationAction();
					}
				}
                _refCount++;
            }
        }

        // ******************************************************************
        private void RemoveRef()
        {
            bool shouldDispose = false;
            lock (_objLock)
            {
                if (_refCount > 0)
                {
                    _refCount--;
                }

                if (_refCount == 0)
                {
					if (_stackDisposeAction != null)
					{
						_stackDisposeAction();
					}
				}
            }
        }

        // ******************************************************************
    }
}


using System;

namespace HQ.Util.General
{
	public delegate void ActionDelegate();

    public class LifeTracker : IDisposable
    {
		private readonly ActionDelegate _actionDispose;
		public LifeTracker(ActionDelegate actionCreation, ActionDelegate actionDispose)
        {
            _actionDispose = actionDispose;

            if (actionCreation != null)
                actionCreation();
        }

        private bool _disposed = false;
        public void Dispose()
        {
            Dispose(true);
            // This object will be cleaned up by the Dispose method.
            // Therefore, you should call GC.SupressFinalize to
            // take this object off the finalization queue
            // and prevent finalization code for this object
            // from executing a second time.
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this._disposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    _actionDispose();
                }

                // Note disposing has been done.
                _disposed = true;
            }
        }
    }
}

And the usage of it:

	_busyStackLifeTracker = new LifeTrackerStack
		(
			() =>
			{
				this.IsBusy = true;
			},
			() =>
			{
				this.IsBusy = false;
			}
		);

Everywhere I have lengthy jog, I do:

		using (this.BusyStackLifeTracker.GetNewLifeTracker())
		{
			// long job
		}

It works for me. Hope it could help any! Eric

Solution 6 - C#

Be careful here because fiddling with the Wait Cursor can cause some problems with STA threads. Make sure that if you use this thing that you are doing it within its own thread. I posted an example here Run inside an STA which uses this to show a WaitCursor while the spawned file is starting up, and does not blow up (the main application) AFAICT.

Solution 7 - C#

Changing the cursor doesn't means the application will not respond to mouse and keyboard events after the long running task has finished. To avoid user missleading, I use the class below that removes all the keyboard and mouse messages from the application message queue.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;

public class WpfHourGlass : IDisposable
{

    [StructLayout(LayoutKind.Sequential)]
    private struct POINTAPI
    {
        public int x;
        public int y;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MSG
    {
        public int hwnd;
        public int message;
        public int wParam;
        public int lParam;
        public int time;
        public POINTAPI pt;
    }
    private const short PM_REMOVE = 0x1;
    private const short WM_MOUSELAST = 0x209;
    private const short WM_MOUSEFIRST = 0x200;
    private const short WM_KEYFIRST = 0x100;
    private const short WM_KEYLAST = 0x108;
    [DllImport("user32", EntryPoint = "PeekMessageA", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
    private static extern int PeekMessage([MarshalAs(UnmanagedType.Struct)]
    ref MSG lpMsg, int hwnd, int wMsgFilterMin, int wMsgFilterMax, int wRemoveMsg);

    public WpfHourGlass()
    {
        Mouse.OverrideCursor = Cursors.Wait;
        bActivated = true;
    }
    public void Show(bool Action = true)
    {
        if (Action)
        {
            Mouse.OverrideCursor = Cursors.Wait;
        }
        else
        {
            Mouse.OverrideCursor = Cursors.Arrow;
        }

        bActivated = Action;

    }
    #region "IDisposable Support"
    // To detect redundant calls
    private bool disposedValue;
    private bool bActivated;
    // IDisposable
    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposedValue)
        {
            if (disposing)
            {
                //remove todas as mensagens de mouse
                //e teclado que tenham sido produzidas
                //durante o processamento e estejam
                //enfileiradas
                if (bActivated)
                {
                    MSG pMSG = new MSG();
                    while (Convert.ToBoolean(PeekMessage(ref pMSG, 0, WM_KEYFIRST, WM_KEYLAST, PM_REMOVE)))
                    {
                    }
                    while (Convert.ToBoolean(PeekMessage(ref pMSG, 0, WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE)))
                    {
                    }
                    Mouse.OverrideCursor = Cursors.Arrow;

                }
            }

            // TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
            // TODO: set large fields to null.
        }
        this.disposedValue = true;
    }

    public void Dispose()
    {
        // Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    #endregion

}

Solution 8 - C#

I used Olivier Jacot-Descombes's solution, it's very simple and working well. thanks. update: it even works well without using a different threads/background worker.

I use it with backgroudworker, mouse cursor looks great when it's busy working and return to normal when the work is done.

public void pressButtonToDoSomeLongTimeWork()
{    
    Mouse.OverrideCursor = Cursors.Wait;
    // before the long time work, change mouse cursor to wait cursor

    worker.DoWork += doWorkLongTimeAsync;
    worker.RunWorkerCompleted += worker_RunWorkerCompleted;
    worker.RunWorkerAsync();  //start doing some long long time work but GUI can update
}

private void worker_RunWorkerCompleted(object sender,     
                                       RunWorkerCompletedEventArgs e)
{
    //long time work is done();
    updateGuiToShowTheLongTimeWorkResult();
    Mouse.OverrideCursor = null;  //return mouse cursor to normal
}

Solution 9 - C#

I know I'm late, I just changed the way I manage cursor Hourglass (busy state) of my application.

This proposed solution is more complex than my first answer but I think it is more complete and better.

I do not said I have an easy solution or complete one. But for me it is the best because it mostly fixes all the troubles I had managing the busy state of my application.

Advantages:

  • Can manage "Busy" state from either the model and the view.
  • Able to manage busy state either if there is no GUI. Decoupled from GUI.
  • Thread safe (can be used from any thread)
  • Support Busy override (show arrow temporarily) when there is a Window (Dialog) that should be visible in the middle of a very long transaction.
  • Able to stack many operations with busy behavior that show a constant hourglass either if it is part a many little long sub tasks. Having an hourglass which would not change frequently from busy to normal to busy. A constant busy state if possible, by using a stack.
  • Support event subsciption with weak pointer because "Global" object instance is global (will never be Garbage Collected - it is rooted).

The code is separated into few classes:

  • No GUI class: "Global" which manage Busy state and should be initialized at application start with the dispatcher. Because it is Global (singleton), I chosen to have weak NotifyPropertyChanged event in order to not keep a hard ref on anyone which want to be notify of any change.
  • A GUI class: AppGlobal which hook to Global and change Mouse appearance in accordance with Gloab.Busy state. It should be initialiazed with the dispatcher also at program start.
  • A GUI class to help Dialog(Window) to have proper mouse behavior when used into a long transaction where the mouse has been overriden to show an hourglass and want the regular arrow when the Dialog(Window) is in use.
  • The code also include some dependencies.

This is the usage:

Init:

public partial class App : Application
{
	// ******************************************************************
	protected override void OnStartup(StartupEventArgs e)
	{
		Global.Init(Application.Current.Dispatcher);
		AppGlobal.Init(Application.Current.Dispatcher);

Prefered usage:

		using (Global.Instance.GetDisposableBusyState())
		{
		...
		}

Other usage:

// ******************************************************************
public DlgAddAggregateCalc()
{
    InitializeComponent();
    Model = DataContext as DlgAddAggregateCalcViewModel;
	this.Activated += OnActivated;
	this.Deactivated += OnDeactivated;
}

// ************************************************************************
private void OnDeactivated(object sender, EventArgs eventArgs)
{
	Global.Instance.PullState();
}

// ************************************************************************
private void OnActivated(object sender, EventArgs eventArgs)
{
	Global.Instance.PushState(false);
}

Auto Window cursor:

public partial class DlgAddSignalResult : Window
{
	readonly WindowWithAutoBusyState _autoBusyState = new WindowWithAutoBusyState();

	// ******************************************************************
	public DlgAddSignalResult()
    {
        InitializeComponent();

		Model = DataContext as DlgAddSignalResultModel;

		_autoBusyState.Init(this);
	}

Code:

// Copyright (c) 2008 Daniel Grunwald
// 
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

/* Usage:
 * 
 * 
 * 
        public delegate void FileChangedHandler(object sender, FileSystemEventArgs e);

        [field: NonSerialized]
        private readonly SmartWeakEvent<FileChangedHandler> _weakFileChanged = new SmartWeakEvent<FileChangedHandler>();

        public event FileChangedHandler FileChanged
        {
            add
            {
                _weakFileChanged.Add(value);
            }
            remove
            {
                _weakFileChanged.Remove(value);
            }
        }
 *
 *
 */


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace HQ.Util.General.WeakEvent
{
	/// <summary>
	/// A class for managing a weak event.
	/// </summary>
	public sealed class SmartWeakEvent<T> where T : class
	{
		struct EventEntry
		{
			public readonly MethodInfo TargetMethod;
			public readonly WeakReference TargetReference;

			public EventEntry(MethodInfo targetMethod, WeakReference targetReference)
			{
				this.TargetMethod = targetMethod;
				this.TargetReference = targetReference;
			}
		}

		readonly List<EventEntry> eventEntries = new List<EventEntry>();

		// EO: Added for ObservableCollectionWeak
		public int CountOfDelegateEntry
		{
			get
			{
				RemoveDeadEntries();
				return eventEntries.Count;
			}
		}

		static SmartWeakEvent()
		{
			if (!typeof(T).IsSubclassOf(typeof(Delegate)))
				throw new ArgumentException("T must be a delegate type");
			MethodInfo invoke = typeof(T).GetMethod("Invoke");
			if (invoke == null || invoke.GetParameters().Length != 2)
				throw new ArgumentException("T must be a delegate type taking 2 parameters");
			ParameterInfo senderParameter = invoke.GetParameters()[0];
			if (senderParameter.ParameterType != typeof(object))
				throw new ArgumentException("The first delegate parameter must be of type 'object'");
			ParameterInfo argsParameter = invoke.GetParameters()[1];
			if (!(typeof(EventArgs).IsAssignableFrom(argsParameter.ParameterType)))
				throw new ArgumentException("The second delegate parameter must be derived from type 'EventArgs'");
			if (invoke.ReturnType != typeof(void))
				throw new ArgumentException("The delegate return type must be void.");
		}

		public void Add(T eh)
		{
			if (eh != null)
			{
				Delegate d = (Delegate)(object)eh;

				if (d.Method.DeclaringType.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Length != 0)
					throw new ArgumentException("Cannot create weak event to anonymous method with closure.");

				if (eventEntries.Count == eventEntries.Capacity)
					RemoveDeadEntries();
				WeakReference target = d.Target != null ? new WeakReference(d.Target) : null;
				eventEntries.Add(new EventEntry(d.Method, target));
			}
		}

		void RemoveDeadEntries()
		{
			eventEntries.RemoveAll(ee => ee.TargetReference != null && !ee.TargetReference.IsAlive);
		}

		public void Remove(T eh)
		{
			if (eh != null)
			{
				Delegate d = (Delegate)(object)eh;
				for (int i = eventEntries.Count - 1; i >= 0; i--)
				{
					EventEntry entry = eventEntries[i];
					if (entry.TargetReference != null)
					{
						object target = entry.TargetReference.Target;
						if (target == null)
						{
							eventEntries.RemoveAt(i);
						}
						else if (target == d.Target && entry.TargetMethod == d.Method)
						{
							eventEntries.RemoveAt(i);
							break;
						}
					}
					else
					{
						if (d.Target == null && entry.TargetMethod == d.Method)
						{
							eventEntries.RemoveAt(i);
							break;
						}
					}
				}
			}
		}

		public void Raise(object sender, EventArgs e)
		{
			int stepExceptionHelp = 0;

			try
			{
				bool needsCleanup = false;
				object[] parameters = {sender, e};
				foreach (EventEntry ee in eventEntries.ToArray())
				{
					stepExceptionHelp = 1;
					if (ee.TargetReference != null)
					{
						stepExceptionHelp = 2;
						object target = ee.TargetReference.Target;
						if (target != null)
						{
							stepExceptionHelp = 3;
							ee.TargetMethod.Invoke(target, parameters);
						}
						else
						{
							needsCleanup = true;
						}
					}
					else
					{
						stepExceptionHelp = 4;
						ee.TargetMethod.Invoke(null, parameters);
					}
				}
				if (needsCleanup)
				{
					stepExceptionHelp = 5;
					RemoveDeadEntries();
				}

				stepExceptionHelp = 6;
			}
			catch (Exception ex)
			{
				string appName = Assembly.GetEntryAssembly().GetName().Name;
				if (!EventLog.SourceExists(appName))
				{
					EventLog.CreateEventSource(appName, "Application");
					EventLog.WriteEntry(appName,
						String.Format("Exception happen in 'SmartWeakEvent.Raise()': {0}. Stack: {1}. Additional int: {2}.", ex.Message, ex.StackTrace, stepExceptionHelp), EventLogEntryType.Error);
				}

				throw;
			}
		}
	}
}


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using System.Xml.Serialization;
using HQ.Util.General.Log;
using HQ.Util.General.WeakEvent;
using JetBrains.Annotations;

// using System.Windows.Forms;

// using Microsoft.CSharp.RuntimeBinder;

// ATTENTION: Can only be used with Framework 4.0 and up

namespace HQ.Util.General.Notification
{
	[Serializable]
	public class NotifyPropertyChangedThreadSafeAsyncWeakBase : INotifyPropertyChanged
	{
		// ******************************************************************
		[XmlIgnore]
		[field: NonSerialized]
		public SmartWeakEvent<PropertyChangedEventHandler> SmartPropertyChanged = new SmartWeakEvent<PropertyChangedEventHandler>();

		[XmlIgnore]
		[field: NonSerialized]
		private Dispatcher _dispatcher = null;

		// ******************************************************************
		public event PropertyChangedEventHandler PropertyChanged
		{
			add
			{
				SmartPropertyChanged.Add(value);
			}
			remove
			{
				SmartPropertyChanged.Remove(value);
			}
		}

		// ******************************************************************
		[Browsable(false)]
		[XmlIgnore]
		public Dispatcher Dispatcher
		{
			get
			{
				if (_dispatcher == null)
				{
					_dispatcher = Application.Current?.Dispatcher;
					if (_dispatcher == null)					
					{ 
						if (Application.Current?.MainWindow != null)
						{
							_dispatcher = Application.Current.MainWindow.Dispatcher;
						}
					}
				}

				return _dispatcher;
			}
			set
			{
				if (_dispatcher == null && _dispatcher != value)
				{
					Debug.Print("Dispatcher has changed??? ");
				}

				_dispatcher = value;
			}
		}

		// ******************************************************************
		[NotifyPropertyChangedInvocator]
		protected void NotifyPropertyChanged([CallerMemberName] String propertyName = null)
		{
			try
			{
				if (Dispatcher == null || Dispatcher.CheckAccess()) // Can be null in case of Console app
				{
					SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
				}
				else
				{
					Dispatcher.BeginInvoke(
						new Action(() => SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName))));
				}
			}
			catch (TaskCanceledException ex) // Prevent MT error when closing app...
			{
				Log.Log.Instance.AddEntry(LogType.LogException, "An exception occured when trying to notify.", ex);
			}
		}

		// ******************************************************************
		[NotifyPropertyChangedInvocator]
		protected void NotifyPropertyChanged<T2>(Expression<Func<T2>> propAccess)
		{
			try
			{
				var asMember = propAccess.Body as MemberExpression;
				if (asMember == null)
					return;

				string propertyName = asMember.Member.Name;

				if (Dispatcher == null || Dispatcher.CheckAccess()) // Can be null in case of Console app
				{
					SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName));
				}
				else
				{
					Dispatcher.BeginInvoke(new Action(() => SmartPropertyChanged.Raise(this, new PropertyChangedEventArgs(propertyName))));
				}
			}
			catch (TaskCanceledException ex) // Prevent MT error when closing app...
			{
				Log.Log.Instance.AddEntry(LogType.LogException, "An exception occured when trying to notify.", ex);
			}

		}



		// ******************************************************************
		protected bool UpdateField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
		{
			if (!EqualityComparer<T>.Default.Equals(field, value))
			{
				field = value;
				NotifyPropertyChanged(propertyName);
				return true;
			}
			return false;
		}

		// ******************************************************************
	}
}


using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using HQ.Util.General.Notification;

namespace HQ.Util.General
{
	/// <summary>
	/// This is the centralized calss to obtain the Dispatcher in order to make sure to get the appropriate one (the MainWindow dispatcher).
	/// If you need a specidifc thread dispatcher, please refer to System.Windows.Threading.Dispatcher or Dispatcher.CurrentDispatcher.
	/// This dispatcher should be set at the initialization (start) of any GUI executable: ex: Global.Instance.Dispatcher = Application.Current.Dispatcher;

	/// </summary>
	public class Global : NotifyPropertyChangedThreadSafeAsyncWeakBase
	{
		public delegate void IsBusyChangeHandler(bool isBusy);

		/// <summary>
		/// This event happen only the UI thread in low priority
		/// </summary>
		public event IsBusyChangeHandler IsBusyChange;

		private readonly ConcurrentStack<bool> _stackBusy = new ConcurrentStack<bool>();

		// ******************************************************************
		public static void Init(Dispatcher dispatcher)
		{
			Instance.Dispatcher = dispatcher;
		}

		// ******************************************************************
		public static Global Instance = new Global();

		// ******************************************************************
		private Global()
		{
			_busyLifeTrackerStack = new LifeTrackerStack(() => PushState(), PullState);
		}

		// ******************************************************************
		/// <summary>
		/// Will set busy state temporary until object is disposed where 
		/// the state will be back to its previous state.
		/// </summary>
		/// <param name="isBusy"></param>
		/// <returns></returns>
		public LifeTracker GetDisposableBusyState(bool isBusy = true)
		{
			return new LifeTracker(() => PushState(isBusy), PullState);
		}

		// ******************************************************************
		private bool _isBusy;

		/// <summary>
		/// This property should be use by the GUI part in order to control the mouse cursor
		/// </summary>
		public bool IsBusy
		{
			get => _isBusy;

			private set
			{
				if (value == _isBusy) return;
				_isBusy = value;
				Dispatcher.BeginInvoke(new Action(() => IsBusyChange?.Invoke(_isBusy)), DispatcherPriority.ContextIdle);
				NotifyPropertyChanged();
			}
		}

		private readonly object _objLockBusyStateChange = new object();
		// ******************************************************************
		/// <summary>
		/// Always prefer usage of "Using(Global.Instance.GetDisposableBusyState())" whenever possible.
		/// Otherwise ensure to call Pull State to get back previous state of the cursor when action is 
		/// completed
		/// </summary>
		/// <param name="isBusy"></param>
		public void PushState(bool isBusy = true)
		{
			lock (_objLockBusyStateChange)
			{
				_stackBusy.Push(isBusy);
				IsBusy = isBusy;
			}
		}

		// ******************************************************************
		public void PullState()
		{
			lock (_objLockBusyStateChange)
			{
				_stackBusy.TryPop(out bool isBusy);

				if (_stackBusy.TryPeek(out isBusy))
				{
					IsBusy = isBusy;
				}
				else
				{
					IsBusy = false;
				}
			}
		}

		// ******************************************************************
		private readonly LifeTrackerStack _busyLifeTrackerStack = null;

		/// <summary>
		/// Only kept for historical reason / compatibility with previous code
		/// </summary>
		[Obsolete("Use direct 'using(Gloabl.Instance.GetDisposableBusyState(isBusy))' which is simpler")]
		public LifeTrackerStack BusyLifeTrackerStack
		{
			get { return _busyLifeTrackerStack; }
		}

		// ******************************************************************
		// private int _latestVersionExecuted = 0;
		private int _currentVersionRequired = 0;
		private readonly object _objLockRunOnce = new object();

		private readonly Dictionary<int, GlobalRunOncePerQueueData> _actionsToRunOncePerQueue =
			new Dictionary<int, GlobalRunOncePerQueueData>();

		private readonly int _countOfRequestInQueue = 0;

		/// <summary>
		/// It will record all action once per key and it
		/// once per Dispatcher queue roll over (on ContextIdle).
		/// When the dispatcher reach the DispatcherPriority.ContextIdle, it will
		/// run all action once.
		/// Thread safe... no action will be lost but can be run twice or more if
		/// some are added by other thread(s) at the same time one is executed.
		/// </summary>
		/// EO: sorry for the name but it is the best found
		/// <param name="key"></param>
		/// <param name="action"></param>
		public void RunOncePerQueueRollOnDispatcherThread(int key, Action action)
		{
			lock (_objLockRunOnce)
			{
				if (!_actionsToRunOncePerQueue.TryGetValue(key, out GlobalRunOncePerQueueData data))
				{
					data = new GlobalRunOncePerQueueData(action);
					_actionsToRunOncePerQueue.Add(key, data);
				}

				_currentVersionRequired++;
				data.VersionRequired = _currentVersionRequired;
			}

			if (_countOfRequestInQueue <= 1)
			{
				Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(ExecuteActions));
			}
		}

		// ******************************************************************
		private void ExecuteActions()
		{
			int versionExecute;

			List<GlobalRunOncePerQueueData> datas = null;
			lock (_objLockRunOnce)
			{
				versionExecute = _currentVersionRequired;
				datas = _actionsToRunOncePerQueue.Values.ToList();
			}

			foreach (var data in datas)
			{
				data.Action();
			}

			lock (_objLockRunOnce)
			{
				List<int> keysToRemove = new List<int>();

				foreach (var kvp in _actionsToRunOncePerQueue)
				{
					if (kvp.Value.VersionRequired <= versionExecute)
					{
						keysToRemove.Add(kvp.Key);
					}
				}

				keysToRemove.ForEach(k => _actionsToRunOncePerQueue.Remove(k));

				if (_actionsToRunOncePerQueue.Count == 0)
				{
					// _latestVersionExecuted = 0;
					_currentVersionRequired = 0;
				}
				else
				{
					// _latestVersionExecuted = versionExecute;
				}
			}
		}

		// ******************************************************************
	}
}

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
using HQ.Util.General;
using HQ.Util.General.Notification;
using Microsoft.VisualBasic.Devices;
using System.Windows;
using Mouse = System.Windows.Input.Mouse;
using System.Threading;

namespace HQ.Wpf.Util
{
	public class AppGlobal
	{
		// ******************************************************************
		public static void Init(Dispatcher dispatcher)
		{
			if (System.Windows.Input.Keyboard.IsKeyDown(Key.LeftShift) || System.Windows.Input.Keyboard.IsKeyDown(Key.RightShift))
			{
				var res = MessageBox.Show($"'{AppInfo.AppName}' has been started with shift pressed. Do you want to wait for the debugger (1 minute wait)?", "Confirmation", MessageBoxButton.YesNo,
					MessageBoxImage.Exclamation, MessageBoxResult.No);

				if (res == MessageBoxResult.Yes)
				{
					var start = DateTime.Now;

					while (!Debugger.IsAttached)
					{
						if ((DateTime.Now - start).TotalSeconds > 60)
						{
							break;
						}
						Thread.Sleep(100);
					}
				}
			}

			if (dispatcher == null)
			{
				throw new ArgumentNullException();
			}

			Global.Init(dispatcher);
			Instance.Init();
		}

		// ******************************************************************
		public static readonly AppGlobal Instance = new AppGlobal();

		// ******************************************************************
		private AppGlobal()
		{
		}

		// ******************************************************************
		private void Init()
		{
			Global.Instance.PropertyChanged += AppGlobalPropertyChanged;
		}

		// ******************************************************************
		void AppGlobalPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
		{
			if (e.PropertyName == "IsBusy")
			{
				if (Global.Instance.IsBusy)
				{
					if (Global.Instance.Dispatcher.CheckAccess())
					{
						Mouse.OverrideCursor = Cursors.Wait;
					}
					else
					{
						Global.Instance.Dispatcher.BeginInvoke(new Action(() => Mouse.OverrideCursor = Cursors.Wait));
					}
				}
				else
				{
					if (Global.Instance.Dispatcher.CheckAccess())
					{
						Mouse.OverrideCursor = Cursors.Arrow;
					}
					else
					{
						Global.Instance.Dispatcher.BeginInvoke(new Action(() => Mouse.OverrideCursor = Cursors.Arrow));
					}
				}
			}
		}

		// ******************************************************************
	}
}



using HQ.Util.General;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;

namespace HQ.Wpf.Util
{
	/// <summary>
	/// Ensure window cursor is "normal" (arrow) when visible.
	/// Usage: In Window class, define a member: OverrideCursorMtForWindow. Instantiate in constructor after Initialisation.
	/// </summary>
	public class WindowWithAutoBusyState
    {
        // ******************************************************************
        Window _window;
        bool _nextStateShoulBeVisible = true;

		// ******************************************************************
		public WindowWithAutoBusyState()
		{

		}

		// ******************************************************************
		public void Init(Window window)
        {
            _window = window;

			_window.Cursor = Cursors.Wait;
            _window.Loaded += (object sender, RoutedEventArgs e) => _window.Cursor = Cursors.Arrow;

            _window.IsVisibleChanged += WindowIsVisibleChanged;
            _window.Closed += WindowClosed;
        }

        // ******************************************************************
        private void WindowIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            if (_window.IsVisible)
            {
                if (_nextStateShoulBeVisible)
                {
					Global.Instance.PushState(false);
                    _nextStateShoulBeVisible = false;
                }
            }
            else
            {
                if (!_nextStateShoulBeVisible)
                {
					Global.Instance.PullState();
                    _nextStateShoulBeVisible = true;
                }
            }
        }

        // ******************************************************************
        private void WindowClosed(object sender, EventArgs e)
        {
            if (!_nextStateShoulBeVisible)
            {
				Global.Instance.PullState();
				_nextStateShoulBeVisible = true;
            }
        }

        // ******************************************************************

    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionsean717View Question on Stackoverflow
Solution 1 - C#CarloView Answer on Stackoverflow
Solution 2 - C#T.J.KjaerView Answer on Stackoverflow
Solution 3 - C#Olivier Jacot-DescombesView Answer on Stackoverflow
Solution 4 - C#John GardnerView Answer on Stackoverflow
Solution 5 - C#Eric OuelletView Answer on Stackoverflow
Solution 6 - C#AllenMView Answer on Stackoverflow
Solution 7 - C#user1780087View Answer on Stackoverflow
Solution 8 - C#ntchrisView Answer on Stackoverflow
Solution 9 - C#Eric OuelletView Answer on Stackoverflow