Search code examples
c#wpfuser-interfacecomponentstextblock

WPF TextBlock shows all log messages only after all work is done


When user clicks on Execute button, I want to do some stuff and output log messages to TextBlock progressively - so user can see what is currently happening.

The problem is that my TextBlock changes it content after all work is finished (too late). How can I force WPF to repaint itself during process ?

Code looks like this:

    private void btn_execute_Click(object sender, RoutedEventArgs e)
    {
        .... stuff ....
    }

I have tried adding output_log.InvalidateVisual(); after any change to TextBlock, didn't work as expected.


Solution

  • If you run synchronous code in a Click handler of a Button, this code is being executed in the Dispatcher thread and thus prevents the Dispatcher from running any other code like displaying the changes of your messages in a TextBlock.

    There are (at least) three possible ways to solve this issue.

    First, you can run your Execute code in another Thread, Task or async event handler and set the Text using the Dispatcher:

    private async void btn_execute_Click(object sender, RoutedEventArgs e)
    {
         for (int i = 0; i < 100; i++)
         {
             // Simulate doing some stuff...
             await Task.Delay(100);
    
             // Thanks to async/await the current context is captured and
             // switches automatically back to the Dispatcher thread after the await.
             output_log.Text += i + ", ";
    
             // If you were using Task.Run() instead then you would have to invoke it manually.
             // Dispatcher.Invoke(() => output_log.Text += i + ", ");
         }
    }
    

    The main advantage is that you are not blocking the Dispatcher - which is highly recommended for everything you do.


    Second, you can keep doing your Execute code in the Dispatcher, but then you have to "flush" the Dispatcher every time when you want to refresh your text, so that it can handle all waiting UI actions:

    private void btn_execute_Click(object sender, RoutedEventArgs e)
    {
        for (int i = 0; i < 100; i++)
        {
            // Simulate doing some stuff...
            Thread.Sleep(100);
            output_log.Text += i + ", ";
            Dispatcher.Invoke(DispatcherPriority.Background, new Action(() => { }));
        }
    }
    

    This is certainly possible but I really wouldn't recommend it.


    Or third,

    • you can use MVVM for your architecture,
    • run your Execute code in an async event handler (or Command),
    • change only the LogText property of your ViewModel and
    • use data binding to bind the TextBlock.Text to this MyLogViewModel.LogText property.

    Unfortunately I can't give you a quick sample code for this scenario, but it's surely worth thinking about it because MVVM is just a really natural architecture for any kind of WPF application.