Search code examples
wpfmvvmdrag-and-dropdiagrammingmo+

Drawing diagram arcs with drag and drop in WPF


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.


Solution

  • 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.