Search code examples
c#wpfmvvmcontentcontrol

How to drag move, resize/scale, rotate a control element on a Canvas?


I'm trying to create Diagram Designer with WPF and using the MVVM pattern, I take information and some tips from this guide: https://www.codeproject.com/Articles/22952/WPF-Diagram-Designer-Part-1

And in one moment, my project looks like:

WPF: Hydrate Canvas with Draggable Controls at Runtime

And of course I had the similar problem as the author mentioned above: when I draw my ContentControl, it's drawing correctly with random coordinates, but when I try to move it, it won't move! And when I debug the class MoveThumb, I see that my ContentControl has not got its Parent. But in my opinion, it should have Canvas as a Parent. I understand that I should override some system/basic method, but I can't understand what and how should I override it. Maybe someone has an idea?

Now I try to describe my implementation, first i create BaseShapeViewModel

abstract public class BaseShapeViewModel : BaseViewModel
{

    public BaseShapeViewModel()
    {

    }

    private double left;
    public double Left
    {
        get => left;
        set => SetField(ref left, value, nameof(Left));
    }

    private double top;
    public double Top
    {
        get => top;
        set => SetField(ref top, value, nameof(Top));
    }

    private int width;
    public int Width
    {
        get => width;
        set => SetField(ref width, value, nameof(Width));
    }

    private int height;
    public int Height
    {
        get => height;
        set => SetField(ref height, value, nameof(Height));
    }

    private string fill;
    public string Fill
    {
        get => fill;
        set => SetField(ref fill, value, nameof(Fill));
    }

    private string text;
    public string Text
    {
        get => text;
        set => SetField(ref text, value, nameof(Text));
    }

}

other ViewModels EllipseViewModel, RectangleViewModel inherit from BaseShapeViewModel. My MainViewModel looks like

class MainViewModel : BaseViewModel
{
    public MainViewModel()
    {          
        BaseShapeViewModels = new ObservableCollection<BaseShapeViewModel>();                    
    }

    public ObservableCollection<BaseShapeViewModel> BaseShapeViewModels { get; set; }

    //public Canvas DesignerCanvas;
   
    private RelayCommand createUseCase;
    private RelayCommand createRectangle;

    public ICommand CreateUseCase
    {
        get
        {
            return createUseCase ?? 
                (
                    createUseCase = new RelayCommand(() => { AddUseCase(); })                    
                );
        }
    }
    public ICommand CreateRectangle
    {
        get
        {
            return createRectangle ??
                (
                    createRectangle = new RelayCommand(() => { AddRectangle(); })
                );
        }
    }

    private void AddUseCase()
    {
        Random rnd = new Random();
        
        int valueLeft = rnd.Next(0, 200);
        int valueTop = rnd.Next(0, 200);
        
        EllipseViewModel useCaseViewModel = new EllipseViewModel {Left=valueLeft,Top=valueTop, Height = 100, Width = 200, Fill="Blue"};
        BaseShapeViewModels.Add(useCaseViewModel);
  
    }

    private void AddRectangle()
    {
        Random rnd = new Random();
        
        int valueLeft = rnd.Next(0, 200);
        int valueTop = rnd.Next(0, 200);

        RectangleViewModel rectangleViewModel = new RectangleViewModel { Left = valueLeft, Top = valueTop, Height = 100, Width = 200, Fill = "Blue" };
        BaseShapeViewModels.Add(rectangleViewModel);
    }
}

My MoveThumb.cs looks like

 public class MoveThumb : Thumb
{
  
    public MoveThumb()
    {
        DragDelta += new DragDeltaEventHandler(this.MoveThumb_DragDelta);
    }

    private void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
        ContentControl designerItem = DataContext as ContentControl;
       
        if (designerItem != null)
        {
            Point dragDelta = new Point(e.HorizontalChange, e.VerticalChange);
            
            RotateTransform rotateTransform = designerItem.RenderTransform as RotateTransform;
            if (rotateTransform != null)
            {
                dragDelta = rotateTransform.Transform(dragDelta);
            }
            double left = Canvas.GetLeft(designerItem);
            double top = Canvas.GetTop(designerItem);
           
            Canvas.SetLeft(designerItem, left + dragDelta.X);
            Canvas.SetTop(designerItem, top + dragDelta.Y);       
        }

    }
}

