Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Draggable PushPins

0.00/5 (No votes)
10 Oct 2010 1  
Draggable PushPins

I must apologise that it has taken me so long to post a blog entry. I’ve been busy playing about with code contracts and working with Bing Maps in Silverlight – there’s been a lot for me to take in. Actually, the Bing maps part is the reason for this posting, and I hope that it really helps you out.

A common requirement in GIS applications is to take a point of interest from one location and move it to another by dragging it. This is behaviour that you would expect to be standard, but surprisingly enough it is not present with a default Pushpin in Silverlight Bing maps. In this post, we’ll discuss adding a Blend Behavior that provides this drag and drop functionality.

As usual, when writing a behavior, you start off by inheriting from the generic Behavior class, telling it which framework element you want to associate the behavior with.

C#
public class DraggablePushpin : Behavior<Pushpin>
{
}

Now we want to tell it what happens when we attach the object. This is done in the OnAttached override.

C#
/// <summary>
/// Hook the event handlers to this instance.
/// </summary>
protected override void OnAttached()
{
  base.OnAttached();
   AssociatedObject.MouseLeftButtonDown +=
    new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
}

Basically, when the left mouse button is pressed on the Pushpin, we’re going to invoke an event handler that is going to do the grunt work for handling the relevant events that we need to work with. The definition of this method looks like this:

C#
/// <summary>
/// Called when the left mouse button is pressed on a
/// Point of Interest.
/// </summary>
void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  if (_map == null)
  // Find the map this pushpin is attached to.
  _map = FindParent<Map>(AssociatedObject);
  if (_map != null)
  {
    if (this.ParentMapMousePanHandler == null)
    {
      this.ParentMapMousePanHandler = new EventHandler<MapMouseDragEventArgs>
        ((s, epan) => epan.Handled = this._isdragging);
      _map.MousePan += ParentMapMousePanHandler;
    } 
    if (this.ParentMapMouseLeftButtonEventHandler == null)
    {
      this.ParentMapMouseLeftButtonEventHandler = new MouseButtonEventHandler 
        ((s, args) => ConfirmMoveLocation());
      _map.MouseLeftButtonUp += this.ParentMapMouseLeftButtonEventHandler;
    }
    if (this.ParentMapMouseMoveHandler == null)
    { 
      // If the mouse is performing a drag operation, convert the
      // underlying location based on the position of the mouse
      // on the viewport to a map based location.
      this.ParentMapMouseMoveHandler = new MouseEventHandler(
        (s, mouseargs) =>
          { 
            if (_isdragging)
              _location = _map.ViewportPointToLocation(mouseargs.GetPosition(_map)); 
          });
      _map.MouseMove += this.ParentMapMouseMoveHandler;
    }
  }
  _isdragging = true;
}

This method starts off by identifying the map that owns the pushpin. This means traversing the visual tree until the map is identified (I’ve done this because a common way of allocating Pushpins to a map is to host them in a DataTemplate which causes problems when attempting to find the parent map just by methods on the Pushpin). Once the map has been identified, we are going to handle the following map events:

  • Mouse pan
  • Mouse left button up
  • Mouse move

When the mouse pans, the event handler uses a flag to determine whether or not a Pushpin is being dragged and sets the Handled parameter as necessary.

When the mouse moves, the event handler converts the position of the mouse on the map into a location coordinate.

When the left button is released, the behavior calls a method to prompt the user whether or not they wish to move the location and updates the position accordingly.
Finally, we have methods available to detach the events. The whole behaviour is shown here:

C#
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Interactivity;
using Microsoft.Maps.MapControl;
using Microsoft.Maps.MapControl.Core;
namespace Pointilist.Behaviors
{
  /// <summary>
  /// This behaviour adds drag capabilities to a Bing Maps <see cref="Pushpin"/> control.
  /// </summary>
  public class DraggablePushpin : Behavior<Pushpin>
  {
    #region Members 
    private bool _isdragging = false;
    private MapBase _map = null;
    private EventHandler<MapMouseDragEventArgs> ParentMapMousePanHandler;
    private MouseButtonEventHandler ParentMapMouseLeftButtonEventHandler;
    private MouseEventHandler ParentMapMouseMoveHandler;
    private Location _location;
    #endregion

