I am trying to achieve effect where each column has its own border, but yet can not find a perfectly working solution.
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>
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.
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>
ColumnItem
, which hosts a collection of
CellItem
items where each CellItem
has a CellItem.RowIndex
.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();
}
}
}
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; }
}
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>
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With