2

I have been googling this for quite a few hours, and read quite a few SO questions where this is discussed but I am sorry to say I just don't get how to use it.

Basically what I am trying to do is to have a label in a WPF/Win Forms app display the following while an async task is running:

Processing .

and at each 1 second interval to add another dot until I get to three and then start over at 1 until the task is done.

As a first step I am only trying to add a dot after each second and have tried it with an IProgress action but the only thing that I have been able to accomplish is either nothing or the label gets populated with dots in one shot and the other task seems to run after that is done.

I next tried doing the following:

   private async void startButton_Click(object sender, RoutedEventArgs e)
   {
        resultsTextBox.Text = "Waiting for the response ...";

        startButton.IsEnabled = false;
        resultsTextBox.Clear();

        var task = SumPageSizesAsync();

        var progress = Task.Run(() =>
        {
           var aTimer = new System.Timers.Timer(1000);
           aTimer.Elapsed += OnTimedEvent;
           aTimer.AutoReset = true;
           aTimer.Enabled = true;

           void OnTimedEvent(object source, ElapsedEventArgs et)
           {
               if (!lblProgress.Dispatcher.CheckAccess())
               {
                   Dispatcher.Invoke(() =>
                   {
                       lblProgress.Content += ".";
                   });
               }
           }
       });

       await task;
       await progress;

       resultsTextBox.Text += "\r\nControl returned to startButton_Click.";

       startButton.IsEnabled = true;
}

But again the label just gets populated with dots at once while the other task keeps running.

I took this example from the Microsoft Docs

UPDATE: I have now tried removing the loop while(!task.IsComplete) which basically makes the label start to be updated after the first task has finished. Then I tried to the following:

var task = SumPageSizesAsync();
var progress = GetProgress();

await Task.WhenAll(SumPageSizesAsync(), GetProgress());

But got the same result, the label begins to update after the first task has concluded.

Thank you for your help.

Sergio Romero
  • 6,477
  • 11
  • 41
  • 71
  • 1
    In what way does this use `Progress` or `IProgress`? You primary issue is that every 3 seconds you spawn a thread that runs in a tight loop that spams the UI thread with updates. – Mike Zboray Dec 14 '18 at 21:01
  • Why are you doing a while-loop in the elapsed event? – LarsTech Dec 14 '18 at 21:02
  • @MikeZboray The code that I display here is NOT showing the IProgress thing that I tried since I was unable to go back to it after trying a lot of different things. I am only showing what I currently have. – Sergio Romero Dec 14 '18 at 21:03
  • @LarsTech Originally I thought that I should need to keep doing this while the original task was still running. I added an update in which I mention that I removed it since I realized I do not need it, but that still hasn't worked as I want it to. – Sergio Romero Dec 14 '18 at 21:04
  • `IProgress` would not be used in this scenario, unless what you want to do is add it as an argument to SumPageSizesAsync so it can report progress as it executes. Use a timer (either Forms.Timer for winforms or DispatcherTimer for wpf) to update the label periodically. – Mike Zboray Dec 14 '18 at 21:06
  • I'm a little out of my comfort zone here, but I would guess the loop should be removed and AutoReset should be false. You said one second, but it looks like a 3 second timer. – LarsTech Dec 14 '18 at 21:08

3 Answers3

1

"Progress(T)" is the wrong pattern for this.

Here is the code for a WPF application that does this with 100% async / await code, no additional threads are created.

It starts two async tasks. The first simulates the long running async process. The second one starts another async Task that takes the first task as a parameter. It loops until the first task is completed, while updating a label with a "..." pattern. It awaits a Task.Delay to control the animation speed.

Both those tasks are placed in to a list, and the we await the completion of both of them.

This could all be wrapped up in in to a ShowProgressUntilTaskCompletes method (or extension method) that takes the worker Task as a parameter, which gives you an easily reusable method of showing a progress indicator for any Task.

MainWindow.xaml:

<Window
    x:Class="LongProcessDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <StackPanel Margin="100" Orientation="Vertical">
        <Button Click="StartProcess_OnClick" Content="Start" />
        <TextBlock
            Name="LoadingText"
            Padding="20"
            Text="Not Running"
            TextAlignment="Center" />
    </StackPanel>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows;

namespace LongProcessDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void StartProcess_OnClick(object sender, RoutedEventArgs e)
        {
            var longRunningTask = SimulateLongRunningTask();
            var spinner = ShowSpinner(longRunningTask);
            var tasks = new List<Task>
            {
                longRunningTask,
                spinner,
            };
            await Task.WhenAll(tasks);
        }

        private async Task ShowSpinner(Task longRunningTask)
        {
            var numDots = 0;
            while (!longRunningTask.IsCompleted)
            {
                if (numDots++ > 3) numDots = 0;
                LoadingText.Text = $"Waiting{new string('.', numDots)}";
                await Task.Delay(TimeSpan.FromSeconds(.5));
            }

            LoadingText.Text = "Done!";
        }

        private async Task SimulateLongRunningTask()
        {
            await Task.Delay(TimeSpan.FromSeconds(10));
        }
    }
}

Here is a recording of it running, with window interaction proving that the UI is not blocked.

enter image description here