    /// <summary>
    /// Hook the event handlers to this instance.
    /// </summary>
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.MouseLeftButtonDown +=
        new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
    }
    /// <summary>
    /// Unhook the events when this is detaching.
    /// </summary>protected override void OnDetaching()
    {
      base.OnDetaching();
      AssociatedObject.MouseLeftButtonDown -=
        new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
      DetachEvents();
    }
    /// <summary>
    /// Make sure that no mouse events are left dangling.
    /// </summary>
    private void DetachEvents()
    {
      if (_map == null) return;
      if (this.ParentMapMousePanHandler != null)
      {
        _map.MousePan -= ParentMapMousePanHandler;
        ParentMapMousePanHandler = null;
      }
      if (ParentMapMouseLeftButtonEventHandler != null)
      {
        _map.MouseLeftButtonUp -= ParentMapMouseLeftButtonEventHandler;
        ParentMapMouseLeftButtonEventHandler = null;
      }
      if (ParentMapMouseMoveHandler != null)
      {
        _map.MouseMove -= ParentMapMouseMoveHandler;
        ParentMapMouseMoveHandler = null;
      }
    }
    /// <summary>
    /// Only move the poi if the user accepts the change
    /// (at which point, it's saved to the database).
    /// </summary>
    private void ConfirmMoveLocation()
    {
      _isdragging = false;
      if (_location == null) return;

      DetachEvents();
      MessageBoxResult result = MessageBox.Show("Are you sure you want to move here?",
        "Move location?", MessageBoxButton.OKCancel);
      if (result == MessageBoxResult.OK)
      {
        AssociatedObject.Location = _location;
      }
      _location = null;  
    }
    /// <summary>
    /// Called when the left mouse button is pressed on a
    /// Point of Interest.
    /// </summary>
    void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
      if (_map == null)
      // Find the map this pushpin is attached to.
      _map = FindParent<Map>(AssociatedObject);
      if (_map != null)
      {
        if (this.ParentMapMousePanHandler == null)
        {
          this.ParentMapMousePanHandler = new EventHandler<MapMouseDragEventArgs>
            ((s, epan) => epan.Handled = this._isdragging);
          _map.MousePan += ParentMapMousePanHandler;
        }
        if (this.ParentMapMouseLeftButtonEventHandler == null)
        {
          this.ParentMapMouseLeftButtonEventHandler = new MouseButtonEventHandler
            ((s, args) => ConfirmMoveLocation());
          _map.MouseLeftButtonUp += this.ParentMapMouseLeftButtonEventHandler;
        }
        if (this.ParentMapMouseMoveHandler == null)
        {
          // If the mouse is performing a drag operation, convert the
          // underlying location based on the position of the mouse
          // on the viewport to a map based location.
          this.ParentMapMouseMoveHandler = new MouseEventHandler(
            (s, mouseargs) =>
              {
                if (_isdragging)
                _location = _map.ViewportPointToLocation(mouseargs.GetPosition(_map));
              });
          _map.MouseMove += this.ParentMapMouseMoveHandler;
        }
       _isdragging = true; style="font-size: 13px; 
		line-height: 16px; white-space: normal; "> 
      } 
    }
    /// <summary>
    /// Find the relevant parent item for a particular dependency object. 
    /// </summary>
    /// <typeparam name="T">The type of object to search for.</typeparam>
    /// <param name="child">The <see cref="DependencyObject"/> to start searching from.
    /// </param>
    /// <returns>The parent object if found, null otherwise.</returns>
    public static T FindParent<T>(DependencyObject child) where T : FrameworkElement
    {
      DependencyObject parentObject = VisualTreeHelper.GetParent(child);
      // If parentObject is null, we've reached the top of the tree 
      // without finding the item we were looking for.
      if (parentObject == null) return null;
      T parent = parentObject as T;
      // If parent is null, recursively call this method.
      if (parent == null)
        return FindParent<T>(parentObject);
      // If we reach this point, we have found the parent item we are looking for.
      return parent;
    }
  }
}

Now, it’s a simple matter to hook it up to a Pushpin.

XML
<m:Pushpin m:MapLayer.Position="{Binding Location, Mode=TwoWay}"
    Visibility="{Binding IsVisible, Converter={StaticResource VisibilityConverter}}"
    PositionOrigin="Center" ToolTipService.ToolTip="{Binding Address}">
  <interact:Interaction.Behaviors>
    <behavior:DraggablePushpin />
  </interact:Interaction.Behaviors>
</m:Pushpin>

There you have it – a draggable Pushpin. I hope that this helps you as much as it’s helping me.


License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here