5

I'm trying to perform a drag and drop approach to creating relationships in a diagram, directly analagous to SQL Server Management Studio diagramming tools. For example, in the illustration below, the user would drag CustomerID from the User entity to the Customer entity and create a foreign key relationship between the two.

The key desired feature is that a temporary arc path would be drawn as the user performs the drag operation, following the mouse. Moving entities or relationships once created isn't the issue I'm running into.

Entity–relationship diagram

Some reference XAML corresponding to an entity on the diagram above:

<!-- Entity diagram control -->
<Grid MinWidth="10" MinHeight="10" Margin="2">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="*" ></RowDefinition>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"></ColumnDefinition>
    </Grid.ColumnDefinitions>
    <Grid Grid.Row="0" Grid.Column="0" IsHitTestVisible="False" Background="{StaticResource ControlDarkBackgroundBrush}">
        <Label Grid.Row="0" Grid.Column="0" Style="{DynamicResource LabelDiagram}" Content="{Binding DiagramHeader, Mode=OneWay}" />
    </Grid>
    <ScrollViewer Grid.Row="1" Grid.Column="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="{StaticResource ControlBackgroundBrush}" >
        <StackPanel VerticalAlignment="Top">
            <uent:EntityDataPropertiesDiagramControl DataContext="{Binding EntityDataPropertiesFolder}" />
            <uent:CollectionEntityPropertiesDiagramControl DataContext="{Binding CollectionEntityPropertiesFolder}" />
            <uent:DerivedEntityDataPropertiesDiagramControl DataContext="{Binding DerivedEntityDataPropertiesFolder}" />
            <uent:ReferenceEntityPropertiesDiagramControl DataContext="{Binding ReferenceEntityPropertiesFolder}" />
            <uent:MethodsDiagramControl DataContext="{Binding MethodsFolder}" />
        </StackPanel>
    </ScrollViewer>
    <Grid Grid.RowSpan="2" Margin="-10">
        <lib:Connector x:Name="LeftConnector" Orientation="Left" VerticalAlignment="Center" HorizontalAlignment="Left" Visibility="Collapsed"/>
        <lib:Connector x:Name="TopConnector" Orientation="Top" VerticalAlignment="Top" HorizontalAlignment="Center" Visibility="Collapsed"/>
        <lib:Connector x:Name="RightConnector" Orientation="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Collapsed"/>
        <lib:Connector x:Name="BottomConnector" Orientation="Bottom" VerticalAlignment="Bottom" HorizontalAlignment="Center" Visibility="Collapsed"/>
    </Grid>
</Grid>

My current approach to doing this is to:

1) Initiate the drag operation in a child control of the entity, such as:

protected override void OnPreviewMouseMove(MouseEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed)
    {
        dragStartPoint = null;
    }
    else if (dragStartPoint.HasValue)
    {
        Point? currentPosition = new Point?(e.GetPosition(this));
        if (currentPosition.HasValue && (Math.Abs(currentPosition.Value.X - dragStartPoint.Value.X) > 10 || Math.Abs(currentPosition.Value.Y - dragStartPoint.Value.Y) > 10))
        {
            DragDrop.DoDragDrop(this, DataContext, DragDropEffects.Link);
            e.Handled = true;
        }
    }
}

2) Create a connector adorner when the drag operation leaves the entity, such as:

protected override void OnDragLeave(DragEventArgs e)
{
    base.OnDragLeave(e);
    if (ParentCanvas != null)
    {
        AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(ParentCanvas);
        if (adornerLayer != null)
        {
            ConnectorAdorner adorner = new ConnectorAdorner(ParentCanvas, BestConnector);
            if (adorner != null)
            {
                adornerLayer.Add(adorner);
                e.Handled = true;
            }
        }
    }
}

