※本ページには広告・PR が含まれます。

DataGridView で「インデックス -1 には値がありません」が出る5つの原因と対処

📋 目次(クリックで展開)

DataGridView を使っていると、次の例外に遭遇することがあります。

Terminal window
System.IndexOutOfRangeException: 'インデックス -1 には値がありません。'

英語環境では次のように表示されます。

Terminal window
System.IndexOutOfRangeException: 'Index -1 does not have a value.'

このメッセージが厄介なのは、原因が1つではないことです。データが空のとき、フィルタで全件除外されたとき、行を削除した直後、ComboBox 列の描画時、初期化中のイベント発火時——いずれも同じ文言が出ます。そのため「対処法」だけを検索して当てはめても、自分のケースに合わないことがよくあります。

この記事では、発生源の特定方法を最初に示したうえで、5つの系統それぞれの再現コードと対処を整理します。

まずスタックトレースの最上段を見る

対処に入る前に、どこから投げられた例外なのかを確定させてください。Visual Studio の例外ダイアログで「詳細の表示」を開き、スタックトレースの最上段付近を確認します。

スタックトレース最上段付近 該当する原因
System.Windows.Forms.CurrencyManager.get_Current 原因1・2・3
System.Windows.Forms.DataGridViewComboBoxCell 配下 原因4
自作のイベントハンドラ(SelectionChanged / CurrentCellChanged など) 原因5

大半のケースは CurrencyManager.get_Current です。つまり BindingSource.Current を参照した瞬間に落ちています。ここが分かるだけで、調査範囲は一気に狭まります。

この例外の正体

型は IndexOutOfRangeException

NullReferenceException ではありません。ここは切り分けの重要な分岐点です。

  • bindingSource1.Current を参照 → IndexOutOfRangeException(インデックス -1 には値がありません)
  • dataGridView1.CurrentRow.Cells[0] を参照 → NullReferenceException

同じ「現在行が無い」状況でも、どちらのAPIを経由したかで例外の種類が変わります。CurrentRow は行が無ければ null を返すだけで、例外は投げません。

発生源は CurrencyManager.get_Current

BindingSource は内部で CurrencyManager を保持しており、Current プロパティは実質的に次の処理をしています。

// CurrencyManager.Current の内部イメージ
public object Current
{
get
{
// Position が -1 のとき、list[-1] にアクセスして例外になる
return this.list[this.listposition];
}
}

listposition(=BindingSource.Position)が -1 のまま Current に触ると、list[-1] が評価されて IndexOutOfRangeException が発生します。メッセージの「インデックス -1」はこの -1 そのものです。

なぜ -1 なのか

Position の仕様はシンプルです。

リストの状態 Position の値
要素が1件以上ある 0 以上
要素が0件 -1
バインド前 -1

つまり Count == 0 なら Position は必ず -1 です。以降で挙げる5つの原因は、すべて「なぜリストが空になったか」「なぜ位置を見失ったか」のバリエーションにすぎません。この一点を押さえておくと、初見のパターンでも自力で切り分けられるようになります。

原因1:BindingSource / DataSource が空

最も単純なケースです。データ0件の状態で現在項目を参照しています。

再現コード

private void MainForm_Load(object sender, EventArgs e)
{
// 検索結果が0件だった、初回起動でデータが無い、などを想定
var products = new List<Product>();
bindingSource1.DataSource = products;
dataGridView1.DataSource = bindingSource1;
// Count == 0 なので Position は -1
var current = (Product)bindingSource1.Current; // ここで例外
label1.Text = current.Name;
}

事象の原因

空のリストをバインドしても BindingSource 自体は正常に動作し、例外は出ません。落ちるのは Current に触れた瞬間です。開発中はテストデータが常に1件以上あるため気付かれず、本番で検索結果が0件になったときに初めて表面化するという典型的な潜伏バグになります。

対処コード

if (bindingSource1.Count == 0 || bindingSource1.Position < 0)
{
label1.Text = string.Empty;
return;
}
var current = (Product)bindingSource1.Current;
label1.Text = current.Name;

CountPosition の両方を見るのが確実です。Count > 0 でも、直後にフィルタや並べ替えが走ると Position-1 に戻る場合があります。

関連記事

原因2:フィルタで全件除外された

BindingSource.Filter の設定は成功しているのに、条件に合致する行が0件になったパターンです。

再現コード

private void SearchButton_Click(object sender, EventArgs e)
{
// 該当0件になる条件を指定
bindingSource1.Filter = $"Name LIKE '%{searchTextBox.Text}%'";
// フィルタ後の Count は 0、Position は -1
var row = (DataRowView)bindingSource1.Current; // ここで例外
}

事象の原因

フィルタ適用は CurrencyManager に対してリストの再構築を通知します。このとき絞り込み結果が0件なら Position-1 にリセットされます。フィルタ設定行そのものでは落ちず、次に現在項目へアクセスした行で落ちるため、原因箇所がずれて見えるのがこのパターンの分かりにくさです。

