WPF DataGridで新規行を先頭に表示する3つの方法【DataTableとObservableCollection対応】

WPF の DataGrid を使っていて、「新しい行を上に出したいのに、いつも一番下に出てしまう…」という経験はないでしょうか。特に DataTable を ItemsSource に使っていると、NewItem(新規行)を先頭に出す方法が分かりづらく、サンプルも少なめです。ここでは、DataTable を前提にしつつ、NewItem をグリッドの一番上へ表示するための実践的なパターンをまとめて解説します。

目次

WPF DataGrid の「新規行(NewItem)」とは

WPF の DataGrid では、既定で一番下に「空の行」が表示されます。これが NewItem(新規行、NewItemPlaceholder) です。ユーザーがここに値を入力すると、新しい行としてコレクションに追加されます。

この動きは内部的に IEditableCollectionView というインターフェースで制御されています。IEditableCollectionView.NewItemPlaceholderPosition を変更することで、

  • 先頭に新規行を表示する(AtBeginning)
  • 末尾に新規行を表示する(AtEnd)
  • 新規行プレースホルダーを表示しない(None)

といった制御が可能です。

しかし、ItemsSource に DataTable / DataView を使っている場合、この NewItemPlaceholderPosition が「このビューでは許可されません」と例外になるケースがあります。ここが今回の悩みどころです。

NewItem を先頭に出す 3 つの基本パターン

まず最初に、この記事で紹介する 3 パターンの概要と、向いているケースを整理します。

パターン概要メリットデメリット / 注意点向いているケース
パターン1DataTable.DefaultView を ItemsSource にし、NewItemPlaceholderPosition を AtBeginning に設定コード量が少なく、シンプルに実装できる環境や設定によっては「許可されません」例外が出るまず試したい簡易な対応、DataTable を変えたくない場合
パターン2RowEditEnding で追加された行を一度抜いてから DataTable.Rows.InsertAt(…, 0)NewItemPlaceholder に依存せず、確実に先頭へ移動できるイベント処理の実装がやや増えるDataTable を使い続けたい、かつ確実に先頭にしたい場合
パターン3ObservableCollection<T> のモデルクラスに切り替え、新規行制御もコレクション側で行う型安全・拡張性・テスト容易性に優れるDataTable 前提のロジックを作り直すコストが発生新規開発や中長期での保守性を重視したいプロジェクト

以降では、それぞれをもう少し掘り下げて解説します。

パターン1:DataTable.DefaultView + NewItemPlaceholderPosition

基本アイデア

DataGrid の ItemsSource を DataTable そのものではなく DataTable.DefaultView にします。そして、

  • CollectionViewSource.GetDefaultView(ItemsSource) でビューを取得
  • IEditableCollectionView にキャスト
  • NewItemPlaceholderPosition = AtBeginning を指定

とすることで、新規行プレースホルダーが DataGrid の先頭に表示されます。

C# の実装例


using System;
using System.ComponentModel;
using System.Data;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

public partial class MainWindow : Window
{
    private DataTable _table;

    public MainWindow()
    {
        InitializeComponent();
        InitializeDataTable();
        SetupDataGrid();
    }

    private void InitializeDataTable()
    {
        _table = new DataTable();
        _table.Columns.Add("Id", typeof(int));
        _table.Columns.Add("Name", typeof(string));

        _table.Rows.Add(1, "りんご");
        _table.Rows.Add(2, "みかん");
    }

    private void SetupDataGrid()
    {
        // DataTable ではなく DefaultView をバインドすることが重要
        myDataGrid.ItemsSource = _table.DefaultView;

        var view = CollectionViewSource.GetDefaultView(myDataGrid.ItemsSource)
                   as IEditableCollectionView;

        if (view != null)
        {
            try
            {
                view.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtBeginning;
            }
            catch (InvalidOperationException ex)
            {
                // 「このビューでは許可されません」などの例外が来る場合がある
                MessageBox.Show("新規行を先頭にできません: " + ex.Message);
            }
        }
    }
}

VB の実装例


Imports System.ComponentModel
Imports System.Data
Imports System.Windows
Imports System.Windows.Data

Class MainWindow

    Private _table As DataTable

    Public Sub New()
        InitializeComponent()
        InitializeDataTable()
        SetupDataGrid()
    End Sub

    Private Sub InitializeDataTable()
        _table = New DataTable()
        _table.Columns.Add("Id", GetType(Integer))
        _table.Columns.Add("Name", GetType(String))

        _table.Rows.Add(1, "りんご")
        _table.Rows.Add(2, "みかん")
    End Sub

    Private Sub SetupDataGrid()
        myDataGrid.ItemsSource = _table.DefaultView

        Dim view = TryCast(CollectionViewSource.GetDefaultView(myDataGrid.ItemsSource),
                           IEditableCollectionView)

        If view IsNot Nothing Then
            Try
                view.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtBeginning
            Catch ex As InvalidOperationException
                MessageBox.Show("新規行を先頭にできません: " &amp; ex.Message)
            End Try
        End If
    End Sub

