5

Possible Duplicate:
How to block Winforms UI while background thread is running

i am using a C# WinForm application

I have a Save button on the screen where the data of the screen is saved to database.

what happens, when user clicks to button application goes to database and saves data. it takes some time.

but mean while if user again click on the Save button the Click event get catched and when the first Click return to main code (after saving database) the caught event get fired..

In short the click event get caught and fired when the thread returns from the first event ( I tried the scenario of enable/disable the button).

How can I stop this behavior.

Regards, Akhil

@Jalal: I tried this code with some modification as

private readonly object _userActivityLocker = new object();
        private void btnSave_Click(object sender, EventArgs e)
        {
            if (System.Threading.Monitor.TryEnter(_userActivityLocker))
            {
                //note that any sub clicks will be ignored while we are here
                try
                {
                    DateTime dt =  DateTime.Now;
                    Thread.Sleep(2000);
                    Debug.Print("FirstClick {0} Second Click {1}",dt.ToLongTimeString(),  DateTime.Now.ToLongTimeString());
                    //here it is safe to call the save and you can disable the btn
                    Application.DoEvents();
                }
                finally
                {
                    System.Threading.Monitor.Exit(_userActivityLocker);
                    //re-enable the btn if you disable it.
                }
            }
        }

but when i rapidly click on button (i checked with 5 times rapid clicks) 5 click events has been fired and console window is showing

FirstClick 1:30:22 PM Second Click 1:30:24 PM FirstClick 1:30:24 PM Second Click 1:30:26 PM FirstClick 1:30:26 PM Second Click 1:30:28 PM FirstClick 1:30:28 PM Second Click 1:30:30 PM FirstClick 1:30:30 PM Second Click 1:30:32 PM

Community
  • 1
  • 1
Akhil
  • 1,421
  • 1
  • 16
  • 21

3 Answers3

5

The problem is that your program is dead to the world while it is saving the data to the database. The user's mouse click is sitting in the message queue, waiting for your UI thread to come back to life. When it does, the button is no longer disabled so the Click event fires.

You can solve it by emptying the message queue before you re-enable the button so the click is processed while the button is disabled:

    private void button1_Click(object sender, EventArgs e) {
        button1.Enabled = false;
        // Save data to database
        //...
        System.Threading.Thread.Sleep(2000);

        Application.DoEvents();   // Empty the message queue
        if (!button1.IsDisposed) button1.Enabled = true;
    }

Do not skip the IsDisposed test, DoEvents is dangerous because it isn't selective about what events get processed. It will happily let the user close the main window while your code is still running.