3) Draw the arc path as the mouse is being moved in the connector adorner, such as:

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            if (!IsMouseCaptured) CaptureMouse();
            HitTesting(e.GetPosition(this));
            pathGeometry = GetPathGeometry(e.GetPosition(this));
            InvalidateVisual();
        }
        else
        {
            if (IsMouseCaptured) ReleaseMouseCapture();
        }
    }

The diagram Canvas is bound to a view model, and the entities and relationships on the Canvas are in turn bound to respective view models. Some XAML relating to the overall diagram:

<ItemsControl ItemsSource="{Binding Items, Mode=OneWay}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <lib:DesignerCanvas VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Canvas.Left" Value="{Binding X}"/>
            <Setter Property="Canvas.Top" Value="{Binding Y}"/>
            <Setter Property="Canvas.Width" Value="{Binding Width}"/>
            <Setter Property="Canvas.Height" Value="{Binding Height}"/>
            <Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

and DataTemplates for the entites and relationships:

<!-- diagram relationship -->
<DataTemplate DataType="{x:Type dvm:DiagramRelationshipViewModel}">
    <lib:Connection />
</DataTemplate>
<!-- diagram entity -->
<DataTemplate DataType="{x:Type dvm:DiagramEntityViewModel}">
    <lib:DesignerItem>
        <lib:EntityDiagramControl />
    </lib:DesignerItem>
</DataTemplate>

Issue: The issue is that once the drag operation begins, mouse moves are no longer tracked and the connector adorner is unable to draw the arc as it does in other contexts. If I release the mouse and click again, then the arc starts drawing, but then I've lost my source object. I'm trying to figure a way to pass the source object in conjunction with mouse movement.

Bounty: Circling back to this issue, I currently plan to not use drag and drop directly to do this. I currently plan to add a DragItem and IsDragging DependencyProperty for the diagram control, which would hold the item being dragged, and flag if a drag operation is occuring. I could then use DataTriggers to change the Cursor and Adorner visibility based on IsDragging, and could use DragItem for the drop operation.

