Search code examples
c#wpftask-parallel-librarytask

Creating and starting a task on the UI thread


When a method that gets called on a worker thread needs to run code on the UI thread and wait for it to complete before doing something else, it can be done like this:

    public int RunOnUi(Func<int> f)
    {
        int res = Application.Current.Dispatcher.Invoke(f);

        return res;
    }

But what if I wanted to do it with tasks? Is there a way for the RunOnUi method to create a task that is started on the UI and return it so that the caller (which runs on a worker thread) can wait for it? Something that will fit the following signature: public Task<int> StartOnUi(Func<int> f) ?

One way to do it is as follows:

public Task<int> RunOnUi(Func<int> f)
{
    var task = new Task<int>(f);
    task.Start(_scheduler);

    return task;
}

Here, assume that _schduler holds the ui TaskScheduler. But I am not too comfortable with creating "cold" tasks and using the start method to run them. Is that the "recommended" way or is there a more elegant way to do it?


Solution

  • Just use InvokeAsync instead of Invoke then return the Task<int> inside the DispatcherOperation<int> the function returns.

    //Coding conventions say async functions should end with the word Async.
    public Task<int> RunOnUiAsync(Func<int> f)
    {
        var dispatcherOperation = Application.Current.Dispatcher.InvokeAsync(f);
        return dispatcherOperation.Task;
    }
    

    If you do not have access to .NET 4.5 it is a little more complicated. You will need to use BeginInvoke and a TaskCompletionSource to wrap the DispaterOperation that BeginInvoke returns

        public Task<int> RunOnUi(Func<int> f)
        {
            var operation = Application.Current.Dispatcher.BeginInvoke(f);
            var tcs = new TaskCompletionSource<int>();
            operation.Completed += (sender, args) => tcs.TrySetResult((int)operation.Result);
            operation.Aborted += (sender, args) => tcs.TrySetException(new SomeExecptionHere());
    
            //The operation may have already finished and this check accounts for 
            //the race condition where neither of the events will ever be called
            //because the events where raised before you subscribed.
            var status = operation.Status;
            if (status == DispatcherOperationStatus.Completed)
            {
                tcs.TrySetResult((int)operation.Result);
            }
            else if (status == DispatcherOperationStatus.Aborted)
            {
                tcs.TrySetException(new SomeExecptionHere());
            }
    
            return tcs.Task;
        }