Search code examples
c#wpfxamlwpf-controls

How to make a control snap to a Grid.Row/Grid.Column in WPF at runtime?


I have a grid with some ColumnDefinitions and RowDefinitions. What I like to do is drag a control at runtime and have it snap to a given GridColumn/GridRow when the control is over that GridColumn/GridRow. I was not able to find any resources on this. Perhaps I am using the wrong key words. Thanks in advance!


Solution

  • You should extend Grid to handle the drop position. Let the Grid add the dropped element to the appropriate cell.

    The following simple but working example shows how to enable dragging of any UIElement from a Panel such as StackPanel or Grid to the custom DrockingGrid.
    The custom Grid simply overrides the relevant drag&drop overrides. It's a minimal but working example, therefore only OnDragEnter and OnDrop are overridden.

    On drop, you basically have to identify the cell the element was dropped in by using the drop position from the DragEventArgs. Then remove the dropped element from its original parent container (where the drag operation has started) and then insert it into the DockingGrid. You then use Grid.Row and Grid.Column to position the element in the appropriate cell:

    DockingGrid.cs

    public class DockingGrid : Grid
    {
      private bool AcceptsDrop { get; set; }
      private Brush OriginalBackgroundBrush { get; set; }
    
      public DockingGrid()
      {
        this.AllowDrop = true;
      }
    
      protected override void OnDragEnter(DragEventArgs e)
      {
        base.OnDragEnter(e);
        e.Effects = DragDropEffects.None;
        this.AcceptsDrop = e.Data.GetDataPresent(typeof(UIElement));
    
        if (this.AcceptsDrop)
        {
          e.Effects = DragDropEffects.Move;
          ShowDropTargetEffects();
        }
      }
    
      protected override void OnDragLeave(DragEventArgs e)
      {
        base.OnDragEnter(e);
        ClearDropTargetEffects();
      }
    
      protected override void OnDrop(DragEventArgs e)
      {
        base.OnDrop(e);
        if (!this.AcceptsDrop)
        {
          return;
        }
    
        ClearDropTargetEffects();
    
        var droppedElement = e.Data.GetData(typeof(UIElement)) as UIElement;
        RemoveDroppedElementFromDragSourceContainer(droppedElement);
        _ = this.Children.Add(droppedElement);
    
        Point dropPosition = e.GetPosition(this);
        SetColumn(droppedElement, dropPosition.X);
        SetRow(droppedElement, dropPosition.Y);
      }
    
      private void SetRow(UIElement? droppedElement, double verticalOffset)
      {
        double totalRowHeight = 0;
        int targetRowIndex = 0;
        foreach (RowDefinition? rowDefinition in this.RowDefinitions)
        {
          totalRowHeight += rowDefinition.ActualHeight;
          if (totalRowHeight >= verticalOffset)
          {
            Grid.SetRow(droppedElement, targetRowIndex);
            break;
          }
    
          targetRowIndex++;
        }
      }
    
      private void SetColumn(UIElement? droppedElement, double horizontalOffset)
      {
        double totalColumnWidth = 0;
        int targetColumntIndex = 0;
        foreach (ColumnDefinition? columnDefinition in this.ColumnDefinitions)
        {
          totalColumnWidth += columnDefinition.ActualWidth;
          if (totalColumnWidth >= horizontalOffset)
          {
            Grid.SetColumn(droppedElement, targetColumntIndex);
            break;
          }
    
          targetColumntIndex++;
        }
      }
    
      private void RemoveDroppedElementFromSourceContainer(UIElement droppedElement)
      {
        DependencyObject parent = droppedElement is FrameworkElement frameworkElement
          ? frameworkElement.Parent
          : VisualTreeHelper.GetParent(droppedElement);
    
        if (parent is null)
        {
          return;
        }
    
        switch (parent)
        {
          case Panel panel:
            panel.Children.Remove(droppedElement);
            break;
          case ContentControl contentControl:
            contentControl.Content = null;
            break;
          case ContentPresenter contentPresenter:
            contentPresenter.Content = null;
            droppedElement.UpdateLayout();
            break;
          case Decorator decorator:
            decorator.Child = null;
            break;
          default:
            throw new NotSupportedException($"Parent type {parent.GetType()} not supported");
        }
      }
    
      private void ShowDropTargetEffects()
      {
        this.ShowGridLines = true;
        this.OriginalBackgroundBrush = this.Background;
        this.Background = Brushes.LightBlue;
      }
    
      private void ClearDropTargetEffects()
      {
        this.Background = this.OriginalBackgroundBrush;
        this.ShowGridLines = false;
      }
    }
    

    Usage

    Use it like a normal Grid.
    Now the user can drag any control into any of the predefined cells.

    <local:DockingGrid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100" />
        <ColumnDefinition Width="200" />
        <ColumnDefinition />
      </Grid.ColumnDefinitions>
    
      <Grid.RowDefinitions>
        <RowDefinition Height="100" />
        <RowDefinition Height="300" />
        <RowDefinition />
      </Grid.RowDefinitions>
    </local:DockingGrid>
    

    In the parent host of the drag&drop context for example the Window, enable/start the drag behavior:

    MainWindow.xaml.cs

    partial class MainWindow : Window
    {
      protected override void OnPreviewMouseMove(MouseEventArgs e)
      {
        base.OnPreviewMouseMove(e);
    
        if (e.LeftButton == MouseButtonState.Pressed
          && e.Source is UIElement uIElement)
        {
          _ = DragDrop.DoDragDrop(uIElement, new DataObject(typeof(UIElement), uIElement), DragDropEffects.Move);
        }
      }
    }
    

    See Microsoft Docs: Drag and Drop Overview to learn more about the feature.