mister_giga
mister_giga

Reputation: 612

WPF DataGrid add separate border to every column set

I am trying to achieve effect where each column has its own border, but yet can not find a perfectly working solution. enter image description here

This kind of look is desired but this is implemented by putting 3 borders in 3 columned Grid, which is not flexible as Grid columns and DataGrid columns are being sized sized separately

<Window x:Class="WpfApp3.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApp3" xmlns:usercontrols="clr-namespace:EECC.UserControls"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Grid Background="LightGray">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5"/>
    <Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="1"/>
    <Border Background="White" CornerRadius="5" BorderThickness="1" BorderBrush="Black" Margin="5" Grid.Column="2"/>

    <DataGrid ItemsSource="{Binding Items}" ColumnWidth="*" AutoGenerateColumns="True" Padding="10" GridLinesVisibility="None" Background="Transparent" Grid.ColumnSpan="3">
        <DataGrid.Resources>
            <Style TargetType="{x:Type DataGridRow}">
                <Setter Property="Background" Value="Transparent"/>
            </Style>
            <Style TargetType="{x:Type DataGridColumnHeader}">
                <Setter Property="Background" Value="Transparent"/>
            </Style>
        </DataGrid.Resources>
    </DataGrid>
</Grid>

Upvotes: 6

Views: 827

Answers (2)

Funk
Funk

Reputation: 11221

With a fair amount of elbow grease, you can get the desired look by extending the native DataGrid.

FirstPic

Custom header and cell templates should take care of the spacing, with the appropriate background color. The AutoGeneratingColumn behavior requires more control than could easily be achieved in XAML, so I chose to create the templates in code to be able to pass the column's PropertyName.

SecondPic

The observant reader will already have asked themselves: "What about the border at the end of the list?". That's right, we need to be able to distinguish the last item from all others, to be able to template its border differently.

This is done with the following contract:

public interface ICanBeLastItem
{
    bool IsLastItem { get; set; }
}

Which the row object needs to implement for the bottom border to be drawn correctly.

This also requires some custom logic when sorting, to update the value of IsLastItem. The pic with the yellow background shows the result of sorting on ThirdNumber.

The native DataGrid provides a Sorting event out of the box, but no Sorted event. The template thingy combined with the need for a custom event, led me to subclass ColumnView from DataGrid instead of declaring it as a UserControl.

I added code-behind to MainWindow, for switching the background color, but that's just for illustration purposes (as I didn't feel like implementing the Command pattern) and has nothing to do with the custom control.

The ColumnView is configured through binding. As always, feel free to extend. The current implementation expects the columns to be auto generated. In either case, the code for generating the templates is provided.

<local:ColumnView ItemsSource="{Binding Items}" Background="LightSteelBlue"/>

Demo code

ColumnView

public class ColumnView : DataGrid
{
    public ColumnView()
    {
        HeadersVisibility = DataGridHeadersVisibility.Column;
        HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;

        // Hidden props from base DataGrid
        base.ColumnWidth = new DataGridLength(1, DataGridLengthUnitType.Star);
        base.AutoGenerateColumns = true;
        base.GridLinesVisibility = DataGridGridLinesVisibility.None;

        // Styling
        ColumnHeaderStyle = CreateColumnHeaderStyle();
        CellStyle = CreateCellStyle(this);

        // Event handling
        AutoGeneratingColumn += OnAutoGeneratingColumn;
        Sorting += OnSorting;
        Sorted += OnSorted;
    }