End Class

このパターンでよくあるエラーと対処

よくあるのが、

  • 「このビューでは許可されません」
  • 「NewItemPlaceholderPosition はこのビューではサポートされません」

といった例外が発生するケースです。主な原因としては次のようなものがあります。

  • ItemsSource が DataTable 本体になっている
  • DataView.AllowNew が false にされている
  • DataGrid 側が追加不可(CanUserAddRows="False" または IsReadOnly="True")になっている
チェック項目確認すべきプロパティ期待値
ItemsSource が DataView かmyDataGrid.ItemsSource_table.DefaultView になっていること
新規追加が許可されているか_table.DefaultView.AllowNewtrue
DataGrid 側で追加禁止にしていないかCanUserAddRows, IsReadOnlyCanUserAddRows="True", IsReadOnly="False"

ここまで確認してもエラーになる場合は、パターン2かパターン3に切り替えるのが無難です。

パターン2:RowEditEnding で DataRow を先頭に差し替える

基本アイデア

パターン2では NewItemPlaceholderPosition 自体は変更せず、

  1. ユーザーが新規行に入力し終えたタイミング(RowEditEnding)を捕まえる
  2. 追加された行(DataRowView)を DataTable から一度削除
  3. DataTable.Rows.InsertAt(row, 0) で先頭に差し込む

という「力技」で位置を制御します。DataTable の API だけで完結するため、NewItemPlaceholderPosition をサポートしないビューでも動作するのが大きなメリットです。

XAML 例(RowEditEnding のハンドラを登録)


&lt;DataGrid x:Name="myDataGrid"
          AutoGenerateColumns="True"
          CanUserAddRows="True"
          RowEditEnding="myDataGrid_RowEditEnding" /&gt;

C# 実装例


using System;
using System.ComponentModel;
using System.Data;
using System.Windows.Controls;
using System.Windows.Data;

private DataTable _table;

private void SetupDataGrid()
{
    _table = CreateSampleTable();
    myDataGrid.ItemsSource = _table.DefaultView;
}

private DataTable CreateSampleTable()
{
    var dt = new DataTable();
    dt.Columns.Add("Id", typeof(int));
    dt.Columns.Add("Name", typeof(string));
    dt.Rows.Add(1, "りんご");
    dt.Rows.Add(2, "みかん");
    return dt;
}

private void myDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)
{
    var grid = (DataGrid)sender;

    // DataGrid の内部ビューを取得
    var view = grid.Items as IEditableCollectionView;
    if (view == null)
    {
        return;
    }

    // 「新規行」を編集中の時だけ処理したい
    if (!view.IsAddingNew)
    {
        return; // 既存行の編集は対象外
    }

    var newItem = view.CurrentAddItem as DataRowView;
    if (newItem == null)
    {
        return;
    }

    // RowEditEnding は Commit 前後で 2回呼ばれることがあるため、
    // Commit が終わったタイミングで処理するようにする
    if (e.EditAction == DataGridEditAction.Commit)
    {
        // 一度コミットさせてから DataRow を並べ替える
        grid.Dispatcher.BeginInvoke(new Action(() =&gt;
        {
            view.CommitNew();

            var dataView = (DataView)grid.ItemsSource;
            var table = dataView.Table;
            var row = newItem.Row;

            // 一度削除してから先頭に InsertAt
            table.Rows.Remove(row);
            table.Rows.InsertAt(row, 0);
        }));
    }
}

このパターンのポイント

  • RowEditEnding 内で即座に行を入れ替えると、DataGrid 側のコミット処理とぶつかることがあるので、Dispatcher.BeginInvoke で後回しにしている
  • 既存行の編集まで移動対象にすると混乱するため、view.IsAddingNew が true のときだけ処理している
  • DataTable のソートを別で行っている場合、ソート条件によっては再ソートされてしまう(後述)

VB 実装のポイント

VB でも考え方はまったく同じです。RowEditEnding のハンドラ内で DataRowView を取得して InsertAt するだけですので、C# コードをそのまま翻訳すれば動作します。


Private Sub myDataGrid_RowEditEnding(sender As Object,
                                     e As DataGridRowEditEndingEventArgs)

    Dim grid = DirectCast(sender, DataGrid)
    Dim view = TryCast(grid.Items, IEditableCollectionView)

    If view Is Nothing Then
        Return
    End If

    If Not view.IsAddingNew Then
        Return
    End If

    Dim newItem = TryCast(view.CurrentAddItem, DataRowView)
    If newItem Is Nothing Then
        Return
    End If

    If e.EditAction = DataGridEditAction.Commit Then
        grid.Dispatcher.BeginInvoke(New Action(
            Sub()
                view.CommitNew()

                Dim dataView = DirectCast(grid.ItemsSource, DataView)
                Dim table = dataView.Table
                Dim row = newItem.Row

                table.Rows.Remove(row)
                table.Rows.InsertAt(row, 0)
            End Sub))
    End If
