Ashigore
Ashigore

Reputation: 4678

Why is Window.ShowDialog not blocking in TaskScheduler Task?

I'm using a custom TaskScheduler to execute a task queue in serial. The task is supposed to display a window and then block until the window closes itself. Unfortunately calling Window.ShowDialog() doesn't seem to block so the task completes and the window never displays.

If I put a breakpoint after the call to ShowDialog I can see the form has opened but under normal execution the Task seems to end so quickly you cant see it.

My TaskScheduler implementation taken from a previous question:

public sealed class StaTaskScheduler : TaskScheduler, IDisposable
{
    private readonly List<Thread> threads;
    private BlockingCollection<Task> tasks;

    public override int MaximumConcurrencyLevel
    {
        get { return threads.Count; }
    }

    public StaTaskScheduler(int concurrencyLevel)
    {
        if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");

        this.tasks = new BlockingCollection<Task>();
        this.threads = Enumerable.Range(0, concurrencyLevel).Select(i =>
        {
            var thread = new Thread(() =>
            {
                foreach (var t in this.tasks.GetConsumingEnumerable())
                {
                    this.TryExecuteTask(t);
                }
            });
            thread.IsBackground = true;
            thread.SetApartmentState(ApartmentState.STA);
            return thread;
        }).ToList();

        this.threads.ForEach(t => t.Start());
    }

    protected override void QueueTask(Task task)
    {
        tasks.Add(task);
    }
    protected override IEnumerable<Task> GetScheduledTasks()
    {
        return tasks.ToArray();
    }
    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        return Thread.CurrentThread.GetApartmentState() == ApartmentState.STA && TryExecuteTask(task);
    }

    public void Dispose()
    {
        if (tasks != null)
        {
            tasks.CompleteAdding();

            foreach (var thread in threads) thread.Join();

            tasks.Dispose();
            tasks = null;
        }
    }
}

My Application Code:

private StaTaskScheduler taskScheduler;

...

this.taskScheduler = new StaTaskScheduler(1);

Task.Factory.StartNew(() =>
{
    WarningWindow window = new WarningWindow(
        ProcessControl.Properties.Settings.Default.WarningHeader,
        ProcessControl.Properties.Settings.Default.WarningMessage,
        processName,
        ProcessControl.Properties.Settings.Default.WarningFooter,
        ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);
    window.ShowDialog();

}, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);

Upvotes: 4

Views: 1437

Answers (3)

PretoriaCoder
PretoriaCoder

Reputation: 886

I had a similar problem where on start up I could not manage to block execution via a ShowDialog. Eventually I discovered that inside the class properties were Command Line Arguments that was used to automatically log in with appropriate credentials. This saved time during development not to enter a username and password every time you compile. As soon as I removed that the Dialog behaved as expected so it's worth while to search for processes that might interfere with the normal execution path.

Upvotes: 0

Hans Passant
Hans Passant

Reputation: 942207

Nothing obviously wrong. Except what is missing, you are not doing anything to ensure that an exception that's raised in the task is reported. The way you wrote it, such an exception will never be reported and you'll just see code failing to run. Like a dialog that just disappears. You'll need to write something like this:

    Task.Factory.StartNew(() => {
        // Your code here
        //...
    }, CancellationToken.None, TaskCreationOptions.None, taskScheduler)
    .ContinueWith((t) => {
        MessageBox.Show(t.Exception.ToString());
    }, TaskContinuationOptions.OnlyOnFaulted);

With good odds that you'll now see an InvalidOperationException reported. Further diagnose it with Debug + Exceptions, tick the Thrown checkbox for CLR exceptions.

Do beware that this task scheduler doesn't magically makes your code thread-safe or fit to run another UI thread. It wasn't made for that, it should only be used to keep single-threaded COM components happy. You must honor the sometimes draconian consequences of running UI on another thread. In other words, don't touch properties of UI on the main thread. And the dialog not acting like a dialog at all since it doesn't have an owner. And it thus randomly disappearing behind another window or accidentally getting closed by the user because he was clicking away and never counted on a window appearing from no-where.

And last but not least the long-lasting misery caused by the SystemEvents class. Which needs to guess which thread is the UI thread, it will pick the first STA thread. If that's your dialog then you'll have very hard to diagnose threading problems later.

Don't do it.

Upvotes: 4

noseratio
noseratio

Reputation: 61736

Apparently, you're using Stephen Toub's StaTaskScheduler. It is not intended to run tasks involving the UI. Essentially, you're trying to display a modal window with window.ShowDialog() on a background thread which has nothing to do with the main UI thread. I suspect the window.ShowDialog() instantly finishes with an error, and so does the task. Await the task and observe the errors:

try
{
    await Task.Factory.StartNew(() =>
    {
        WarningWindow window = new WarningWindow(
            ProcessControl.Properties.Settings.Default.WarningHeader,
            ProcessControl.Properties.Settings.Default.WarningMessage,
            processName,
            ProcessControl.Properties.Settings.Default.WarningFooter,
            ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);
        window.ShowDialog();

    }, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);

}
catch(Exception ex)
{
    MessageBox.Show(ex.Message)
}

If you really want to show a window from a background STA thread, you need to run a Dispatcher loop:

    var task = Task.Factory.StartNew(() =>
    {
        System.Windows.Threading.Dispatcher.InvokeAsync(() =>
        {
            WarningWindow window = new WarningWindow(
                ProcessControl.Properties.Settings.Default.WarningHeader,
                ProcessControl.Properties.Settings.Default.WarningMessage,
                processName,
                ProcessControl.Properties.Settings.Default.WarningFooter,
                ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);

            window.Closed += (s, e) =>
                window.Dispatcher.InvokeShutdown();
            window.Show();
        });

        System.Windows.Threading.Dispatcher.Run();

    }, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);

Note however, this window will not be modal as to any main UI thread window. Moreover, you'd block the StaTaskScheduler loop, so its other scheduled tasks won't run until the window is closed and Dispatcher.Run() exits.

Upvotes: 2

Related Questions