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 パターンの概要と、向いているケースを整理します。
| パターン | 概要 | メリット | デメリット / 注意点 | 向いているケース |
|---|---|---|---|---|
| パターン1 | DataTable.DefaultView を ItemsSource にし、NewItemPlaceholderPosition を AtBeginning に設定 | コード量が少なく、シンプルに実装できる | 環境や設定によっては「許可されません」例外が出る | まず試したい簡易な対応、DataTable を変えたくない場合 |
| パターン2 | RowEditEnding で追加された行を一度抜いてから DataTable.Rows.InsertAt(…, 0) | NewItemPlaceholder に依存せず、確実に先頭へ移動できる | イベント処理の実装がやや増える | DataTable を使い続けたい、かつ確実に先頭にしたい場合 |
| パターン3 | ObservableCollection<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("新規行を先頭にできません: " & 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.AllowNew | true |
| DataGrid 側で追加禁止にしていないか | CanUserAddRows, IsReadOnly | CanUserAddRows="True", IsReadOnly="False" |
ここまで確認してもエラーになる場合は、パターン2かパターン3に切り替えるのが無難です。
パターン2:RowEditEnding で DataRow を先頭に差し替える
基本アイデア
パターン2では NewItemPlaceholderPosition 自体は変更せず、
- ユーザーが新規行に入力し終えたタイミング(
RowEditEnding)を捕まえる - 追加された行(
DataRowView)を DataTable から一度削除 DataTable.Rows.InsertAt(row, 0)で先頭に差し込む
という「力技」で位置を制御します。DataTable の API だけで完結するため、NewItemPlaceholderPosition をサポートしないビューでも動作するのが大きなメリットです。
XAML 例(RowEditEnding のハンドラを登録)
<DataGrid x:Name="myDataGrid"
AutoGenerateColumns="True"
CanUserAddRows="True"
RowEditEnding="myDataGrid_RowEditEnding" />
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(() =>
{
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 行分のデータを表すモデルクラス(例:
ExpenseItem)を定義 ObservableCollection<ExpenseItem>を ViewModel に持たせる- DataGrid の ItemsSource にコレクションをバインドする
NewItemPlaceholderPositionをAtBeginningに設定する
モデルクラスの例
public class ExpenseItem : INotifyPropertyChanged
{
private int _id;
private string _name;
private decimal _amount;
public int Id
{
get => _id;
set
{
if (_id != value)
{
_id = value;
OnPropertyChanged(nameof(Id));
}
}
}
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name));
}
}
}
public decimal Amount
{
get => _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<ExpenseItem> Items { get; }
= new ObservableCollection<ExpenseItem>();
public ExpenseViewModel()
{
Items.Add(new ExpenseItem { Id = 1, Name = "会議費", Amount = 1000m });
Items.Add(new ExpenseItem { Id = 2, Name = "交通費", Amount = 1500m });
}
}
<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">
<Window.DataContext>
<local:ExpenseViewModel />
</Window.DataContext>
<Grid>
<DataGrid x:Name="myDataGrid"
AutoGenerateColumns="False"
ItemsSource="{Binding Items}"
CanUserAddRows="True">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding Id}" />
<DataGridTextColumn Header="名称" Binding="{Binding Name}" />
<DataGridTextColumn Header="金額" Binding="{Binding Amount}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
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.ItemsCollectionViewSource.GetDefaultView(dataGrid.ItemsSource)
のどちらから取るかで悩むことがあります。NewItem 制御の観点では、どちらでも動くケースが多いですが、
- ItemsSource を差し替える可能性がある
- ビューを明示したい
といった場合には、CollectionViewSource.GetDefaultView(ItemsSource) を使う方が意図がはっきりして分かりやすくなります。
要件別:どのパターンを選ぶべきか
ここまでの内容を踏まえ、よくあるシナリオ別に「このパターンが無難」という選び方の目安をまとめます。
| シナリオ | おすすめのパターン | 理由 |
|---|---|---|
| 既存の DataTable ベースの画面を少しだけ改修したい | パターン2 | DataTable のまま、確実に先頭に移動できる。影響範囲が限定的。 |
| 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 をトリガーにスタイルを変えることもできます。
<DataGrid.Resources>
<Style TargetType="DataGridRow">
<Style.Triggers>
<Trigger Property="IsNewItem" Value="True">
<Setter Property="FontStyle" Value="Italic" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
これにより、NewItem 行だけ斜体で表示されるなど、ユーザーにとって「ここから新しい行を追加できます」と分かりやすい UI にできます。
まとめ
WPF の DataGrid で NewItem(新規行)を先頭に表示するには、
- パターン1:DataTable.DefaultView +
NewItemPlaceholderPosition(手軽だが、環境依存のエラーに注意) - パターン2:
RowEditEndingで DataRow を先頭に差し替える(DataTable でも安定して動く力技) - パターン3:
ObservableCollection<T>に移行して根本的に見直す(新規開発・大規模改修向き)
という 3 つの方向性があります。
既存の DataTable ベース画面を崩さずに実現したいならパターン2、今後も WPF を使い続けていくのであればパターン3を検討すると、後からの拡張や保守がかなり楽になります。
まずは自分のプロジェクトの制約(既存資産、工数、担当者のスキルセット)を踏まえて、「短期的に楽をするのか」「中長期で楽をするのか」を決め、そのうえで最適なパターンを選択してみてください。

コメント