locked
How to deactivate really a button in C# Winforms RRS feed

  • Question

  • Hello,

    With C# winforms, I try to create a small application, and I try to deactivate a button after clicking on it.
    To disable the button, I use the following line:

    button.Enabled = false;

    but I realize that the button is grayed out but not disabled because when I click on it, it starts processing again.

    How to actually disable a button under C# winforms?

    Thanks.

    • Moved by CoolDadTx Saturday, July 11, 2020 1:08 PM Winforms related
    Saturday, July 11, 2020 11:17 AM

All replies

  • Setting the `Enabled` property will disable a button and prevent it from being clicked. This will prevent the click event from being raised. Here's a very simple example where clicking `btnState` toggles the state of `btnTarget`. Every time `btnTarget` is clicked it updates a counter tied to a label. Disabling the button and then clicking it does nothing.

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    
        private void btnState_Click ( object sender, EventArgs e )
        {
            btnTarget.Enabled = !btnTarget.Enabled;
        }
    
        private int _count = 0;
    
        private void btnTarget_Click ( object sender, EventArgs e )
        {
            label1.Text = (_count++).ToString();
        }
    }

    If you are seeing the button getting clicked there are a couple of possibilities.

    1. You have the event handler tied to more than just that button. Set a breakpoint on the event handler and then look at the `sender` property and the call stack to see where the event is coming from.

    2. You are directly calling the handler yourself rather than it being triggered by the system. Find all references on the event handler.

    If you cannot figure this out then post the relevant code that is disabling the button and any place you are calling that handler.



    Michael Taylor http://www.michaeltaylorp3.net

    Saturday, July 11, 2020 1:08 PM
  • Do you also re-enable the button at the end of long-running handler?

    Saturday, July 11, 2020 1:18 PM
  • you have the correct code to disable a button.

    Possible causes for the click handler to be reentered:

    You have code that reenters the message pump (e.g. Application.DoEvents) before this line and a click was generated in that message loop.

    You reenabled the button somewhere else. 



    Visual C++ MVP

    Saturday, July 11, 2020 5:35 PM
  • Thank you for your reply.

    Here is the code:

    private void buttonWriteFast_Click (object sender, EventArgs e)
    {
      buttonWriteFast.Enabled = false;
      WriteFast();
      buttonWriteFast.Enabled = true;
    }


    When I click on the button, the button turns gray, and the WriteFast() function starts for 3 minutes, but if I click on the gray button which is supposed to be inactive, the function starts again after 3 minutes.
    I do not understand why the button is not disabled when it is grayed out.

    Would you know why and have a sample code to fix this, please?

    Thanks.

    Monday, July 13, 2020 12:56 AM
  • The button is disabled. Likely your 3 minute WriteFast function is blocking your UI thread so your click, whilst remembered, is not processed until after WriteFast returns. You will have the same issue if you use separate events like in your other question.

    Look at the Background Task Component

    https://docs.microsoft.com/en-us/dotnet/framework/winforms/controls/how-to-implement-a-form-that-uses-a-background-operation

    and

    https://docs.microsoft.com/en-us/dotnet/framework/winforms/controls/walkthrough-implementing-a-form-that-uses-a-background-operation


    Monday, July 13, 2020 1:26 AM
  • You have a threading issue. In order to update a UI element the code must run on the UI thread. A thread can do only one thing at a time. Most UI interactions trigger a message which has to be processed by the message pump that is running on the UI thread. Your button click handler is triggered when the message pump gets the click message on your button.

    So you are on the UI thread and click your button. This trigger a call to your function. You set the Enabled property to false but for that to take effect the UI thread has to do work. But it can't because you are already in the middle of processing a message. Your WriteFast method then begins to run. Meanwhile your UI thread is frozen waiting for the method to complete. During this time the user can interact with the UI but nothing will happen other than messages getting added to the message queue for the UI thread to be processed after your button handler finishes running. After your WriteFast finishes then the Enabled button is changed back. But since the UI thread has been blocked all this time nothing has really happened until your button handler returns.

    This is not a good way to do long things in the UI. If you absolutely must block the UI for a long running process then you have to update the UI, force the message queue to be emptied and then make your blocking call. Otherwise all the UI changes before the blocking call are not going to have much effect. Here's sample code demoing the problem.

    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
    
        private void buttonWriteFast_Click ( object sender, EventArgs e )
        {
            buttonWriteFast.Enabled = false;
            label1.Text = (++_count).ToString();
    
            WriteFast();
    
            buttonWriteFast.Enabled = true;
        }
    
        private void WriteFast ()
        {
            System.Threading.Thread.Sleep(5000);
        }
    
        private int _count = 0;
    }

    When the button is clicked it is disabled, a label is updated with the # of times it was clicked and your method is run. When you click the button the label isn't going to get updated because that requires a repaint which requires a message that cannot be run yet. To fix this you have to force the message pump to run.

    private void buttonWriteFast_Click ( object sender, EventArgs e )
    {
        buttonWriteFast.Enabled = false;
        label1.Text = (++_count).ToString();
    
        //Force message pump
        Application.DoEvents();
    
        WriteFast();
    
        buttonWriteFast.Enabled = true;
    }

    Now the UI updates before the blocking call is made. But you will still find your button is clickable while it is disabled. Why is that? Because clicking the button triggers a message on the UI thread to click the button. Disabling a control doesn't prevent the click button from triggering. What the button does, when clicked, is check to see if it enabled or not and then react. However since the UI thread cannot process messages that click event is sitting in the queue. When the queue is finally allowed to be processed the button has already been enabled by your code so the button click is processed again.

    The correct solution to this problem is to not block your UI. The easiest way to do that is to use an async method.

    private async void buttonWriteFast_Click ( object sender, EventArgs e )
    {
        buttonWriteFast.Enabled = false;
        label1.Text = (++_count).ToString();
    
        await WriteFastAsync();
    
        buttonWriteFast.Enabled = true;
    }
    
    private Task WriteFastAsync ()
    {
        return Task.Delay(5000);
    }

    A couple of changes here. Firstly the event handler is marked async. This is the only case where an async method should have a void return type. The Winforms library handles this case specially and will behave properly. All the code before the `await` is run on the UI thread just like before. However when the `await` is reached the method is pushed to a separate thread and a task is returned to the caller. This allows the UI thread to continue on so it starts processing messages again and you see the UI updates. Furthermore if the user clicks the button again it is disabled and is ignored. Eventually the awaited method finishes and executes resumes back on the UI thread (just like a message would be handled). So everything after that await also runs on the UI thread which allows you to make more UI changes. You can weave `await` and UI interactions together to make it easier to write non-blocking UIs. Note that you need to be a little careful about deadlocking but there are plenty of blog articles on that.

    The other change I made was to put the blocking logic into an async method. To await a method it must return a Task and so you should generally wrap the logic in an async method. There are plenty of articles on how to do that. If you absolutely must call a method that does not understand async then you can use Task.Run but that should fixed as soon as possible.

    The other approach to solving this problem is to use a BackgroundWorker component. However with the advent of async/await you shouldn't need BWC in most cases. async/await requires less code from you.


    Michael Taylor http://www.michaeltaylorp3.net

    Monday, July 13, 2020 1:41 AM
  • I hope it's ok to say this, I'm not contradicting you your answer is detailed and accurate but  I don't think you should be sending him on the async route at this time. He hasn't got his head around events fully yet. Async will just complicate things even more for him. You need to understand what's going on to properly implement async/await.
    • Edited by stevemrf Monday, July 13, 2020 1:55 AM
    Monday, July 13, 2020 1:55 AM
  • If you do not need to perform other operations or to cancel the task, then consider this preliminary workaround too:

    [DllImport( "User32" )]
    static extern UInt32 GetMessageTime( );
    
    private UInt32 mt = 0;
    
    private void buttonWriteFast_Click( object sender, EventArgs e )
    {
       if( GetMessageTime( ) < mt ) return;
    
       buttonWriteFast.Enabled = false;
       buttonWriteFast.Refresh( );
    
       try
       {
          WriteFast( );
       }
       finally
       {
          buttonWriteFast.Enabled = true;
          mt = (UInt32)Environment.TickCount;
       }
    }

    Although, it is usually inconvenient to block the application in this manner.


    • Edited by Viorel_MVP Monday, July 13, 2020 9:28 AM
    Monday, July 13, 2020 6:48 AM