WPFのDataGridのソートを初期化する方法

WPF DataGrid のソート状態を初期化する代表的な方法を整理し、単一列ソートと複数列ソートの両方で使える実装例を示す。

WPF の DataGrid は便利なソート機能を持っていますが、要件によっては「初期状態に戻す(ソートを解除する)」動作を明示的に実装したいことがあります。
本記事では、DataGrid のソート初期化を実現する代表的な方法を整理します。

概要

本記事では、以下の方法で DataGrid のソート初期化を扱います。

前提・対象環境

本記事の図は、上記の環境で ICollectionViewSortDescriptions と列の SortDirection を読み出して得たものである。
この環境で確認しているのは次の点である。

問題

WPF DataGrid では、業務要件として「現在の並び替え状態を初期状態へ戻す」操作が必要になる場合があります。
このとき、標準操作だけでは意図したタイミングでの初期化を統一しづらいケースがあります。

原因・背景

まず前提として、WPF DataGridShift + 列ヘッダークリック は、標準では複数列ソートの追加です。
つまり、既存ソートに列を積み増すための操作であり、ソート解除のショートカットではありません。

そのため、「初期状態に戻す」にはコードによる制御が必要です。

並び替えの状態は 2 か所に分かれている。操作ごとに両方を測った結果が次の図である。

操作ごとに SortDescriptions の件数と列の SortDirection、並び順を測った表。コードから SortDescriptions を足しただけでは SortDirection は null のまま。SortDescriptions を消しただけでは SortDirection が Ascending のまま残る。両方を消して初めて初期状態に戻る。最終行の列ヘッダークリックでは両方が同時に更新される。
.NET 10 / Windows 11 での実測結果。SortDescriptions はビュー側の並び替え条件の件数、column.SortDirection は列ヘッダーの矢印を決めるプロパティである。最終行以外はコードから直接操作した場合である。

SortDescriptions.Clear() を呼んだ行を見ると、並び順は初期状態に戻っているのに column.SortDirectionAscending のまま残っている。
この状態ではヘッダーに矢印が表示されたままになり、並び替えが効いているように見える。

逆に SortDescriptions を足しただけの行では、並び順が変わっているのに SortDirectionnull のままである。
コードから一方を操作しても、もう一方は追随しない。 どちらの向きにも、両方を明示的に設定する必要がある。

SortDescriptions を 2 つ足した行は複数列ソートである。Shift + クリックはこの状態を作る操作であり、解除ではない。

最終行はその対照で、列ヘッダーのクリックで走る標準の並び替えである。この経路では SortDescriptionsSortDirection が同時に更新される。
ユーザーが並び替えたときに矢印と並び順が食い違わないのはこのためであり、食い違いが生じるのはコードから一方だけを触ったときである。


4 つの初期化方法

以下の方針で要件に応じて初期化方法を選択します。

コードで明示的にソートをクリアする

最もシンプルなのは、SortDescriptions と列ヘッダー矢印の両方をクリアする方法です。

using System.Windows.Controls;

public static class DataGridSortHelper
{
    public static void ClearDataGridSort(DataGrid dataGrid)
    {
        if (dataGrid == null) return;

        // データ側のソート条件をクリア
        dataGrid.Items.SortDescriptions.Clear();

        // ヘッダー矢印をクリア
        foreach (var column in dataGrid.Columns)
        {
            column.SortDirection = null;
        }

        // 表示更新
        dataGrid.Items.Refresh();
    }
}

ポイント

3回目のクリックで自動的に初期化する(カスタム挙動)

「昇順 → 降順 → 未ソート」の3状態にしたい場合は、Sorting イベントを使って制御します。
3つの状態を実際に表示すると次のようになります。

3 つの DataGrid を並べた画面。左は Name 列に昇順の矢印が付き名前順、中央は降順の矢印が付き逆順、右は矢印が無くデータ本来の並びになっている。
同じデータに対する 3 状態の表示。未ソート(右)では対象列の SortDescription とヘッダーの矢印がどちらも消える。この例は 1 列だけでソートしているため、解除するとコレクション本来の並びに戻る。複数列でソートしている場合は他の列の条件が残るため、必ずしも元の並びには戻らない。

XAML

<DataGrid x:Name="MyDataGrid"
          Sorting="DataGrid_Sorting" />

この設定により、列ヘッダークリック時の既定ソート処理へ独自ロジックを差し込める。
また、DataGridTemplateColumn などで列ソートを解除対象にする場合は、事前に SortMemberPath を設定しておく必要がある。

C#

using System.ComponentModel;
using System.Linq;
using System.Windows.Controls;

private void DataGrid_Sorting(object sender, DataGridSortingEventArgs e)
{
    if (sender is not DataGrid dataGrid) return;

    if (e.Column.SortDirection == ListSortDirection.Descending)
    {
        e.Handled = true; // 標準処理をキャンセル

        // 対象列の SortDescription のみ削除
        var target = dataGrid.Items.SortDescriptions
            .FirstOrDefault(sd => sd.PropertyName == e.Column.SortMemberPath);

        if (!string.IsNullOrEmpty(target.PropertyName))
        {
            dataGrid.Items.SortDescriptions.Remove(target);
        }

        e.Column.SortDirection = null;
        dataGrid.Items.Refresh();
    }
}

この実装では、降順状態でさらにクリックされた場合のみ対象列のソートを解除し、複数列ソート時でも他列の条件は維持される。

CollectionView を使って ViewModel から初期化する

