none
DataGridView でデータ検証をやってみる (1) RRS feed

  • 全般的な情報交換

  • DataGridView でのデータ検証を試みますので、ご忠告お願いします。

     

    下記テーブルを用意しました。

     

    コード ブロック

    users
    ------------------------------------------------
    id          INT             NOT NULL PRIMARYKEY
    name        VARCHAR(50)     NOT NULL              # 表示用ユーザー名
    fullname    VARCHAR(50)     NULL                  # フルネーム
    age         TINYINT         NOT NULL              # 年齢
    registered  SMALLDATETIME   NOT NULL              # 登録年月日
    ------------------------------------------------

     

    取り敢えずイベントは以下を想定。
    それ以外にも考えるべきイベントがあったらご指摘ください。

     

    コード ブロック

    (e.1) データのロード時
    (e.2) データの入力時
    (e.3) データ入力の終了時

     

    各フィールドの入力制限事項は以下の通り。

     

    コード ブロック

    name
      (r.1.1) 半角アルファベットのみ
      (r.1.2) 6 文字以上 12 文字以内

     

    fullname
      (r.2.1) 半角カタカナ不可
      (r.2.2) 半角英字、または全角ひらがな・かたかな、または漢字
      (r.2.3) 1 文字以上 20 文字以下
     
    age
      (r.3.1) 半角数字のみ
      (r.3.2) 0 以上 100 以下
      (r.3.3) 自然数

     

    registered
      (r.4.1) yyyy/mm/dd 形式

     

    (r.4.1) の年月日は DateTimePicker で入力させるので、
    検証は必要ないのかな、と思っています。

     

    2007年12月20日 14:55