End Sub

このパターンが特に向いているケース

  • 既存システムで DataSet / DataTable を強く使っている
  • データアクセス層が DataTable で固められていて、コレクション型に作り直すのが難しい
  • とにかく確実に「最後に追加された行を先頭に出したい」

NewItemPlaceholderPosition に依存しないため、「このビューでは許可されません」問題を確実に回避できるのが最大の利点です。

パターン3:ObservableCollection<T> に移行する

なぜ ObservableCollection に切り替えるのか

DataTable は「行列」で扱いやすい反面、

  • 型安全ではない(列名・型ミスがコンパイル時に検出されない)
  • ビューや NewItem 周りの制御がややトリッキー
  • テストコードを書きにくい

といった課題があります。新規開発や中長期の保守を考えると、ObservableCollection<T> + INotifyPropertyChanged のモデルクラスに移行した方が、結果的には楽になるケースがかなり多くあります。

基本的な構成

  1. 1 行分のデータを表すモデルクラス(例:ExpenseItem)を定義
  2. ObservableCollection<ExpenseItem> を ViewModel に持たせる
  3. DataGrid の ItemsSource にコレクションをバインドする
  4. NewItemPlaceholderPosition を AtBeginning に設定する

モデルクラスの例


public class ExpenseItem : INotifyPropertyChanged
{
    private int _id;
    private string _name;
    private decimal _amount;

    public int Id
    {
        get =&gt; _id;
        set
        {
            if (_id != value)
            {
                _id = value;
                OnPropertyChanged(nameof(Id));
            }
        }
    }

    public string Name
    {
        get =&gt; _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                OnPropertyChanged(nameof(Name));
            }
        }
    }

    public decimal Amount
    {
        get =&gt; _amount;
        set
        {
            if (_amount != value)
            {
                _amount = value;
                OnPropertyChanged(nameof(Amount));
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this,
            new PropertyChangedEventArgs(propertyName));
    }
}

ObservableCollection と DataGrid のバインディング


public class ExpenseViewModel
{
    public ObservableCollection&lt;ExpenseItem&gt; Items { get; }
        = new ObservableCollection&lt;ExpenseItem&gt;();

    public ExpenseViewModel()
    {
        Items.Add(new ExpenseItem { Id = 1, Name = "会議費", Amount = 1000m });
        Items.Add(new ExpenseItem { Id = 2, Name = "交通費", Amount = 1500m });
    }
}

&lt;Window x:Class="Sample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Sample"&gt;
    &lt;Window.DataContext&gt;
        &lt;local:ExpenseViewModel /&gt;
    &lt;/Window.DataContext&gt;

    &lt;Grid&gt;
        &lt;DataGrid x:Name="myDataGrid"
                  AutoGenerateColumns="False"
                  ItemsSource="{Binding Items}"
                  CanUserAddRows="True"&gt;
            &lt;DataGrid.Columns&gt;
                &lt;DataGridTextColumn Header="ID" Binding="{Binding Id}" /&gt;
                &lt;DataGridTextColumn Header="名称" Binding="{Binding Name}" /&gt;
                &lt;DataGridTextColumn Header="金額" Binding="{Binding Amount}" /&gt;
            &lt;/DataGrid.Columns&gt;
        &lt;/DataGrid&gt;
    &lt;/Grid&gt;
&lt;/Window&gt;

NewItemPlaceholderPosition を設定する


public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        var view = CollectionViewSource.GetDefaultView(myDataGrid.ItemsSource)
                   as IEditableCollectionView;

        if (view != null)
        {
            view.NewItemPlaceholderPosition = NewItemPlaceholderPosition.AtBeginning;
        }
    }
}

多くの環境では、ObservableCollection<T> を ItemsSource にした場合、この書き方で素直に NewItem が先頭に表示されます。

DataTable と併用したい場合

既に DataTable を使ったデータアクセス層があり、

  • DB ⇔ DataTable ⇔ ObservableCollection

のように変換レイヤーを挟むのも現実的な選択肢です。

層役割技術
データアクセス層DB とのやりとりDataAdapter / DataSet / Dapper など
変換層DataRow ⇔ モデルクラス拡張メソッド / Mapper クラス
表示層画面表示・バリデーション・編集ObservableCollection<T> + WPF DataGrid

