none
EndEdit でエラーが発生するとキャンセルされてしまう RRS feed

  • 質問

  • 環境:Visual Studio 2005(Windows アプリケーション)、WindowsXP(sp2)

    ADO.NET の基本機能を使って、データメンテプログラムを作成しようとしています。
    以下の仕様を考えていますが、EndEdit() で問題があります。
    おそらく ADO.NET の仕様だろうと考えていますが、回避したいと考えています。

    1. DataTable にバインドした TextBox で値をメンテする。
    2. 登録ボタンを用意し、ここで EndEdit() を行うことで DataRow の変更を完了する(データベースへの登録はまだ)。

    上記の場合、EndEdit() で null チェックなどが .NET によって自動的に行われますが、もしここでエラーがあると CancelEdit() が自動的に実行されてしまうようです。
    つまり、1つの入力ミスのために、すべての項目の入力が無駄になってしまいます。
    ユーザに「それは仕様です」とはとても言えません(^^;

    希望としては、EndEdit() でエラーがあれば、EndEdit() を行う前の状態のままであって欲しいのですが、どう対処すればよいかわかりません。
    DataError のようなイベントハンドラで、e.Cancel = true とかできないか調べたのですが、無さそうでした。
    ただ、思わぬところのプロパティで指定できるなど、.NET の底力に期待しています。

    ちなみに、EndEdit() の直前で自前でチェック、というのは最終手段と考えています。
    未入力チェックなどの簡単なものならいいですが、整合性制約等については本来、定義だけにすませておきたいからです。

    以下のサンプルコードは、上記仕様をコードでシミュレートしたものです。
    「状態の確認 a」の時点では編集中の状態ですが、「状態の確認 b」の時点ではそうではなくなっています。



    /* テストデータの準備 */
    DataTable wTable1 = new DataTable();
    DataColumn wField1 =
        new DataColumn("Field1", System.Type.GetType("System.String"));
    wField1.AllowDBNull = false;
    wTable1.Columns.Add(wField1);
    DataColumn wField2 =
        new DataColumn("Field2", System.Type.GetType("System.String"));
    wTable1.Columns.Add(wField2);
    DataRow wRow = wTable1.Rows.Add("Value1-1", "Value1-2");
    wRow.AcceptChanges();
    /* テスト */
    /* 1. ユーザが入力を開始 */
    wRow.BeginEdit();
    /* 2. ユーザのメンテ内容の Field1 値が正しくない */
    wRow["Field1"] = DBNull.Value;
    wRow["Field2"] = "Value1-2の修正値";
    /* (状態の確認 a) */
    MessageBox.Show(
            string.Format(
                "HasVersion:{0}\r\n" +
                "Field1:{1}\r\n" +
                "Field2:{2}",
                wRow.HasVersion(DataRowVersion.Proposed),
                wRow["Field1", wRow.HasVersion(DataRowVersion.Proposed)
                    ? DataRowVersion.Proposed : DataRowVersion.Current],
                wRow["Field2", wRow.HasVersion(DataRowVersion.Proposed)
                    ? DataRowVersion.Proposed : DataRowVersion.Current]));
    try
    {
        /* 3. ユーザが登録ボタンを押す */
        wRow.EndEdit();
    }
    catch (Exception wE)
    {
        MessageBox.Show(wE.Message, "エラー",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    /* (状態の確認 b) */
    ~ここに a と同じメッセージボックスのコードをお願いします~

     

    2006年6月23日 13:21

回答

  • 菊池です。

    この件についてBlogに記事をかかせてもらいました。

    http://www.ailight.jp/blog/kazuk/archive/2006/06/26/11617.aspx

     記事に書いたとおりですが、ちょっと見てみた限りではこの挙動を改善するのは難物だという感触しか得られませんでした。

    1. EndEditの置き換えは著しく困難(DataRowViewを間に挟むのを大前提にするならある程度可能ですが DataRowのEndEditはinterfaceでもvirtualでもないため)
    2. EndEdit前にRowが一貫性を持っているか確認できれば良いと考えましたが、EndEditが内部で行っているRowの一貫性/整合性チェックは殆どinternalで実装されておりユー***ードから呼び出し出来ないのでDataColumnや、Constraintsを舐めながらチェックするコードをすべて書き直ししないとRowの一貫性/整合性チェックを実現できない。

     この問題が合わせ技一本で大幅なコード変更無しにこの挙動は改善できないだろうという結論になりました。

     DataRowの一貫性/整合性チェックを実現できてしまえば EndEdit 前にそれを呼んでみて駄目と言われたらメッセージを表示するとかでEndEditによって値が戻ってしまう挙動を抑止できます。

     ただし、CancelEditを呼ばずに再度BeginEditすることはできないと思いますので、ユーザをエラー状態にモーダルに閉じ込めないとEndEditは呼べないし、CancelEditも呼べないで内部状態の管理がむちゃくちゃになります。

     画面とDataRowの間にエンティティクラスをかます事ができるなら、画面はエンティティクラスに値を反映、エンティティクラスからDataRowに値を反映の流れでDataRowがエラー時に値がもどってしまっても中間のエンティティクラスには値が保持されてますのでユーザに問題点だけを修正させる流れが作れると思います。

     これを実現するには型付データセットを2種類作って、片方は一貫性チェックの設定を弱くしておき画面上のニーズにあわせ、もう一方はがちがちにしておいて本当のデータソースのニーズに合わせておき、この間でのデータの反映の実現と、反映タイミングを制御する、反映時のエラーは入力エラーだからユーザに提示という方針がもっとも簡単になる気がします。

    2006年6月26日 2:24

すべての返信

  • 自己レスです。以下の手を思いつきました。
    他に良い方法がありましたら、お願いします。



    /* 編集内容の退避 */
    DataRow wSaveRow = wTable1.NewRow();
    wSaveRow.ItemArray = wRow.ItemArray;
    try
    {
        /* 3. ユーザが登録ボタンを押す */
        wRow.EndEdit();
    }
    catch (Exception wE)
    {
        /* 編集内容の復元 */
        wRow.BeginEdit();
        for (int wI = 0; wI < wRow.Table.Columns.Count; wI++)
            wRow[wI] = wSaveRow[wI];
        /**/
        MessageBox.Show(wE.Message, "エラー",
           
    MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

     

    2006年6月23日 15:46
  • 回避方法を思いついたため、つい、解決できたように思ってしまったのですが、引き続きどなたかご指導願います。(データメンテプログラムを作成することで基本をマスターしようと考えていますので、色々拘ってみたいと思っています。)

    例えば DataGridView にバインドする場合にも、今回の件が絡みます。

    行を抜けようとすると自動的に EndEdit() が実行されるため、たとえ DataError イベントにて e.Cancel = true として行の移動を抑制したとしても、編集中の値はキャンセルされてしまいます。
    これは一応、前回の回避方法を応用(?)し、以下のコードで回避できました。

    しかしこれでは、何にバインドするかで、都度、対処方法を考える必要がありそうに思います。
    そこでできれば、DataTable などの大元の一個所で、私が期待する動作にしてしまいたいと考えているのですが、可能でしょうか?



    DataRow _SaveRow;
    private void dataGridView1_RowValidating(
        object sender, DataGridViewCellCancelEventArgs e)
    {
        /* エラー時に編集値を復元するための準備 */
        _SaveRow = _Table1.NewRow();
        _SaveRow.ItemArray = _Table1.Rows[e.RowIndex].ItemArray;
    }
    private void dataGridView1_DataError(
        object sender, DataGridViewDataErrorEventArgs e)
    {
        /* 編集値を復元 */
        _Table1.Rows[e.RowIndex].BeginEdit();
        for (int wI = 0; wI < _Table1.Columns.Count; wI++)
            _Table1.Rows[e.RowIndex][wI] = _SaveRow[wI];
        /* 行を抜け出さない */
        e.Cancel = true;
        MessageBox.Show(
            e.Exception.Message, "エラー",
            MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

     

    2006年6月24日 12:12
  • 確かに値は戻ってしまうようですね。

    BindingSource.EndEdit() だとこのような問題はないようです。
    BindingSourceを使ってみたらどうでしょうか?

    2006年6月24日 14:41
  • ちょっと違うようです。
    例えば私の@ITでもサンプルではプログラム上でDBNull.Valueを代入した時点でエラーになります。
    http://www.atmarkit.co.jp/fdotnet/special/win20review02/win20review02_01.html

    つまり、BindingSourceはBeginEditしていないことになります。

    逆に考えてNull値をセットしてはいけない場所にセットした時点でエラーになるので、
    EndEdit(保存ボタン)以前に入力時にすぐエラーになります。
    つまり入力時に制約が有効になっている状態です。

    2006年6月24日 17:36
  • 返信ありがとうございます。

     えムナウ さんからの引用
    例えば私の@ITでもサンプルではプログラム上でDBNull.Valueを代入した時点でエラーになります。
    http://www.atmarkit.co.jp/fdotnet/special/win20review02/win20review02_01.html
    つまり、BindingSourceはBeginEditしていないことになります。
    あ、この記事、最近読ませていただいていました。とても参考になりました。

    試して頂いた手順についてですが、以下は関係ないでしょうか?

    DataGridView で編集を行った後、CausesValidation が true のボタンで DBNull.Value を代入するとエラーになりますが、false のボタンではエラーになりませんでした。
    そのため、DataGridView は、CausesValidation が true のコントロールにフォーカスが移る前に EndEdit を行っているのではと思います。

    BeginEdit は行われていると考えています。

    2006年6月25日 13:50
  • 菊池です。

    この件についてBlogに記事をかかせてもらいました。

    http://www.ailight.jp/blog/kazuk/archive/2006/06/26/11617.aspx

     記事に書いたとおりですが、ちょっと見てみた限りではこの挙動を改善するのは難物だという感触しか得られませんでした。

    1. EndEditの置き換えは著しく困難(DataRowViewを間に挟むのを大前提にするならある程度可能ですが DataRowのEndEditはinterfaceでもvirtualでもないため)
    2. EndEdit前にRowが一貫性を持っているか確認できれば良いと考えましたが、EndEditが内部で行っているRowの一貫性/整合性チェックは殆どinternalで実装されておりユー***ードから呼び出し出来ないのでDataColumnや、Constraintsを舐めながらチェックするコードをすべて書き直ししないとRowの一貫性/整合性チェックを実現できない。

     この問題が合わせ技一本で大幅なコード変更無しにこの挙動は改善できないだろうという結論になりました。

     DataRowの一貫性/整合性チェックを実現できてしまえば EndEdit 前にそれを呼んでみて駄目と言われたらメッセージを表示するとかでEndEditによって値が戻ってしまう挙動を抑止できます。

     ただし、CancelEditを呼ばずに再度BeginEditすることはできないと思いますので、ユーザをエラー状態にモーダルに閉じ込めないとEndEditは呼べないし、CancelEditも呼べないで内部状態の管理がむちゃくちゃになります。

     画面とDataRowの間にエンティティクラスをかます事ができるなら、画面はエンティティクラスに値を反映、エンティティクラスからDataRowに値を反映の流れでDataRowがエラー時に値がもどってしまっても中間のエンティティクラスには値が保持されてますのでユーザに問題点だけを修正させる流れが作れると思います。

     これを実現するには型付データセットを2種類作って、片方は一貫性チェックの設定を弱くしておき画面上のニーズにあわせ、もう一方はがちがちにしておいて本当のデータソースのニーズに合わせておき、この間でのデータの反映の実現と、反映タイミングを制御する、反映時のエラーは入力エラーだからユーザに提示という方針がもっとも簡単になる気がします。

    2006年6月26日 2:24
  •  菊池 さんからの引用
    記事に書いたとおりですが、ちょっと見てみた限りではこの挙動を改善するのは難物だという感触しか得られませんでした。

    読ませていただきました。
    もやもやしていた部分をハッキリ書いてくださり、問題の切り分けが1つできたと感じました。今回私が疑問に感じたことは、私の知識不足だけが原因なのではないと...(^^;
    知らないプロパティ等でこの挙動を変更できる、というのは甘かったようですね。

    「DataRowViewを間に挟むのを大前提にするならある程度可能ですが」と書かれた点に興味を持ち、まだよくわかっていない DataRowView について調べようと思っています。

    ブログの中の話ですが、通常は(というかグリッドは)直接 DataTable ではなく DataView を介して処理するようになっているのだろうと思いますが、すると行データも DataRowView 経由でアクセスすることになり、これが DataRowView 側のみに IEditableObject が実装されている理由の一つではないかなぁなどと、わからないなりに想像しています。

     菊池 さんからの引用
    画面とDataRowの間にエンティティクラスをかます事ができるなら、画面はエンティティクラスに値を反映、エンティティクラスからDataRowに値を反映の流れでDataRowがエラー時に値がもどってしまっても中間のエンティティクラスには値が保持されてますのでユーザに問題点だけを修正させる流れが作れると思います。

    本来は間にクラスを入れるベキで必然的に書かれたようになるのかな、、それが問題に感じた人が少なかった(?)理由なのかな、、と思いました。
    また、具体的な対処方法についても考えていただき、ありがとうございます。
    直感的にあまりしたくない方法(^^;;に思えましたが、参考にさせていただきます。

    2006年6月26日 9:01
  • DataGridViewの場合はボタンを押したことで行確定となります。
    CausesValidation がfalse のボタンではエラーにならないということなので、
    行確定のEndEditを行っていないほうが不思議です。
    残念ながらDataGridViewの検証は行えていません。

    私の@ITでのサンプルでは単なるTextBoxやComboBoxへのバインドですので、
    検証したところCausesValidation がfalse のボタンでもエラーになりました。

    2006年6月26日 14:09
  • 返信ありがとうございます!

     えムナウ さんからの引用
    私の@ITでのサンプルでは単なるTextBoxやComboBoxへのバインドですので、
    検証したところCausesValidation がfalse のボタンでもエラーになりました。

    バインドした TextBox 等での編集後に、ボタンを押されましたでしょうか?
    編集時の暗黙の BeginEdit が実行される前にボタンを押すとエラーになります。
    これまでの話では、UIからの入力時にエラーになるハズと書かれていたので、UIでの変更が始点として話をさせていただきました。
    話が違っていたらすみません。


     えムナウ さんからの引用
    DataGridViewの場合はボタンを押したことで行確定となります。
    CausesValidation がfalse のボタンではエラーにならないということなので、
    行確定のEndEditを行っていないほうが不思議です。

    TextBox 等の場合は、CausesValidation の値が何であっても同じ動作になりますが、DataGridView では挙動に変化がありました。
    その点を示すサンプルを作成して体感していただこうと思ったのですが、よくわからない結果になってしまいました。

    まず次の場合、これまで私が書いていた挙動になります。

    1. 1行目を編集(リターン押下せず)
    2. false ボタン押下 → エラーにはならない。
    3. いったん終了。終了時に検証が入るためエラーになりますがOK
    4. 1行目を編集(リターン押下せず)
    5. true ボタン押下 → エラーになる。

    しかしこうするとなぜ?となります。

    1. 1行目を編集(リターン押下せず)
    2. true ボタン押下 → エラーになる。
    3. 1行目を編集(リターン押下せず)
    4. false ボタン押下 → エラーになる。

    一度 true ボタンを押すと false の効能が切れるような感じです。
    そもそも、えムナウさんがおっしゃるように(仰っていないかも)、最初の挙動の方が変なのでしょうか...?



    public partial class Form1 : Form
    {
        DataTable _Table1;
        BindingSource _BindingSource1;
        DataGridView _DataGridView1;
        Button _FalseButton;
        Button _TrueButton;
        public Form1()
        {
            InitializeComponent();
            /* テストデータの準備 */
            _Table1 = new DataTable("Table1");
            DataColumn wField1 = new DataColumn(
                "Field1", System.Type.GetType("System.String"));
            wField1.AllowDBNull = false;
            _Table1.Columns.Add(wField1);
            DataColumn wField2 = new DataColumn(
                "Field2", System.Type.GetType("System.String"));
            _Table1.Columns.Add(wField2);
            _Table1.Rows.Add("V1-1", "V1-2");
            _Table1.Rows.Add("V2-1", "V2-2");
            _Table1.AcceptChanges();
            _BindingSource1 = new BindingSource(_Table1, "");
            /* UI の準備 */
            _DataGridView1 = new DataGridView();
            _DataGridView1.Parent = this;
            _DataGridView1.DataSource = _BindingSource1;
            _FalseButton = new Button();
            _FalseButton.Parent = this;
            _FalseButton.Location =
                new Point(_DataGridView1.Left, _DataGridView1.Bottom);
            _FalseButton.CausesValidation = false;
            _FalseButton.Text = "false";
            _FalseButton.Click += new EventHandler(_FalseButton_Click);
            _TrueButton = new Button();
            _TrueButton.Parent = this;
            _TrueButton.Location =
                new Point(_FalseButton.Right, _FalseButton.Top);
            _TrueButton.CausesValidation = true;
            _TrueButton.Text = "true";
            _TrueButton.Click += new EventHandler(_TrueButton_Click);
        }
        void _FalseButton_Click(object sender, EventArgs e)
        {
            /* CausesValidation が false の場合は
               セルの編集が自動的に終了することはないため、
               これでグリッドのセルの編集を終了する。
               Row.EndEdit までは実行されない */
            _DataGridView1.EndEdit();
            /* CausesValidation が false の場合の検証 */
            ShowRowState();
            TrySetNull();
        }
        void _TrueButton_Click(object sender, EventArgs e)
        {
            /* CausesValidation が true の場合の検証 */
            ShowRowState();
            TrySetNull();
        }
        void ShowRowState()
        {
            string wState =
                ((DataRowView)_BindingSource1.Current)
                .Row.HasVersion(DataRowVersion.Proposed)
                ? "編集中" : "編集中でない";
            /* IsEdit は初回は常に true になってしまう??
            string wState =
                ((DataRowView)_BindingSource1.Current).IsEdit
                ? "編集中" : "編集中でない";
            */
            MessageBox.Show(wState);
        }
        void TrySetNull()
        {
            DataRow wRow = ((DataRowView)_BindingSource1.Current).Row;
            try
            {
                wRow["Field1"] = DBNull.Value;
                MessageBox.Show("NULLの代入が成功");
            }
            catch (Exception wE)
            {
                //_BindingSource1.ResetCurrentItem();
                MessageBox.Show(wE.Message);
            }
        }
    }

     


    話が脱線してきました。
    今回とは関係ないですが、DataRowView.IsEdit も??です...。

    2006年6月27日 2:41
  •  TH01 さんからの引用

    バインドした TextBox 等での編集後に、ボタンを押されましたでしょうか?
    編集時の暗黙の BeginEdit が実行される前にボタンを押すとエラーになります。
    これまでの話では、UIからの入力時にエラーになるハズと書かれていたので、UIでの変更が始点として話をさせていただきました。

    TextBox 等での編集してフォーカス移動後にボタンを押すとエラーにはなりませんでした。
    しかしこの場合「列XXにNullを使用することは出来ません」が保存時に出て変更値もそのままです。
    割と許容できる動作のような気がします。

    編集前に押すとその時点でエラー、編集後は保存時にエラーになってしまいますが・・・

    制約という意味ではNull以外に色々あるので一度体系だててまとめてみる必要があると実感しました。

    2006年6月27日 3:26
  •  えムナウ さんからの引用
    変更値もそのままです。

    この点ですが、TextBox の表示値はそのままですが、DataRow はキャンセルされてしまっています。もう一度保存ボタンを押すと、保存できたかのように見えますよね?
    キャンセルされてしまうのは本スレの本題ですが、バインド先とデータソースで整合性が崩れる点の対策は別スレで教えてもらいました。(その際、自分の投稿で「回答済み」ボタンを押してしまって、私が回答者トップ10に入ってしまっていました(なおしました))

     えムナウ さんからの引用
    制約という意味ではNull以外に色々あるので一度体系だててまとめてみる必要があると実感しました。

    チェックする内容(自動的に行われる場合を含む)としては以下のものがあると思いますが、エンドユーザのことを考えると、それぞれをどのタイミングでチェックしてどのように伝えるかは悩むところです。
    ただ基本的には、.NET の機能をフル活用して、シンプルにしたいと思っています。
    (それが思惑通りにいかなくて、悪戦苦闘中...)

    • DataColumn で定義するもの
    • Constraints で定義するようなもの
    • アプリケーション仕様

    この辺りを書いていると、本題とどんどん離れ、キリがなくなりそうでした。(^^;
    ただ、そのうち私なりのルールを考えようと思っています。

    2006年6月27日 9:39