Search code examples
c#wpfdatagridadornermicrosoft-file-explorer

Selection Rectangle not moving along with WPF datagrid content during scroll


I am trying to replicate the selection behavior of Windows File Explorer within a WPF DataGrid. Specifically, I aim to implement a selection rectangle that mimics the functionality of File Explorer's selection mechanism. Currently, I have created an adorner rectangle to facilitate the selection of items. However, I encounter an issue when attempting to select rows while scrolling. As the DataGrid's content scrolls within its own ScrollViewer, the rows that are not intersecting with the selection rectangle are being deselected as the Selection rectangle is static and not moving along with the datagrid content during scroll. Its like the rectangle is in the air and not pasted with the datagrid. To provide a clearer understanding of the issue, I have included images below for reference.

screenshot

I have tried multiple solution but nothing worked. I am expecting that this rectangle should move along with datagrid content during scroll as we see in windows file explorer.

  `<DataGrid
             Grid.Row="2"
             x:Name="FileDataGrid"
             HeadersVisibility="Column"
            
             HorizontalAlignment="Stretch"
            
             VerticalAlignment="Stretch"
            
             AutoGenerateColumns="False"
            
             CanUserResizeColumns="True"
            
             GridLinesVisibility="None"
            
             IsReadOnly="False"
            
             HorizontalScrollBarVisibility="Auto"
            
             PreviewMouseLeftButtonDown="DataGrid_PreviewMouseLeftButtonDown"
            
             PreviewMouseLeftButtonUp="DataGrid_PreviewMouseLeftButtonUp"
            
             RowHeight="25"
            
             Loaded="Grid_Loaded"
            
             PreviewMouseMove="DataGrid_PreviewMouseMove"
            
            ScrollViewer.ScrollChanged="FileDataGrid_ScrollChanged"
            
             SelectionChanged="FileDataGrid_SelectionChanged"
            
            
            
             SelectionMode="Extended">
            
                    
            
                     <DataGrid.CellStyle>
            
                         <Style TargetType="{x:Type DataGridCell}">
            
                             <Style.Triggers>
            
                                 <Trigger Property="DataGridCell.IsSelected" Value="True">
            
                                     <Setter Property="BorderBrush">
            
                                         <Setter.Value>
            
                                             <SolidColorBrush Color="Transparent" />
            
                                         </Setter.Value>
            
                                     </Setter>
            
                                     <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
            
                                     <Setter Property="Background">
            
                                         <Setter.Value>
            
                                             <SolidColorBrush Color="Yellow" />
            
                                         </Setter.Value>
            
                                     </Setter>
            
                                 </Trigger>
            
                             </Style.Triggers>
            
                         </Style>
            
                     </DataGrid.CellStyle>
            
                     <DataGrid.RowStyle>
            
                         <Style BasedOn="{StaticResource @DataGridRowStyle}" TargetType="DataGridRow">
            
                             <Setter Property="IsHitTestVisible" Value="True" />
            
                             <EventSetter Event="MouseEnter" Handler="DataGridRow_MouseEnter"/>
            
                         </Style>
            
                     </DataGrid.RowStyle>
            
                     <DataGrid.Columns>
            
                         <DataGridTemplateColumn
            
                         x:Name="LeftName"
            
                     
            
                           Width="1.5*"
            
                         MinWidth="180"
            
                         CanUserResize="True"
            
                         CanUserSort="True"
            
                         Header="Name"
            
                         IsReadOnly="False"
            
                         SortMemberPath="Name">
            
                             <DataGridTemplateColumn.CellTemplate>
            
                                 <DataTemplate>
            
                                     <StackPanel Orientation="Horizontal">
            
                                         <Image
            
                                         Width="17"
            
                                         Height="17"
            
                                         Margin="15,0,-25,0"
            
                                         HorizontalAlignment="Right"
            
                                         Source="Images/sms-mobile.png" />
            
                                         <TextBlock
            
                                         Margin="35,4,0,0"
            
                                         Text="{Binding Name}"
            
                                         ToolTip="{Binding Folder}" />
            
                                     </StackPanel>
            
                                 </DataTemplate>
            
                             </DataGridTemplateColumn.CellTemplate>
            
                             <DataGridTemplateColumn.CellEditingTemplate>
            
                                 <DataTemplate>
            
                                     <StackPanel Orientation="Horizontal">
            
                                         <Image
            
                                         Width="17"
            
                                         Height="17"
            
                                         Margin="15,0,-25,0"
            
                                         HorizontalAlignment="Right"
            
                                         Source="Images/sms-mobile.png" />
            
                                         <TextBox
            
                                         Width="auto"
            
                                         Height="Auto"
            
                                         Margin="35,0,10,0"
            
                                         Padding="0"
            
                                         HorizontalAlignment="Left"
            
                                         VerticalAlignment="Center"
            
                                         BorderBrush="Blue"
            
                                         BorderThickness="1"
            
                                         Loaded="TextBox_Loaded"
            
                                         Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
            
                                     </StackPanel>
            
                                 </DataTemplate>
            
                             </DataGridTemplateColumn.CellEditingTemplate>
            
                         </DataGridTemplateColumn>
            
                         <DataGridTextColumn
            
                         x:Name="sizecolumn"
            
                        MaxWidth="100"
            
                         Binding="{Binding FormattedSize}"
            
                         Header="Size" />
            
                         <DataGridTextColumn
            
                         x:Name="lastModifiedColumn"
            
                       MaxWidth="120"
            
                         Binding="{Binding LastModified}"
            
                         Header="Date Modified " />
            
                     </DataGrid.Columns>      
            
             </DataGrid> 
        
        private void DataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        
        {
        
        
        
          isleftbuttonpressed = true;
        
          isSelecting = true;
        
          selectionStartPoint = e.GetPosition(FileDataGrid);
        
        
        
          adorner = new SelectionAdorner(FileDataGrid, selectionStartPoint);
        
          var adornerLayer = AdornerLayer.GetAdornerLayer(FileDataGrid);
        
          adornerLayer.Add(adorner);
        
          adorner.SetStartPoint(selectionStartPoint);
        }
    private void DataGrid_PreviewMouseMove(object sender, MouseEventArgs e)
    
    {
    
    
    
    if (e.LeftButton == MouseButtonState.Pressed)
    
    {
    
        isleftbuttonpressed = true;
    
      
    
    }
    
    if (isSelecting && adorner != null)
    
    {
    
        adorner.SetEndPoint(e.GetPosition(FileDataGrid));
    
        var selectionRect = adorner.GetSelectionRect();
    
        SelectItemsInRectangle(selectionRect, FileDataGrid, sender, e);
    
    }  
    
     private void SelectItemsInRectangle(Rect selectionRect, DataGrid dataGrid, object sender, MouseEventArgs e)
    
    {
    
    
    
     foreach (var item in dataGrid.Items)
    
     {
    
         var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(item);
    
         if (row != null)
    
         {
    
             var rowBounds = GetRowBoundsRelativeToDataGrid(row, dataGrid);
    
             // Check if the row intersects with the selection rectangle
    
             if (selectionRect.IntersectsWith(rowBounds))
    
             {
    
                 // Select the item and add it to the collection of selected items
    
                 row.IsSelected = true;
    
             }
    
             else
    
             {
    
                 row.IsSelected = false;
    
             }
    
         }
    
     }
    } `