(But, I'm looking to award a bounty on another interesting approach. Please comment if more information or code is needed to clarify this question.)

Edit: Lower priority, but I'm still on the lookout for a better solution for a drag and drop diagramming approach. Want to implement a better approach in the open source Mo+ Solution Builder.

Dave Clemmer
  • 3,757
  • 12
  • 47
  • 72

3 Answers3

4

This is a fairly involved answer. Let me know if any part of it isn't clear.

I’m currently trying to solve a similar problem. In my case, I want to bind my ListBox ItemsSource to a collection and then represent every item in that collection as either a node i.e a draggable object or a connection i.e a line between nodes that redraws itself when the nodes are dragged. I’ll show you my code and detail where I think you might need to make changes to fit your needs.

Dragging

Dragging is accomplished by setting attached properties owned by the Dragger class. In my opinion, this has an advantage over using the MoveThumb to perform dragging in that making an object draggable does not involve changing its control template. My first implementation actually used MoveThumb in control templates to achieve dragging, but I found that doing so made my application very brittle (adding new features often broke the dragging). Here's the code for the Dragger:

public static class Dragger
    {
        private static FrameworkElement currentlyDraggedElement;
        private static FrameworkElement CurrentlyDraggedElement
        {
            get { return currentlyDraggedElement; } 
            set
            {
                currentlyDraggedElement = value;
                if (CurrentlyDraggedElement != null)
                {
                    CurrentlyDraggedElement.MouseMove += new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
                    CurrentlyDraggedElement.MouseLeftButtonUp +=new MouseButtonEventHandler(CurrentlyDraggedElement_MouseLeftButtonUp);
                }
            }           
        }

        private static ItemPreviewAdorner adornerForDraggedItem;
        private static ItemPreviewAdorner AdornerForDraggedItem
        {
            get { return adornerForDraggedItem; }
            set { adornerForDraggedItem = value; }
        }

        #region IsDraggable

        public static readonly DependencyProperty IsDraggableProperty = DependencyProperty.RegisterAttached("IsDraggable", typeof(Boolean), typeof(Dragger),
            new FrameworkPropertyMetadata(IsDraggable_PropertyChanged));

        public static void SetIsDraggable(DependencyObject element, Boolean value)
        {
            element.SetValue(IsDraggableProperty, value);
        }
        public static Boolean GetIsDraggable(DependencyObject element)
        {
            return (Boolean)element.GetValue(IsDraggableProperty);
        }

        #endregion

        #region IsDraggingEvent

        public static readonly RoutedEvent IsDraggingEvent = EventManager.RegisterRoutedEvent("IsDragging", RoutingStrategy.Bubble,
            typeof(RoutedEventHandler), typeof(Dragger));

        public static event RoutedEventHandler IsDragging;

        public static void AddIsDraggingHandler(DependencyObject d, RoutedEventHandler handler)
        {
            UIElement uie = d as UIElement;
            if (uie != null)
            {
                uie.AddHandler(Dragger.IsDraggingEvent, handler);
            }
        }

        public static void RemoveIsDraggingEventHandler(DependencyObject d, RoutedEventHandler handler)
        {
            UIElement uie = d as UIElement;
            if (uie != null)
            {
                uie.RemoveHandler(Dragger.IsDraggingEvent, handler);
            }
        }

        #endregion

        public static void IsDraggable_PropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if ((bool)args.NewValue == true)
            {
                FrameworkElement element = (FrameworkElement)obj;
                element.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(itemToBeDragged_MouseLeftButtonDown);
            }
        }

        private static void itemToBeDragged_MouseLeftButtonDown(object sender, MouseEventArgs e)
        {
            var element = sender as FrameworkElement;
            if (element != null)
            {                
                CurrentlyDraggedElement = element;
            }           
        }

        private static void CurrentlyDraggedElement_MouseMove(object sender, MouseEventArgs e)
        {
            var element = sender as FrameworkElement;
            if (element.IsEnabled == true)
            {
                element.CaptureMouse();
                //RaiseIsDraggingEvent();
                DragObject(sender, new Point(Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).X,
                    Mouse.GetPosition(PavilionVisualTreeHelper.GetAncestor(element, typeof(CustomCanvas)) as CustomCanvas).Y));
            }         
        }

        private static void CurrentlyDraggedElement_MouseLeftButtonUp(object sender, MouseEventArgs e)
        {
            FrameworkElement element = sender as FrameworkElement;
            element.MouseMove -= new MouseEventHandler(CurrentlyDraggedElement_MouseMove);
            element.ReleaseMouseCapture();
            CurrentlyDraggedElement = null;
        }

        private static void DragObject(object sender, Point startingPoint)
        {
            FrameworkElement item = sender as FrameworkElement;

            if (item != null)
            {
                var canvas = PavilionVisualTreeHelper.GetAncestor(item, typeof(CustomCanvas)) as CustomCanvas;

                double horizontalPosition = Mouse.GetPosition(canvas).X - item.ActualWidth/2;
                double verticalPosition = Mouse.GetPosition(canvas).Y - item.ActualHeight/2;

                item.RenderTransform = ReturnTransFormGroup(horizontalPosition, verticalPosition);
                item.RaiseEvent(new IsDraggingRoutedEventArgs(item, new Point(horizontalPosition, verticalPosition), IsDraggingEvent));
            }
        }

        private static TransformGroup ReturnTransFormGroup(double mouseX, double mouseY)
        {
            TransformGroup transformGroup = new TransformGroup();
            transformGroup.Children.Add(new TranslateTransform(mouseX, mouseY));
            return transformGroup;
        }
    }

    public class IsDraggingRoutedEventArgs : RoutedEventArgs
    {
        public Point LocationDraggedTo { get; set;}
        public FrameworkElement ElementBeingDragged { get; set; }

        public IsDraggingRoutedEventArgs(DependencyObject elementBeingDragged, Point locationDraggedTo, RoutedEvent routedEvent)
            : base(routedEvent)
        {
            this.ElementBeingDragged = elementBeingDragged as FrameworkElement;
            LocationDraggedTo = locationDraggedTo;            
        }
    }