As an extra bonus, I got bored and implemented the extension method I mentioned (with super special bonus, a "local function" feature from C# 7!).

public static class TaskExtensions
{
    public static async Task WithSpinner(this Task longRunningTask, TextBlock spinnerTextBox)
    {
        async Task ShowSpinner()
        {
            var numDots = 0;
            while (!longRunningTask.IsCompleted)
            {
                if (numDots++ > 3) numDots = 0;
                spinnerTextBox.Text = $"Waiting{new string('.', numDots)}";
                await Task.Delay(TimeSpan.FromSeconds(.5));
            }
            spinnerTextBox.Text = "Done!";
        }

        var spinner = ShowSpinner();
        var tasks = new List<Task>
        {
            longRunningTask,
            spinner,
        };
        await Task.WhenAll(tasks);
    }
}

You use it like this:

await SimulateLongRunningTask().WithSpinner(LoadingTextBlock);
Bradley Uffner
  • 16,641
  • 3
  • 39
  • 76
0

If you use the await, it means that your code will wait for the async operation to finish at that line, and then continue. That is why your progress task is not started until task task is finished. You can create a background thread that runs in parallel with the task until it is finished and in there you can tell the UI thread to animate the dots once per second. Since the UI thread is NOT blocked (but only waiting for the task to finish), this works.

Example code:

string originalLblContent = (lblProgress.Content as string) ?? "";
bool taskStarted = false;

var progressThread = new Thread((ThreadStart)delegate
{
    // this code will run in the background thread
    string dots = "";
    while(!taskStarted || !task.IsCompleted) {
        if(dots.Length < 3) {
            dots += ".";
        } else {
            dots = "";
        }
        // because we are in the background thread, we need to invoke the UI thread
        // we can invoke it because your task is running asynchronously and NOT blocking the UI thread
        Dispatcher.Invoke(() =>
        {
            lblProgress.Content = originalLblContent + dots;
        });
        Thread.Sleep(1000);
    }
});

progressThread.Start();
taskStarted = true;
await task;

// the task is now finished, and the progressThread will also be after 1 second ...
GregorMohorko
  • 2,739
  • 2
  • 22
  • 33
  • I'm not the person who down-voted you, but I assume it was because the entire point of the `async` / `await` pattern is to avoid multi-threading. – Bradley Uffner Dec 14 '18 at 21:28
  • Yes, indeed. That is why he doesn't need to create a new thread for the `task` to run in the background, he can use the `await`. And since the current UI thread is not blocked (because of the `await`), you can pump invoke it somewhere else (like in OUR background thread) to do some other UI things (like animate dots). – GregorMohorko Dec 14 '18 at 21:31
  • It is possible to do what he wants to do without spinning up *any* new threads, using only `async` / `await`. – Bradley Uffner Dec 15 '18 at 00:36
  • Okay, I see your answer now. I forgot about the `Task.WhenAll`. Nice work. – GregorMohorko Dec 16 '18 at 13:02
  • @GregaMohorko - Thank you so much. This solution works and it's very clean so I up-voted it. Bradley's solution is closer to what I was looking for, though. – Sergio Romero Dec 17 '18 at 16:00
-1

Your approach is a little funky here. The await statements will prevent the method returning until each thread is finished. The await feature is not a completely asynchronous execution (why would it be? you have threads for that).

You need to re-think your approach to the problem. Fundamentally, you want to update the UI while another process is in progress. This calls for multithreading.

From Microsoft:

"Handling blocking operations in a graphical application can be difficult. We don’t want to call blocking methods from event handlers because the application will appear to freeze up. We can use a separate thread to handle these operations, but when we’re done, we have to synchronize with the UI thread because we can’t directly modify the GUI from our worker thread. We can use Invoke or BeginInvoke to insert delegates into the Dispatcher of the UI thread. Eventually, these delegates will be executed with permission to modify UI elements.

In this example, we mimic a remote procedure call that retrieves a weather forecast. We use a separate worker thread to execute this call, and we schedule an update method in the Dispatcher of the UI thread when we’re finished."

enter image description here

https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/threading-model

Scuba Steve
  • 1,541
  • 1
  • 19
  • 47
  • 1
    The whole *point* of `await` is that it *doesn't* block the thread. It's *asynchronous*. Creating a thread just to sit there synchronously blocking on an asynchronous operation is bad design, and exactly what that feature is created to prevent people from doing. – Servy Dec 14 '18 at 21:11
  • @Servy - I think you're misunderstanding how Await works. "The complete structure of async-await transforms your call to a state-machine such that when the state-machine hits the first await, it will first check to see if the called method completed, and if not will register the continuation and return from that method call. Later, once that method completes, it will re-enter the state-machine in order to complete the method. And that is logically how you see the line after await being hit." https://stackoverflow.com/questions/34705703/does-await-completely-blocks-the-thread – Scuba Steve Dec 17 '18 at 19:10
  • Yes, I know that. Yet your answer says that `await` will block until the task has finished, when it doesn't in fact do that. – Servy Dec 17 '18 at 19:12
  • @Servy - The method won't complete until the awaited tasks have completed. That's a block, no? Some of the other answers above say things to the same effect. – Scuba Steve Dec 17 '18 at 19:13
  • No, blocking would be the method not returning and preventing the thread from continuing to execute, as opposed to what it actually does which is returning to the caller immediately and letting it continue doing whatever it pleases, but executing the remainder of the method when the awaited task is finished. The other answer is even more wrong than this one, so you're correct that it isn't better. – Servy Dec 17 '18 at 19:17
  • @Servy - You're arguing semantics. The method fundamentally CANNOT complete until the awaited method returns. In my books, it's blocked. – Scuba Steve Dec 17 '18 at 19:21
  • Yes, and it's a very important semantic to understand, and not get wrong. The method is returning *immediately*, and *not* blocking the caller, but simply marking the returned task as completed at some point in the future. Saying it's blocking is just wrong. That's not what it does. – Servy Dec 17 '18 at 19:45
  • @Servy - I can see your point, in terms of the semantics being important. I've edited the answer. – Scuba Steve Dec 17 '18 at 19:48