Solution

  • Since the DataGrid already supports multi-select using mouse press + mouse move you only have to handle the adorner clipping to the scrollable area (ScrollContentPresenter).

    I suggest extending DataGrid and move the adorner handling to this custom DataGrid.

    See the full example including a fixed FileExplorer (there were some bugs in your tree structure) at GitHub: DataGrid_Drag_Select_Example. This is just raw code. You likely have to improve the behavior in terms of bugs and you want to prettify the look. However, the example is usable and a good starting point.

    Adorner.cs

    public partial class SelectionAdorner : Adorner
    {
      public Brush SelectionBrush { get; set; }
      private Point startPoint;
      private Point endPoint;
    
      public SelectionAdorner(UIElement adornedElement)
        : base(adornedElement)
      {
        this.startPoint = new Point(0, 0);
        this.endPoint = new Point(0, 0);
        this.IsHitTestVisible = false;
    
        this.SelectionBrush = new SolidColorBrush(Colors.LightBlue) 
        { 
          Opacity = 0.5 
        };
        this.SelectionBrush.Freeze();
      }
    
      protected override void OnRender(DrawingContext drawingContext)
      {
        Rect clippingBounds = LayoutInformation.GetLayoutSlot((FrameworkElement)this.AdornedElement);
    
        Point coercedStartPoint = clippingBounds.Contains(this.startPoint) ? this.startPoint : ClipLocation(this.startPoint);
        Point coercedEndPoint = clippingBounds.Contains(this.endPoint) ? this.endPoint : ClipLocation(this.endPoint);
        var rectangle = new Rect(coercedStartPoint, coercedEndPoint);
        drawingContext.DrawRectangle(this.SelectionBrush, null, rectangle);
      }
    
      public void SetStartPoint(Point point)
      {
        this.startPoint = point;
        this.startPoint = ClipLocation(this.startPoint);
    
        InvalidateVisual();
      }
    
      public void SetEndPoint(Point point)
      {
        this.endPoint = point;
        this.endPoint = ClipLocation(this.endPoint);
    
        InvalidateVisual();
      }
    
      public void Move(double horizontalOffset, double verticalOffset)
      {
        this.startPoint.Offset(horizontalOffset, verticalOffset);
        InvalidateVisual();
      }
    
      // Clip new location:
      // Y = 0 < location_Y < adorned_element_height
      // X = 0 < location_X < adorned_element_width
      private Point ClipLocation(Point point)
      {
        double horizontalOffset = point.X;
        double coercedHorizontalOffset = Math.Min(Math.Max(0, horizontalOffset), ((FrameworkElement)this.AdornedElement).ActualWidth);
    
        double verticalOffset = point.Y;
        double coercedVerticalOffset = Math.Min(Math.Max(0, verticalOffset), ((FrameworkElement)this.AdornedElement).ActualHeight);
    
        return new Point(coercedHorizontalOffset, coercedVerticalOffset);
      }
    }
    

    DragSelectDataGrid.cs

    public class DragSelectDataGrid : DataGrid
    {
      private SelectionAdorner selectionAdorner;
      private AdornerLayer adornerLayer;
      private double actualRowHeight;
      private ScrollViewer scrollViewer;
      private ScrollContentPresenter scrollContentPresenter;
      private bool canPerfromDragSelect;
    
      public DragSelectDataGrid()
      {
        this.Loaded += OnLoaded;
        this.canPerfromDragSelect = true;
      }
    
      private void OnLoaded(object sender, RoutedEventArgs e)
      {
        this.adornerLayer = AdornerLayer.GetAdornerLayer(this);
        if (this.adornerLayer == null)
        {
          throw new InvalidOperationException("No AdornerDecorator found in the parent tree.");
        }
      }
    
      public override void OnApplyTemplate()
      {
        base.OnApplyTemplate();
        if (!TryFindVisualChild(this, out this.scrollViewer))
        {
          throw new InvalidOperationException("No ScrollViewer found");
        }
    
        this.scrollViewer.ScrollChanged += OnScrollChanged;
        if (!scrollViewer.IsLoaded)
        {
          this.scrollViewer.Loaded += OnScrollViewerLoaded;
        }
        else
        {
          EnsureScrollContentPresenter();
        }
      }
    
      protected override void OnLoadingRow(DataGridRowEventArgs e)
      {
        base.OnLoadingRow(e);
        if (e.Row.ActualHeight == 0 || this.actualRowHeight == 0)
        {
          return;
        }
    
        // Get the lates row height. Row height is relevant when CanContentScroll is TRUE
        // in order to convert the actual scroll offset from item to pixel.
        this.actualRowHeight = e.Row.ActualHeight;
      }
    
      protected override void OnBeginningEdit(DataGridBeginningEditEventArgs e)
      {
        base.OnBeginningEdit(e);
        this.canPerfromDragSelect = false;
      }
    
      protected override void OnCellEditEnding(DataGridCellEditEndingEventArgs e)
      {
        base.OnCellEditEnding(e);
        this.canPerfromDragSelect = true;
      }
    
      private void OnScrollViewerLoaded(object sender, RoutedEventArgs e)
        => EnsureScrollContentPresenter();
    
      private void EnsureScrollContentPresenter()
      {
        if (!TryFindVisualChild(this.scrollViewer, out this.scrollContentPresenter))
        {
          throw new InvalidOperationException("No valid ScrollViewer found");
        }
    
        this.scrollContentPresenter.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
        this.scrollContentPresenter.PreviewMouseMove += OnPreviewMouseMove;
        this.scrollContentPresenter.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
    
        this.selectionAdorner = new SelectionAdorner(this.scrollContentPresenter);
      }
    
      private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
      {
        bool canContentScroll = ScrollViewer.GetCanContentScroll(this);
        double verticalOffset = canContentScroll
          ? this.actualRowHeight * e.VerticalChange
          : e.VerticalChange;
    
        double horizontalOffset = e.HorizontalChange;
    
        this.selectionAdorner?.Move(-horizontalOffset, -verticalOffset);
      }
    
      private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
      {
        // For example, do not perform a select while any row is in edit mode
        if (!this.canPerfromDragSelect)
        {
          return;
        }
    
        // Get the current row height and store it as it usually won't change.
        // Row height is relevant when CanContentScroll is TRUE
        // in order to convert the actual scroll offset from scrolled items to scrolled pixels.
        if (this.HasItems)
        {
          FrameworkElement rowItemContainer;
    
          bool canContentScroll = ScrollViewer.GetCanContentScroll(this);
          if (canContentScroll)
          {
            int rowItemContainerIndex = (int)this.scrollViewer.VerticalOffset;
            rowItemContainer = (FrameworkElement)this.ItemContainerGenerator.ContainerFromIndex(rowItemContainerIndex);
          }
          else
          {
            rowItemContainer = GetFirstVisibleIndex();
          }
    
          this.actualRowHeight = rowItemContainer?.ActualHeight ?? 0;
        }
    
        Point selectionStartPoint = e.GetPosition(this.scrollContentPresenter);
        this.selectionAdorner.SetStartPoint(selectionStartPoint);
        this.selectionAdorner.SetEndPoint(selectionStartPoint);
        this.adornerLayer.Add(this.selectionAdorner);
      }
    
      private FrameworkElement GetFirstVisibleIndex()
      {
        for (int itemIndex = 0; itemIndex < this.Items.Count; itemIndex++)
        {
          var rowItemContainer = (FrameworkElement)this.ItemContainerGenerator.ContainerFromIndex(itemIndex);
          if (rowItemContainer != null)
          {
            return rowItemContainer;
          }
        }
    
        return null;
      }
    
      private void OnPreviewMouseMove(object sender, MouseEventArgs e)
      {
        base.OnPreviewMouseMove(e);
        if (e.LeftButton != MouseButtonState.Pressed)
        {
          return;
        }
    
        Point selectionEndPoint = e.GetPosition(this.scrollContentPresenter);
        this.selectionAdorner.SetEndPoint(selectionEndPoint);
      }
    
      private void OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
      {
        base.OnPreviewMouseLeftButtonUp(e);
        this.adornerLayer.Remove(this.selectionAdorner);
      }
    
      private bool TryFindVisualChild<TChild>(DependencyObject parent, out TChild child)
        where TChild : DependencyObject
      {
        child = null;
    
        if (parent is Popup popup)
        {
          parent = popup.Child;
        }
    
        if (parent == null)
        {
          return false;
        }
    
        for (int childElementIndex = 0; childElementIndex < VisualTreeHelper.GetChildrenCount(parent); childElementIndex++)
        {
          DependencyObject childElement = VisualTreeHelper.GetChild(parent, childElementIndex);
          if (childElement is TChild resultChildElement)
          {
            child = resultChildElement;
    
            return true;
          }
    
          if (TryFindVisualChild(childElement, out child))
          {
            return true;
          }
        }
    
        return false;
      }
    }