最初は少し手間に感じますが、後々の「列追加」「仕様変更」「テストコード」などのコストを考えると、十分元が取れる構成になることが多いです。

よくあるハマりどころと対策

「許可されません」例外が出る

パターン1・3 で NewItemPlaceholderPosition を設定したときに、

  • InvalidOperationException
  • NotSupportedException

などが発生するケースがあります。チェックすべきポイントをもう一度整理します。

  • ビューの型:CollectionViewSource.GetDefaultView で取得しているか
  • ItemsSource:DataTable ではなく DataView になっているか
  • 新規追加の許可:AllowNew / CanUserAddRows / IsReadOnly

どうしても解決しない場合は、パターン2に切り替えるのが手っ取り早く、安定性も高くなります。

ソートと NewItem の位置の関係

DataGrid にソートを設定している場合(SortDescriptions を追加したり、列ヘッダーをクリックして並べ替えたりした場合)、

  • NewItem プレースホルダー自体は先頭に出る
  • しかし、入力を確定すると行はソート順に従って別の位置に移動する

という挙動になります。「常に一番上に新しく追加された行を置きたい」という要件の場合は、次のいずれかの工夫が必要です。

  • ソートキーとして「作成日時」や「登録番号」を用意し、それを逆順ソートにする
  • パターン2のように、確定後に InsertAt(0) で強制的に先頭に戻す

DataGrid.Items と ItemsSource の違い

IEditableCollectionView を取るときに、

  • dataGrid.Items
  • CollectionViewSource.GetDefaultView(dataGrid.ItemsSource)

のどちらから取るかで悩むことがあります。NewItem 制御の観点では、どちらでも動くケースが多いですが、

  • ItemsSource を差し替える可能性がある
  • ビューを明示したい

といった場合には、CollectionViewSource.GetDefaultView(ItemsSource) を使う方が意図がはっきりして分かりやすくなります。

要件別:どのパターンを選ぶべきか

ここまでの内容を踏まえ、よくあるシナリオ別に「このパターンが無難」という選び方の目安をまとめます。

シナリオおすすめのパターン理由
既存の DataTable ベースの画面を少しだけ改修したいパターン2DataTable のまま、確実に先頭に移動できる。影響範囲が限定的。
NewItem プレースホルダー(空行)自体を先頭に出したいパターン1 または パターン3ビューの機能として NewItem の位置を制御できる。
新規開発で WPF 画面を作るパターン3型安全でテストしやすい。将来的な拡張性・保守性が高い。
画面側で複雑なバリデーションや状態管理を行いたいパターン3モデルクラスにロジックをまとめやすく、MVVM パターンにも乗せやすい。

実運用で役立つ小技

新規行に初期値をセットする

NewItem が先頭に出せるようになったら、次に欲しくなるのが「新規行に自動で初期値を入れたい」というニーズです。これは、

  • パターン2:DataTable 側で Column.DefaultValue を使う
  • パターン3:モデルクラスのコンストラクタで初期値を入れる

といった方法で実現できます。


// DataTable の列に既定値を設定
_table.Columns["CreatedAt"].DefaultValue = DateTime.Now;

NewItem の行だけ見た目を変えたい

「新規行であることを強調したい」という場合は、DataGrid の RowStyle や CellStyle を使って、IsNewItem をトリガーにスタイルを変えることもできます。


&lt;DataGrid.Resources&gt;
    &lt;Style TargetType="DataGridRow"&gt;
        &lt;Style.Triggers&gt;
            &lt;Trigger Property="IsNewItem" Value="True"&gt;
                &lt;Setter Property="FontStyle" Value="Italic" /&gt;
            &lt;/Trigger&gt;
        &lt;/Style.Triggers&gt;
    &lt;/Style&gt;
&lt;/DataGrid.Resources&gt;

これにより、NewItem 行だけ斜体で表示されるなど、ユーザーにとって「ここから新しい行を追加できます」と分かりやすい UI にできます。

まとめ

WPF の DataGrid で NewItem(新規行)を先頭に表示するには、

  • パターン1:DataTable.DefaultView + NewItemPlaceholderPosition(手軽だが、環境依存のエラーに注意)
  • パターン2:RowEditEnding で DataRow を先頭に差し替える(DataTable でも安定して動く力技)
  • パターン3:ObservableCollection<T> に移行して根本的に見直す(新規開発・大規模改修向き)

という 3 つの方向性があります。

既存の DataTable ベース画面を崩さずに実現したいならパターン2、今後も WPF を使い続けていくのであればパターン3を検討すると、後からの拡張や保守がかなり楽になります。

まずは自分のプロジェクトの制約(既存資産、工数、担当者のスキルセット)を踏まえて、「短期的に楽をするのか」「中長期で楽をするのか」を決め、そのうえで最適なパターンを選択してみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次