Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drag and Drop in Silverlight with F# and Asynchronous Workflows

I'm trying to implement drag and drop in Silverlight using F# and asynchronous workflows.

I'm simply trying to drag around a rectangle on the canvas, using two loops for the the two states (waiting and dragging), an idea I got from Tomas Petricek's book "Real-world Functional Programming", but I ran into a problem:

Unlike WPF or WinForms, Silverlight's MouseEventArgs do not carry information about the button state, so I can't return from the drag-loop by checking if the left mouse button is no longer pressed. I only managed to solve this by introducing a mutable flag.

Would anyone have a solution for this, that does not involve mutable state?

Here's the relevant code part (please excuse the sloppy dragging code, which snaps the rectangle to the mouse pointer):

type MainPage() as this =
    inherit UserControl()
    do
        Application.LoadComponent(this, new System.Uri("/SilverlightApplication1;component/Page.xaml", System.UriKind.Relative))
    let layoutRoot : Canvas = downcast this.FindName("LayoutRoot")
    let rectangle1 : Rectangle = downcast this.FindName("Rectangle1")

    let mutable isDragged = false

    do
        rectangle1.MouseLeftButtonUp.Add(fun _ -> isDragged <- false)

        let rec drag() = async {
            let! args = layoutRoot.MouseMove |> Async.AwaitEvent
            if (isDragged) then
                Canvas.SetLeft(rectangle1, args.GetPosition(layoutRoot).X)
                Canvas.SetTop(rectangle1, args.GetPosition(layoutRoot).Y)
                return! drag()
            else
                return()
            } 
        let wait() = async {
            while true do
                let! args = Async.AwaitEvent rectangle1.MouseLeftButtonDown
                isDragged <- true
                do! drag()
            }

        Async.StartImmediate(wait())
        ()

Thank you very much for your time!

like image 621
knotig Avatar asked Dec 14 '25 09:12

knotig


1 Answers

The way to solve this issue is to use an overloaded AwaitEvent that allows you to wait for two events. Instead of just waiting for MouseMove, you can also wait for the MouseUp event - in the first case, you can continue moving and in the second case, you can return from the loop and stop drag&drop (this is actually discussed later in the book in section 16.4.5).

Here is the code - it actually uses AwaitObservable variant of the method (see below), which is a better choice in general, because it works with Observable.map and similar combinators (in case you wanted to use these).

let! args = Async.AwaitObservable(layoutRoot.MouseMove, layoutRoot.MouseUp)
match args with
| Choice1Of2(args) ->
    // Handle the 'MouseMove' event with 'args' here
    Canvas.SetLeft(rectangle1, args.GetPosition(layoutRoot).X)  
    Canvas.SetTop(rectangle1, args.GetPosition(layoutRoot).Y)  
    return! drag()  
| Choice2Of2(_) ->
    // Handle the 'MouseUp' event here
    return()  

As far as I know, the overloaded AwaitObservable method is not available in the F# libraries (yet), but you can get it from the book's web site, or you can use the following code:

// Adds 'AwaitObservable' that takes two observables and returns
// Choice<'a, 'b> containing either Choice1Of2 or Choice2Of2 depending
// on which of the observables occurred first
type Microsoft.FSharp.Control.Async with   
  static member AwaitObservable(ev1:IObservable<'a>, ev2:IObservable<'b>) = 
    Async.FromContinuations((fun (cont,econt,ccont) -> 
      let rec callback1 = (fun value ->
        remover1.Dispose()
        remover2.Dispose()
        cont(Choice1Of2(value)) )
      and callback2 = (fun value ->
        remover1.Dispose()
        remover2.Dispose()
        cont(Choice2Of2(value)) )
      // Attach handlers to both observables
      and remover1 : IDisposable  = ev1.Subscribe(callback1) 
      and remover2 : IDisposable  = ev2.Subscribe(callback2) 
      () ))
like image 148
Tomas Petricek Avatar answered Dec 17 '25 01:12

Tomas Petricek



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!