WPF DataGrid の並び替えを実装する方法

DataGrid のソート処理の基本と、実務で使いやすい実装パターンを解説する。

概要

WPF の DataGrid コントロールは、列ヘッダーをクリックするだけで行を昇順・降順に並び替える機能を標準で備えている。
CanUserSortColumnstrue(デフォルト)の状態で SortMemberPath を解決できれば、追加コードなしにソートを操作できる。
一方、実務では標準機能だけでは足りず、プログラムからのソート、独自の比較ロジック、未ソート状態への復帰などが必要になる場面が多い。
本記事では基本の使い方に加えて、これらの要件それぞれに対する実装パターンを解説する。

前提・対象環境

以降の例では、NamePrice プロパティを持つ Product 型のコレクションに DataGrid がバインドされていることを前提とする。

本記事の図は、上記の環境で列の宣言だけを変えた DataGrid を実際に表示し、各列の SortMemberPathCanUserSort を読み出して得たものである。
この環境で確認しているのは次の点である。

デフォルトのソート設定

DataGrid の各列は、SortMemberPath がバインド先のデータソースに対して解決できる限り、標準で並び替え可能になる。

<DataGrid ItemsSource="{Binding Products}"
          AutoGenerateColumns="False"
          CanUserSortColumns="True">
  <DataGrid.Columns>
    <DataGridTextColumn Header="商品名"  Binding="{Binding Name}"  SortMemberPath="Name" />
    <DataGridTextColumn Header="価格"   Binding="{Binding Price}" SortMemberPath="Price" />
  </DataGrid.Columns>
</DataGrid>

列ヘッダーを 1 回クリックで昇順、もう 1 回で降順に切り替わる。
標準機能では未ソート状態への復帰は行われないため、解除が必要な場合はコードで明示的に制御する。
未ソート状態へ戻す実装については WPF DataGrid のソート状態をリセットする方法 を参照する。

列の作り方を変えて、SortMemberPathCanUserSort に何が入るかを測った結果が次の図である。

列の宣言方法ごとに SortMemberPath と CanUserSort を測った表。Binding だけの DataGridTextColumn では SortMemberPath に Binding のパスが入り CanUserSort は True。SortMemberPath を明示するとそちらが使われる。CanUserSort を False にすると並び替えられない。Binding を持たないテンプレート列では SortMemberPath が空になり CanUserSort も False になる。
.NET 10 / Windows 11 で、列の宣言だけを変えて測った結果。order after sorting は、その列の SortMemberPath で昇順に並べ替えた後の並びである(並べ替えられない列では元の並びのまま)。

SortMemberPath を書かなくても、Binding のパスが自動で入る。 上の XAML で SortMemberPath を明示しているのは意図を明確にするためであり、省いても同じ結果になる。

注目すべきは最終行である。Binding を持たない DataGridTemplateColumn では SortMemberPath が空になり、CanUserSortFalse になる
CanUserSortColumnsTrue にしていても、並び替えの対象にならない。テンプレート列を並び替え可能にするには SortMemberPath を明示する。


コードでソートを制御する

DataGrid.Items.SortDescriptions を直接操作することで、ユーザー操作を介さずプログラムからソートをかけられる。

using System.ComponentModel;

dataGrid.Items.SortDescriptions.Clear();
dataGrid.Items.SortDescriptions.Add(
    new SortDescription(nameof(Product.Price), ListSortDirection.Descending));
dataGrid.Items.Refresh();

このとき、列ヘッダーに表示されるソートグリフ(矢印)も同期させる。

foreach (var col in dataGrid.Columns)
    col.SortDirection = null;

var priceCol = dataGrid.Columns.First(c => c.SortMemberPath == nameof(Product.Price));
priceCol.SortDirection = ListSortDirection.Descending;

グリフを更新しないと、行は正しく並んでいるのにヘッダーの矢印が以前の列を指したままになり、ソート状態がユーザーには不整合に見える。

2 つの DataGrid を並べた画面。左は行が Price の降順に並んでいるのに Name 列に昇順の矢印が残っている。右は Price 列に降順の矢印が付いており、行の並びと一致している。
いずれも Name の昇順で表示していた状態から、SortDescriptionsPrice の降順へ差し替えた直後。左は SortDescriptions だけを更新したため、行は Price の降順になっているのに矢印が Name 列に残る。右は SortDirection も更新しており、矢印が Price 列の降順を指している。

ListCollectionView によるカスタムソート

大文字小文字を区別しない文字列ソートや、public プロパティとして公開されていない式による並び替えなど、標準の SortDescription では表現できない比較規則が必要なケースでは ListCollectionView.CustomSort を使う(SortDescription は getter が値を計算する public プロパティなら並び替えできるが、プロパティとして公開されていない比較には対応できない)。
なお、複数キーによる多段ソートは SortDescriptions に複数の SortDescription を追加すれば表現できるため、CustomSort は不要である。
ここで注意が必要なのは戻り値の型である。
CollectionViewSource.GetDefaultView が返すのは ICollectionView であり、この型は CustomSort を公開していない。
インメモリのコレクションでは具象型が ListCollectionView になるが、DataView などをソースとするビューはそうではない。
そのため、例外を投げうる無条件のキャストではなく、パターンマッチで型を絞り込む。

if (CollectionViewSource.GetDefaultView(dataGrid.ItemsSource) is ListCollectionView view)
{
    view.CustomSort = Comparer<Product>.Create((a, b) =>
        StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name));
}

CustomSortSortDescriptions より優先される。
標準の SortDescriptions によるソートへ戻す場合、SortDescriptions.Clear() だけでは CustomSort が残るため、先に view.CustomSort = null を設定してから SortDescriptions を構成する。

注意点

まとめ

シナリオ 推奨アプローチ
単純な列ソート CanUserSortColumns="True"(デフォルト)
コードからのソート SortDescriptionsSortDirection 更新
カスタムソートロジック ListCollectionView.CustomSort

業務アプリの多くはデフォルト機能で対応できる。
CustomSortSortDescription では表現できない特殊な並び替えが必要なときだけ使用するのが運用上の目安である。