    #region Hidden props

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public new DataGridLength ColumnWidth
    {
        get => base.ColumnWidth;
        set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(ColumnWidth)}.");
    }

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public new DataGridGridLinesVisibility GridLinesVisibility
    {
        get => base.GridLinesVisibility;
        set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(GridLinesVisibility)}.");
    }

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public new bool AutoGenerateColumns
    {
        get => base.AutoGenerateColumns;
        set => new InvalidOperationException($"{nameof(ColumnView)} doesn't allow changing {nameof(AutoGenerateColumns)}.");
    }

    #endregion Hidden props

    #region Styling

    private static Style CreateColumnHeaderStyle()
        => new Style(typeof(DataGridColumnHeader))
        {
            Setters =
            {
                new Setter(BackgroundProperty, Brushes.Transparent),
                new Setter(HorizontalAlignmentProperty, HorizontalAlignment.Stretch),
                new Setter(HorizontalContentAlignmentProperty, HorizontalAlignment.Stretch)
            }
        };

    private static Style CreateCellStyle(ColumnView columnView)
        => new Style(typeof(DataGridCell))
        {
            Setters =
            {
                new Setter(BorderThicknessProperty, new Thickness(0.0)),
                new Setter(BackgroundProperty, new Binding(nameof(Background)) { Source = columnView})
            }
        };

    #endregion Styling

    #region AutoGeneratingColumn

    // https://stackoverflow.com/questions/25643765/wpf-datagrid-databind-to-datatable-cell-in-celltemplates-datatemplate
    private static void OnAutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
    {
        if (sender is ColumnView columnView)
        {
            if (e.PropertyName == nameof(ICanBeLastItem.IsLastItem))
            {
                e.Cancel = true;
            }
            else
            {
                var column = new DataGridTemplateColumn
                {
                    CellTemplate = CreateCustomCellTemplate(e.PropertyName),
                    Header = e.Column.Header,
                    HeaderTemplate = CreateCustomHeaderTemplate(columnView, e.PropertyName),
                    HeaderStringFormat = e.Column.HeaderStringFormat,
                    SortMemberPath = e.PropertyName
                };
                e.Column = column;
            }
        }
    }

    private static DataTemplate CreateCustomCellTemplate(string path)
    {
        // Create the data template
        var customTemplate = new DataTemplate();

        // Set up the wrapping border
        var border = new FrameworkElementFactory(typeof(Border));
        border.SetValue(BorderBrushProperty, Brushes.Black);
        border.SetValue(StyleProperty, new Style(typeof(Border))
        {
            Triggers =
            {
                new DataTrigger
                {
                    Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
                    Value = false,
                    Setters =
                    {
                        new Setter(BackgroundProperty, Brushes.White),
                    }
                },
                new DataTrigger
                {
                    Binding = new Binding(nameof(DataGridCell.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(DataGridCell), 1) },
                    Value = true,
                    Setters =
                    {
                        new Setter(BackgroundProperty, SystemColors.HighlightBrush),
                    }
                },
                new DataTrigger
                {
                    Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
                    Value = false,
                    Setters =
                    {
                        new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, -1.0)),
                        new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 0.0)),
                    }
                },
                new DataTrigger
                {
                    Binding = new Binding(nameof(ICanBeLastItem.IsLastItem)),
                    Value = true,
                    Setters =
                    {
                        new Setter(MarginProperty, new Thickness(5.0, -1.0, 5.0, 0.0)),
                        new Setter(BorderThicknessProperty, new Thickness(1.0, 0.0, 1.0, 1.0)),
                        new Setter(Border.CornerRadiusProperty, new CornerRadius(0.0, 0.0, 5.0, 5.0)),
                        new Setter(Border.PaddingProperty, new Thickness(0.0, 0.0, 0.0, 5.0)),
                    }
                }
            }
        });

        // Set up the TextBlock
        var textBlock = new FrameworkElementFactory(typeof(TextBlock));
        textBlock.SetBinding(TextBlock.TextProperty, new Binding(path));
        textBlock.SetValue(MarginProperty, new Thickness(10.0, 0.0, 5.0, 0.0));

        // Set the visual tree of the data template
        border.AppendChild(textBlock);
        customTemplate.VisualTree = border;

        return customTemplate;
    }

    private static DataTemplate CreateCustomHeaderTemplate(ColumnView columnView, string propName)
    {
        // Create the data template
        var customTemplate = new DataTemplate();

        // Set up the wrapping border
        var border = new FrameworkElementFactory(typeof(Border));
        border.SetValue(MarginProperty, new Thickness(5.0, 0.0, 5.0, 0.0));
        border.SetValue(BackgroundProperty, Brushes.White);
        border.SetValue(BorderBrushProperty, Brushes.Black);
        border.SetValue(BorderThicknessProperty, new Thickness(1.0, 1.0, 1.0, 0.0));
        border.SetValue(Border.CornerRadiusProperty, new CornerRadius(5.0, 5.0, 0.0, 0.0));

        // Set up the TextBlock
        var textBlock = new FrameworkElementFactory(typeof(TextBlock));
        textBlock.SetValue(TextBlock.TextProperty, propName);
        textBlock.SetValue(MarginProperty, new Thickness(5.0));

        // Set the visual tree of the data template
        border.AppendChild(textBlock);
        customTemplate.VisualTree = border;

        return customTemplate;
    }

    #endregion AutoGeneratingColumn

    #region Sorting

    #region Custom Sorted Event

    // https://stackoverflow.com/questions/9571178/datagrid-is-there-no-sorted-event

    // Create a custom routed event by first registering a RoutedEventID
    // This event uses the bubbling routing strategy
    public static readonly RoutedEvent SortedEvent = EventManager.RegisterRoutedEvent(
        nameof(Sorted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ColumnView));

    // Provide CLR accessors for the event
    public event RoutedEventHandler Sorted
    {
        add => AddHandler(SortedEvent, value);
        remove => RemoveHandler(SortedEvent, value);
    }

    // This method raises the Sorted event
    private void RaiseSortedEvent()
    {
        var newEventArgs = new RoutedEventArgs(ColumnView.SortedEvent);
        RaiseEvent(newEventArgs);
    }

    protected override void OnSorting(DataGridSortingEventArgs eventArgs)
    {
        base.OnSorting(eventArgs);
        RaiseSortedEvent();
    }

    #endregion Custom Sorted Event

    private static void OnSorting(object sender, DataGridSortingEventArgs e)
    {
        if (sender is DataGrid dataGrid && dataGrid.HasItems)
        {
            if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
            {
                lastItem.IsLastItem = false;
            }
        }
    }

    private static void OnSorted(object sender, RoutedEventArgs e)
    {
        if (sender is DataGrid dataGrid && dataGrid.HasItems)
        {
            if (dataGrid.Items[dataGrid.Items.Count - 1] is ICanBeLastItem lastItem)
            {
                lastItem.IsLastItem = true;
            }
        }
    }

    #endregion Sorting
}

