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

WPF DataGrid の列ヘッダーによる標準のソートに加え、コードからのソート制御と ListCollectionView.CustomSort による独自ロジックの並び替えを実装例とともに解説し、用途ごとの推奨アプローチを表で整理する。

概要

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

前提・対象環境

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

本記事の表は、上記の環境で実際に計測したものである。
計測の条件は、それぞれの表の説明に書いた。
この環境で確認しているのは次の点である。

デフォルトのソート設定

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のソートを初期化する方法 を参照する。

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

列 SortMemberPath CanUserSort 見出しをクリックした後の並び順
DataGridTextColumn、Binding だけ Name True alice, bob, carol
+ SortMemberPath=Score を明示 Score True bob, carol, alice
+ CanUserSort=False Name False carol, alice, bob
DataGridTemplateColumn、SortMemberPath なし (空) False carol, alice, bob
DataGridTemplateColumn + SortMemberPath=Name Name True alice, bob, carol

.NET 10 / Windows 11 で、列の宣言だけを変えて測った結果。「見出しをクリックした後の並び順」は、その列のヘッダーのクリックで走るのと同じ並べ替えを実行した後の並びである。元の並びは carol, alice, bob。

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

注目すべきは最終行である。Binding を持たない DataGridTemplateColumn では SortMemberPath が空になり、CanUserSort も False になる。
CanUserSortColumns を True にしていても、並び替えの対象にならない。テンプレート列を並び替え可能にするには SortMemberPath を明示する。最終行の、SortMemberPath="Name" を明示したテンプレート列は名前順に並んだ。


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

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

using System.ComponentModel;

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

ビューは SortDescription を足した時点で並べ替えられるため、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 の昇順で表示していた状態から、SortDescriptions を Price の降順へ差し替えた直後。左は SortDescriptions だけを更新したため、行は Price の降順になっているのに矢印が Name 列に残る。右は SortDirection も更新しており、矢印が Price 列の降順を指している。

ListCollectionView によるカスタムソート

カルチャに依存しない序数比較の文字列ソート(SortDescription はカルチャの規則で文字列を比べる)や、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));
}

CustomSort と SortDescriptions は重ならず、後から設定したほうだけが残る。CustomSort を設定すると SortDescriptions は消え、その後に SortDescription を足すと CustomSort は null に戻った。ListCollectionView.CustomSort のリファレンスには、前半(このプロパティを設定すると SortDescriptions が消えること)が書かれている。そのため、SortDescriptions に戻すのに特別な手順は要らない。

操作 SortDescriptions CustomSort 並び順 / 選択
SortDescriptions: Name の昇順 1 null alice, bob, carol
続けて CustomSort = 名前の長さ順 0 設定あり bob, alice, carol
続けて SortDescriptions.Add(Name の降順) 1 null carol, bob, alice
行を選んだ状態で Items.Refresh() - - SelectedItem bob, CurrentCell bob

.NET 10 / Windows 11 で、3 件のコレクションの既定のビューと、それにバインドした DataGrid(最終行)で測った結果。

注意点

まとめ

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

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