But the better solution is to not let your UI thread go dead like this. Use a BackgroundWorker to perform the save on a worker thread. This will also avoid the ugly "Not Responding" ghost window that Windows puts up when your save takes more than a couple of seconds. It probably doesn't do this yet right now, but it will a year from now when the dbase has grown. You can re-enable the button in the BGW's RunWorkerCompleted event handler.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • @Hans Passant: if the user pressed the button twice too quickly the enable/disable would not help, this could happens when the server is too load... – Jalal Said Jun 21 '11 at 13:07
  • Hmm, you clearly didn't try this code. – Hans Passant Jun 21 '11 at 13:18
  • @Hans Passant: no!, sure I tested this, you can test it your self now, just do the following: create a new Form and add button1 to it and on its click event handler paste this code `button1.Enabled = false; Console.WriteLine("First Message"); Thread.Sleep(2000); Console.WriteLine("second Message"); button1.Enabled = true;` and of course you have to double click the button or click twice quickly to see that the method will be called twice..... the output **will be** First Message second Message First Message second Message – Jalal Said Jun 21 '11 at 13:27
  • You forgot to add the DoEvents call, the crux of the answer. – Hans Passant Jun 21 '11 at 19:19
  • @Hans Passant: yes, you are right, I forget about adding Application.DoEvents(); However clearing the message pomp is something that I will not do for the next 1000 years, only if disable the whole Form and its control like your own [suggesion](http://stackoverflow.com/questions/5181777/c-application-doevents/5183623#5183623) "+1 for that answer", and even then I would not use it. – Jalal Said Jun 22 '11 at 05:48
  • @Hans Passant: I also agree with Jalal Aldeen Saa'd, if i m quickly pressing mouse twicely two mouse_click events are raised – Akhil Jun 22 '11 at 07:43
1

By enabling, then re-enabling again as you eluded to. What was the problem with this?

public void SaveButton_Click(..., ...)
{
    this.SaveButton.Enabled = false;
    Save();
    this.SaveButton.Enabled = true;
}
Josh Smeaton
  • 47,939
  • 24
  • 129
  • 164
  • To prevent the data from being saved twice, you could also add a Boolean [dataSaved], intialize it to false. Before saving the data you would check the Boolean variable : if false, then save, else show a warning saying that the data has been already saved. Going further, you could also set the flag boolean variable to false whenever the user updates the data in you dataset/datatable in order for the user to be able to save the modified data again. – M0-3E Jun 21 '11 at 10:24
  • @Josh Smeaton: i tried the enable/disabled stuff. – Akhil Jun 21 '11 at 10:32
  • @ Moez: is any other solution..? – Akhil Jun 21 '11 at 10:33
  • 3
    @Akhil, and what was WRONG with this solution? You're not helping us to help you. – Josh Smeaton Jun 21 '11 at 10:34
  • 3
    @Josh, don't you get it? He tried that stuffs already. – Winston Smith Jun 21 '11 at 11:51
1

using a System.Threading.Monitor class will do the trick like:

private readonly object _userActivityLocker = new object();


private void btnSave_Click(object sender, EventArgs e)
{
    new Thread(delegate()
    {
    if (System.Threading.Monitor.TryEnter(_userActivityLocker))
    {
        //note that any sub clicks will be ignored while we are here
        try
        {
            //here it is safe to call the save and you can disable the btn
        }
        finally
        {
            System.Threading.Monitor.Exit(_userActivityLocker);
            //re-enable the btn if you disable it.
        }
    }
    }) { IsBackground = true }.Start();
}

To prove that changing the button to enable or disable state is not enough here a simple test:

Add a new form and add a button1 to it and inside the button1 click event handler write the following code:

private void button1_Click(object sender, EventArgs e)
{
    button1.Enabled = false;

    Console.WriteLine("First Message");

    Thread.Sleep(2000);

    Console.WriteLine("second Message");

    button1.Enabled = true;
}

and then build and run the application, double click on the button1 and the result int the output window will be:

First Message
second Message
First Message
second Message

so we have to make sure only one click is executed even when double click or so and that accomplished simply by using the System.Threading.Monitor

Update: Note that you can use a Task "if C# 4.0", a ThreadPool.QueueUserWorkItem or BackgroundWorker as a substitute of Thread.

Jalal Said
  • 15,906
  • 7
  • 45
  • 68
  • i modified the question,please take a look – Akhil Jun 22 '11 at 08:29
  • @kkhil: I forget to wrap the code in a new Thread.. my fault. however I will update my answer. – Jalal Said Jun 22 '11 at 08:42
  • @Akhil: Did you try this after the edit? Is it serves your needs? – Jalal Said Jun 22 '11 at 09:50
  • @Hans Passant : is creating a thread better than Hans Passant's answer..? I tried the Application.DoEvents() before to enable button and found that it's working fine. – Akhil Jun 23 '11 at 04:43
  • @Akhil: Execute the long operation on a different thread is much better than Application.DoEvents() Note that 'Hans Passant' is suggesting that as well, he said "`But the better solution is to not let your UI thread go dead like this. Use a BackgroundWorker to perform the save on a worker thread`" [BackgroundWorker](http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx#Y300) purpose is to executes an operation on a separate thread. – Jalal Said Jun 23 '11 at 10:06
  • is it possible to use this global for every form and every button and not copy and paste it every time for all button...? – senzacionale Oct 21 '11 at 12:32
  • @senzacionale Of course, why not, you can just make a generic method that accepts a control. and pass to it your control or form... – Jalal Said Oct 23 '11 at 15:39
  • Jalal can you give me an example please. I try but none of my example works. – senzacionale Oct 24 '11 at 05:24