RowItem

public class RowItem : INotifyPropertyChanged, ICanBeLastItem
{
    public RowItem(int firstNumber, string secondNumber, double thirdNumber)
    {
        FirstNumber = firstNumber;
        SecondNumber = secondNumber;
        ThirdNumber = thirdNumber;
    }

    public int FirstNumber { get; }
    public string SecondNumber { get; }
    public double ThirdNumber { get; }

    private bool _isLastItem;
    public bool IsLastItem
    {
        get => _isLastItem;
        set
        {
            _isLastItem = value;
            OnPropertyChanged();
        }
    }

    #region INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion INotifyPropertyChanged
}

public interface ICanBeLastItem
{
    bool IsLastItem { get; set; }
}

MainWindow.xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Button Content="Switch Background" Click="ButtonBase_OnClick" />
        <local:ColumnView x:Name="columnView" Grid.Row="1" Padding="10"
                          ItemsSource="{Binding Items}"
                          Background="LightSteelBlue"/>
    </Grid>
</Window>

MainWindow.xaml.cs

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        if (columnView.Background == Brushes.LightSteelBlue)
        {
            columnView.Background = Brushes.DarkRed;
        }
        else if (columnView.Background == Brushes.DarkRed)
        {
            columnView.Background = Brushes.Green;
        }
        else if (columnView.Background == Brushes.Green)
        {
            columnView.Background = Brushes.Blue;
        }
        else if (columnView.Background == Brushes.Blue)
        {
            columnView.Background = Brushes.Yellow;
        }
        else
        {
            columnView.Background = Brushes.LightSteelBlue;
        }
    }
}

MainViewModel

public class MainViewModel
{
    public MainViewModel()
    {
        Items = InitializeItems(200);
    }

    private ObservableCollection<RowItem> InitializeItems(int numberOfItems)
    {
        var rowItems = new ObservableCollection<RowItem>();
        var random = new Random();
        for (var i = 0; i < numberOfItems; i++)
        {
            var firstNumber = Convert.ToInt32(1000 * random.NextDouble());
            var secondNumber = Convert.ToString(Math.Round(1000 * random.NextDouble()));
            var thirdNumber = Math.Round(1000 * random.NextDouble());
            var rowItem = new RowItem(firstNumber, secondNumber, thirdNumber);
            rowItems.Add(rowItem);
        }

        rowItems[numberOfItems - 1].IsLastItem = true;

        return rowItems;
    }

    public ObservableCollection<RowItem> Items { get; }
}

Upvotes: 1

BionicCode
BionicCode

Reputation: 29028

This is not trivial if you want to use the DataGrid. The "problem" is that the DataGrid uses a Grid to host the cells. The cell borders are drawn using the feature of the Grid to show grid lines. You can hide the grid lines but still you have the Grid controlling the layout.
Creating the gaps you want should not come easy. The layout system of the Grid makes it an effort to implement a solution that scales well. You can extend the SelectiveScrollingGrid (the hosting panel of the DataGrid) and add the gaps when laying out the items. Knowing about DataGrid internals, I can say it is possible, but not worth the effort.

