Sunday, 16 November 2008

Using attached properties to compose new behaviour

I was inspired by a posting on stackoverflow.com - "How do I drag an image around a canvas in WPF" - where the first response in most peoples heads is to work with mouse up/down/move events, track offset positions and move the UI element around in response - pretty reasonable right?

Seeing as WPF's principles favours composition over inheritance and such like, what if instead we used the power of attached properties to attach new behaviour to a UIElement? Here's how to do it, first the code for the dependency property:

public class DraggableExtender : DependencyObject
{
    // This is the dependency property we're exposing - we'll 
    // access this as DraggableExtender.CanDrag="true"/"false"
    public static readonly DependencyProperty CanDragProperty =
        DependencyProperty.RegisterAttached("CanDrag",
        typeof(bool),
        typeof(DraggableExtender),
        new UIPropertyMetadata(false, OnChangeCanDragProperty));

    // The expected static setter
    public static void SetCanDrag(UIElement element, bool o)
    {
        element.SetValue(CanDragProperty, o);
    }

    // the expected static getter
    public static bool GetCanDrag(UIElement element)
    {
        return (bool) element.GetValue(CanDragProperty);
    }

    // This is triggered when the CanDrag property is set. We'll
    // simply check the element is a UI element and that it is
    // within a canvas. If it is, we'll hook into the mouse events
    private static void OnChangeCanDragProperty(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        UIElement element = d as UIElement;
        if (element == null) return;

        if (e.NewValue != e.OldValue)
        {
            if ((bool)e.NewValue)
            {
                element.PreviewMouseDown += element_PreviewMouseDown;
                element.PreviewMouseUp += element_PreviewMouseUp;
                element.PreviewMouseMove += element_PreviewMouseMove;
            }
            else
            {
                element.PreviewMouseDown -= element_PreviewMouseDown;
                element.PreviewMouseUp -= element_PreviewMouseUp;
                element.PreviewMouseMove -= element_PreviewMouseMove;
            }
        }
    }

    // Determine if we're presently dragging
    private static bool _isDragging = false;
    // The offset from the top, left of the item being dragged 
    // versus the original mouse down
    private static Point _offset;

    // This is triggered when the mouse button is pressed 
    // on the element being hooked
    static void element_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        // Ensure it's a framework element as we'll need to 
        // get access to the visual tree
        FrameworkElement element = sender as FrameworkElement;
        if (element == null) return;

        // start dragging and get the offset of the mouse 
        // relative to the element
        _isDragging = true;
        _offset = e.GetPosition(element);
    }

    // This is triggered when the mouse is moved over the element
    private static void element_PreviewMouseMove(object sender, 
        MouseEventArgs e)
    {
        // If we're not dragging, don't bother
        if (!_isDragging) return;

        FrameworkElement element = sender as FrameworkElement;
        if (element == null) return;

        Canvas canvas = element.Parent as Canvas;
        if( canvas == null ) return;
        
        // Get the position of the mouse relative to the canvas
        Point mousePoint = e.GetPosition(canvas);

        // Offset the mouse position by the original offset position
        mousePoint.Offset(-_offset.X, -_offset.Y);

        // Move the element on the canvas
        element.SetValue(Canvas.LeftProperty, mousePoint.X);
        element.SetValue(Canvas.TopProperty, mousePoint.Y);
    }

    // this is triggered when the mouse is released
    private static void element_PreviewMouseUp(object sender, 
        MouseButtonEventArgs e)
    {
        _isDragging = false;
    }
}

As you can see, we hook into the events exposed from the target element whenever we detect the property being changed. This allows us to inject any logic we like!

To use the behaviour, we include the namespace in XAML:

<Window x:Class="WPFFunWithDragging.Window1"
        xmlns:local="clr-namespace:WPFFunWithDragging"

And then just attach the behaviour to the elements we want to be able to drag like so;

<Canvas>
       <Image Source="Garden.jpg" 
              Width="50" 
              Canvas.Left="10" Canvas.Top="10" 
              local:DraggableExtender.CanDrag="true"/>
   </Canvas>

Cool huh? Sample code attached....

No comments:

Post a Comment