MVVM 構成では、DataGrid を直接操作せず ICollectionView を使うと管理しやすくなります。

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;

public class SampleViewModel
{
    public ObservableCollection<RowItem> Items { get; } = new();
    public ICollectionView ItemsView { get; }

    public SampleViewModel()
    {
        ItemsView = CollectionViewSource.GetDefaultView(Items);
    }

    public void ClearSort()
    {
        ItemsView.SortDescriptions.Clear();
        ItemsView.Refresh();
    }
}

public class RowItem
{
    public string Name { get; set; } = "";
    public int Value { get; set; }
}

SortDescriptionsItemsView 側で管理することで、UI コンポーネントへの依存を減らし、テスト容易性を確保できる。

ただし、この ClearSort が解除するのはビューの並び順だけである。 DataGridColumn.SortDirectionDataGrid 側に残るため、並び順は初期状態に戻ってもヘッダーの矢印は表示されたままになる(前掲の表の then SortDescriptions.Clear() only の行がその実測である)。
矢印まで戻すには、ViewModel から列の状態を触れない以上、DataGrid 側で SortDirectionnull にする処理が別に要る。Behavior 化する場合は、その処理を Behavior に含めるとコマンドから一度に初期化できる。

<DataGrid ItemsSource="{Binding ItemsView}" />

ビューは ItemsView を表示するだけに限定されるため、ソート初期化の責務を ViewModel 側に集約できる。

Behavior として共通化する

同じカスタムソート挙動を複数画面で使うなら、Behavior 化が有効です。
以下は Microsoft.Xaml.Behaviors.Wpf を利用する例です。

using Microsoft.Xaml.Behaviors;
using System.ComponentModel;
using System.Linq;
using System.Windows.Controls;

public class TriStateSortBehavior : Behavior<DataGrid>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Sorting += OnSorting;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Sorting -= OnSorting;
        base.OnDetaching();
    }

    private void OnSorting(object sender, DataGridSortingEventArgs e)
    {
        if (sender is not DataGrid grid) return;

        if (e.Column.SortDirection == ListSortDirection.Descending)
        {
            e.Handled = true;

            var sd = grid.Items.SortDescriptions
                .FirstOrDefault(x => x.PropertyName == e.Column.SortMemberPath);

            if (!string.IsNullOrEmpty(sd.PropertyName))
            {
                grid.Items.SortDescriptions.Remove(sd);
            }

            e.Column.SortDirection = null;
            grid.Items.Refresh();
        }
    }
}

Behavior 化により、同じ三状態ソート解除ロジックを画面ごとに重複実装せず適用できる。

<Window
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:local="clr-namespace:YourApp.Behaviors">
    <DataGrid>
        <i:Interaction.Behaviors>
            <local:TriStateSortBehavior />
        </i:Interaction.Behaviors>
    </DataGrid>
</Window>

XAML 側は Behavior を宣言するだけで済むため、利用画面追加時の実装コストを抑制しやすい。

選択の分岐点

どの方法を使うかは、初期化を起こす操作と、同じ挙動を何画面に広げるかで決まります。

ボタンやメニューから即時に全解除するなら、明示クリア。
SortDescriptions.Clear()SortDirection = null を並べるだけで済み、導入が最も速い。ただしコードビハインドから DataGrid を参照するため、MVVM の分離は緩みます。

ヘッダークリックだけで完結させるなら、Sorting イベントで三状態にする。
「昇順 → 降順 → 未ソート」を 1 つの操作系にまとめられます。列単位で解除するのか全解除なのかを先に決めておかないと、イベント処理が複雑になります。

MVVM を徹底し、コマンド経由で初期化するなら ICollectionView
ソート状態を ViewModel 側に置けるため、UI へ依存せずテストできます。View と ViewModel の責務分離が前提になります。

同じルールを複数の DataGrid へ適用するなら Behavior 化。
XAML に宣言を 1 行足すだけで横展開でき、重複コードを減らせます。画面ごとの差分を吸収する拡張ポイントを設計しておく必要があります。


方法別の比較

方法 メリット デメリット 適するケース
明示クリア(SortDescriptions.Clear + SortDirection = null 実装が単純で導入が速い。 DataGrid 参照が必要で MVVM 純度は下がる。 画面単位で即時に全解除したい場合。
Sorting イベントで三状態制御 UX を「昇順→降順→未ソート」に統一できる。 イベント処理が複雑化しやすく列単位要件の整理が必要。 ヘッダークリックだけで完結する操作性を重視する場合。
ICollectionView で ViewModel 管理 テストしやすく UI 依存を最小化できる。 View と ViewModel の責務分離設計が前提となる。 MVVM を徹底し、コマンド経由で初期化する場合。
Behavior 化 複数画面へ横展開しやすく重複コードを削減できる。 画面差分要件を吸収する拡張ポイント設計が必要。 同一ルールのソート挙動を複数 DataGrid へ適用する場合。

注意点


まとめ

WPF DataGrid のソート状態は ICollectionView.SortDescriptionsDataGridColumn.SortDirection の 2 か所に分かれており、コードから一方だけを触ると食い違います。初期化ではその両方を戻す必要があります。

分岐点は、初期化を起こす操作と、同じ挙動を何画面に広げるかにあります。
ボタンから即時に解除するなら明示クリア、ヘッダークリックで完結させるなら Sorting イベント、MVVM 徹底なら ICollectionView、複数画面へ広げるなら Behavior 化を選びます。

関連記事