The alternative solution would be to use a ListView with a GridView as host. The problem here is that the GridView is designed to show rows as single item. You have no chance to modify margins column based. You can only adjust the content. I have not tried to modify the ListView internal layout elements or override the layout algorithm, but in context of alternative solutions I would also rule the ListView using a GridView out - but it is possible. It's not worth the effort.


Solution: Custom View

The simplest solution I can suggest is to adjust the data structure to show data column based. This way you can use a horizontal ListBox. Each item makes a column. Each column is realized as vertical ListBox. You basically have nested ListBox elements.

You would have to take care of the row mapping in order to allow selecting cells of a common row across the vertical ListBox columns.
This can be easily achieved by adding a RowIndex property to the CellItem models.

The idea is to have the horizontal ListBox display a collection of ColumnItem models. Each column item model exposes a collection of CellItem models. The CellItem items of different columns but the same row must share the same CellItem.RowIndex.
As a bonus, this solution is very easy to style. ListBox template has almost no parts compared to the significantly more complex DataGrid or the slightly more complex GridView.

To make showcasing the concept less confusing I chose to implement the grid layout as UserControl. For the sake of simplicity the logic to initialize and host the models and source collections is implemented inside this UserControl. I don't recommend this. Instantiation and hosting of the items should be outside the control e.g., inside a view model. You should add a DependencyProperty as ItemsSource for the control as data source for the internal horizontal ListBox.

Usage Example

<Window>
  <ColumnsView />
</Window>

enter image description here

  1. First create the data structure to populate the view.
    The structure is based on the type ColumnItem, which hosts a collection of CellItem items where each CellItem has a CellItem.RowIndex.
    The CellItem items of different columns, that logically form a row must share the same CellItem.RowIndex.

ColumnItem.cs

public class ColumnItem
{
  public ColumnItem(string header, IEnumerable<CellItem> items)
  {
    Header = header;
    this.Items = new ObservableCollection<CellItem>(items);
  }

  public CellItem this[int rowIndex] 
    => this.Items.FirstOrDefault(cellItem => cellItem.RowIndex.Equals(rowIndex));

  public string Header { get; }
  public ObservableCollection<CellItem> Items { get; }
}

CellItem.cs

public class CellItem : INotifyPropertyChanged
{
  public CellItem(int rowIndex, object value)
  {
    this.RowIndex = rowIndex;
    this.Value = value;
  }

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") 
    => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

  public event PropertyChangedEventHandler PropertyChanged;
  public int RowIndex { get; }

  private object value;
  public object Value
  {
    get => this.value;
    set
    {
      this.value = value;
      OnPropertyChanged();
    }
  }

  private bool isSelected;
  public bool IsSelected
  {
    get => this.isSelected;
    set
    {
      this.isSelected = value;
      OnPropertyChanged();
    }
  }
}
  1. Build and initialize the data structure.
    In this example this is all implemented in the UserControl itself with the intend to keep the example as compact as possible.

ColumnsView.xaml.cs

public partial class ColumnsView : UserControl
{
  public ColumnsView()
  {
    InitializeComponent();
    this.DataContext = this;

    InitializeSourceData();
  }

  public InitializeSourceData()
  {
    this.Columns = new ObservableCollection<ColumnItem>();

    for (int columnIndex = 0; columnIndex < 3; columnIndex++)
    {
      var cellItems = new List<CellItem>();
      int asciiChar = 65;

      for (int rowIndex = 0; rowIndex < 10; rowIndex++)
      {
        var cellValue = $"CellItem.RowIndex:{rowIndex}, Value: {(char)asciiChar++}";
        var cellItem = new CellItem(rowIndex, cellValue);
        cellItems.Add(cellItem);
      }

      var columnHeader = $"Column {columnIndex + 1}";
      var columnItem = new ColumnItem(columnHeader, cellItems);
      this.Columns.Add(columnItem);
    }
  }

  private void CellsHostListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
  {
    var cellsHost = sender as Selector;
    var selectedCell = cellsHost.SelectedItem as CellItem;
    SelectCellsOfRow(selectedCell.RowIndex);
  }

  private void SelectCellsOfRow(int selectedRowIndex)
  {
    foreach (ColumnItem columnItem in this.Columns)
    {
      var cellOfRow = columnItem[selectedRowIndex];
      cellOfRow.IsSelected = true;
    }
  }

