2

I'm trying to asynchronously show a progress form that says the application is running while the actual application is running.

As following this question, I have the following:

Main Form:

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
    }

    async Task<int> LoadDataAsync()
    {
        await Task.Delay(2000);
        return 42;
    }

    private async void Run_Click(object sender, EventArgs e)
    {
        var runningForm = new RunningForm();

        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.Close();
        await progressFormTask;

        MessageBox.Show(data.ToString());
    }
}

Progress Form

public partial class RunningForm : Form
{
    private readonly SynchronizationContext synchronizationContext;

    public RunningForm()
    {
        InitializeComponent();
        synchronizationContext = SynchronizationContext.Current;
    }

    public async void ShowRunning()
    {
        this.RunningLabel.Text = "Running";
        int dots = 0;

        await Task.Run(() =>
        {
            while (true)
            {
                UpadateUi($"Running{new string('.', dots)}");

                Thread.Sleep(300);

                dots = (dots == 3) ? 0 : dots + 1;
            }
        });
    }

    public void UpadateUi(string text)
    {
        synchronizationContext.Post(
            new SendOrPostCallback(o =>
            {
                this.RunningLabel.Text = text;
            }),
            text);
    }

    public void CloseThread()
    {
        synchronizationContext.Post(
            new SendOrPostCallback(o =>
            {
                this.Close();
            }),
            null);
    }
}

internal static class DialogExt
{
    public static async Task<DialogResult> ShowDialogAsync(this Form form)
    {
        await Task.Yield();
        if (form.IsDisposed)
        {
            return DialogResult.OK;
        }
        return form.ShowDialog();
    }
}

The above works fine, but it doesn't work when I'm calling from outside of another from. This is my console app:

class Program
{
    static void Main(string[] args)
    {
        new Test().Run();
        Console.ReadLine();
    }
}

class Test
{
    private RunningForm runningForm;

    public async void Run()
    {
        var runningForm = new RunningForm();

        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.CloseThread();

        await progressFormTask;

        MessageBox.Show(data.ToString());
    }

    async Task<int> LoadDataAsync()
    {
        await Task.Delay(2000);
        return 42;
    }
}

Watching what happens with the debugger, the process gets to await Task.Yield() and never progresses to return form.ShowDialog() and thus you never see the RunningForm. The process then goes to LoadDataAsync() and hangs forever on await Task.Delay(2000).

Why is this happening? Does it have something to do with how Tasks are prioritized (ie: Task.Yield())?

noseratio
  • 59,932
  • 34
  • 208
  • 486
Alex McLean
  • 2,524
  • 5
  • 30
  • 53
  • 1
    I believe your tasks are not hanging by themselves, but that your problems are rather caused by trying to establish a WinForms-based UI in a console application without setting up a message pump (that is required for the WinForms UI to work). Try to instantiate and run your main form in a similar fashion as to what is shown in this answer: https://stackoverflow.com/a/277776/2819245. Try it and see whether it improves (or otherwise changes) the behavior. –  Dec 20 '18 at 19:13
  • Also, since you work with tasks (and thus possibly with multiple threads), pay attention to that you create the message pump (i.e., execute `Application.Run`) in the main thread of your program and not in some background thread. –  Dec 20 '18 at 19:16
  • 1
    The code you linked (which I'm the author of) is meant to be run from a WinForms UI thread with a message loop, as @elgonzo pointed out. You said *" "but it doesn't work when I'm calling from outside of another from. This is my console app..."* So, do you ultimately need to run it from another form, or from a non-UI worker thread? – noseratio Dec 20 '18 at 21:30
  • @noseratio I don't need it to, I'm more interested in learning why it works for WinForms and not for Console Applications. – Alex McLean Dec 20 '18 at 22:33

1 Answers1

2

Watching what happens with the debugger, the process gets to await Task.Yield() and never progresses to return form.ShowDialog() and thus you never see the RunningForm. The process then goes to LoadDataAsync() and hangs forever on await Task.Delay(2000).

Why is this happening?

What happens here is that when you do var runningForm = new RunningForm() on a console thread without any synchronization context (System.Threading.SynchronizationContext.Current is null), it implicitly creates an instance of WindowsFormsSynchronizationContext and installs it on the current thread, more on this here.

Then, when you hit await Task.Yield(), the ShowDialogAsync method returns to the caller and the await continuation is posted to that new synchronization context. However, the continuation never gets a chance to be invoked, because the current thread doesn't run a message loop and the posted messages don't get pumped. There isn't a deadlock, but the code after await Task.Yield() is never executed, so the dialog doesn't even get shown. The same is true about await Task.Delay(2000).

I'm more interested in learning why it works for WinForms and not for Console Applications.

You need a UI thread with a message loop in your console app. Try refactoring your console app like this:

public void Run()
{
    var runningForm = new RunningForm();
    runningForm.Loaded += async delegate 
    {
        runningForm.ShowRunning();

        var progressFormTask = runningForm.ShowDialogAsync();

        var data = await LoadDataAsync();

        runningForm.Close();

        await progressFormTask;

        MessageBox.Show(data.ToString());
    };
    System.Windows.Forms.Application.Run(runningForm);
}

Here, the job of Application.Run is to start a modal message loop (and install WindowsFormsSynchronizationContext on the current thread) then show the form. The runningForm.Loaded async event handler is invoked on that synchronization context, so the logic inside it should work just as expected.

That however makes Test.Run a synchronous method, i. e., it only returns when the form is closed and the message loop has ended. If this is not what you want, you'd have to create a separate thread to run your message loop, something like I do with MessageLoopApartment here.

That said, in a typical WinForms or WPF application you should almost never need a secondary UI thread.

noseratio
  • 59,932
  • 34
  • 208
  • 486