Overview
The WPF DataGrid control provides built-in column sorting out of the box.
When CanUserSortColumns is set to true (the default), clicking a column header sorts the rows by that column.
Built-in sorting covers the common cases, but real applications frequently need programmatic sorting, custom comparison rules, or a way to reset the grid to its unsorted state.
This article covers the essentials and explores patterns for each of those requirements.
Prerequisites / Environment
- Framework / Language: .NET 6 or later / C# 10
- Target control: WPF
DataGrid(System.Windows.Controls) - Architecture: applicable to both code-behind and MVVM
- Verification environment: .NET 10 / Windows 11
The examples assume the grid is bound to an observable collection of a Product type that exposes Name and Price properties.
The tables in this article were measured in the environment above.
Each caption states how its table was measured.
The following points were confirmed in that environment:
- A
DataGridTextColumnwithout an explicitSortMemberPathpicks up the path from itsBinding. - A template column with no
Bindingends up with an emptySortMemberPathand cannot be sorted.
Enabling Default Sorting
By default, every column in a DataGrid is sortable as long as its SortMemberPath can be resolved against the bound data source:
<DataGrid ItemsSource="{Binding Products}"
AutoGenerateColumns="False"
CanUserSortColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" SortMemberPath="Name" />
<DataGridTextColumn Header="Price" Binding="{Binding Price}" SortMemberPath="Price" />
</DataGrid.Columns>
</DataGrid>
Clicking the Name header once sorts ascending, and clicking again reverses to descending.
The default behavior does not return to an unsorted state, so clear logic must be implemented explicitly when needed — see How to Reset DataGrid Sorting in WPF for that pattern.
The table below records what lands in SortMemberPath and CanUserSort for each way of declaring a column.
| column | SortMemberPath | CanUserSort | order after a header click |
|---|---|---|---|
| DataGridTextColumn, Binding only | Name | True | alice, bob, carol |
| + explicit SortMemberPath=Score | Score | True | bob, carol, alice |
| + CanUserSort=False | Name | False | carol, alice, bob |
| DataGridTemplateColumn, no SortMemberPath | (empty) | False | carol, alice, bob |
| DataGridTemplateColumn + SortMemberPath=Name | Name | True | alice, bob, carol |
Measured on .NET 10 / Windows 11, varying only the column declaration. order after a header click is the order after running the same sort that a click on that column’s header runs; the original order is carol, alice, bob.
SortMemberPath is filled in from the Binding path even when it is not written. Stating it in the XAML above makes the intent explicit; omitting it produces the same result.
The last row is the one to note. A DataGridTemplateColumn without a Binding ends up with an empty SortMemberPath, and CanUserSort becomes False as well.
It stays out of sorting even with CanUserSortColumns set to True. Making a template column sortable requires stating SortMemberPath explicitly; the last row, a template column with SortMemberPath="Name", sorted by name.
Sorting via Code-Behind
Sorting can be triggered programmatically by manipulating DataGrid.Items.SortDescriptions:
using System.ComponentModel;
dataGrid.Items.SortDescriptions.Clear();
dataGrid.Items.SortDescriptions.Add(
new SortDescription(nameof(Product.Price), ListSortDirection.Descending));
The view re-sorts as soon as a SortDescription is added, so no Items.Refresh() call is needed.
Reset the column-header sort glyph too so the UI stays in sync:
foreach (var col in dataGrid.Columns)
col.SortDirection = null;
var priceCol = dataGrid.Columns.First(c => c.SortMemberPath == nameof(Product.Price));
priceCol.SortDirection = ListSortDirection.Descending;
Without this glyph update the header arrow still points at the previous column, even though the rows are ordered correctly, which makes the sorted state look inconsistent to the user.
Name ascending, and then had their SortDescriptions replaced with Price descending. The left grid updated only SortDescriptions, so the rows follow Price descending while the arrow stays on the Name column. The right grid also updated SortDirection, so the arrow points at Price descending.Custom Sort Logic with ListCollectionView
For comparison rules that SortDescriptions cannot express — such as an ordinal, culture-independent string comparison (a SortDescription compares strings with culture-aware rules) or sorting by an expression not exposed as a public property — use ListCollectionView.CustomSort. (SortDescriptions can still sort by a public property whose getter computes a value; the limit is comparisons not exposed as a property.) Multi-level sorting does not need it: add several SortDescription entries instead.
CollectionViewSource.GetDefaultView returns an ICollectionView, which does not expose CustomSort. For an in-memory collection the concrete type is ListCollectionView, but a view over another source (such as a DataView) is not, so narrow the type with a pattern match rather than an unconditional cast that could throw InvalidCastException:
if (CollectionViewSource.GetDefaultView(dataGrid.ItemsSource) is ListCollectionView view)
{
view.CustomSort = Comparer<Product>.Create((a, b) =>
StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name));
}
CustomSort and SortDescriptions do not stack; the one set last replaces the other. Setting CustomSort cleared SortDescriptions, and adding a SortDescription afterward reset CustomSort to null. The reference for ListCollectionView.CustomSort states the first half: setting the property clears SortDescriptions. Switching back to SortDescriptions therefore needs no extra step.
| operation | SortDescriptions | CustomSort | order / selection |
|---|---|---|---|
| SortDescriptions: Name asc | 1 | null | alice, bob, carol |
| then CustomSort = by name length | 0 | set | bob, alice, carol |
| then SortDescriptions.Add(Name desc) | 1 | null | carol, bob, alice |
| Items.Refresh() with a row selected | - | - | SelectedItem bob, CurrentCell bob |
Measured on .NET 10 / Windows 11 on the default view of a three-item collection, and on a DataGrid bound to it for the last row.
Notes
SortMemberPathis required only when it differs from the column’sBindingpath. For a plainDataGridTextColumnbound to a simple property, sorting works without it, but specifying it explicitly avoids surprises when the binding path is complex.Items.Refresh()rebuilds the view, but it is not needed after changingSortDescriptions. In the measurement above, aRefreshwith a row selected kept bothSelectedItemandCurrentCell.CustomSortis a property ofListCollectionView, the default view returned for in-memory collections. A view over another source — such as theBindingListCollectionViewreturned for aDataView— is not aListCollectionViewand offers noCustomSort, which is why the pattern match above skips it. For those sources the ordering must come from another mechanism, such asSortDescriptionsor sorting at the data source.- Sorting changes only the display order, not the underlying collection. Code that iterates the bound collection directly still sees the original order.
Summary
| Scenario | Recommended approach |
|---|---|
| Simple column sorting | CanUserSortColumns="True" (default) |
| Programmatic sort | SortDescriptions + update SortDirection |
| Custom sort logic | ListCollectionView.CustomSort |
For most line-of-business apps the default mechanism covers the common cases.
Reach for CustomSort only when the data requires special ordering that SortDescription cannot express.
- DataGrid (WPF Standard Control Demo App): measures the order produced by header clicks and why sorting from code fails while a cell is being edited