  private void ColumnGripper_DragStarted(object sender, DragStartedEventArgs e) 
    => this.DragStartX = Mouse.GetPosition(this).X;

  private void ColumnGripper_DragDelta(object sender, DragDeltaEventArgs e)
  {
    if ((sender as DependencyObject).TryFindVisualParentElement(out ListBoxItem listBoxItem))
    {
      double currentMousePositionX = Mouse.GetPosition(this).X;
      listBoxItem.Width = Math.Max(0 , listBoxItem.ActualWidth - (this.DragStartX - currentMousePositionX));
      this.DragStartX = currentMousePositionX;
    }
  }

  public static bool TryFindVisualParentElement<TParent>(DependencyObject child, out TParent resultElement)
    where TParent : DependencyObject
  {
    resultElement = null;

    DependencyObject parentElement = VisualTreeHelper.GetParent(child);

    if (parentElement is TParent parent)
    {
      resultElement = parent;
      return true;
    }

    return parentElement != null 
      ? TryFindVisualParentElement(parentElement, out resultElement) 
      : false;
  }

  public ObservableCollection<ColumnItem> Columns { get; }
  private double DragStartX { get; set; }
}
  1. Create the view using a horizontal ListView that renders it's ColumnItem source collection as a list of vertical ListBox elements.

ColumnsView.xaml

<UserControl x:Class="ColumnsView">
  <UserControl.Resources>
    <Style x:Key="ColumnGripperStyle"
         TargetType="{x:Type Thumb}">
      <Setter Property="Margin"
            Value="-2,8" />
      <Setter Property="Width"
            Value="4" />
      <Setter Property="Background"
            Value="Transparent" />
      <Setter Property="Cursor"
            Value="SizeWE" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type Thumb}">
            <Border Background="{TemplateBinding Background}"
                  Padding="{TemplateBinding Padding}" />
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </UserControl.Resources>

  <!-- Column host. Displays cells of a column. -->
  <ListBox ItemsSource="{Binding Columns}">
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <VirtualizingStackPanel Orientation="Horizontal" />
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>

    <ListBox.ItemTemplate>
      <DataTemplate>
        <Border Padding="4" 
                BorderThickness="1" 
                BorderBrush="Black" 
                CornerRadius="8">
          <StackPanel>
            <TextBlock Text="{Binding Header}" />

            <!-- Cell host. Displays cells of a column. -->
            <ListBox ItemsSource="{Binding Items}" 
                     BorderThickness="0" 
                     Height="150"
                     Selector.SelectionChanged="CellsHostListBox_SelectionChanged">
              <ListBox.ItemTemplate>
                <DataTemplate>
                  <TextBlock Text="{Binding Value}" />
                </DataTemplate>
              </ListBox.ItemTemplate>
          
              <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
              
                  <!-- Link item container selection to CellItem.IsSelected -->
                  <Setter Property="IsSelected" Value="{Binding IsSelected}" />
                </Style>
              </ListBox.ItemContainerStyle>
            </ListBox>
          </StackPanel>
        </Border>
      </DataTemplate>
    </ListBox.ItemTemplate>

    <ListBox.ItemContainerStyle>
      <Style TargetType="ListBoxItem">
        <Setter Property="Margin" Value="0,0,8,0" /> <!-- Define the column gap -->    
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
              <Grid>
                <Grid.ColumnDefinitions>
                  <ColumnDefinition />
                  <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <ContentPresenter />
                <Thumb Grid.Column="1" 
                       Style="{StaticResource ColumnGripperStyle}"
                       DragStarted="ColumnGripper_DragStarted" 
                       DragDelta="ColumnGripper_DragDelta" />
              </Grid>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>
    </ListBox.ItemContainerStyle>
  </ListBox>
</UserControl>

Notes for improvement

The ColumnsView.Columns property should be a DependencyProperty to allow to use the control as binding target.
The column gap can also be a DependencyProperty of ColumnsView.
By replacing the TextBlock that displays the column header with a Button, you can easily add sorting. Having the active column that triggers the sorting e.g. lexically, you would have to sync the other passive columns and sort them based on the CellItem.RowIndex order of the active sorted column.
Maybe choose to extend Control rather than UserControl.
You can implement the CellItem to use a generic type parameter to declare the Cellitem.Value property like CellItem<TValue>.
You can implement the ColumnItem to use a generic type parameter to declare the ColumnItem.Items property like ColumnItem<TColumn>.
Add a ColumnsView.SelectedRow property that returns a collection of all CellItem items of the current selected row

Upvotes: 3

Related Questions