I believe that Dragger requires that the object be on a Canvas or CustomCanvas, but there isn't any good reason, besides lazyness, for this. You could easily modify it to work for any Panel. (It’s in my backlog!).

The Dragger class is also using the PavilionVisualTreeHelper.GetAncestor() helper method, which simply climbs the Visual Tree looking for the appropriate element. The code for that is below.

 /// <summary>
    /// Gets ancestor of starting element
    /// </summary>
    /// <param name="parentType">Desired type of ancestor</param>
    public static DependencyObject GetAncestor(DependencyObject startingElement, Type parentType)
    {
        if (startingElement == null || startingElement.GetType() == parentType)
            return startingElement;
        else
            return GetAncestor(VisualTreeHelper.GetParent(startingElement), parentType);
    }

Consuming the Dragger class is very simple. Simply set Dragger.IsDraggable = true in the appropriate control’s xaml markup. Optionally, you can register to the Dragger.IsDragging event, which bubbles up from the element being dragged, to perform any processing you might need.

Updating the Connection Position

My mechanism for informing the connection that it needs to be redrawn is a little sloppy, and definitely needs readdressing.

The Connection contains two DependencyProperties of type FrameworkElement: Start and End. In the PropertyChangedCallbacks, I try to cast them as DragAwareListBoxItems (I need to make this an interface for better reusability). If the cast is successful, I register to the DragAwareListBoxItem.ConnectionDragging event. (Bad name, not mine!). When that event fires, the connection redraws its path.

The DragAwareListBoxItem doesn’t actually know when it’s being dragged, so someone has to tell it. Because of the ListBoxItem’s position in my visual tree, it never hears the Dragger.IsDragging event. So to tell it that it’s being dragged, the ListBox listens to the event and and informs the appropriate DragAwareListBoxItem.

The was going to post the code for the Connection, the DragAwareListBoxItem, and the ListBox_IsDragging, but I think it's way too much to be readable here. You can check out the project at http://code.google.com/p/pavilion/source/browse/#hg%2FPavilionDesignerTool%2FPavilion.NodeDesigner or clone the respository with hg clone https://code.google.com/p/pavilion/ . It's an open source project under the MIT license, so you can adapt it as you see fit. As a warning, there is no stable release, so it can change at any time.

Connectability

As with the Connection Updating, I won't paste the code. Instead, I'll tell you which classes in the project to examine and what to look for in each class.

From a user perspective, here's how creating a connection works. The user right-clicks on a node. This brings up a context menu from which the user selects "Create New Connection". That option creates a straight line whose starting point is rooted to the selected node, and whose end point follows the mouse. If the user clicks on another node, then a connection is created between the two. If the user clicks anywhere else, no connection is created and the line disappears.