なお FilterDataView / DataTable バインド時に有効です。List<T> バインドでは Filter を設定しても無視されるため、そちらの構成では発生しません。

対処コード

bindingSource1.Filter = $"Name LIKE '%{searchTextBox.Text}%'";
if (bindingSource1.Count == 0)
{
statusLabel.Text = "該当するデータがありません。";
detailPanel.Enabled = false;
return;
}
// 明示的に先頭へ位置付けてから参照する
bindingSource1.Position = 0;
var row = (DataRowView)bindingSource1.Current;

フィルタ適用後は「0件の可能性がある」ことを前提に、UI 側の表示もセットで切り替えるのが実務的です。

並べ替えでも同じことが起きる

同じ現象は、フィルタだけでなく並べ替えの直後にも発生します。ApplySortCore が発行する ListChangedType.Reset によってリストが再構築され、Position が一時的に -1 に戻るためです。

SortableBindingList<T> のような自作の並べ替え実装を導入した途端にこの例外が顕在化するケースは珍しくありません。実装と対処は次の記事にまとめています。

関連記事

原因3:行削除直後に現在行を参照した

削除イベントのハンドラ内で現在項目を参照するパターンです。最後の1行を削除したときだけ落ちるため、再現条件が限定的で見落とされがちです。

再現コード

private void DataGridView1_UserDeletedRow(object sender, DataGridViewRowEventArgs e)
{
// 最後の1行を削除すると Count == 0、Position == -1
var current = (Product)bindingSource1.Current; // ここで例外
UpdateDetailPanel(current);
}

事象の原因

UserDeletedRow は削除が確定した後に発火します。この時点でリストからは既に該当要素が除かれており、残り0件なら Position-1 です。複数行あるうちの1行を削除した場合は次の行へ位置が移るため例外は出ません。 「最後の1行を消したときだけ落ちる」 という症状であれば、ほぼこのパターンです。

前述のとおり、CurrentRow 経由でアクセスしている場合は NullReferenceException になります。どちらの例外が出ているかで、コード上のどのAPIを疑うべきかが分かります。

対処コード

private void DataGridView1_UserDeletedRow(object sender, DataGridViewRowEventArgs e)
{
if (bindingSource1.Count == 0 || bindingSource1.Position < 0)
{
ClearDetailPanel();
return;
}
var current = (Product)bindingSource1.Current;
UpdateDetailPanel(current);
}

なお、新規入力行(一番下の空行)が絡む場合は、そもそも削除対象として扱ってよいかの判定も必要になります。

関連記事

原因4:ComboBox 列の候補リストが空/未設定

DataGridViewComboBoxColumn は、セル自身が内部で CurrencyManager を持ちます。そのため候補リスト側が空のまま描画されると、グリッド本体のデータが正常でも同じ例外が発生します。

再現コード

private void MainForm_Load(object sender, EventArgs e)
{
var column = new DataGridViewComboBoxColumn
{
DataPropertyName = nameof(Product.CategoryId),
DisplayMember = nameof(Category.Name),
ValueMember = nameof(Category.Id),
// マスタ取得に失敗して空のまま、というケース
DataSource = new List<Category>(),
};
dataGridView1.Columns.Add(column);
dataGridView1.DataSource = productBindingSource; // 描画時に例外
}

事象の原因

セルの表示値は、候補リストから ValueMember に一致する項目を検索して決定されます。候補リストが空の場合、この検索が位置 -1 のまま評価され、描画のタイミングで例外に至ります。フォームを開いた瞬間に落ちる特定の列をスクロールで画面内に入れた瞬間に落ちるといった症状が特徴です。

似た状況で発生する別のエラーとして「DataGridViewComboBoxCell の値が有効ではありません」がありますが、こちらは候補リストは存在するもののセルの値が候補に含まれていない場合のメッセージです。切り分けの目安は次のとおりです。

メッセージ 候補リストの状態
インデックス -1 には値がありません 空、または未設定
値が有効ではありません 存在するが、値が候補外

対処コード

順序として、候補リストを先に確定させてから本体をバインドするのが基本です。

var categories = categoryRepository.GetAll();
if (categories.Count == 0)
{
// 候補が無いなら ComboBox 列にしない、という判断も選択肢
MessageBox.Show("カテゴリマスタが登録されていません。");
return;
}
column.DataSource = categories;
dataGridView1.Columns.Add(column);
dataGridView1.DataSource = productBindingSource;

そのうえで、想定外の値が紛れ込んだ場合の保険として DataError を用意します。

private void DataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
{
if (dataGridView1.Columns[e.ColumnIndex] is DataGridViewComboBoxColumn)
{
// 候補外の値による描画エラーを無視する
e.Cancel = true;
return;
}
e.ThrowException = false;
}

ただし DataError はあくまで応急処置です。これを入れると例外ダイアログは消えますが、値の不整合そのものは残り続けます。恒久的にはマスタとトランザクションの整合を取る、または外部キー制約で不整合を発生させない設計にしてください。エラーを握りつぶしたまま運用に乗せると、後で原因不明の表示崩れとして再燃します。

