none
BindingSource の RemoveAt メソッドについて RRS feed

  • 質問

  • いつもお世話になります。

    BindingSource.RemoveAt メソッドを使って DataGridView に表示された複数行のデータを削除したいのですが、期待通りに動作しません。

    ご教示いただけますでしょうか。 

    現在、Visual Studio 2010、.NET Framework 4.0 で Windows フォーム アプリケーションを作成しています。

    以下の手順で SQL Server の TEST テーブルのデータを DataGridView に表示しています。

    1. プロジェクトに DataSet2.xsd を追加します。

    2. 1.上で右クリックして TableAdapter を追加し、TableAdapter 構成ウィザードで TEST テーブルを選択してウィザードを完了させます。

    3. Form2 をデザイナーで開き、データソース ウィンドウから、DataSet2 の TEST テーブルをフォームへドラッグします。

    この時点で、フォームの Load イベントに Fill メソッドのコードが自動で記載されていますので、その後ろに、DataGridView にチェックボックスを表示する処理を手動で記述しました。


    private void Form2_Load(object sender, EventArgs e)
    {
        // TODO: このコード行はデータを 'dataSet2.TEST' テーブルに読み込みます。必要に応じて移動、または削除をしてください。
        this.tESTTableAdapter.Fill(this.dataSet2.TEST);

        // (追加記述) DataGridView にチェックボックス列を追加します。
        DataGridViewCheckBoxColumn column = new DataGridViewCheckBoxColumn();
        tESTDataGridView.Columns.Add(column);

        // (追加記述) DataGridView の列をクリックされソートされると順番が狂ってしまうのでソートできないようにします。
        foreach (DataGridViewColumn c in tESTDataGridView.Columns)
            c.SortMode = DataGridViewColumnSortMode.NotSortable;
    }


    (実現したいこと)
    bindingNavigator の削除アイコンは使用せず(非表示とします)、フォーム上に配置した[削除] ボタンを押した時に、チェックボックスがオンになっているデータを Remove(論理削除)後、最終的に TableAdapterManager の UpdateAll メソッドで更新したいと考えています。
    そこで、Button1 のイベントに以下のような記述をしました。

    private void button1_Click(object sender, EventArgs e)
    {
        // DataGridView の行数分ループします。
        for (int i = 0; i < tESTDataGridView.Rows.Count; i++)
        {
            // チェックボックスがオンかどうか判定します
            if (tESTDataGridView.Rows[i].Cells[4].Value != null && (bool)tESTDataGridView.Rows[i].Cells[4].Value == true)
            {
                // チェックが付いていれば削除(Remove)します。
                tESTBindingSource.RemoveAt(i);
            }
        }
       
        // Remove 後、最後に UpdateAll メソッドで削除データを更新します。
        tableAdapterManager.UpdateAll(this.dataSet2);
    }

    ただ、例えば、DataGridView に 5 行表示されていて、2 行目と、3 行目をチェックしてボタンを押すと 2 行目の行は消えるのですが 3 行目はチェックが付いたまま残ってしまいます。

    ※1 行目と 5 行目にチェックをつけた場合は期待した通り 1 行目と 5 行目が削除され、UpdateAll 後、完全に削除されます。

    (ご質問)
    私が実現したいことを実装する場合、最善策はどうすべきかご教示いただけますでしょうか。

    些細なことでもかまいませんので、アドバイスやご指摘いただければ幸いです。よろしくお願いいたします。

    • 移動 佐伯玲 2016年5月6日 4:39 SQL Server から Visual C#へ
    2016年5月4日 15:57

