Windows Forms アプリの GataGridView の新規追加行に未入力のままフォーカスを移動したり[データの保存]ボタンをクリックした時にスローされる NoNullAllowedException に対処する方法を書きます。
NoNullAllowedException というのは、DataColumn.AllowDBNull プロパティが false に設定されている列に null 値を挿入しようとした場合にスローされる例外です。Visual Studio のデータソース構成ウィザードを使って型付 DataSet / DataTable を作ると、元の DB の列が NULL 不可になっている場合に false に設定されます。
DataGridView 上で当該列を空白のままにしてフォーカスを移動したり[データの保存]ボタンをクリックしたりすると DataTable の当該 Column に null 値を挿入しようとするので NoNullAllowedException がスローされます。
その際、エラーメッセージを表示するダイアログが、上の画像のものとは違って、下の画像のような「DataGridView の既定のエラーダイアログ」であれば、DataGridView.DataError イベントを処理することで対応できます。
具体例は Microsoft のドキュメント「DataGridView.DataError イベント」や「ユーザーがDataGridViewのセルに正しくない値を入力した時に発生するエラーを捕捉する」を見てください。下に載せたサンプルコードにも実装してあります。
しかしながら、DataError イベントをハンドルしても NoNullAllowedException を補足できないケースがあるのが問題です。
その補足できないケースというのは、自分が調べた限りですが、(1) BindingNavigator 上の[新規追加]ボタンを続けて 2 回クリック、(2) 新規追加行に未入力のまま BindingNavigator 上の[最初に移動][前に戻る][データの保存]ボタンをクリックした場合です。(他にもあるかも)
その場合は、この記事の一番上の画像のような、例のおなじみのエラーメッセージのダイアログが表示されます。
これを何とかしようとすると try - catch 句を追加して NoNullAllowedException を補足するということになると思います。
(他にもっとスマートな手段があるかもしれませんが分かりませんでした。DataGridView.RowValidating イベントをハンドルして対応できないかと考えたのですが、上に書いた「補足できないケース」ではイベントが発生しません)
どこに try - catch 句を追加するかですが、データソース構成ウィザードを使って型付 DataSet / DataTable を作り、それをデザイン画面で「データソース」ウィンドウから Form にドラッグ&ドロップして作ったアプリのコードにはその場所がありません。(Program.cs の Application.Run(new Form1()); で try - catch するのはやりすぎと思うので対象外)
そこで考えたのが、BindingNavigator の AddNewItem, DeleteItem, MoveFirstItem, MoveLastItem, MoveNextItem, MoveProviousItem プロパティの設定を以下のように (なし) に変更し(デフォルトでは当該 ToolStripButton が設定されている)、
AddNewItem, DeleteItem, MoveFirstItem, MoveLastItem, MoveNextItem, MoveProviousItem に設定されていた各 ToolStripButton の Click イベントのハンドラを追加し、
その各ハンドラの中で BindingSource の AddNew, RemoveCurrent, MoveFirst, MoveLast, MoveNext, MoveProvious を実行するようにして try - catch 句を使うということです。
BindingNavigation の操作やマウスやキーボードを使っての BindingSource のポジションの変更に伴って BindingNavigator 上の ToolStripButton の Enabled プロパティの true/false の設定を変更する必要がありますが、それは BindingSource.PositionChanged イベントのハンドラで行います。
ただし、ここまでの設定だけでは[データの保存]ボタンをクリックした場合に対応できません。そこは、自動生成されたクリックイベントのハンドラの中の BindingSource.EndEdit(); というコードに try - catch を追加して NoNullAllowedException を補足して対処します。
以下にサンプルコードを書きます。例として、下の画像の SQL Server データベースを使います。
まず、上の Product テーブルのレコード一覧表示・編集を行う Windows Forms + DataGridView アプリを作ります。それは Microsoft のドキュメント「新しいデータ ソースの追加」のように「データソース構成ウィザード」を使って型付 DataSet / DataTable を作り、それをデザイン画面で「データソース」ウィンドウから Form にドラッグ&ドロップすれば、自力ではコードを一行も書くことなく簡単に作成できます。
その後、上に述べたように、自動生成されたコードに手を加えてたのが下のサンプルコードです。
using System;
using System.Data;
using System.Data.SqlClient;
using System.Windows.Forms;
namespace WindowsFormsPkUnique
{
public partial class Form3 : Form
{
public Form3()
{
InitializeComponent();
// DataGridView.DataError イベントのハンドラ設定
this.productDataGridView.DataError +=
ProductDataGridView_DataError;
// BindingSource.PositionChanged イベントのハンドラ設定
this.productBindingSource.PositionChanged +=
ProductBindingSource_PositionChanged;
}
private void Form3_Load(object sender, EventArgs e)
{
this.productTableAdapter.Fill(this.productDataSet.Product);
}
// DataGridView.DataError イベントを処理。これで補足できない
// ケースがあるのが問題
private void ProductDataGridView_DataError(object sender,
DataGridViewDataErrorEventArgs e)
{
if (e.Exception != null)
{
MessageBox.Show(this,
$"Index {e.RowIndex} の行でエラーが発生しました。\n\n" +
$"説明: {e.Exception.Message}",
"エラーが発生しました",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}
// BindingSource の Position の変更に伴って、BindingNavigator の
// 各ボタンの Enabled プロパティの true/false 設定を変更する
private void ProductBindingSource_PositionChanged(object sender,
EventArgs e)
{
int count = productBindingSource.Count;
int position = productBindingSource.Position;
if (count <= 1)
{
bindingNavigatorMoveFirstItem.Enabled = false;
bindingNavigatorMovePreviousItem.Enabled = false;
bindingNavigatorMoveNextItem.Enabled = false;
bindingNavigatorMoveLastItem.Enabled = false;
if (count == 0)
bindingNavigatorDeleteItem.Enabled = false;
else
bindingNavigatorDeleteItem.Enabled = true;
}
else
{
bindingNavigatorDeleteItem.Enabled = true;
if (position == 0)
{
bindingNavigatorMoveFirstItem.Enabled = false;
bindingNavigatorMovePreviousItem.Enabled = false;
bindingNavigatorMoveNextItem.Enabled = true;
bindingNavigatorMoveLastItem.Enabled = true;
}
else if (position == count - 1)
{
bindingNavigatorMoveFirstItem.Enabled = true;
bindingNavigatorMovePreviousItem.Enabled = true;
bindingNavigatorMoveNextItem.Enabled = false;
bindingNavigatorMoveLastItem.Enabled = false;
}
else
{
bindingNavigatorMoveFirstItem.Enabled = true;
bindingNavigatorMovePreviousItem.Enabled = true;
bindingNavigatorMoveNextItem.Enabled = true;
bindingNavigatorMoveLastItem.Enabled = true;
}
}
}
// BindingNavigator の[データの保存]ボタンクリックのハンドラ
private void productBindingNavigatorSaveItem_Click(object sender,
EventArgs e)
{
this.Validate();
try
{
// EndEdit を try に含めないと NoNullAllowedException が
// catch できない
this.productBindingSource.EndEdit();
this.tableAdapterManager.UpdateAll(this.productDataSet);
}
catch (NoNullAllowedException ex)
{
MessageBox.Show(this,
ex.Message + "\n\nデータを修正してください。",
"「データの保存」ボタン操作エラー",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
catch (SqlException sqlEx) // オマケの重複チェック
{
if (sqlEx.Number == 2601)
{
MessageBox.Show(this,
$"Unique 制約違反です。\n\n説明: {sqlEx.Message}",
"エラーが発生しました",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
throw;
}
}
}
// BindingNavigator の[最初に移動]ボタンクリックのハンドラ
private void bindingNavigatorMoveFirstItem_Click(object sender,
EventArgs e)
{
try
{
productBindingSource.MoveFirst();
}
catch (NoNullAllowedException ex)
{
MessageBox.Show(this,
ex.Message + "\n\nデータを修正してください。",
"「最初に移動」ボタン操作エラー",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}
// BindingNavigator の「前に戻る」ボタンクリックのハンドラ
private void bindingNavigatorMovePreviousItem_Click(object sender,
EventArgs e)
{
try
{
productBindingSource.MovePrevious();
}
catch (NoNullAllowedException ex)
{
MessageBox.Show(this,
ex.Message + "\n\nデータを修正してください。",
"「前に戻る」ボタン操作エラー",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}
// BindingNavigator の「次に移動」ボタンクリックのハンドラ
private void bindingNavigatorMoveNextItem_Click(object sender,
EventArgs e)
{
try
{
productBindingSource.MoveNext();
}
catch (NoNullAllowedException ex)
{
MessageBox.Show(this,
ex.Message + "\n\nデータを修正してください。",
"「次に移動」ボタン操作エラー",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}
// BindingNavigator の「最後に移動」ボタンクリックのハンドラ
private void bindingNavigatorMoveLastItem_Click(object sender,
EventArgs e)
{
try
{
productBindingSource.MoveLast();
}
catch (NoNullAllowedException ex)
{
MessageBox.Show(this,
ex.Message + "\n\nデータを修正してください。",
"「最後に移動」ボタン操作エラー",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}
// BindingNavigator の[新規追加]ボタンクリックのハンドラ
private void bindingNavigatorAddNewItem_Click(object sender,
EventArgs e)
{
try
{
productBindingSource.AddNew();
}
catch (NoNullAllowedException ex)
{
MessageBox.Show(this,
ex.Message + "\n\nデータを修正してください。",
"「新規追加」ボタン操作エラー",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
}
// BindingNavigator の[削除]ボタンクリックのハンドラ
private void bindingNavigatorDeleteItem_Click(object sender,
EventArgs e)
{
productBindingSource.RemoveCurrent();
}
}
}
上のコードには一つオマケで重複入力の検証を追加しています。上の SQL Server のデータベースの画像にあるように ProductName には Unique 制約がかかっていますが、それも検証してエラーメッセージを MessageBox を使って出すようにしています。
SQL Server ですから複数のユーザーが同時にアクセスするのが前提です。なので、重複しているか否かは実際に INSERT する時でないと分かりません。それは、INSERT 操作を行ったとき SqlException がスローされ、エラーの種類を表す番号が 2601 であることで判断できます。
その具体的な例は上のコードの productBindingNavigatorSaveItem_Click メソッドを見てください。
以上ですが、厳しく検証したわけではありませんので、上のコードはあくまで参考程度にお願いします。ひょっとしたら検証漏れがあるかもしれません。