関連記事

原因5:バインド完了前にイベントが発火した

コードとしては正しく見えるのに、イベントハンドラの登録タイミングが早すぎるために起きるパターンです。

再現コード

public MainForm()
{
InitializeComponent();
// ここで SelectionChanged が登録済み(デザイナ登録も同様)
// DataSource を代入した瞬間に SelectionChanged が発火する
dataGridView1.DataSource = bindingSource1;
}
private void DataGridView1_SelectionChanged(object sender, EventArgs e)
{
// バインド処理の途中で呼ばれ、Position はまだ -1
var current = (Product)bindingSource1.Current; // ここで例外
UpdateDetailPanel(current);
}

事象の原因

DataSource への代入は、列生成・行生成・選択状態の初期化といった複数の処理を内部で連続実行します。この過程で SelectionChangedCurrentCellChanged中間状態のまま発火します。バインドが完了していない一瞬のあいだ Position-1 のままなので、そこで Current に触れると落ちます。

デザイナでイベントを登録していると、コード上はハンドラ登録の行が見えないため、原因の特定が特に難しくなります。

対処コード

対処は3通りあり、状況に応じて選びます。

A. バインド後にハンドラを登録する

dataGridView1.DataSource = bindingSource1;
dataGridView1.SelectionChanged += DataGridView1_SelectionChanged;

最も素直な方法ですが、デザイナ登録を外す必要があり、後任者が見落としやすい難点があります。

B. 初期化中フラグで抑制する

private bool _isInitializing;
private void LoadData()
{
_isInitializing = true;
try
{
bindingSource1.DataSource = productRepository.GetAll();
dataGridView1.DataSource = bindingSource1;
}
finally
{
_isInitializing = false;
}
}
private void DataGridView1_SelectionChanged(object sender, EventArgs e)
{
if (_isInitializing)
{
return;
}
// 通常処理
}

C. ハンドラ側を防御的に書く

次章の共通アクセサを使う方法です。フラグ管理が不要になるため、ハンドラ数が多いフォームではこれが最も保守しやすくなります。

恒久対応:防御的アクセサに集約する

ここまでの5系統は、いずれも「Position-1 の状態で Current に触った」という一点に収束します。裏を返せば、Current を直接使わないというルールを徹底すれば全系統をまとめて塞げます。

拡張メソッドとして1箇所に集約します。

using System.Windows.Forms;
public static class BindingSourceExtensions
{
/// <summary>
/// 現在項目を安全に取得します。取得できない場合は null を返します。
/// </summary>
public static T CurrentOrDefault<T>(this BindingSource source) where T : class
{
if (source == null)
{
return null;
}
if (source.Count == 0 || source.Position < 0)
{
return null;
}
return source.Current as T;
}
/// <summary>
/// 現在項目を取得できたかどうかを返します。
/// </summary>
public static bool TryGetCurrent<T>(this BindingSource source, out T current)
where T : class
{
current = source.CurrentOrDefault<T>();
return current != null;
}
}

呼び出し側は次のようになります。

private void DataGridView1_SelectionChanged(object sender, EventArgs e)
{
if (!bindingSource1.TryGetCurrent<Product>(out var product))
{
ClearDetailPanel();
return;
}
UpdateDetailPanel(product);
}

このアプローチの要点は、try-catch で例外を握りつぶしていない点です。「現在項目は存在しないことがある」という仕様上の事実を戻り値の型で表現しているだけなので、正常系と異常系の区別が呼び出し側に強制されます。例外を捕捉する方式と違って、想定外の別バグまで隠してしまう心配もありません。

チーム開発であれば、コードレビューの観点に「BindingSource.Current の直接参照を禁止」を加えておくと、同種の不具合の再発をほぼ防げます。

切り分け早見表

# 症状が出るタイミング 確認する値 対処
1 起動直後・検索結果0件 Count 空判定でガード
2 フィルタ設定・並べ替えの直後 Filter / Count / Position 適用後に件数確認、Position を再設定
3 最後の1行を削除した直後 Count / Position 削除イベント内でガード
4 フォームを開いた瞬間・スクロール時 候補リストの件数 候補を先にバインド、DataError は保険
5 初期化処理の途中 ハンドラの登録順序 バインド後に登録、またはフラグ制御

まとめ

  • 「インデックス -1 には値がありません」は BindingSource.Position-1 の状態で Current に触ったときに発生する
  • メッセージは同一でも、原因は空・フィルタ・削除・ComboBox 列・初期化順序の5系統に分かれる
  • 最初にやるべきはスタックトレース最上段の確認。CurrencyManager.get_Current なら原因1〜3に絞り込める
  • 個別対処を積み重ねるより、CurrentOrDefault のような防御的アクセサに集約するほうが再発しにくい
  • DataError による抑制は応急処置であり、データ整合の根治とは分けて考える

💬 コメント