回答

  • RemoveAt をすることで、その後の行数のインデックスがずれているのが問題なのではと思いました。

    private void button1_Click(object sender, EventArgs e)
    {
        // DataGridView の行数分ループします。
        for (int i = tESTDataGridView.Rows.Count - 1; i >= 0 ; i--)
        {
            // チェックボックスがオンかどうか判定します
            if (tESTDataGridView.Rows[i].Cells[4].Value != null && (bool)tESTDataGridView.Rows[i].Cells[4].Value == true)
            {
                // チェックが付いていれば削除(Remove)します。
                tESTBindingSource.RemoveAt(i);
            }
        }
        
        // Remove 後、最後に UpdateAll メソッドで削除データを更新します。
        tableAdapterManager.UpdateAll(this.dataSet2);
    }

    上記のように行を逆順から走査していくのはどうでしょうか?

    • 編集済み kenjinoteMVP 2016年5月4日 16:16
    • 回答としてマーク yumi08 2016年5月5日 2:47
    2016年5月4日 16:09
  • BindingSource.RemoveAt メソッドは元の DataTable の DataRow を Deleted としてマークするはずです。(DataRow.Delete メソッドと同じ)

    Deleted としてマークされると DataGridView には表示されなくなり(DataTable には残っていますが)、その後で TableAdapterManager.UpdateAll メソッドを実行すると DB の当該レコードは削除され、DataRow には AcceptChanges メソッドが実行されるはずです。

    なので、期待通りに行かないのは、当該(チェックマークを入れた)DataRow に Deleted マークがうまく付けられてないのではと想像します。

    button1_Click メソッド内で for ループを出た後、this.dataSet2.TEST の各 DataRow の RowState プロパティで DataRowState を調べてみてください。削除対象(チェックマークを入れた)DataRow の DataRowState は Deleted になっているでしょうか?

    • 回答としてマーク yumi08 2016年5月5日 2:47
    2016年5月5日 2:08
  • DataGridViewの行というコレクションを順次走査中に、そのバインド元のデータを削除してしまうと、その削除通知がDataGridViewに伝わり、DataGridViewの行コレクションが変化してしまいます。これがうまく動作しない原因です。
    kenjinoteさんが書かれているように後ろから行を消していけば、行コレクションが変化してもこれから走査する行には影響が無いので問題なく動作します。

    要するにバインド元のデータが更新されてもその更新通知がDataGridViewに行かなければうまく動作するので、この通知を一時的に止めてしまう方法でもうまくいきます。更新を一時的に止めるには、BindingSourceのSuspendBindingメソッドを使用します。
    一時的に止めた更新通知を再開するにはResumeBindingメソッドを使います。
    以下でうまく動作すると思います。

    private void button1_Click(object sender, EventArgs e)
    {
        //バインド元からバインド先への変更通知を一時的に停止する。
        tESTBindingSource.SuspendBinding();
     
         // DataGridView の行数分ループします。
        for (int i = 0; i < tESTDataGridView.Rows.Count; i++)
         {
             // チェックボックスがオンかどうか判定します
            if (tESTDataGridView.Rows[i].Cells[4].Value != null && (bool)tESTDataGridView.Rows[i].Cells[4].Value)
             {
                 // チェックが付いていれば削除(Remove)します。
                tESTBindingSource.RemoveAt(i);
             }
         }
         
         // Remove 後、最後に UpdateAll メソッドで削除データを更新します。
        tableAdapterManager.UpdateAll(this.dataSet2);
        
        //バインド元からバインド先への変更通知を再開する。
        tESTBindingSource.ResumeBinding();
    }

    また、以下のようにインデックスで操作しなければ、DataGridViewでソートされても問題なく当該の行を削除することができます。

    private void button1_Click(object sender, EventArgs e)
    {
        tESTBindingSource.SuspendBinding();
    
        //// DataGridView の行数分ループします。
        //for(int i = 0; i < tESTDataGridView.Rows.Count; i++)
        //{
        //    // チェックボックスがオンかどうか判定します
        //    if(tESTDataGridView.Rows[i].Cells[4].Value != null && (bool)tESTDataGridView.Rows[i].Cells[4].Value)
        //    {
        //        // チェックが付いていれば削除(Remove)します。
        //        tESTBindingSource.RemoveAt(i);
        //    }
        //}
    
        // DataGridView の行数分ループします。
        foreach (DataGridViewRow row in tESTDataGridView.Rows)
        {
            // チェックボックスがオンかどうか判定します
            if(row.Cells[4].Value != null && (bool)row.Cells[4].Value)
            {
                // チェックが付いていれば削除(Remove)します。
                tESTBindingSource.Remove(row.DataBoundItem);
            }
        }
    
        // Remove 後、最後に UpdateAll メソッドで削除データを更新します。
        tableAdapterManager.UpdateAll(this.dataSet2);
    
        tESTBindingSource.ResumeBinding();
    
    }

    また、テーブルアダプターを作成する際に、選択用の列をあらかじめ仕込む方法もあります。例えば以下のようなSQLとします。

    select id, name, convert(bit, 0) as 選択 from TEST;

    こうやって出来合ったデータテーブルの「選択」列はReadOnlyになっていますから、これをfalseとし、その後にDataGridViewのデータソースとして下さい。削除は、このデータテーブルで選択にチェックが入っている行を削除すれば良いことになります。
    LINQを使うと1行で書けてしまいます。

    private void button1_Click(object sender, EventArgs e)
    {
        this.dataSet2.TEST.Where(p => p.選択).ToList().ForEach(p => p.Delete());
    
        tableAdapterManager.UpdateAll(this.dataSet2);
    }

    この方法だとDataGridViewの行を走査することがないので、素直な実装になります。基本的にはバインド元のデータのみで判定し削除するのが簡潔でわかりやすく、トラブルの少ない実装になります。

    (追記)
    「== true」と比較するのは冗長なので、削除しました。



    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/




    • 編集済み trapemiyaModerator 2016年5月6日 2:20 誤字修正
    • 回答としてマーク yumi08 2016年5月6日 13:08
    2016年5月6日 2:06
    モデレータ

