ICloneable
ICloneable

Reputation: 633

How to pause task running on a worker thread and wait for user input?

If I have a task running on a worker thread and when it finds something wrong, is it possible to pause and wait for the user to intervene before continuing?

For example, suppose I have something like this:

async void btnStartTask_Click(object sender, EventArgs e)
{
    await Task.Run(() => LongRunningTask());
}

// CPU-bound
bool LongRunningTask()
{
    // Establish some connection here.

    // Do some work here.

    List<Foo> incorrectValues = GetIncorrectValuesFromAbove();

    if (incorrectValues.Count > 0)
    {
        // Here, I want to present the "incorrect values" to the user (on the UI thread)
        // and let them select whether to modify a value, ignore it, or abort.
        var confirmedValues = WaitForUserInput(incorrectValues);
    }
    
    // Continue processing.
}

Is it possible to substitute WaitForUserInput() with something that runs on the UI thread, waits for the user's intervention, and then acts accordingly? If so, how? I'm not looking for complete code or anything; if someone could point me in the right direction, I would be grateful.

Upvotes: 7

Views: 1841

Answers (4)

Idle_Mind
Idle_Mind

Reputation: 39132

Another example using Invoke() and a ManualResetEvent. Let me know if you need help with the form code; setting up a constructor, using DialogResult, or creating a property to hold the "confirmedValues":

bool LongRunningTask()
{
    // Establish some connection here.

    // Do some work here.

    List<Foo> incorrectValues = GetIncorrectValuesFromAbove();
    var confirmedValues;
    if (incorrectValues.Count > 0)
    {
        DialogResult result;
        ManualResetEvent mre = new ManualResetEvent(false);
        this.Invoke((MethodInvoker)delegate
        {
            // pass in incorrectValues to the form
            //  you'll have to build a constructor in it to accept them
            frmSomeForm frm = new frmSomeForm(incorrectValues);
            result = frm.ShowDialog();
            if (result == DialogResult.OK)
            {
                confirmedValues = frm.confirmedValues; // get the confirmed values somehow
            }
            mre.Set(); // release the block below
        });
        mre.WaitOne(); // blocks until "mre" is set
                        
    }

    // Continue processing.

}

Upvotes: 2

Theodor Zoulias
Theodor Zoulias

Reputation: 43545

There are several ways to solve this problem, with the Control.Invoke being probably the most familiar. Here is a more TPL-ish approach. You start by declaring a UI related scheduler as a class field:

private TaskScheduler _uiScheduler;

Then initialize it:

public MyForm()
{
    InitializeComponent();
    _uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}

Then you convert your synchronous LongRunning method to an asynchronous method. This means that it must return Task<bool> instead of bool. It must also have the async modifier, and by convention be named with the Async suffix:

async Task<bool> LongRunningAsync()

Finally you use the await operator in order to wait for the user's input, which will be a Task configured to run on the captured UI scheduler:

async Task<bool> LongRunningAsync()
{
    // Establish some connection here.

    // Do some work here.

    List<Foo> incorrectValues = GetIncorrectValuesFromAbove();

    if (incorrectValues.Count > 0)
    {
        // Here, I want to present the "incorrect values" to the user (on the UI thread)
        // and let them select whether to modify a value, ignore it, or abort.
        var confirmedValues = await Task.Factory.StartNew(() =>
        {
            return WaitForUserInput(incorrectValues);
        }, default, TaskCreationOptions.None, _uiScheduler);
    }

    // Continue processing.
}

Starting the long running task is the same as before. The Task.Run understands async delegates, so you don't have to do something special after making the method async.

var longRunningTask = Task.Run(() => LongRunningAsync());

This should be enough, provided that you just intend to show a dialog box to the user. The Form.ShowDialog is a blocking method, so the WaitForUserInput method needs not to be asynchronous. If you had to allow the user to interact freely with the main form, the problem would be much more difficult to solve.

Upvotes: 2

Servy
Servy

Reputation: 203824

What you're looking for is almost exactly Progress<T>, except you want to have the thing that reports progress get a task back with some information that they can await and inspect the results of. Creating Progress<T> yourself isn't terribly hard., and you can reasonably easily adapt it so that it computes a result.

public interface IPrompt<TResult, TInput>
{
    Task<TResult> Prompt(TInput input);
}

public class Prompt<TResult, TInput> : IPrompt<TResult, TInput>
{
    private SynchronizationContext context;
    private Func<TInput, Task<TResult>> prompt;
    public Prompt(Func<TInput, Task<TResult>> prompt)
    {
        context = SynchronizationContext.Current ?? new SynchronizationContext();
        this.prompt += prompt;
    }

    Task<TResult> IPrompt<TResult, TInput>.Prompt(TInput input)
    {
        var tcs = new TaskCompletionSource<TResult>();
        context.Post(data => prompt((TInput)data)
            .ContinueWith(task =>
            {
                if (task.IsCanceled)
                    tcs.TrySetCanceled();
                if (task.IsFaulted)
                    tcs.TrySetException(task.Exception.InnerExceptions);
                else
                    tcs.TrySetResult(task.Result);
            }), input);
        return tcs.Task;
    }
}

Now you simply need to have an asynchronous method that accepts the data from the long running process and returns a task with whatever the user interface's response is.

Upvotes: 9

Jason
Jason

Reputation: 1555

You can use TaskCompletionSource to generate a task that can be awaited within the LongRunningTask.

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

namespace ConsoleApp5
{
    class Program
    {
        private static event Action<string> Input;
        public static async Task Main(string[] args)
        {
            var inputTask = InputTask();
            var longRunningTask = Task.Run(() => LongRunningTask());

            await Task.WhenAll(inputTask, longRunningTask);
        }

        private static async Task InputTask()
        {
            await Task.Yield();
            while(true)
            {
                var input = await Console.In.ReadLineAsync();
                Input?.Invoke(input);
            }
        }

        static async Task<bool> LongRunningTask()
        {
            SomeExpensiveCall();

            var incorrectValues = GetIncorrectValuesFromAbove();

            if (incorrectValues.Count > 0)
            {
                var confirmedValues = await WaitForUserInput(incorrectValues).ConfigureAwait(false);
            }

            // Continue processing.
            return true;
        }

        private static void SomeExpensiveCall()
        {
        }

        private static Task<string> WaitForUserInput(IList<string> incorrectValues)
        {
            var taskCompletionSource = new TaskCompletionSource<string>();
            Console.Write("Input Data: ");
            try
            {
                void EventHandler(string input)
                {
                    Input -= EventHandler;
                    taskCompletionSource.TrySetResult(input);
                }
                Input += EventHandler;
            }
            catch(Exception e)
            {
                taskCompletionSource.TrySetException(e);
            }
            return taskCompletionSource.Task;
        }

        private static IList<string> GetIncorrectValuesFromAbove()
        {
            return new List<string> { "Test" };
        }
    }
}

Of course in this example you could have just called await Console.In.ReadLineAsync() directly, but this code is to simulate an environment where you only have an event based API.

Upvotes: 2

Related Questions