Two classes are involved in this process. The ConnectionManager (which doesn't actually manage any connections) houses Attached Properties. The consuming control sets the ConnectionManager.IsConnectable property to true and sets the ConnectionManager.MenuItemInvoker property to the menu item that should start the process. Additionally, some control in your visual tree has to listen to the ConnectionPending routed event. This is where the actual creation of the connection takes place.

When the menu item is selected, the ConnectionManager creates a LineAdorner. The ConnectionManager listens to the LineAdorner LeftClick event. When that event is fired, I perform hit-testing to find the control that was selected. I then raise the ConnectionPending event, passing into the event args the two controls I want to create the connection between. It's up to the subscriber of the event to actually do the work.

Vish
  • 443
  • 5
  • 18
  • +1 for the interesting drag approach, and I'll look deeper into your linked project tomorrow. Say for example that you are dragging from node A to node B (not moving either node) and you create the connection on the drop (and the connection at that point is added to ItemsSource). Could you add to your post here and explain how you do or would draw the arc while you are dragging from A to B, and how the connection is created on the drop (connecting A to B)? Thanks. – Dave Clemmer Aug 14 '11 at 22:53
  • +50 for the answer attempt. If you can cover the key question scenario where a temporary arc is drawn while dragging, I can accept the answer. – Dave Clemmer Aug 18 '11 at 01:09
  • Sure thing. Expect it sometime this weekend. – Vish Aug 19 '11 at 15:45
2

I think you'll want to look into the WPF Thumb control. It wraps up some of this functionality in a convenient package.

Here's MSDN Documentation:

http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.thumb.aspx

Here's an example:

http://denisvuyka.wordpress.com/2007/10/13/wpf-draggable-objects-and-simple-shape-connectors/

Unfortunately I don't have a lot of experience in this area, but I do think that this is what you're looking for. Good luck!

Adam
  • 1,126
  • 9
  • 24
  • Thanks Adam. I do use the Thumb control to move the entities and relationships and resize the entities once rendered. I don't think I can use thumbs in this case, as the arc path is being drawn dynamically during the drag, and then rendered as a control on the drop. Unless you know of a trick! – Dave Clemmer Jun 16 '11 at 04:22
2

As mentioned above, my current approach is to not use drag and drop directly, but to use a combination of DependencyProperties and handling mouse events to mimic a drag and drop.

The DependencyProperties in the parent diagram control are:

public static readonly DependencyProperty IsDraggingProperty = DependencyProperty.Register("IsDragging", typeof(bool), typeof(SolutionDiagramControl));
public bool IsDragging
{
    get
    {
        return (bool)GetValue(IsDraggingProperty);
    }
    set
    {
        SetValue(IsDraggingProperty, value);
    }
}

public static readonly DependencyProperty DragItemProperty = DependencyProperty.Register("DragItem", typeof(IWorkspaceViewModel), typeof(SolutionDiagramControl));
public IWorkspaceViewModel DragItem
{
    get
    {
        return (IWorkspaceViewModel)GetValue(DragItemProperty);
    }
    set
    {
        SetValue(DragItemProperty, value);
    }
}

The IsDragging DependencyProperty is used to trigger a cursor change when a drag is taking place, such as:

<Style TargetType="{x:Type lib:SolutionDiagramControl}">
    <Style.Triggers>
        <Trigger Property="IsDragging" Value="True">
            <Setter Property="Cursor" Value="Pen" />
        </Trigger>
    </Style.Triggers>
</Style>

Wherever I need to perform an arc drawing form of drag and drop, instead of calling DragDrop.DoDragDrop, I set IsDragging = true and DragItem to the source item being dragged.

Within the entity control on mouse leave, the connector adorner which draws the arc during the drag is enabled, such as:

protected override void OnMouseLeave(MouseEventArgs e)
{
    base.OnMouseLeave(e);
    if (ParentSolutionDiagramControl.DragItem != null)
    {
        CreateConnectorAdorner();
    }
}

The diagram control must handle additional mouse events during the drag, such as:

protected override void OnMouseMove(MouseEventArgs e)
{
    base.OnMouseMove(e);
    if (e.LeftButton != MouseButtonState.Pressed)
    {
        IsDragging = false;
        DragItem = null;
    }
}

The diagram control must also handle the "drop" upon a mouse up event (and it must figure out which entity is being dropped on based on mouse position), such as:

protected override void OnMouseUp(MouseButtonEventArgs e)
{
    base.OnMouseUp(e);
    if (DragItem != null)
    {
        Point currentPosition = MouseUtilities.GetMousePosition(this);
        DiagramEntityViewModel diagramEntityView = GetMouseOverEntity(currentPosition );
        if (diagramEntityView != null)
        {
            // Perform the drop operations
        }
    }
    IsDragging = false;
    DragItem = null;
}

I am still looking for a better solution to draw the temporary arc (following the mouse) on the diagram while a drag operation is taking place.

Dave Clemmer
  • 3,757
  • 12
  • 47
  • 72