And know i want to say that im not profi at Xaml, but i check material from the first link and create MoveThumb.xaml like this

<ResourceDictionary 

<ControlTemplate x:Key="MoveThumbTemplate" TargetType="{x:Type s:MoveThumb}">
    <Rectangle Fill="Transparent"/>
</ControlTemplate>

after i create ResizeDecorator and RotateDecorator, but right now it doesnt matter, and create DesignerItem.xaml

ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="MoveThumb.xaml"/>
    <ResourceDictionary Source="ResizeDecorator.xaml"/>
    <ResourceDictionary Source="RotateDecorator.xaml"/>
</ResourceDictionary.MergedDictionaries>

<!-- ContentControl style to move, resize and rotate items -->
<Style x:Key="DesignerItemStyle" TargetType="ContentControl">
    <Setter Property="MinHeight" Value="50"/>
    <Setter Property="MinWidth" Value="50"/>
    <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ContentControl">
                <Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
                    <Control Name="RotateDecorator"
                             Template="{StaticResource RotateDecoratorTemplate}"
                             Visibility="Collapsed"/>
                    <s:MoveThumb Template="{StaticResource MoveThumbTemplate}"
                                 Cursor="SizeAll"/>
                    <Control x:Name="ResizeDecorator"
                             Template="{StaticResource ResizeDecoratorTemplate}"
                             Visibility="Collapsed"/>
                    <ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="Selector.IsSelected" Value="True">
                        <Setter TargetName="ResizeDecorator" Property="Visibility" Value="Visible"/>
                        <Setter TargetName="RotateDecorator" Property="Visibility" Value="Visible"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And i try to Bind this Style in my MainWindow.xaml for my ContentControls

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Resources/DesignerItem.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Window.Resources>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100"/>
        <ColumnDefinition Width="*"/>
         </Grid.ColumnDefinitions>

    <StackPanel Background="Gray" Grid.RowSpan="2">
        <TextBlock Text="Shapes"                      
                   FontSize="18"
                   TextAlignment="Center" />
        <Button Content="Initial Node" />
        <Button Content="Final Node" />
        <Button Content="Line" />
        <Button Content="Action" />
        <Button Content="Decision Node" />
        <Button Content="Actor" />
        <Button Content="Class" Command="{Binding Path=CreateRectangle}"/>
        <Button Content="Use Case" Command="{Binding Path = CreateUseCase}"/>
    </StackPanel>

    <Grid Grid.Column="2">

        <ItemsControl ItemsSource="{Binding Path= BaseShapeViewModels}">

            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>

                    <Canvas/>

                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>

            <ItemsControl.Resources>
                <DataTemplate DataType="{x:Type viewModel:EllipseViewModel}">
                    <ContentControl
                                           Selector.IsSelected="True"
                                           Style="{StaticResource DesignerItemStyle}">

                        <Ellipse 
                                           Fill="{Binding Fill}"                                               
                                           IsHitTestVisible="False"/>

                    </ContentControl>
                </DataTemplate>

                <DataTemplate DataType="{x:Type viewModel:RectangleViewModel}">
                    <ContentControl             
                                           Selector.IsSelected="True"
                                           Style="{StaticResource DesignerItemStyle}">

                        <Rectangle 
                                           Fill="{Binding Fill}"                                              
                                           IsHitTestVisible="False"/>
                    </ContentControl>
                </DataTemplate>
            </ItemsControl.Resources>

            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Left" Value="{Binding Left, Mode=TwoWay}"/>
                    <Setter Property="Canvas.Top" Value="{Binding Top, Mode=TwoWay}"/>
                    <Setter Property="Width" Value="{Binding Width, Mode=TwoWay}"/>
                    <Setter Property="Height" Value="{Binding Height,Mode=TwoWay}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
        </ItemsControl>
        
    </Grid>
</Grid>
               

