概要
WPF の DataGrid コントロールは、列ヘッダーをクリックするだけで行を昇順・降順に並び替える機能を標準で備えている。
CanUserSortColumns が true(デフォルト)の状態で SortMemberPath を解決できれば、追加コードなしにソートを操作できる。
一方、実務では標準機能だけでは足りず、プログラムからのソート、独自の比較ロジック、未ソート状態への復帰などが必要になる場面が多い。
本記事では基本の使い方に加えて、これらの要件それぞれに対する実装パターンを解説する。
前提・対象環境
- フレームワーク / 言語: .NET 6 以降 / C# 10
- 対象コントロール: WPF
DataGrid(System.Windows.Controls) - アーキテクチャ: コードビハインド・MVVM のいずれにも適用可能
- 検証環境: .NET 10 / Windows 11
以降の例では、Name と Price プロパティを持つ Product 型のコレクションに DataGrid がバインドされていることを前提とする。
本記事の表は、上記の環境で実際に計測したものである。
計測の条件は、それぞれの表の説明に書いた。
この環境で確認しているのは次の点である。
SortMemberPathを明示しないDataGridTextColumnでは、Bindingのパスが入る。Bindingを持たないテンプレート列ではSortMemberPathが空になり、並び替えられない。
デフォルトのソート設定
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;
グリフを更新しないと、行は正しく並んでいるのにヘッダーの矢印が以前の列を指したままになり、ソート状態がユーザーには不整合に見える。
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(最終行)で測った結果。
注意点
SortMemberPathが必須になるのは、列のBindingパスと並び替えキーが異なる場合のみである。単純なプロパティにバインドしたDataGridTextColumnでは省略してもソートは機能するが、バインドパスが複雑な場合は明示しておくと予期しない挙動を避けられる。Items.Refresh()はビューを再構築するが、SortDescriptionsを変えた後に呼ぶ必要はない。上の計測では、行を選択した状態でRefreshを呼んでもSelectedItemとCurrentCellは残った。CustomSortはListCollectionViewのプロパティである。DataViewに対して返されるBindingListCollectionViewなどはListCollectionViewではなくCustomSortを持たないため、上記のパターンマッチはこれをスキップする。この種のソースでは、SortDescriptionsやデータソース側での並び替えなど別の手段で順序を決める。- ソートが変えるのは表示上の順序だけであり、元のコレクションの並びは変わらない。バインド先のコレクションを直接反復するコードは元の順序のままを見る。
まとめ
| シナリオ | 推奨アプローチ |
|---|---|
| 単純な列ソート | CanUserSortColumns="True"(デフォルト) |
| コードからのソート | SortDescriptions + SortDirection 更新 |
| カスタムソートロジック | ListCollectionView.CustomSort |
業務アプリの多くはデフォルト機能で対応できる。
CustomSort は SortDescription では表現できない特殊な並び替えが必要なときだけ使用するのが運用上の目安である。
- DataGrid(WPF 標準コントロールデモアプリ):見出しのクリックで並ぶ順序と、セルの編集中にコードから並べ替えると失敗する理由を実測したページ