Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Disabled Button fires events in WPF

Tags:

c#

events

wpf

private void but_Click(object sender, RoutedEventArgs e)
{
    (sender as Button).IsEnabled = false;
    doSomeThing();//e.g run for more than 20 seconds
    (sender as Button).IsEnabled = true;
}

When I press the button at first time it disables. Then starts doSomeThing() and it contains UI code or some UI variable updated. I mean while if I press the button again while doSomeThing() is in progress then but_Click event fires again after this button enables back. It maintains queue of event fired,i.e. n number of times which i pressed.

So, how to prevent firing event while button is disabled? Please consider in this scenario 'doSomething' contains UI controls bind to code. So we can't run background Thread in this case. Help me with solution.

like image 239
HelloWorld Avatar asked Mar 02 '12 03:03

HelloWorld


3 Answers

It seems some clarifications are in order...

  • Controls (e.g.- buttons) are properly disabled.
  • As long as the main thread is processing the original click event, subsequent events are not processed - instead, they are queued.
  • As soon as that first click event is done, the next queued event is processed.
  • At this point, since the controls were already enabled, a queued click event will trigger the event handler again.

There are several possible solutions, and I think @Amit was on the right track - using threads is probably the best way to go.

Here's a simplified version of what worked for me:

private void Button1_Click(object sender, EventArgs e)
{
    disableControls(); // e.g.- Button1.Enabled = false;

    // run "doSomething" in a separate thread
    new Thread(new ThreadStart(doSomething)).Start();
}

private void doSomething()
{
    // do something... make sure it's thread-safe!!
    // ...

    enableControls();  // a thread safe enabling of relevant controls
}

For a good explanation why NOT to use Application.DoEvents(), look here

For thread safe manipulation of controls' state, see: How to: Make Thread-Safe Calls to Windows Forms Controls

like image 149
Alon Brontman Avatar answered Nov 02 '22 19:11

Alon Brontman


The issue with this code is doSomeThing() method is running in UI Thread. So, the Button is not properly disabled. If we refactor the code so that doSomeThing() method runs in a different thread, it will just work fine. Here is a simple example using BackgroundWorker; however the idea is we should not run time consuming stuffs in UI thread. Here is the refactored code:

public partial class ButtonEnableTest : Window
{
    private BackgroundWorker worker = new BackgroundWorker();

    public ButtonEnableTest()
    {
        InitializeComponent();
        this.worker.DoWork += new DoWorkEventHandler(worker_DoWork);
        this.worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
    }

    void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if (this.btn.IsEnabled == false)
        {
            this.btn.IsEnabled = true;
        }
    }

    void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        doSomeThing();
    }

    private void doSomeThing()
    {
        int i = 5;
        while ( i > 0)
        {
            Thread.Sleep(TimeSpan.FromMilliseconds(2000));
            System.Diagnostics.Debug.WriteLine("Woke up " + i);
            i--;
        }
    }

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        Button btn = (Button) sender;
        System.Diagnostics.Debug.WriteLine("at ButtonClick");
        if (btn.IsEnabled)
        {
            btn.IsEnabled = false;
            this.worker.RunWorkerAsync();
        }
    }
}

I did not follow any coding conversions here as I just wanted to share my idea. Please note that, I named the WPF button as "btn".

like image 37
Amit Avatar answered Nov 02 '22 18:11

Amit


Another approach is to use the PeekMessage API function, as shown in the following code.

using System.Runtime.InteropServices;
using System.Security;

[StructLayout(LayoutKind.Sequential)]
public struct NativeMessage
{
    public IntPtr handle;
    public uint msg;
    public IntPtr wParam;
    public IntPtr lParam;
    public uint time;
    public System.Drawing.Point p;
}

[SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("User32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool PeekMessage(out NativeMessage message,
    IntPtr handle, uint filterMin, uint filterMax, uint flags);
private const UInt32 WM_MOUSEFIRST = 0x0200;
private const UInt32 WM_MOUSELAST = 0x020D;
public const int PM_REMOVE = 0x0001;

// Flush all pending mouse events.
private void FlushMouseMessages()
{
    NativeMessage msg;
    // Repeat until PeekMessage returns false.
    while (PeekMessage(out msg, IntPtr.Zero,
        WM_MOUSEFIRST, WM_MOUSELAST, PM_REMOVE))
        ;
}

This code includes a bunch of declarations for the API function and its parameters. (You also need to add using statements for the System.Runtime.InteropServices and System.Security namespaces. Download the example for the details.)

The FlushMouseMessages method calls PeekMessage telling it to discard any message in the range WM_MOUSELAST to PM_REMOVE. The code calls PeekMessage repeatedly until it returns false to indicate that there are no such messages.

The following button event handler calls FlushMouseMessage so you cannot click the button while its code is still running.

private void but_Click(object sender, RoutedEventArgs e)
{
    (sender as Button).IsEnabled = false;
    doSomeThing();//e.g run for more than 20 seconds
    (sender as Button).IsEnabled = true;
    FlushMouseMessages();
}

I picked the above code from the site http://csharphelper.com/blog/2015/08/flush-click-events-in-c/ This works for me.

like image 41
Praveen sharma Avatar answered Nov 02 '22 19:11

Praveen sharma