Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Employing Tasks to Handle Events in the .NET Framework

4.64/5 (11 votes)
4 Jun 2018CPOL5 min read 14.6K   133  
This piece shows that, by returning a Task from an Event Handler instead of the usual void, the management of the event can be more structured and versatile.
This article explains how to utilize Tasks for managing asynchronous events in the .NET Framework, including returning Tasks from control-based events, managing Task-based events in the ViewModel, canceling Tasks returned from events, and an example application demonstrating these concepts.

Introduction

When you have void returned from an event handler, there is not a lot you can do with it, but, if you get a Task back, you have access to one of the most powerful types for managing asynchronous events in the .NET Framework. This article shows ways to encapsulate events within Tasks and how these Tasks can be used to control program flow in a manner that's both logical and easy to maintain.

Returning Tasks from Controls

The technique for returning Tasks from control-based events is quite straightforward. In this WPF example, an extension method is added to the ButtonControl.

C#
public static async Task ClickRaisedAsync(this Button button)
        {
            var tcs = new TaskCompletionSource<bool>();
            RoutedEventHandler clickHandler = (s, e) => tcs.SetResult(true);
            button.Click += clickHandler;
            await tcs.Task;
            button.Click -= clickHandler;
        }

The extension method instantiates a TaskCompletionSource and attaches a handler to the event. The handler simply sets the TaskCompletionSource result. The TaskCompletionSource's Task is then awaited. Finally, the continuation after the await statement removes the handler.

Managing Task-based Events in the ViewModel

The arrangement shown above is fine for dealing with events that are only concerned with visual presentation such as Storyboard events, but, for other events, it's better to abstract the event from the control and manage it within the ViewModel. The Prism Framework has a DelegateCommand that's used to bind events in the view to handler methods in the ViewModel. But the method that's called when an event is raised returns void. In order to be able to await the event, the returned object needs to be a Task.

AwaitableCommand

The AwaitableCommand class, shown below, has a CommandRaisedAsync() method that returns a Task and can be awaited. The class has a private DelegateCommand instance and most of the class' methods simply call the corresponding method of that instance.

C#
public class AwaitableCommand<T> : ICommand
{
    private readonly DelegateCommand<T> delegateCommand;
    public AwaitableCommand(Func<T, bool> canExecuteMethod)
    {

        delegateCommand =
                new DelegateCommand<T>((t) => Execute(t), canExecuteMethod);
    }
    public event EventHandler<T> CommandRaisedEvent;

    public event EventHandler CanExecuteChanged
    {
        add { delegateCommand.CanExecuteChanged += value; }
        remove { delegateCommand.CanExecuteChanged -= value; }
    }

    public bool CanExecute(object parameter)
    {
        return delegateCommand.CanExecute((T)parameter);
    }

    public void Execute(object parameter)
    {

        CommandRaisedEvent?.Invoke(this, (T)parameter);
    }
    public void RaiseCanExecuteChanged()
    {
        delegateCommand.RaiseCanExecuteChanged();
    }
    public async Task<T> CommandRaisedAsync()
    {
        var tcs = new TaskCompletionSource<T>();

        EventHandler<T> handler = (s, p) => tcs.SetResult(p);
        CommandRaisedEvent += handler;
        var parameter = await tcs.Task;
        CommandRaisedEvent -= handler;
        return parameter;
    }
}

Cancelling Tasks Returned from Events

One of the advantages of returning a Task from an event is that Tasks can be cancelled. Returning a cancellable Task from CommandRaisedAsync would enable the event to be timed out and the Task could be used effectively with other Tasks in calls to Task.WhenAny. The Task.WhenAny method returns the first Task of a series of Tasks to complete and it's sometimes necessary, when using this method, to cancel any uncompleted Tasks. The technique for cancelling a Task is to instantiate a CancellationTokenSource and pass the CancellationToken that it generates to an async method that returns a Task.  Cancellation is effected by calling CancellationTokenSource.Cancel. Cancellation is not automatic, it needs to be implemented within the async method by monitoring the state of the CancellationToken and taking action to cancel the method when the token's IsCancellationRequested property changes to true.  Here's an implementation of CommandRaisedAsync method that allows the method to be cancelled.

C#
public async Task<T> CommandRaisedAsync(CancellationToken token)
      {
          var commandRaisedTcs = new TaskCompletionSource<T>();

          //create linked CancellationTokenSource so 'delayUntilCancelled'
          //can be cancelled from inside this method.
          //With linked sources, the parent cts.Cancel() will cancel
          //its own token and the token generated by a child cts. But a child cts
          //cannot cancel the parent's token.
          using (var cts = CancellationTokenSource.CreateLinkedTokenSource(token))
          {
              var localToken = cts.Token;
              EventHandler<T> handler = (s, p) => commandRaisedTcs.SetResult(p);
              CommandRaisedEvent += handler;
              var commandRaisedTask = commandRaisedTcs.Task;
              var delayUntilCancelledTask = Task.Delay(-1, localToken);

              var awaitedTask = await Task.WhenAny
                  (commandRaisedTask, delayUntilCancelledTask);

              CommandRaisedEvent -= handler;
              if (awaitedTask.Status == TaskStatus.Canceled)
              {
                  //it's the delayUntilCancelledTask
                  //that has completed in Canceled state
                  //so cancel the commandRaisedTask
                  commandRaisedTcs.SetCanceled();
                  throw new OperationCanceledException("The Event was  cancelled");
              }

              //commandRaisedTask has completed so
              //cancel the delayUntilCancelledTask
              cts.Cancel();
              return commandRaisedTask.Result;
           }
      }