すべての返信

  • RemoveAt をすることで、その後の行数のインデックスがずれているのが問題なのではと思いました。

    private void button1_Click(object sender, EventArgs e)
    {
        // DataGridView の行数分ループします。
        for (int i = tESTDataGridView.Rows.Count - 1; i >= 0 ; i--)
        {
            // チェックボックスがオンかどうか判定します
            if (tESTDataGridView.Rows[i].Cells[4].Value != null && (bool)tESTDataGridView.Rows[i].Cells[4].Value == true)
            {
                // チェックが付いていれば削除(Remove)します。
                tESTBindingSource.RemoveAt(i);
            }
        }
        
        // Remove 後、最後に UpdateAll メソッドで削除データを更新します。
        tableAdapterManager.UpdateAll(this.dataSet2);
    }

    上記のように行を逆順から走査していくのはどうでしょうか?

    • 編集済み kenjinoteMVP 2016年5月4日 16:16
    • 回答としてマーク yumi08 2016年5月5日 2:47
    2016年5月4日 16:09
  • BindingSource.RemoveAt メソッドは元の DataTable の DataRow を Deleted としてマークするはずです。(DataRow.Delete メソッドと同じ)

    Deleted としてマークされると DataGridView には表示されなくなり(DataTable には残っていますが)、その後で TableAdapterManager.UpdateAll メソッドを実行すると DB の当該レコードは削除され、DataRow には AcceptChanges メソッドが実行されるはずです。

    なので、期待通りに行かないのは、当該(チェックマークを入れた)DataRow に Deleted マークがうまく付けられてないのではと想像します。

    button1_Click メソッド内で for ループを出た後、this.dataSet2.TEST の各 DataRow の RowState プロパティで DataRowState を調べてみてください。削除対象(チェックマークを入れた)DataRow の DataRowState は Deleted になっているでしょうか?

    • 回答としてマーク yumi08 2016年5月5日 2:47
    2016年5月5日 2:08
  • Kenjinote さん、SurferOnWww さん、早速のアドバイスをありがとうございます。

    まずは SurferOnWww さんご指摘の件をループを抜けた後調べました。

    その結果、私のコードでは 2 行削除したつもりが、そのうちの 1 件の DataRowState は Unchanged になっていました。

    一方、Kenjinote さんご指摘のコードでは、チェックをつけた 2 行の DataRowState が Deleted になっていました。

    今後は、Kenjinote さんのコードをもとに、本番データを使ってテストを進めて行きたいと思います。

    取り急ぎお礼までとなりますが、原因と回避策をご教示いただきありがとうございました。今後ともよろしくお願いいたします。

    2016年5月5日 2:47
  • DataGridViewの行というコレクションを順次走査中に、そのバインド元のデータを削除してしまうと、その削除通知がDataGridViewに伝わり、DataGridViewの行コレクションが変化してしまいます。これがうまく動作しない原因です。
    kenjinoteさんが書かれているように後ろから行を消していけば、行コレクションが変化してもこれから走査する行には影響が無いので問題なく動作します。

    要するにバインド元のデータが更新されてもその更新通知がDataGridViewに行かなければうまく動作するので、この通知を一時的に止めてしまう方法でもうまくいきます。更新を一時的に止めるには、BindingSourceのSuspendBindingメソッドを使用します。
    一時的に止めた更新通知を再開するにはResumeBindingメソッドを使います。
    以下でうまく動作すると思います。

    private void button1_Click(object sender, EventArgs e)
    {
        //バインド元からバインド先への変更通知を一時的に停止する。
        tESTBindingSource.SuspendBinding();
     
         // DataGridView の行数分ループします。
        for (int i = 0; i < tESTDataGridView.Rows.Count; i++)
         {
             // チェックボックスがオンかどうか判定します
            if (tESTDataGridView.Rows[i].Cells[4].Value != null && (bool)tESTDataGridView.Rows[i].Cells[4].Value)
             {
                 // チェックが付いていれば削除(Remove)します。
                tESTBindingSource.RemoveAt(i);
             }
         }
         
         // Remove 後、最後に UpdateAll メソッドで削除データを更新します。
        tableAdapterManager.UpdateAll(this.dataSet2);
        
        //バインド元からバインド先への変更通知を再開する。
        tESTBindingSource.ResumeBinding();
    }

    また、以下のようにインデックスで操作しなければ、DataGridViewでソートされても問題なく当該の行を削除することができます。

    private void button1_Click(object sender, EventArgs e)
    {
        tESTBindingSource.SuspendBinding();
    
        //// DataGridView の行数分ループします。
        //for(int i = 0; i < tESTDataGridView.Rows.Count; i++)
        //{
        //    // チェックボックスがオンかどうか判定します
        //    if(tESTDataGridView.Rows[i].Cells[4].Value != null && (bool)tESTDataGridView.Rows[i].Cells[4].Value)
        //    {
        //        // チェックが付いていれば削除(Remove)します。
        //        tESTBindingSource.RemoveAt(i);
        //    }
        //}
    
        // DataGridView の行数分ループします。
        foreach (DataGridViewRow row in tESTDataGridView.Rows)
        {
            // チェックボックスがオンかどうか判定します
            if(row.Cells[4].Value != null && (bool)row.Cells[4].Value)
            {
                // チェックが付いていれば削除(Remove)します。
                tESTBindingSource.Remove(row.DataBoundItem);
            }
        }
    
        // Remove 後、最後に UpdateAll メソッドで削除データを更新します。
        tableAdapterManager.UpdateAll(this.dataSet2);
    
        tESTBindingSource.ResumeBinding();
    
    }

    また、テーブルアダプターを作成する際に、選択用の列をあらかじめ仕込む方法もあります。例えば以下のようなSQLとします。

    select id, name, convert(bit, 0) as 選択 from TEST;

    こうやって出来合ったデータテーブルの「選択」列はReadOnlyになっていますから、これをfalseとし、その後にDataGridViewのデータソースとして下さい。削除は、このデータテーブルで選択にチェックが入っている行を削除すれば良いことになります。
    LINQを使うと1行で書けてしまいます。

    private void button1_Click(object sender, EventArgs e)
    {
        this.dataSet2.TEST.Where(p => p.選択).ToList().ForEach(p => p.Delete());
    
        tableAdapterManager.UpdateAll(this.dataSet2);
    }

    この方法だとDataGridViewの行を走査することがないので、素直な実装になります。基本的にはバインド元のデータのみで判定し削除するのが簡潔でわかりやすく、トラブルの少ない実装になります。

    (追記)
    「== true」と比較するのは冗長なので、削除しました。



    ★良い回答には回答済みマークを付けよう! MVP - .NET  http://d.hatena.ne.jp/trapemiya/




    • 編集済み trapemiyaModerator 2016年5月6日 2:20 誤字修正
    • 回答としてマーク yumi08 2016年5月6日 13:08
    2016年5月6日 2:06
    モデレータ
  • こんにちは、yumi08 さん
    フォーラムオペレータの佐伯 玲 です。

    ご質問内容的に「Visual C#」フォーラム寄りの内容かと思いますので私のほうからスレッドをこのまま移動させて頂きますね。
    回答マークは既に設定頂いておりますがtrapemiyaさんからもさらに情報が寄せられておりますので併せてご確認頂けましたらと思います。

    宜しくお願い致します。

    TechNet Community Support 佐伯 玲

    2016年5月6日 4:39
  • trapemiya さん、ご返事遅れてすみません。そして、いつも有用な情報のご提供をありがとうございます!

    最後の方法が最良だと思いました。実際に試した結果、私の期待通りの結果になりました。

    ほんの少しだけソースの改修が必要ですが、trapemiya さんがおっしゃるとおり、今後のことを考えると簡潔でトラブルの少ない実装だと思います。

    まだまだ未熟な私ですが、今後ともどうぞよろしくお願いいたします。

    そして、佐伯さん、適切なカテゴリへの移動、ありがとうございました。データベースに関するご質問でしたので SQL Server カテに投稿してしまいました。

    • 編集済み yumi08 2016年5月6日 13:10
    2016年5月6日 13:08