(Print this page)

WaitCursor/HourGlass cursor for .Net Windows Forms project
Published date: Friday, July 31, 2020
On: Moer and Éric Moreau's web site

Lately, I was trying to use the Edit-And-Continue feature of Visual Studio 2019 and remembered that this feature has not been working for me on 2 different computers but never had the chance to dig to find the root cause. I was able to edit my code, but the modifications were not recompiled on the fly and considered until I stopped the debugging session and start the debugging all over again. Not really the spirit of that feature and counter-productive! Now that I have some time, I went the extra mile and found the culprit: PostSharp.

In this article, I will not bash on PostSharp. I still think that it is an unbelievably valuable tool for some projects but maybe not all of them as the Edit-And-Continue feature is too important. I will show you how I replaced the cursor aspect (easily implemented using an attribute through Aspect Oriented Programming) with code.

First, why isn’t PostSharp and Edit-And-Continue working hand in hand?

I have been using PostSharp in many projects over the last few years to save some redundant lines of code here and there in my projects. Examples of things I let PostSharp handle with great success were caching, logging, and the HourGlass/WaitCursor management. This is very convenient and valuable.

After I confirmed that the one thing preventing Edit-And-Continue to work as expected, I have contacted PostSharp regarding my issue and got an answer from them confirming my problem:

As a post-compiler, PostSharp needs to integrate into the project's build chain. Edit-and-continue recompiles the source code outside of the standard build chain and it makes it very difficult for us to integrate with this feature.

Now that I think of it, it is totally understandable. The code I wrote is not the same one that is being debugged because of the code injected by PostSharp. The debugger can still navigate through the code correctly, but the Edit-And-Continue feature cannot just recompile the code on the fly.

It is clearly identified as a known issue in the PostSharp’s documentation.

Available source code

Both VB and C# projects are provided this month.

The solution was created using Visual Studio 2019 but should also work in earlier versions.

Figure 1: The demo application in action

What is wrong with simply setting the Current cursor?

You may ask what is wrong with this kind of syntax:

Cursor.Current = Cursors.WaitCursor
' .. do something
Cursor.Current = Cursors.Default

There is usually nothing wrong with setting the Current cursor like in this example.

The first thing that can happen here is that your code is using too many resources and the statement is not executed right away giving the user the impression that nothing is running. That might lead the users to launch the action a second time.

You may also find situations in which you have a calling chain of methods in which one of the methods will reset the Current cursor to Default before the full chain has completed. This can give users an incorrect feeling that the process has completed when in reality, it is not.

Many implementations found

When you search the Internet, this problem as old as the Windows Forms themselves and does not have one clear pattern to handle it. Many people are offering different solutions. I have tested many and they all had a little something I was not fan of (and many are simply not working!).

Most of the time, the cursor was not always changed on a timely manner (which is exactly my first argument of the previous section).

I took ideas from a few of these implementations and build up my own. I also simplified it to a single class (whereas other implementations were spreading over multiple classes).

First, the little reusable class

Let us first show that little class that I named cHourGlass. This class implements the IDisposable interface so we can use the Using pattern to automatically get rid of the instance and return to the default cursor.

Public Class cHourGlass

    Implements IDisposable

    <Runtime.InteropServices.DllImport("user32.dll")>
    Private Shared Function SendMessage(ByVal hWnd As IntPtr, ByVal msg As Integer, ByVal wp As IntPtr, ByVal lp As IntPtr) As IntPtr
    End Function


    Private ReadOnly _formInstance As Form
    Private Shared _instanceCounter As Integer

    Public Sub New(ByVal pFormInstance As Form)
        _formInstance = pFormInstance
        _instanceCounter += 1
        Enabled = True
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        _instanceCounter -= 1
        Enabled = False
    End Sub

    Public Property Enabled As Boolean
        Get
            Return _formInstance.UseWaitCursor
        End Get
        Set(ByVal value As Boolean)
            If _formInstance Is Nothing OrElse Enabled = value Then Return
            If Application.UseWaitCursor AndAlso value = False Then Return
            If value = False AndAlso _instanceCounter > 0 Then Return

            _formInstance.UseWaitCursor = value

            If _formInstance.InvokeRequired Then
                _formInstance.BeginInvoke(New Action(Sub()
                                                         If _formInstance.Handle <> IntPtr.Zero Then
                                                             SendMessage(_formInstance.Handle, &H20, _formInstance.Handle, CType(1, IntPtr))
                                                         End If
                                                     End Sub))
            Else
                If _formInstance.Handle <> IntPtr.Zero Then
                    SendMessage(_formInstance.Handle, &H20, _formInstance.Handle, CType(1, IntPtr))
                End If
            End If
        End Set
    End Property