すべての返信

  • え、百一歳とかの人は登録できないのですか?(仕様につっこみかよ。ww

     

    2007年12月20日 15:21
  • はい、残念ながら。
    2007年12月20日 15:36
  • 参考
    - データの検証
    - データの妥当性検査の概要
    - 方法 : Windows フォーム DataGridView コントロールのデータを検証する
    - 方法 : データセットの機能を拡張する
    - ErrorProvider コンポーネント (Windows フォーム)
    - チュートリアル : Windows フォーム DataGridView コントロールのデータの妥当性検査
    - チュートリアル : Windows フォーム DataGridView コントロールでのデータ入力中に発生したエラーの処理
    - ErrorProvider コンポーネントの概要 (Windows フォーム)
    - 方法 : Windows フォーム ErrorProvider コンポーネントで DataSet 内にエラーを表示する
    - 方法 : 列の変更時にデータを検証する
    - 方法 : 行の変更時にデータを検証する
    - チュートリアル : データセットへの検証の追加
    - DataTable イベントの使用
    - 方法 : Windows フォーム DataGridView コントロールのデータの書式を設定する
    - 方法 : Windows フォーム DataGridView コントロールのデータの書式設定をカスタマイズする
    - Walkthrough: Validating Data in the Windows Forms DataGridView Control
    - How to: Customize Cells and Columns in the Windows Forms DataGridView Control by Extending Their Behavior and Appearance
    - How to: Customize Data Formatting in the Windows Forms DataGridView Control
    - How to: Validate Input with the Windows Forms DataGrid Control
    - 例外のトラブルシューティング : System.Data.NoNullAllowedException

    - DataGridViewコントロールを操作する101の方法

    2007年12月21日 5:39
  • まず何も入力されていない時に出る例外を見てみる。

    null が許容されている fullname に適当な文字(列)を入れて、行を離れると、
    下記例外が出る。これは name, age, registered でそれぞれ出る。

    - NoNullAllowedException (画像)

    先々、一々面倒なので、これを何とかする。

     

    Code Snippet

    this.usersDataGridView.DataError
      += new DataGridViewDataErrorEventHandler(usersDataGridView_DataError);

     

    Code Snippet

    private void usersDataGridView_DataError(
      object sender,
      DataGridViewDataErrorEventArgs e)
    {
      if (e.Exception != null && e.Context == DataGridViewDataErrorContexts.Commit)
      {
        MessageBox.Show(e.Exception.Message);
      }
    }

     

    - Walkthrough: Handling Errors that Occur During Data Entry in the Windows Forms DataGridView Control

     

    例をそのまま使った。

    2007年12月21日 6:23
  • さて、細かいことは後にして、
    string, integer, datetime であることをチェックできるようにする。

     

    どのタイミングで?

     

    データは未だないのだから (e.1) は後回し。

     

    まず (e.2) データ入力時を見る。

     

    TextBox などは ImeMode をもつが、DataGridView ではどう使うのだろう?

     

    Code Snippet

    +-----------+---------------+-------------------+
    |           | IME 制御      | 入力検証          |
    +-----------+---------------+-------------------+
    | string    | NoControl     | string だぞ       |
    |           |               | 数字じゃないぞ    |
    +-----------+---------------+-------------------+
    | integer   | Disable       | 数字だぞ          |
    +-----------+---------------+-------------------+

     

    使えそうなイベント

     

    - DataGridView.CellEnter
      現在のセルが変更されたとき、またはこのコントロールが入力フォーカスを受け取ったとき

     

    - DataGridView.CellClick
      セルの一部がクリックされた場合

     

    - DataGridView.CellContentClick
      セル内の内容がクリックされた場合

     

    - DataGridView.EditingControlShowing
      セルを編集するためのコントロールが表示されている場合

     

    - Control.Enter
      コントロールが入力されると

     

    - Control.KeyDown
      コントロールにフォーカスがあるときにキーが押されると

    「セルを編集するためのコントロールが表示されている場合」が最も無難そう。
    何故なら、入力する為のお膳立てが済んだ状態だと思うから。

     

    # それにしても、このフォーラムのシステムは遅い上に、使いづらいなぁ。画面も乱れる。

    2007年12月21日 7:01
  • # 急に表示が速くなったので復活。何故?

     

    取り敢えず age に注目して書いてみました。

     

    usersDataGridView_EditingControlShowing

     

      編集コントロールが現れた時の挙動を ColumnIndex で振り分けてみました。
      age の列にキーが押された時のイベントハンドラーを設けました。
      その他は取り敢えず無視。

     

    age_KeyPress

     

      押されたキーの文字が数字であるか、コントロールキーであるかを見ます。
      数字やコントロールの場合は処理が続き、
      それ以外の場合は処理が中断されるようにしました。

     

    usersDataGridView_CellValidating

     

      セルから移動しようとした時の動作を ColumnIndex で振り分けました。
      セルの値が "" (Empty) もしくは Int32 じゃない時、半角数字入れろ、とメッセージを出します。
      e.Cancel = true で抜けられないようにします。

     

      追加したイベントハンドラーを削除します。
      これは果たして必要なのかどうか、分かりません。
      でもやらないと、セルへの入力の度にハンドラーの追加が行われるので、
      ハンドラーが重複しないのかな、と疑問になっています。
      内部構造が分からないので不明です。

     

    usersDataGridView_CellEndEdit

     

      セルから抜けられたら ErrorText のメッセージを無効にします。


    と、まぁ凄く初歩的なことをやっていますが、初心者なので、
    熟練者の意見を聞きたいと希望しています。

     

    セルの値が空白かどうかの判定や、
    セルの値が Integer かどうかの判定など、
    私が書いたのが適当かどうかよく分かっていません。
    一般的にはこうするよ、というような意見を聞けたら幸いです。

     

    まだ age への入力しか見ていませんので、その点へのご意見をお願いします。
    それ以外は後日追加予定です。

     

    コード ブロック

    private void usersDataGridView_EditingControlShowing(
      object sender,
      DataGridViewEditingControlShowingEventArgs e)
    {
      if (e.Control != null)
      {
        var grid = (DataGridView)sender;

     

        switch (grid.CurrentCell.ColumnIndex)
        {
          case 1: // name
            break;

     

          case 2: // fullname
            break;

     

          case 3: // age
            grid.KeyPress += new KeyPressEventHandler(age_KeyPress);
            break;

     

          case 4: // registered
            break;

     

          default:
            break;
        }
      }
    }

     

    private void age_KeyPress(object sender, KeyPressEventArgs e)
    {
      if (!(Char.IsDigit(e.KeyChar) || Char.IsControl(e.KeyChar)))
      {
        e.Handled = true;
        return;
      }
    }

     

    private void usersDataGridView_CellValidating(
      object sender,
      DataGridViewCellValidatingEventArgs e)
    {
      var grid = (DataGridView)sender;

     

      switch (e.ColumnIndex)
      {
        case 1: // name
          break;

     

        case 2: // fullname
          break;

     

        case 3: // age

          grid.KeyPress -= new KeyPressEventHandler(tb_age_KeyPress);

     

          if (!String.IsNullOrEmpty(e.FormattedValue.ToString()))
          {
            int intVal;

     

            if (!Int32.TryParse(e.FormattedValue.ToString(), out intVal))
            {
              grid.Rows[e.RowIndex].ErrorText = "半角数字を入力して";
              e.Cancel = true;
            }
            else
            {
              intVal = Int32.Parse(e.FormattedValue.ToString());

     

              if (intVal < 0 || 100 < intVal)
              {
                grid.Rows[e.RowIndex].ErrorText = "0 以上 100 以下の自然数を入力して";
                e.Cancel = true;
              }
            }
          }
          break;


     

        case 4: // datetime
          break;

     

        default:
          break;
      }
    }

     

    private void usersDataGridView_CellEndEdit(

      object sender,

      DataGridViewCellEventArgs e)
    {
      ((DataGridView)sender).Rows[e.RowIndex].ErrorText = String.Empty;
    }

     

    2007年12月21日 15:13
  • ImeMode による入力制限を行おうと考えた場合、


    コード ブロック

    usersDataGridView_EditingControlShowing()
    {
      ....
      case 3: // age
        grid.ImeMode = ImeMode.Disable;
        grid.KeyPress += new KeyPressEventHandler(age_KeyPress);
        break;
      ....
    }


    usersDataGridView_CellValidating()
    {
      ....
      case 3: // age
        grid.ImeMode = ImeMode.On;
        int intVal;

     

        if (e.FormattedValue.ToString() == String.Empty
      ....
    }

     

    とすればよさそうだけど、EditingControlShowing でフォーカスが当たってい
    るのは、セルの編集コントロール DataGridViewTextBoxEditingControl なので、
    これの ImeMode を On/Off すべきだと思います。


    とすると、CellValidating で渡される引数 DataGridViewCellValidatingEventArgs e
    には、もう前述の編集コントロールは含まれていません。


    ですので、ImeMode = Disable とした編集コントロールを CellValidating で
    元に戻すのは、簡単ではないと思いました。


    DataGridView での ImeMode による入力制限としは、どのようにするのが堅牢
    なやり方なのでしょう?

     

    編集コントロールが消える直前に発せられるイベントがないかなぁ?

    例えば EditingControlHiding のような。

     

     

    IME 制御のために、DataGridView の EditingControl を操作したいので以下のようにした。


    EditingControl の ImeMode の操作を行えるよう、tb_age を作った。


    EditingControl から離れようとした瞬間を捉えるため、Leave イベントを使った。

     

    コード ブロック

    private void usersDataGridView_EditingControlShowing(
      object sender,
      DataGridViewEditingControlShowingEventArgs e)
    {
      ....
          case "age":
            var tb_age = (DataGridViewTextBoxEditingControl)e.Control;
            tb_age.ImeMode = ImeMode.Disable;
            tb_age.Leave += new EventHandler(tb_age_Leave);

            grid.KeyPress += new KeyPressEventHandler(tb_age_KeyPress);
            break;
      ....
    }

     

    private void tb_age_Leave(object sender, EventArgs e)
    {
      var tb = (DataGridViewTextBoxEditingControl)sender;
      tb.ImeMode = ImeMode.NoControl;
      tb.Leave -= new EventHandler(tb_age_Leave);
    }

     

    疑問なのは、イベントハンドラ tb_age_Leave() の扱い。
    tb_age_Leave() 内で tb_age_Leave() をデリゲートから削除してもいいのだろうか?

     

    EditingControlShowing の度に tb_age_Leave() が追加されたらたまらない、
    と考え、削除したつもりです。
    いいのかなぁ?

    2007年12月22日 7:39
  • switch() を ColumnIndex でやると列が入れ替わった時にずれるので、
    Column.Name に変更した。

     

    コード ブロック
    Form1.Designer.cs
    //
    // dataGridViewTextBoxColumn4
    //
    this.dataGridViewTextBoxColumn4.DataPropertyName = "age";
    this.dataGridViewTextBoxColumn4.HeaderText = "age";
    this.dataGridViewTextBoxColumn4.Name = "age";

     

    コード ブロック

    Form1.cs


    private void usersDataGridView_EditingControlShowing(
      object sender,
      DataGridViewEditingControlShowingEventArgs e)
    {
      ....
      switch (grid.Columns[grid.CurrentCell.ColumnIndex].Name)
      {
        case "name":
        case "fullname":
        case "age":
        case "registered":
      ....

     


    private void usersDataGridView_CellValidating(
      object sender,
      DataGridViewCellValidatingEventArgs e)
    {
      ....
      switch (grid.Columns[e.ColumnIndex].Name)
      {
        case "name":
        case "fullname":
        case "age":
        case "registered":
      ....

     

    2007年12月22日 8:19
  • - String.IsNullOrEmpty メソッド

     

      指定された String オブジェクトが null 参照または Empty 文字列であるか
      どうかを示します。String が null 参照であるかどうかと、値が Empty で
      あるかどうかを一度に判定できます。


    コード ブロック

    case "age":
      int intVal;

     

      if (!String.IsNullOrEmpty(e.FormattedValue.ToString())
         && !Int32.TryParse(e.FormattedValue.ToString(), out intVal))
      {
        grid.Rows[e.RowIndex].ErrorText = "半角数字を入力して";
        e.Cancel = true;
      }

     

    2007年12月22日 8:30
  • DataGridViewTextBoxEditingControl での処理に変更した。


    コード ブロック

    private void usersDataGridView_EditingControlShowing(
      object sender,
      DataGridViewEditingControlShowingEventArgs e)
    {
      ....
          case "age":
            var tb_age       = (DataGridViewTextBoxEditingControl)e.Control;
            tb_age.ImeMode   = ImeMode.Disable;
            tb_age.Leave    += new EventHandler(tb_age_Leave);
            tb_age.KeyPress += new KeyPressEventHandler(tb_age_KeyPress);
            break;

     

    private void tb_age_Leave(object sender, EventArgs e)
    {
      var tb       = (DataGridViewTextBoxEditingControl)sender;
      tb.ImeMode   = ImeMode.NoControl;
      tb.Leave    -= new EventHandler(tb_age_Leave);
      tb.KeyPress -= new KeyPressEventHandler(tb_age_KeyPress);
    }


    private void tb_age_KeyPress(object sender, KeyPressEventArgs e)
    {
      if (!(Char.IsDigit(e.KeyChar) || Char.IsControl(e.KeyChar)))
      {
        e.Handled = true;
        return;
      }
    }

     

    続いて registered に CalendarColumn を適用し、

    フォーカスが当たった時に日時が表示されているようにしたい。

    今、選択時には何も表示されない。

    2007年12月22日 13:25
  • 現状でも負の値は入力できないが、念のためチェック。


    コード ブロック

    case "age":
      int intVal;
      ....

     

      intVal = Int32.Parse(e.FormattedValue.ToString());

     

      if (intVal < 0 || 100 < intVal)
      {
        grid.Rows[e.RowIndex].ErrorText = "0 以上 100 以下の自然数を入力して";
        e.Cancel = true;
      }

     

     

    2007年12月22日 14:15
  • 当初の usersDataGridView_DataError() ではメッセージを閉じた後、既入力部
    分まで消されてしまう。折角入力していたのに、それはない。

     

    メッセージを消したら、

    既入力セルはそのままにする。

     

     

    RowLeave イベントを利用。

    (1) で行の削除を止め、
    (2) で空セルがないか調べ、
    (3) で空セルの背景色を戻した。

     

    果たしてこれでいいのか分からない。
    # これでいいのでしょうか? better なやり方は?

     

    (2) の switch に書いた name, age, registered は直接制御してしまっている。
    半自動化したいな。DataTable に各フィールドの Rule みたいなものがあって、
    それを入力済みか否かの判断基準にできないだろうか。

     

    そんなのあるのかな?

     

    (1) の usersDataGridView_DataError() から null が駄目だって言うメッセー
    ジが勝手に来るくらいだから、DataTable を基準にした何かしら裏にある筈。

     

    コード ブロック

    private void usersDataGridView_DataError(
      object sender,
      DataGridViewDataErrorEventArgs e)
    {
      if (e.Exception != null && e.Context == DataGridViewDataErrorContexts.Commit)
      {
        MessageBox.Show(e.Exception.Message);
        e.Cancel = true; .................................. (1)
      }
    }

     

     

    private void usersDataGridView_RowLeave(
      object sender,
      System.Windows.Forms.DataGridViewCellEventArgs e)
    {
      var grid = (DataGridView)sender;

     

      foreach (DataGridViewCell cell in grid.Rows[e.RowIndex].Cells)
      {
        switch (grid.Columns[cell.ColumnIndex].Name) ...... (2)
        {
          case "name":
          case "age":
          case "registered":
            if (cell.FormattedValueType == Type.GetType("System.String"))
            {
              string value = (grid.IsCurrentCellDirty == true)
                ? cell.EditedFormattedValue.ToString()
                : cell.FormattedValue.ToString();

     

              if (String.IsNullOrEmpty(value))
              {
                cell.Style.BackColor = Color.Red;
              }
            }
            break;

     

          case "fullname":
            break;

     

          default:
            break;
        }
      }
    }

     

     

    private void usersDataGridView_CellEndEdit(
      object sender,
      DataGridViewCellEventArgs e)
    {
      var grid = (DataGridView)sender;
      grid.Rows[e.RowIndex].ErrorText = String.Empty;

     

      var cell = grid.Rows[e.RowIndex].Cells[e.ColumnIndex];

     

      if (cell.Style.BackColor == Color.Red)
      {
        cell.Style.BackColor = Color.Empty; ............... (3)
      }
    }

     

    2007年12月22日 14:28