0

In below WPF code, there is only a button and a label. I expected the behaviour to be like this: I will see button is disabled and "Doing Stuff" is written on the label and THEN UI will be frozen. But this is not the case. I do not even see "Doing Stuff" label UI is frozen till the beginning. Can you tell me the logic behind this? Why Thread.Sleep makes the UI instantly freeze?

using System.Threading.Tasks;
using System.Threading;
using System.Windows;

namespace MessagePump
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void btnDoStuff_Click(object sender, RoutedEventArgs e)
    {
        btnDoStuff.IsEnabled = false;
        lblStatus.Content = "Doing Stuff";
        Thread.Sleep(4000);
        lblStatus.Content = "Not doing anything";
        btnDoStuff.IsEnabled = true;
    }
}
}
;
Lyrk
  • 1,936
  • 4
  • 26
  • 48
  • You should use dispatcher instead: this.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => { await Task.Delay(4000); }); – Przemek Marcinkiewicz Dec 01 '17 at 12:45
  • Replace `Thread.Sleep(4000)` with `await Task.Delay(TimeSpan.FromSeconds(4));` and make the event handler `async` to avoid that the UI thread is blocked. – Tim Schmelter Dec 01 '17 at 12:49

2 Answers2

5

Thread.Sleep blocks the current thread - in this case the UI thread - meaning that no other tasks can be done. So the UI thread is now unable to perform any messages like 'refreshing the screen' until the long blocking operation has completed.

There will be a pending message like WM_PAINT that tells Windows that the UI must be repainted, but that message cannot be executed since the thread is blocked by the Sleep operation. If you add the following line of code just before the Thread.Sleep, you'll see that the label will be refreshed before the UI is frozen

Application.DoEvents();

Do note however that blocking the UI thread and calling Application.DoEvents is in most of the cases not a good solution. There is more likely a better approach to solve a certain problem than blocking the UI thread and calling DoEvents. (For instance using a background thread or Task to perform a long-running operation and using events to inform the UI of the progress of that task).

Long running tasks should never be executed on the UI thread since your application will become unresponsive. Therefore, it is better to offload the work to another thread. Since .NET 4 , the preferred way is by using TAP (Task-based Async Pattern).

I presume that in your question you are using Thread.Sleep to 'mock' some cpu intensive algorithm that you want to execute. You can do this by starting a new Task like :

await Task.Run ( () => SomeLongRunningMethod())
Frederik Gheysels
  • 56,135
  • 11
  • 101
  • 154
  • 1
    Ok I understand that. But before executing thread.sleep line, I change the label's content as "Doing Stuff". Why can't I see that change on screen? That line is above the Thread.Sleep line...Is it so quick or what? – Lyrk Dec 01 '17 at 12:43
  • 1
    _" I change the label's content as "Doing Stuff". Why can't I see that change on screen?"_ because the UI thread has to repaint the control so you can see that change. But it cannot, because you sent it to sleep. It's like Team A shot a goal so the data is increased but the guy who updates the arena's score board is asleep ... – Fildor Dec 01 '17 at 12:45
  • lol thank you I understood now. UI does not even have time to reflect that change.... – Lyrk Dec 01 '17 at 12:50
  • 1
    @Lyrk You are getting confused as to what is actually happening. It isn't that the UI "doesn't have time" to make the changes. You changed the value of lblStatus.Content to "Doing Stuff". This change did take place. This would be a data change. The UI now needs to update **visually** to make this change. So your label did have its content changed, the UI just hasn't visually displayed that change. As Frederik mentioned in his answer, there is a pending message that needs to get to the UI to inform it to display the change visually. – Sudsy1002 Dec 01 '17 at 13:24
  • @sudsy1002 You said "UI just hasn't visually displayed that change." To give the UI some time, I added a for loop that calculates the square of the numbers from 0 to 1000 between label content change statement and sleep statement but still the same. I cant see that label changed. – Lyrk Dec 01 '17 at 13:29
  • 1
    @Lyrk A great way to see this is to only have `lblStatus.Content = "Doing Stuff";` followed immediately by `lblStatus.Content = "Not doing anything";` in your btnDoStuff_Click event. Then run the debugger and step through this. You will notice that after `lblStatus.Content = "Doing Stuff";`, the content hasn't changed visually. You can view that the **data** has changed, but **visually** the message has not gotten to the UI yet. The message won't get to the UI until your btnDoStuff_Click event has finished, where you will then see `lblStatus.Content = "Not doing anything";` applied visually. – Sudsy1002 Dec 01 '17 at 13:30
  • Are the changes applied to UI after all the statements executed in DoStuff_Click handler? I did not know this. This clears all the confusion I think. My for loop doesn't change anything then. – Lyrk Dec 01 '17 at 13:32
  • 1
    @Lyrk The issue is not one of time, but rather an issue in the order of operations. The UI's pending message to repaint will not start until the btnDoStuff_Click event has finished. It doesn't matter how much time you put in between 'lblStatus.Content = "Doing Stuff";' and 'Thread.Sleep(4000);' because the message is waiting for the event to finish first. That is why Frederik's answer works, because he is manually telling the UI to do the repaint event before the btnDoStuff_Click event has finished. – Sudsy1002 Dec 01 '17 at 13:35
  • Is it possible to give you gold for this? Thank you so much... – Lyrk Dec 01 '17 at 13:36
  • @Lyrk - Using `DoEvents` should be avoided at all costs. This is **NOT** the way to solve this problem. It will eventually burn you bad with bugs in your code. Clemens' answer is by far the better approach. – Enigmativity Dec 16 '17 at 06:03
  • I am not advising to use Application.DoEvents; this is also mentionned in my answer. I mention Application.doEvents to explain that calling this method allows the messaging queue to process messages before you start a long running task on the same thread (ui thread in this case). Although processing work on a seperate thread using a Task is a better approach, it is not an explanation to the question of the topic-starter – Frederik Gheysels Dec 16 '17 at 15:42
5

Thread.Sleep blocks the UI thread. Declare the Click handler async and call Task.Delay instead:

private async void btnDoStuff_Click(object sender, RoutedEventArgs e)
{
    btnDoStuff.IsEnabled = false;
    lblStatus.Content = "Doing Stuff";

    await Task.Delay(4000);

    lblStatus.Content = "Not doing anything";
    btnDoStuff.IsEnabled = true;
}
Clemens
  • 123,504
  • 12
  • 155
  • 268