How to make such a Drag & Drop with a preview like on a gif in this case as part of MVVM?

There is a Canvas with an attached collection of objects. Each object is a CustomControl in which the usual Drag & Drop is already implemented following the example from this answer .

I thought when MouseMove to do a VisualBrush current object, draw a new rectangle with this brush and drag it. But after all Canvas attached to a collection and it turns out that this rectangle also needs to be added to the collection so that it is displayed.

 <ItemsControl ItemsSource="{Binding itemsList}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas IsItemsHost="True" Width="1000" Height="1000"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <local:CustomControl/> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Canvas.Left" Value="{Binding Position.X}"/> <Setter Property="Canvas.Top" Value="{Binding Position.Y}"/> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl> 

enter image description here

    1 answer 1

    Let's try to correct the code from this answer so that it does not transfer the square in reality until you release it.

    The idea with VisualBrush is a good one, so it will be easy for us to β€œduplicate” existing elements. But adding a dummy item to the collection is not good, after all, we have to move - the care of the UI-level, not the model.

    Therefore, there will be no changes at the VM level at all, which is good.

    Add another level over our list of squares. That it does not interfere, we will make it transparent for the mouse. It will be necessary to bind the sizes of both elements, for this we need the name and Binding :

     <Window x:Class="MvvmDraggable2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MvvmDraggable2" Title="Draggable squares 2" Height="350" Width="525"> <Grid> <ItemsControl ItemsSource="{Binding Squares}" Width="300" Height="300" Background="Beige" Name="DraggableItemsHost"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas IsItemsHost="True"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <local:DraggableSquare DraggedImageContainer="{Binding ElementName=DraggedImageContainer}"/> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemContainerStyle> <Style> <Setter Property="Canvas.Left" Value="{Binding Position.X}"/> <Setter Property="Canvas.Top" Value="{Binding Position.Y}"/> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl> <Canvas IsHitTestVisible="False"> <Rectangle Opacity="0.4" Name="DraggedImageContainer" Visibility="Collapsed"/> </Canvas> </Grid> </Window> 

    The name of the ItemsControl was added, the DraggableSquare had an additional binding (about it later), and a Canvas appeared below, containing the desired element.

    Next, DraggbleSquare . We change our internal logic a bit. UpdatePosition will now be called only when you release the mouse or lose the focus of the window.

     void OnDragMove(object sender, MouseEventArgs e) { //UpdatePosition(e); UpdateDraggedSquarePosition(e); } void FinishDrag(object sender, MouseEventArgs e) { MouseMove -= OnDragMove; LostMouseCapture -= OnLostCapture; UpdatePosition(e); UpdateDraggedSquarePosition(null); } 

    The new part of the logic: UpdateDraggedSquarePosition . For it, we need a new dependency property DraggedImageContainer , in which we in XAML have already put an element representing the moving image.

    (Put it higher, near RequestMoveCommand .)

     #region dp Shape DraggedImageContainer public Shape DraggedImageContainer { get { return (Shape)GetValue(DraggedImageContainerProperty); } set { SetValue(DraggedImageContainerProperty, value); } } public static readonly DependencyProperty DraggedImageContainerProperty = DependencyProperty.Register( "DraggedImageContainer", typeof(Shape), typeof(DraggableSquare)); #endregion 

    Actually the new logic itself:

     void UpdateDraggedSquarePosition(MouseEventArgs e) { var dragImageContainer = DraggedImageContainer; if (dragImageContainer == null) return; var needVisible = e != null; var wasVisible = dragImageContainer.Visibility == Visibility.Visible; // Π²ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ/Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌ Π²ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Ρ‰Π°Π΅ΠΌΠΎΠΉ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ dragImageContainer.Visibility = needVisible ? Visibility.Visible : Visibility.Collapsed; if (!needVisible) // Ссли ΠΌΡ‹ Π²Ρ‹ΠΊΠ»ΡŽΡ‡ΠΈΠ»ΠΈΡΡŒ, Π½Π°ΠΌ большС Π½Π΅Ρ‡Π΅Π³ΠΎ Π΄Π΅Π»Π°Ρ‚ΡŒ return; if (!wasVisible) // Π° Ссли ΠΌΡ‹ Π±Ρ‹Π»ΠΈ Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½Ρ‹ ΠΈ Π²ΠΊΠ»ΡŽΡ‡ΠΈΠ»ΠΈΡΡŒ, { // Π½Π°ΠΌ Π½Π°Π΄ΠΎ ΠΏΡ€ΠΈΠ²ΡΠ·Π°Ρ‚ΡŒ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сСбя dragImageContainer.Fill = new VisualBrush(this); dragImageContainer.SetBinding( // Π° Ρ‚Π°ΠΊΠΆΠ΅ ΡˆΠΈΡ€ΠΈΠ½Ρƒ/высоту Shape.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this }); dragImageContainer.SetBinding( Shape.HeightProperty, new Binding(nameof(ActualHeight)) { Source = this }); // Binding Π½ΡƒΠΆΠ΅Π½ ΠΏΠΎΡ‚ΠΎΠΌΡƒ, Ρ‡Ρ‚ΠΎ наш Ρ€Π°Π·ΠΌΠ΅Ρ€ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΠΎ ΠΈΠ΄Π΅Π΅ ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚ΡŒΡΡ } // ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Ρ‰Π°Π΅ΠΌ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΡƒ Π½Π° Π½ΡƒΠΆΠ½ΡƒΡŽ ΠΏΠΎΠ·ΠΈΡ†ΠΈΡŽ var parent = FindParent<Canvas>(dragImageContainer); var position = e.GetPosition(parent) - relativeMousePos; Canvas.SetLeft(dragImageContainer, position.X); Canvas.SetTop(dragImageContainer, position.Y); } 

    Yes, and I had to slightly change GetParent so that it works with an arbitrary element:

     static private T FindParent<T>(FrameworkElement from) where T : FrameworkElement { FrameworkElement current = from; T t; do { t = current as T; current = (FrameworkElement)VisualTreeHelper.GetParent(current); } while (t == null && current != null); return t; } 

    and in OnMouseDown write

     container = FindParent<Canvas>(this); 

    It's all.

    In order to get a beautiful picture, I also added color in SquareVM :

     Color color; public Color Color { get { return color; } set { if (color != value) { color = value; NotifyPropertyChanged(); } } } 

    and set the colors in MainVM :

     class MainVM : VM { public ObservableCollection<SquareVM> Squares { get; } = new ObservableCollection<SquareVM>() { new SquareVM() { Position = new Point(30, 30), Color = Color.FromRgb(0x3D, 0x31, 0x5B) }, new SquareVM() { Position = new Point(100, 70), Color = Color.FromRgb(0x44, 0x4B, 0x6E) }, new SquareVM() { Position = new Point(80, 0), Color = Color.FromRgb(0x70, 0x8B, 0x75) }, new SquareVM() { Position = new Point(90, 180), Color = Color.FromRgb(0x9A, 0xB8, 0x7A) }, new SquareVM() { Position = new Point(200, 200), Color = Color.FromRgb(0xF8, 0xF9, 0x91) } }; } 

    And I also attached to him in DraggableSquare.xaml :

     <UserControl x:Class="MvvmDraggable2.DraggableSquare" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="50" Height="50" MouseDown="OnMouseDown" MouseUp="OnMouseUp"> <UserControl.Background> <SolidColorBrush Color="{Binding Color}"/> </UserControl.Background> <Grid/> </UserControl> 

    It turns out that:

    but what happened


    If you want the image to be moved to be smaller, the easiest way to reduce it is through the RenderTransform :

     <Canvas IsHitTestVisible="False"> <Rectangle Opacity="0.4" Name="DraggedImageContainer" Visibility="Collapsed" RenderTransformOrigin="0.5,0.5"> <Rectangle.RenderTransform> <ScaleTransform ScaleX="0.9" ScaleY="0.9"/> </Rectangle.RenderTransform> </Rectangle> </Canvas> 

    the same with transformation

    • And unless the sizes of the second canvas will not be exposed by? Why indicate them through binding? - Pavel Mayorov
    • @PavelMayorov: Yeah, it will. This is a copy-paste from the real code, in which the real structure is more complicated (and the ItemsControl with Canvas 'is not in the same Grid ' e, there are still intermediate layers). In this case, yes, you can simplify. - VladD
    • @PavelMayorov: Stop, in this case it will not, due to <ItemsControl ItemsSource="{Binding Squares}" Width="300" Height="300" . There are clearly indicated dimensions. - VladD
    • Well, the sizes can be transferred to the grid. - Pavel Mayorov
    • one
      @PavelMayorov: Although if you remove the binding, it still works correctly. Recalculation of coordinates from a position, it turns out, does not depend. What a smart code! :) - VladD