Thanks to rgqancy for pointing out the bug; it has been fixed.
First, here’s a screenshot of the result:

Usage code:
<l:GridLineDecorator>
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="Id" DisplayMemberBinding="{Binding Id}"/>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
</GridView>
</ListView.View>
</ListView>
</l:GridLineDecorator>
------------------------Main Content-------------------------------
People often ask how to add grid lines when using WPF's ListView. For example: http://www.bbniu.com/forum/thread-1090-1-1.html
The first solution that comes to mind is to place a Border inside the GridViewColumn's CellTemplate and set the Border's BorderBrush and BorderThickness. For example:
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1" UseLayoutRounding="True">
<TextBlock Text="{Binding Id}"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
But soon you'll find that the Border doesn't resize with the column width, like this:

Moreover, even setting the ListView's HorizontalContentAlignment to Stretch won't work. You have to set HorizontalContentAlignment="Stretch" on the ListViewItem itself. Therefore, you must add a ListViewItem style to set it globally:
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
But the problem still isn't solved because the Border doesn't fill the entire cell, like this:

So you have to carefully adjust each Border's Margin so they "just" connect together, making them look like continuous lines. Perhaps adjusting the Margin isn't enough; you might also need to modify the ListViewItem template. After modifying the template, you find that creating so many Borders has performance issues. The most troublesome part is that you have to specify a CellTemplate for each column, and what if one day you need to uniformly adjust the border colors...
Therefore, while this approach is feasible, it's actually quite troublesome to implement.
Is there a way to directly "draw lines" on the ListView? We could certainly write a custom ListView and draw lines in its OnRender method, but the ideal scenario is to achieve this grid-drawing functionality without modifying any existing controls. Additionally, being able to adjust the grid line color freely would be even better.
So the overall requirements are:
Be able to draw grid lines.
No need to modify the ListView or write a custom ListView.
Be able to adjust the grid line color.
If you're familiar with design patterns, the idea of "adding new functionality without changing existing code" immediately suggests the Decorator pattern. In fact, WPF already has the Decorator control, and the commonly used Border is a Decorator that can help draw background colors and borders for controls.
Therefore, wouldn't it be great to have a Decorator that you can wrap around a ListView and get line-drawing functionality? However, I don't plan to directly inherit Decorator here because WPF's Decorator works for all UIElement, but we only want to target ListView.
GridLineDecorator directly inherits from FrameworkElement and overrides the VisualChild and LogicalChild related code to display the wrapped ListView.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Threading;
namespace ListViewWithLines
{
[ContentProperty("Target")]
public class GridLineDecorator : FrameworkElement
{
private ListView _target;
private DrawingVisual _gridLinesVisual = new DrawingVisual();
private GridViewHeaderRowPresenter _headerRowPresenter = null;
public GridLineDecorator()
{
this.AddVisualChild(_gridLinesVisual);
this.AddHandler(ScrollViewer.ScrollChangedEvent, new RoutedEventHandler(OnScrollChanged));
}
#region GridLineBrush
/// <summary>
/// GridLineBrush Dependency Property
/// </summary>
public static readonly DependencyProperty GridLineBrushProperty =
DependencyProperty.Register("GridLineBrush", typeof(Brush), typeof(GridLineDecorator),
new FrameworkPropertyMetadata(Brushes.LightGray,
new PropertyChangedCallback(OnGridLineBrushChanged)));
/// <summary>
/// Gets or sets the GridLineBrush property. This dependency property
/// indicates ....
/// </summary>
public Brush GridLineBrush
{
get { return (Brush)GetValue(GridLineBrushProperty); }
set { SetValue(GridLineBrushProperty, value); }
}
/// <summary>
/// Handles changes to the GridLineBrush property.
/// </summary>
private static void OnGridLineBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GridLineDecorator)d).OnGridLineBrushChanged(e);
}
/// <summary>
/// Provides derived classes an opportunity to handle changes to the GridLineBrush property.
/// </summary>
protected virtual void OnGridLineBrushChanged(DependencyPropertyChangedEventArgs e)
{
DrawGridLines();
}
#endregion
#region Target
public ListView Target
{
get { return _target; }
set
{
if (_target != value)
{
if (_target != null) Detach();
RemoveVisualChild(_target);
RemoveLogicalChild(_target);
_target = value;
AddVisualChild(_target);
AddLogicalChild(_target);
if (_target != null) Attach();
InvalidateMeasure();
}
}
}
private void GetGridViewHeaderPresenter()
{
if (Target == null)
{
_headerRowPresenter = null;
return;
}
_headerRowPresenter = Target.GetDesendentChild<GridViewHeaderRowPresenter>();
}
#endregion
#region DrawGridLines
private void DrawGridLines()
{
if (Target == null) return;
if (_headerRowPresenter == null) return;
var itemCount = Target.Items.Count;
if (itemCount == 0) return;
var gridView = Target.View as GridView;
if (gridView == null) return;
// Get the drawing context
var drawingContext = _gridLinesVisual.RenderOpen();
var startPoint = new Point(0, 0);
var totalHeight = 0.0;
// Parameters for pixel snapping, otherwise some lines will appear blurry
var dpiFactor = this.GetDpiFactor();
var pen = new Pen(this.GridLineBrush, 1 * dpiFactor);
var halfPenWidth = pen.Thickness / 2;
var guidelines = new GuidelineSet();
// Draw horizontal lines
for (int i = 0; i < itemCount; i++)
{
var item = Target.ItemContainerGenerator.ContainerFromIndex(i) as ListViewItem;
if (item != null)
{
var renderSize = item.RenderSize;
var offset = item.TranslatePoint(startPoint, this);
var hLineX1 = offset.X;
var hLineX2 = offset.X + renderSize.Width;
var hLineY = offset.Y + renderSize.Height;
// Add guidelines for pixel snapping
guidelines.GuidelinesY.Add(hLineY + halfPenWidth);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawLine(pen, new Point(hLineX1, hLineY), new Point(hLineX2, hLineY));
drawingContext.Pop();
// Calculate total height for vertical lines
totalHeight += renderSize.Height;
}
}
// Draw vertical lines
var columns = gridView.Columns;
var headerOffset = _headerRowPresenter.TranslatePoint(startPoint, this);
var headerSize = _headerRowPresenter.RenderSize;
var vLineX = headerOffset.X;
var vLineY1 = headerOffset.Y + headerSize.Height;
foreach (var column in columns)
{
var columnWidth = column.GetColumnWidth();
vLineX += columnWidth;
// Add guidelines for pixel snapping
guidelines.GuidelinesX.Add(vLineX + halfPenWidth);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawLine(pen, new Point(vLineX, vLineY1), new Point(vLineX, totalHeight));
drawingContext.Pop();
}
drawingContext.Close();
}
#endregion
#region Overrides to show Target and grid lines
protected override int VisualChildrenCount
{
get { return Target == null ? 1 : 2; }
}
protected override System.Collections.IEnumerator LogicalChildren
{
get { yield return Target; }
}
protected override Visual GetVisualChild(int index)
{
if (index == 0) return _target;
if (index == 1) return _gridLinesVisual;
throw new IndexOutOfRangeException(string.Format("Index of visual child '{0}' is out of range", index));
}
protected override Size MeasureOverride(Size availableSize)
{
if (Target != null)
{
Target.Measure(availableSize);
return Target.DesiredSize;
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Target != null)
Target.Arrange(new Rect(new Point(0, 0), finalSize));
return base.ArrangeOverride(finalSize);
}
#endregion
#region Handle Events
private void Attach()
{
_target.Loaded += OnTargetLoaded;
_target.Unloaded += OnTargetUnloaded;
}
private void Detach()
{
_target.Loaded -= OnTargetLoaded;
_target.Unloaded -= OnTargetUnloaded;
}
private void OnTargetLoaded(object sender, RoutedEventArgs e)
{
if (_headerRowPresenter == null)
GetGridViewHeaderPresenter();
DrawGridLines();
}
private void OnTargetUnloaded(object sender, RoutedEventArgs e)
{
DrawGridLines();
}
private void OnScrollChanged(object sender, RoutedEventArgs e)
{
DrawGridLines();
}
#endregion
}
}
Here, Target is a property of type ListView, and _gridLinesVisual is a DrawingVisual used for drawing the grid. Some may ask, why not simply override the OnRender method and draw lines there?
The reason is that if you override OnRender to draw lines, when the ListView sets a background, our drawn lines will be covered. This is because the control's background is drawn by a Border placed in its template, and the Border also draws in OnRender; it draws later, covering our earlier lines. Additionally, you'll find that when a ListView column is resized, it does not trigger a redraw of the GridLineDecorator, so the grid lines won't update synchronously.
In fact, the GetVisualChild override in GridLineDecorator is quite deliberate:
protected override Visual GetVisualChild(int index)
{
if (index == 0) return _target;
if (index == 1) return _gridLinesVisual;
throw new IndexOutOfRangeException(string.Format("Index of visual child '{0}' is out of range", index));
}
The ListView is returned first, then _gridLinesVisual.
However, even with DrawingVisual, there is still the problem that column width changes don't notify the decorator to redraw. There are several ideas to solve this:
- Listen for changes in the GridViewColumn's width.
- Listen for the
CompositionTarget.Renderingevent.
The first method is not feasible because there is no width change event for GridViewColumn. The second method works but has efficiency issues...
After some research, a feasible solution was found: listen to the ScrollViewer's ScrollChanged event. Inside the ListView, there are two ScrollViewers: one for the header and one for the items. When a column width changes, it triggers the ScrollViewer's ScrollChanged event.
Therefore, in the constructor:
public GridLineDecorator()
{
this.AddVisualChild(_gridLinesVisual);
this.AddHandler(ScrollViewer.ScrollChangedEvent, new RoutedEventHandler(OnScrollChanged));
}
The logic for drawing lines mainly involves iterating through all containers (actually ListViewItems), calculating their offset relative to the GridLineDecorator, determining the coordinates and lengths of horizontal and vertical lines, and drawing them. The code is extensive; you can download and check it for yourself.
Attentive readers might notice that sometimes the bottom lines are not drawn to the very end when ListViewItems are not fully displayed. This is because the ListView uses virtualization. You can force drawing by setting VirtualizingStackPanel.IsVirtualizing="False".
Attached code: https://files.cnblogs.com/RMay/ListViewWithLines.zip
Site Admin Note:
The original author's work is excellent, and the effect is good. For small amounts of data, such as a few thousand records, the above solution works perfectly. If the program needs to handle hundreds of thousands of records (received in pages), using the decorator approach has average efficiency (consider how to optimize). The following code can simply add horizontal lines:
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="BorderThickness" Value="0 0 1 1" />
<Setter Property="BorderBrush" Value="Black" />
</Style>
</ListView.ItemContainerStyle>

- Original title: [WPF] Custom GridLineDecorator to draw grid lines for ListView
- Original author: Big Buddha underfoot (Dafo Jiaoxia)
- Original link: https://www.cnblogs.com/RMay/archive/2010/12/27/1918048.html
- Original sample code: https://files.cnblogs.com/RMay/ListViewWithLines.zip
- Final example: https://github.com/dotnet9/CsharpSocketTest