The trick here is to use the Task returned from a call to Task.Delay(-1,localToken) to monitor the state of the CancellationToken and use a second Task to represent the command raised event. The call to await Task.WhenAny(commandRaisedTask, delayUntilCancelledTask) asynchronously waits for the first Task of the two to complete. If the delayUntilCancelled Task completes first, an OperationCanceledException is thrown and the commandRasedTask is cancelled. If the commandRaisedTask completes first, the delayUntilCancelledTask is cancelled and the commandRaisedTask.Result is returned.

Cancellation using Linked CancellationTokenSources

In the example above, the CancellationToken passed to the Task.Delay method. is linked to the token passed into CommandRaisedAsync. The effect of this linkage is that the token generated by the linked source will be set to cancelled when the token passed into CommandRaisedAsync is cancelled. So the delayUntilCancelledTask can be cancelled by two sources, the local source and an external source. The using statement ensures that the linked CompletionTokenSource's Dispose method is called when the class goes out of scope. This precaution is not necessary with unlinked CompletionTokenSources.

Example Application

In this somewhat contrived WPF example, a behaviour is implemented where, when a mouse button is clicked and held down within a Rectangle, a counter is updated and records the period of time that the button is held in that position. Releasing the button or moving the mouse outside the Rectangle stops the count. The behaviour is timed out after 6 seconds but it can be cancelled before then by clicking a cancel Button. Apart from the start and cancel button events, four other events need to be managed. They are, the MouseButtonDown, MouseButtonUp, MouseLeave events and a timer event that's used to update the counter. In the code, the ICommand instances are initialised in the ViewModel's constructor and bound to their events in the XAML.

C#
public TestCounterVM()
       {
           MouseDownCommand = new AwaitableCommand<object>(o => true);
           MouseUpCommand = new AwaitableCommand<object>(o => true);
           MouseLeaveCommand = new AwaitableCommand<object>(o => true);
           CancelCommand = new DelegateCommand<object>(OnCancel, o => IsStarted);
           StartCommand = new DelegateCommand<object>(OnStartAsync, o => !IsStarted);
       }
XAML
<Rectangle Width="60" Height="60" Fill="Green" Margin="5"  >
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseDown">
                        <cmd:InvokeCommandAction Command="{Binding MouseDownCommand}"/>
                    </i:EventTrigger>
                    <i:EventTrigger EventName="MouseUp">
                        <i:InvokeCommandAction Command="{Binding MouseUpCommand}"/>
                    </i:EventTrigger>
                    <i:EventTrigger EventName="MouseLeave">
                        <i:InvokeCommandAction Command="{Binding MouseLeaveCommand}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Rectangle>

The ViewModel has a ManageCounterAsync method that awaits the Task returned from MouseDownCommand.CommandRaisedAsync. If the Task completes in a cancelled state, an OperationCanceledException is thrown and the method exits. If the Task ran to completion, the continuation is run and StartCountingAsync is called. This method uses Task.Delay to update the counter periodically. An instance of Task.WhenAny is then awaited. It returns the first Task to complete from the Tasks generated by calling MouseUpCommand.CommandRaisedAsync, MouseLeaveCommand.CommandRaisedAsync and StartCountingAsync. The continuation from this await statement cancels the other two Tasks by calling localCts.Cancel(). The next iteration of the while loop starts the process off again.

C#
private async Task ManageCounterAsync(CancellationToken cancelDemoToken)
       {
          while (true)
           {
               using (var localCts =
               CancellationTokenSource.CreateLinkedTokenSource(cancelDemoToken))
               {
                   var token = localCts.Token;
                   await MouseDownCommand.CommandRaisedAsync(cancelDemoToken);
                   Task startCountingTask = StartCountingAsync(token);
                   var whenAnyTask = await Task.WhenAny
                                     (MouseUpCommand.CommandRaisedAsync(token),
                    MouseLeaveCommand.CommandRaisedAsync(token), startCountingTask);
                   localCts.Cancel();
               }
           }
       }
       private async Task StartCountingAsync(CancellationToken token)
       {
           while (true)
           {
               //Delay 300millisecs
               await Task.Delay(300, token);
               Counter += 1;
           }
       }

OnStartAsync is called when a start Button is clicked. A catch block catches an exception thrown by cancelling the ManageCounterAsync method and updates the enabled property of the start and cancel Buttons. The timeout is activated by instructing the CancellationTokenSource to cancel the token after 6 seconds.

C#
private async void OnStartAsync(object arg)
   {
       Counter = 0;
       IsStarted = true;
       cts = new CancellationTokenSource();
       cts.CancelAfter(6000);
       var token = cts.Token;
       try
       {
           await ManageCounterAsync(token);
       }
       catch (OperationCanceledException)
       {
           IsStarted = false;
       }
   }

The OnCancel method simply cancels the ManageCounterAsync method.

C#
private void OnCancel(object arg)
   {
       cts.Cancel();
   }

Conclusion

Managing events in the conventional manner can be a tedious process involving the 'wiring up' of numerous event handlers scattered throughout the ViewModel and connected by multiple if then statements. By getting events to return a Task, their management can be greatly simplified. In the example above, four events are effectively managed within one method, there's little wiring and no if statements.

References

History

  • 5th June, 2018: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)