End Class
using System;
using System.Windows.Forms;

namespace DemoHourGlassCS
{
    public class cHourGlass : IDisposable
    {

        [System.Runtime.InteropServices.DllImport("user32.dll")] 
        private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);


        private readonly Form _formInstance;
        private static int _instanceCounter;

        public cHourGlass(Form pFormInstance)
        {
            _formInstance = pFormInstance;
            _instanceCounter += 1;
            Enabled = true;
        }
        
        public void Dispose()
        {
            _instanceCounter -= 1;
            Enabled = false;
        }

        public bool Enabled
        {
            get => _formInstance.UseWaitCursor;
            set
            {
                if (_formInstance == null || Enabled == value) return;
                if (Application.UseWaitCursor && value == false) return;
                if (value == false && _instanceCounter > 0) return;

                _formInstance.UseWaitCursor = value;

                if (_formInstance.InvokeRequired)
                {
                    _formInstance.BeginInvoke(new Action(() =>
                    {
                        if (_formInstance.Handle != IntPtr.Zero)
                            SendMessage(_formInstance.Handle, 0x20, _formInstance.Handle, (IntPtr)1); // Send WM_SETCURSOR
                    }));
                }
                else
                {
                    if (_formInstance.Handle != IntPtr.Zero)
                        SendMessage(_formInstance.Handle, 0x20, _formInstance.Handle, (IntPtr)1); // Send WM_SETCURSOR
                }
            }
        }
    }
}

Two things are worth mentioning.

The first one is the shared (or static in C#) variable named _instanceCounter. When a new instance is created, it is incremented. When it is disposed, it is decremented. This helps us fix the second issue (one method called revert the cursor to the default before the full process has complete).

The other thing worth mentioning is the API call SendMessage. This will force to update the mouse cursor immediately.

And as a bonus, do you see that the code that checks InvokeRequired. Yes, it can be called from a different thread and should not break anything!

How do we use that class now?

My demo form is also amazingly simple. It is composed of a button and a label. When the button is clicked, it creates an instance of the cHourGlass class and call 2 methods.

Public Class Form1

    Private Sub button1_Click(sender As Object, e As EventArgs) Handles button1.Click
        label1.Text = "Process started"
        'Application.DoEvents()
        Using New cHourGlass(Me)
            DoProcess()
            label1.Text = "Process 1 has completed, now going to Process 2"
            Application.DoEvents()
            DoProcess2()
        End Using

        label1.Text = "Process completed"
    End Sub

    Private Sub DoProcess()
        Using New cHourGlass(Me)
            For i As Integer = 0 To 7000 - 1
                label1.Text = $"DoProcess: {i}"
                'Application.DoEvents()
            Next
        End Using
    End Sub

    Private Sub DoProcess2()
        For i As Integer = 0 To 7000 - 1
            label1.Text = $"DoProcess2: {i}"
            'Application.DoEvents()
        Next
    End Sub
End Class
using System;
using System.Windows.Forms;

namespace DemoHourGlassCS
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            label1.Text = "Process started";
            //Application.DoEvents();
            using (new cHourGlass(this))
            {
                DoProcess();
                label1.Text = "Process 1 has completed, now going to Process 2";
                Application.DoEvents();
                DoProcess2();
            }
            label1.Text = "Process completed";
        }

        private void DoProcess()
        {
            using (new cHourGlass(this))
            {
                for (int i = 0; i < 7000; i++)
                {
                    label1.Text = $"DoProcess: {i}";
                    //Application.DoEvents();
                }
            }
        }

        private void DoProcess2()
        {
            for (int i = 0; i < 7000; i++)
            {
                label1.Text = $"DoProcess2: {i}";
                //Application.DoEvents();
            }
        }
    }
}

So, my DoProcess method is an example of a method being called that does reset the cursor to Default but which is part of a larger process that is still not completely done yet. When you run the demo application, you can see that we still see the WaitCursor even while the DoProcess2 method runs.

Conclusion

Not as elegant as simply adding an attribute to a method but at least I have more control of when it is instantiated and disposed with not too many additional lines of code.

And now I have my Edit-And-Continue feature working in my projects.


(Print this page)