Freigeben über


Drag and Drop with Tasks & Async

Lucian has an excellent series on turning things into Tasks so that you can can be compose them and await them. The Drag and Drop one caught my eye because it is a similar example to the common Reactive Extension demo of the same thing. Drag and Drop is interesting because it is started by an event (MouseDown) and starts processing another event (MouseMove) until a third event happens (MouseUp). The pseudo code looks like this

  1. When a PointerDown event happens capture the pointer
  2. Wait until either a PointerMovement happens or we lose the pointer capture
  3. If the pointer moved, update the UI and go to step 2
  4. If the pointer lost capture (e.g. pointer was lifted) complete the drag operation

Traditionally this is a number of different event handlers plus some shared state that all have to act in concert. It can get a little messy to read and maintain.

Lucian shows nicely how to hook into the events and create a single method which returns a Task which represents the drag operation. Then you can await the operation and the result of the Task is the location of the drop like this

    1: private async void Rectangle_PointerPressed(object sender, PointerRoutedEventArgs e)
    2: {
    3:     Point result = await DragAsync((UIElement)sender, e);
    4:     string message = string.Format("Drag operation complete! at {0:N}, {1:N}", result.X, result.Y);
    5:     MessageDialog dlg = new MessageDialog(message);
    6:     await dlg.ShowAsync();
    7: }

 

The DragAsync method looks like this after I converted it to C# from the original VB.NET

 

    1: public Task<Point> DragAsync(UIElement shape, PointerRoutedEventArgs origE, IProgress<Point> progress = null)
    2: {
    3:     TaskCompletionSource<Point> taskCompletionSource = new TaskCompletionSource<Point>();
    4:     Point point = new Point(Canvas.GetLeft(shape), Canvas.GetTop(shape));
    5:     Point position = origE.GetCurrentPoint(null).Position;
    6:  
    7:     PointerEventHandler pointerMovedHandler = null;
    8:     PointerEventHandler pointerReleasedHandler = null;
    9:  
   10:     pointerMovedHandler = (Object s, PointerRoutedEventArgs e) =>
   11:     {
   12:         UIElement localShape = (UIElement)s;
   13:         Debug.WriteLine("enter pointerMovedHandler");
   14:         Point pt = e.GetCurrentPoint(null).Position;
   15:         if (progress != null)
   16:         {
   17:             progress.Report(pt);
   18:         }
   19:         var x = point.X + pt.X - position.X;
   20:         var y = point.Y + pt.Y - position.Y;
   21:         Canvas.SetLeft(localShape, x);
   22:         Canvas.SetTop(localShape, y);
   23:     };
   24:  
   25:     pointerReleasedHandler = (Object s, PointerRoutedEventArgs e) =>
   26:     {
   27:         UIElement localShape = (UIElement)s;
   28:         localShape.PointerMoved -= pointerMovedHandler;
   29:         localShape.PointerReleased -= pointerReleasedHandler;
   30:  
   31:         localShape.ReleasePointerCapture(origE.Pointer);
   32:  
   33:         taskCompletionSource.SetResult(e.GetCurrentPoint(null).Position);
   34:     };
   35:  
   36:     shape.CapturePointer(origE.Pointer);
   37:     shape.PointerMoved += pointerMovedHandler;
   38:     shape.PointerReleased += pointerReleasedHandler;
   39:     return taskCompletionSource.Task;
   40: }

 

but I was thinking of this in terms of more declarative programming. You still have to follow the flow in and out of the various events. Taking that example and adding in the concept of wrapping the events as Tasks we can eliminate the lambda expressions and write a method that actually represents the pseudo code directly and is easier to follow.

 

    1: private async Task<Point> DragAsync(UIElement shape, PointerRoutedEventArgs e)
    2: {
    3:     Point point = new Point(Canvas.GetLeft(shape), Canvas.GetTop(shape));
    4:     Point position = e.GetCurrentPoint(null).Position;
    5:  
    6:     shape.CapturePointer(e.Pointer);
    7:  
    8:     Task<PointerRoutedEventArgs> stopTracking = shape.WhenPointerLost();
    9:  
   10:     while (!stopTracking.IsCompleted)
   11:     {
   12:         Task<PointerRoutedEventArgs> moved = shape.WhenPointerMoved();
   13:         await Task.WhenAny(stopTracking, moved);
   14:         {
   15:             if (moved.IsCompleted)
   16:             {
   17:                 Point pt = moved.Result.GetCurrentPoint(null).Position;
   18:                 var x = point.X + pt.X - position.X;
   19:                 var y = point.Y + pt.Y - position.Y;
   20:                 Canvas.SetLeft(shape, x);
   21:                 Canvas.SetTop(shape, y);
   22:             }
   23:         }
   24:     }
   25:  
   26:     shape.ReleasePointerCapture(e.Pointer);
   27:  
   28:     return stopTracking.Result.GetCurrentPoint(null).Position;
   29: }

 

The two items that make this possible are WhenPointersLost() on line 8 and WhenPointerMoved() on line 12. These are extension methods that wrap all the pointer events. WhenPointerMoved() wraps the PointerMoved event (of course). WhenPointersLost() actually wraps 3 different events since there are multiple events that indicate the loss of the capture of a pointer: PointerReleased, PointerCanceled, and PointerCaptureLost.

 

    1: public static Task<PointerRoutedEventArgs> WhenPointerLost(this UIElement element)
    2: {
    3:     TaskCompletionSource<PointerRoutedEventArgs> taskCompletionSource = new TaskCompletionSource<PointerRoutedEventArgs>();
    4:     PointerEventHandler routedEventHandler = null;
    5:  
    6:     routedEventHandler = (Object s, PointerRoutedEventArgs e) =>
    7:     {
    8:         UIElement localBtn = element;
    9:         localBtn.PointerReleased -= routedEventHandler;
   10:         localBtn.PointerCanceled -= routedEventHandler;
   11:         localBtn.PointerCaptureLost -= routedEventHandler;
   12:         taskCompletionSource.SetResult(e);
   13:     };
   14:  
   15:     UIElement button = element;
   16:     button.PointerReleased += routedEventHandler;
   17:     button.PointerCanceled += routedEventHandler;
   18:     button.PointerCaptureLost += routedEventHandler;
   19:  
   20:     var task = taskCompletionSource.Task;
   21:     return task;
   22: }
   23:  
   24: public static Task<PointerRoutedEventArgs> WhenPointerMoved(this UIElement element)
   25: {
   26:     TaskCompletionSource<PointerRoutedEventArgs> taskCompletionSource = new TaskCompletionSource<PointerRoutedEventArgs>();
   27:     PointerEventHandler routedEventHandler = null;
   28:  
   29:     routedEventHandler = (Object s, PointerRoutedEventArgs e) =>
   30:     {
   31:         UIElement localBtn = element;
   32:         localBtn.PointerMoved -= routedEventHandler;
   33:         taskCompletionSource.SetResult(e);
   34:     };
   35:  
   36:     UIElement button = element;
   37:     button.PointerMoved += routedEventHandler;
   38:     var task = taskCompletionSource.Task;
   39:     return task;
   40: }

 

There is nothing exceptional in the extension methods. They are boilerplate wrapping but they enable the above scenario and potentially can be reused for others.

And of course since Tasks are not necessarilly Threads, there isn't any extra CPU being used or any extra threads created. All the work is the same work we would have done before its just reorganized into a linear method.