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

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.

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; }
}