wdonahoe
wdonahoe

Reputation: 1073

Use a CancellationToken to interrupt nested tasks

Here is my scenario: The user clicks a WPF button which initiates an open-ended period for the collection of points on a map. When the user clicks the "finished collecting" button, I want the CollectPoints() task to complete.

Here are pieces of my SegmentRecorder class:

    private CancellationTokenSource _cancellationToken;     

    public virtual async void RecordSegment(IRoadSegment segment)
    {
        _cancellationToken = new CancellationTokenSource();
        var token = _cancellationToken.Token;

        // await user clicking points on map
        await CollectPoints(token);

        // update the current segment with the collected shape.
        CurrentSegment.Shape = CurrentGeometry as Polyline;
    }

    // collect an arbitrary number of points and build a polyline.
    private async Task CollectPoints(CancellationToken token)
    {
        var points = new List<MapPoint>();
        while (!token.IsCancellationRequested)
        {
            // wait for a new point.
            var point = await CollectPoint();
            points.Add(point);

            // add point to current polyline
            var polylineBuilder = new PolylineBuilder(points, SpatialReferences.Wgs84);
            CurrentGeometry = polylineBuilder.ToGeometry();

            // draw points
            MapService.DrawPoints(CurrentGeometry);
        }
    }

    // collect a point from map click.
    protected override Task<MapPoint> CollectPoint()
    {
        var tcs = new TaskCompletionSource<MapPoint>();
        EventHandler<IMapClickedEventArgs> handler = null;
        handler = (s, e) =>
        {
            var mapPoint = e.Geometry as MapPoint;
            if (mapPoint != null)
            {
                tcs.SetResult(new MapPoint(mapPoint.X, mapPoint.Y, SpatialReferences.Wgs84));
            }
            MapService.OnMapClicked -= handler;
        };
        MapService.OnMapClicked += handler;

        return tcs.Task;
    }

    public void StopSegment(){
        // interrupt the CollectPoints task.
        _cancellationToken.Cancel();
    }

Here are the relevant parts of my view model:

public SegmentRecorder SegmentRecorder { get; }
public RelayCommand StopSegment { get; }

public ViewModel(){
    StopSegment = new RelayCommand(ExecuteStopSegment);
    SegmentRecorder = new SegmentRecorder();
}

// execute on cancel button click.
public void ExecuteStopSegment(){
    SegmentRecorder.StopSegment();
}

When I put a breakpoint on the line while (!token.IsCancellationRequested) and click the cancel button, I never get to that point.

Am I using the cancellation token in the correct way here?

Upvotes: 3

Views: 1086

Answers (1)

mm8
mm8

Reputation: 169200

The CollectPoints method will return whenever it hits the while condition !token.IsCancellationRequested the first time after you have called the Cancel() method of the CancellationTokenSource.

The task won't be cancelled while the code inside the while loop is still executing.

As @JSteward suggests in his comment, you should cancel or complete the TaskCompletionSource in your StopSegment() method.

Something like this:

public virtual async void RecordSegment(IRoadSegment segment)
{
    _cancellationToken = new CancellationTokenSource();
    var token = _cancellationToken.Token;

    // await user clicking points on map
    await CollectPoints(token);

    // update the current segment with the collected shape.
    CurrentSegment.Shape = CurrentGeometry as Polyline;
}

// collect an arbitrary number of points and build a polyline.
private async Task CollectPoints(CancellationToken token)
{
    var points = new List<MapPoint>();
    while (!token.IsCancellationRequested)
    {
        try
        {
            // wait for a new point.
            var point = await CollectPoint(token);

            //...
        }
        catch (Exception) { }
    }
}

private TaskCompletionSource<MapPoint> tcs;
protected override Task<MapPoint> CollectPoint()
{
    tcs = new TaskCompletionSource<MapPoint>();
    //...
    return tcs.Task;
}

public void StopSegment()
{
    // interrupt the CollectPoints task.
    _cancellationToken.Cancel();
    tcs.SetCanceled();
}

Upvotes: 4

Related Questions