Solution

  • You can't drag the items across the Canvas, because you are using an ItemsControl. This control wraps each item into a container. You are currently trying to drag the content of this container, but not the container. It is the container that is actually positioned within the Canvas.

    To reduce complexity I recommend to implement a custom control that extends ItemsControl. This way you can make the item container itself draggable. Alternatively you can implement an attached behavior.

    To provide a resize and rotate user interface (e.g. resize grip), I recommend to use the Adorner.

    The following example implements a custom item container, which supports drag, scale and rotation, and an extended ItemsControl that uses this container for its items.
    Since the draggable element is a ContentControl, it doesn't need any additional wrapping. This will simplify the usage as shown below.

    Implementing DraggableContentControl

    DraggableContentControl.cs
    The DraggableContentControl class is used as the item container of the ItemsCanvas control (the specialized ItemsControl - see below).

    In addition, the DraggableContentControl can be used as stand-alone control: the DraggableContentControl is a very convenient and reusable way to add mouse drag, scale and rotation to any UIElement on a Canvas. For example to allow to drag a Rectangle across the Canvas simply assign the Rectangle to the DraggableContentControl.Content property and position the DraggableContentControl on a Canvas:

    <Canvas Width="1000" Height="1000">
    
     <!-- Ad mouse drag, rotation and scaling to a Rectangle -->
      <DraggableContentControl Canvas.Left="50" 
                               Canvas.Top="50" 
                               Angle="45">
            <Rectangle Height="100" 
                       Width="100" 
                       Fill="Coral" />
      </DraggableContentControl>
    </Canvas>
    

    Rotation : setting the DraggableContentControl.Angle allows to rotate the hosted element.
    Scaling: setting DraggableContentControl.Width and DraggableContentControl.Height allows to scale/resize the content (automatically, because the DraggableContentControl.Content value is wrapped into a Viewbox - see the default Style below).
    By default, when the size is set to Auto, the DraggableContentControl will dynamically adopt the size of its content.

    public class DraggableContentControl : ContentControl
    {
      public static readonly DependencyProperty AngleProperty = DependencyProperty.Register(
        "Angle",
        typeof(double),
        typeof(DraggableContentControl),
        new PropertyMetadata(default(double), DraggableContentControl.OnAngleChanged));
    
      public double Angle
      {
        get => (double) GetValue(DraggableContentControl.AngleProperty);
        set => SetValue(DraggableContentControl.AngleProperty, value);
      }
    
      private RotateTransform RotateTransform { get; set; }
      private IInputElement ParentInputElement { get; set; }
      private bool IsDragActive { get; set; }
      private Point DragOffset { get; set; }
    
      static DraggableContentControl()
      {
        // Remove this line if you don't plan to define the default Style
        // inside the Generic.xaml file.
        DefaultStyleKeyProperty.OverrideMetadata(typeof(DraggableContentControl), new FrameworkPropertyMetadata(typeof(DraggableContentControl)));
      }
    
      public DraggableContentControl()
      {
        this.PreviewMouseLeftButtonDown += InitializeDrag_OnLeftMouseButtonDown;
        this.PreviewMouseLeftButtonUp += CompleteDrag_OnLeftMouseButtonUp;
        this.PreviewMouseMove += Drag_OnMouseMove;
        this.RenderTransformOrigin = new Point(0.5, 0.5);
    
        var transformGroup = new TransformGroup();
        this.RotateTransform = new RotateTransform();
        transformGroup.Children.Add(this.RotateTransform);
        this.RenderTransform = transformGroup;
      }
    
      #region Overrides of FrameworkElement
    
      public override void OnApplyTemplate()
      {
        base.OnApplyTemplate();
    
        // Parent is required to calculate the relative mouse coordinates.
        DependencyObject parentControl = this.Parent;
        if (parentControl == null
            && !TryFindParentElement(this, out parentControl)
            && !(parentControl is IInputElement))
        {
          return;
        }
    
        this.ParentInputElement = parentControl as IInputElement;
      }
    
      #endregion
    
      private void InitializeDrag_OnLeftMouseButtonDown(object sender, MouseButtonEventArgs e)
      {
        // Do nothing if disabled
        this.IsDragActive = this.IsEnabled;
        if (!this.IsDragActive)
        {
          return;
        }
    
        Point relativeDragStartPosition = e.GetPosition(this.ParentInputElement);
    
        // Calculate the drag offset to allow the content to be dragged 
        // relative to the clicked coordinates (instead of the top-left corner)
        this.DragOffset = new Point(
          relativeDragStartPosition.X - Canvas.GetLeft(this),
          relativeDragStartPosition.Y - Canvas.GetTop(this));
    
        // Prevent other controls from stealing mouse input while dragging
        CaptureMouse();
      }
    
      private void CompleteDrag_OnLeftMouseButtonUp(object sender, MouseButtonEventArgs e)
      {
        this.IsDragActive = false;
        ReleaseMouseCapture();
      }
    
      private void Drag_OnMouseMove(object sender, MouseEventArgs e)
      {
        if (!this.IsDragActive)
        {
          return;
        }
    
        Point currentPosition = e.GetPosition(this.ParentInputElement);
    
        // Apply the drag offset to drag relative to the 
        // initial mouse down coordinates (instead of the top-left corner)
        currentPosition.Offset(-this.DragOffset.X, -this.DragOffset.Y);
        Canvas.SetLeft(this, currentPosition.X);
        Canvas.SetTop(this, currentPosition.Y);
      }
    
      private static void OnAngleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
        (d as DraggableContentControl).RotateTransform.Angle = (double) e.NewValue;
      }
    
      private bool TryFindParentElement<TParent>(DependencyObject child, out TParent resultElement)
        where TParent : DependencyObject
      {
        resultElement = null;
    
        if (child == null)
        {
          return false;
        }
    
        DependencyObject parentElement = VisualTreeHelper.GetParent(child);
    
        if (parentElement is TParent)
        {
          resultElement = parentElement as TParent;
          return true;
        }
    
        return TryFindParentElement(parentElement, out resultElement);
      }
    }
    

    Implementing ItemsCanvas

    ItemsCanvas.cs
    ItemsCanvas is a ItemsControl configured to use DraggableContentControl as the item container.

    class ItemsCanvas : ItemsControl
    {
      static ItemsCanvas()
      {
        // Remove this line if you don't plan to define the default Style
        // inside the Generic.xaml file.
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ItemsCanvas), new FrameworkPropertyMetadata(typeof(ItemsCanvas)));
      }
    
      #region Overrides of ItemsControl
    
      protected override bool IsItemItsOwnContainerOverride(object item) => item is DraggableContentControl;
    
      protected override DependencyObject GetContainerForItemOverride() => new DraggableContentControl();
    
      #endregion
    }
    

    Generic.xaml

    The default styles for the DraggableContentControl and the ItemsCanvas:

    <Style TargetType="DraggableContentControl">
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="DraggableContentControl">
            <Border Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
    
              <!-- 
                Optional: wrapping the content into a Viewbox
                allows to automatically resize/scale the content 
                based on the container's (DraggableContentControl) size.
              -->
              <Viewbox Stretch="Fill">
                <ContentPresenter />
              </Viewbox>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
    
    
    <Style TargetType="ItemsCanvas">
      <Setter Property="ItemsPanel">
        <Setter.Value>
          <ItemsPanelTemplate>
            <Canvas />
          </ItemsPanelTemplate>
        </Setter.Value>
      </Setter>
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="ItemsCanvas">
            <Border Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
              <ScrollViewer>
                <ItemsPresenter />
              </ScrollViewer>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
    

    Example
    This example is based on your data models.

    <Window>
      <Window.Resources>
        <DataTemplate DataType="{x:Type RectangleViewModel}">
          <Rectangle Height="{Binding Height}" 
                     Width="{Binding Width}"
                     Fill="{Binding Fill}" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type EllipseViewModel}">
          <Ellipse Height="{Binding Height}" 
                   Width="{Binding Width}"
                   Fill="{Binding Fill}" />
        </DataTemplate>
      </Window.Resources>
    
      <ItemsCanvas ItemsSource="{Binding BaseShapeViewModels}" 
                   Height="500" 
                   Width="500">
        <ItemsCanvas.ItemContainerStyle>
    
          <!-- Optional Style that adds the possibility to position items on the Canvas 
               using the e.g., Top and Left properties of the data model. 
          -->
          <Style TargetType="main:DraggableContentControl">
            <Setter Property="Canvas.Left" Value="{Binding Left, Mode=TwoWay}" />
            <Setter Property="Canvas.Top" Value="{Binding Top, Mode=TwoWay}" />
          </Style>
        </ItemsCanvas.ItemContainerStyle>
      </ItemsCanvas>
    </Window>