BindingSourceのサブリストへのDataGridViewのバインドについて

已答覆 BindingSourceのサブリストへのDataGridViewのバインドについて

  • 2012年6月18日 1:47
     
     

    C#でWindowsフォームアプリケーションを開発しております。

    画面(フォーム)と独自のクラス(モデル)をBindingSourceを使用してバインドしておりますが、次のようなケースでDataGridViewが勝手にスクロールされてしまい困っております。

    • フォームはヘッダと一覧で構成される。一覧には選択用チェックボックス列を配置する。
    • ヘッダには一覧で選択されている項目の集計値(例えば販売数量の合計)を表示する。
    • フォームにバインドするモデルクラスはINotifyPropertyChangedインタフェースを実装し、フォームの一覧に表示するサブリスト(BindingList<T>)と、サブリストの集計値をプロパティとして公開する
    • モデルクラスは自身が持つサブリスト(BindingList<T>)のListChangedイベントを監視して、集計値プロパティの値を更新する。

    上記実装で、画面で一覧のチェックボックスをチェックすると画面上の集計値が正しく更新されるのですが、

    その際、チェックした一覧項目がDataGridViewの最下行になるように勝手にスクロールされてしまいます。

    ※DataGridViewである程度スクロールした状態(上下共に非表示の項目がある状態)でチェックを付加したときに現象が発生します。

    チェックをするたびに一覧がチラチラとスクロールしてしまうので、非常に使いにくく、このままユーザに使用してもらうことはできません。

    なお、この現象はフォームにバインドするBindingSourceのリストメンバに対してDataGridViewをバインドしたときに発生します。

    これは集計値プロパティの変更時に、サブリストの表示更新が内部的行われてしまう事によるものだと考えられます。

    実際に、一覧にバインドするBindingSourceを別途作成し、フォームのコンストラクタ等でモデルクラスのリストプロパティをデータソースに設定することでBindingSourceの関連を無くせば、現象を回避することができました。

    しかし、本来1つでよいBindingSourceを、上記理由で2つに分離することは、今後多くのフォームを作成していく上であまり好ましいことではない気がします。

    何かご存知の方がおられましたらご教示ください。

    宜しくお願いいたします。


すべての返信

  • 2012年6月19日 8:08
     
     
    BindingSource の RaiseListChangedEvents を false にしてみてはいかがでしょうか。
    試していませんが。初期表示するのにResetBindingsが必要かもわかりません。


    http://systemartlaboratory.com/


  • 2012年6月20日 6:01
    モデレータ
     
     
    これは集計値プロパティの変更時に、サブリストの表示更新が内部的行われてしまう事によるものだと考えられます。

    回答ではないのですが、この推測はちょっと不思議な気がしました。この推測が正しければ、集計値プロパティを変更しなければ、勝手にスクロールする不具合は発生しないことになります。
    集計値プロパティを変更しない場合、この不具合は発生しないのでしょうか?

    また、DataGridViewに、選択行が最下行になるように勝手にスクロールする機能があるということを私は知らないのですが、もし、この機能を突き止めていらっしゃれば、教えて下さい。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

  • 2012年6月21日 2:34
     
      コードあり

    返信が遅くなり申し訳ありません。

    仰るとおり、集計値プロパティを更新しなければスクロールはされませんでした。

    次の流れで現象発生しているように思えます。

    1. フォームの一覧でチェックボックスをチェック
    2. チェックボックスにバインドされているモデルクラス.Items の要素の boolean プロパティ値が更新される
    3. モデルクラスは自身の Items (BindingList) の ListChanged を検出して集計値プロパティをセットする
    4. BindingSource を通じてフォーム上の集計値が表示更新される
    5. 4 にひきずられて一覧の表示更新が行われる <- ここさえ発生しなければOKだと思います。
    6. 一覧の表示が更新されたことで、スクロール位置が再計算され、カレント(チェックした行)が最下行になる

    BindingSource は PropertyChanged イベントを受信したプロパティ以外のプロパティも表示更新する仕様なのでしょうか。

    DataGridView が勝手にスクロールする機能は、次のコードで再現可能です。

    単純に独自クラス (Class1) を BindingSource でフォーム上の DataGridView へバインドしてあります。button1 をクリックするとスクロールがずれるはずです。

    要は独自クラスを DataGridView へバインドしている状態で BindingSource の ResetBindings が呼び出されるとまずいようです。

    namespace WindowsFormsApplication1
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
    
                var list = new BindingList<Class1>();
                for (int i = 0; i < 100; i++)
                    list.Add(new Class1() { Value1 = "v1-" + i, Value2 = "v2-" + i });
                this.class1BindingSource.DataSource = list;
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
                this.class1BindingSource.ResetBindings(false);
            }
        }
    
        public class Class1
        {
            public string Value1 { get; set; }
            public string Value2 { get; set; }
        }
    }

  • 2012年6月21日 2:45
     
     

    三輪の牛様

    返信遅くなりまして申し訳ありません。

    ご提示頂いた方法は、本件とは少し異なるようです。

    集計値の更新自体は、リアルタイムに画面へ通知されなければならないので、BindingSource の RaiseListChangedEvent は false にできません。

    集計値の更新だけでよいのに、一覧の内容まで表示更新(スクロール)されてしまうのが問題なのではないかと考えています。

    私の質問の仕方が煩雑なので、わかりにくいかとは思いますが

    宜しくお願いいたします。


  • 2012年6月21日 8:43
    モデレータ
     
      コードあり

    なぜ、ResetBindingsメソッドを出されたのか理由がわかりませんでした。BindingSourceがResetBindingsメソッドを呼び出すということが、どこかに書かれていたのでしょうか?

    それは置いておいて、一番最初に書かれた内容でテストコードを書いてみましたが、確かに私の環境でも発生しました。

    >BindingSource は PropertyChanged イベントを受信したプロパティ以外のプロパティも表示更新する仕様なのでしょうか。

    と、書かれていますが、どのプロパティを指定しても、string.emptyを指定した場合のように、全てのプロパティの表示を更新するように思いました。
    ためしにBindingSourceを使わずに直接バインドしてみましたが、こちらは勝手にスクロールすることはなく、正常に動作しました。
    以上より、どうやらBindingSourceに問題がありそうですが、回避先は私も今のところ見つけられていません。

    直接バインドした場合のテストコードを一応あげておきます。

    public partial class BindingSourceReset : Form
    {
        UIObject ui = new UIObject();
        
        public BindingSourceReset()
        {
            InitializeComponent();
    
            for (int i = 0; i < 100; i++)
                ui.UIObjects.Add(new 明細() { Value1 = "v1-" + i, Value2 = i });
    
            //this.uIObjectBindingSource.DataSource = ui;
    
            this.dataGridView1.DataSource = ui.UIObjects;
            this.textBox1.DataBindings.Add("Text", ui, "合計値");
    
    
        }
    
        private void button1_Click(object sender, EventArgs e)
        {
            //this.uIObjectBindingSource.ResetBindings(false);
        }
    
    }
    
    
    public class UIObject : INotifyPropertyChanged
    {
        public BindingList<明細> UIObjects { get; set; }
        public decimal 合計値 { get; set; }
    
        public UIObject()
        {
            UIObjects = new BindingList<明細>();
            UIObjects.ListChanged += new ListChangedEventHandler(UIObjects_ListChanged);
        }
    
        void UIObjects_ListChanged(object sender, ListChangedEventArgs e)
        {
            合計値 = UIObjects.Sum(p => p.Value2);
            NotifyPropertyChanged("合計値");
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
    
    }
    
    public class 明細 : INotifyPropertyChanged 
    {
        public string Value1 { get; set; }
        decimal _Value2;
        public decimal Value2 { get { return _Value2; } set { _Value2 = value; NotifyPropertyChanged("Value2"); } }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }
    }


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/


  • 2012年6月22日 0:18
     
     

    trapemiya 様

    お忙しい中、検証ありがとうございました。

    ResetBindings メソッドは一覧のみの単純バインディングで勝手にスクロールされる現象を再現する目的と、BindingSource がプロパティ変更の通知を受けた際にすべてのプロパティの表示更新を行っているように見えることから、例に出させて頂きました。

    実際のコードでは仰るとおり ResetBindings メソッドの呼び出しはありませんので、紛らわしい書き方をしてしまい申し訳ありません。

    また、ソースコードを提供頂きありがとうございます。

    提供頂いたソースコードでスクロールが発生しないことが確認できました。恐らく、私が冒頭で記述した集計値と一覧で別の BindingSource を使用すると問題が発生しないというケースと同じだと思われます。

    親子関係のあるモデルを表示・編集するシナリオとしては一般的だと思うので、なにか解決策があるかと模索しておりましたが

    やはり、1 つBindingSource で実現するのは難しく、集計値の表示更新を行う機能 (BindingSource や直接バインド) と、一覧の表示更新を行う機能を分離するしかないという結果に至りそうですね。。。

  • 2012年6月22日 7:42
    モデレータ
     
     回答済み

    なるほど。以下を読んでいると、BindingSourceの問題というわけじゃないようですね。

    .NET WinForms INotifyPropertyChanged updates all bindings when one is changed. Better way?
    http://ne.runcode.us/q/net-winforms-inotifypropertychanged-updates-all-bindings-when-one-is-changed-better-way


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

  • 2012年6月22日 8:24
     
     

    trapemiya

    返信ありがとうございます。

    BindingSource の問題ではないというのは、何を判断されたのでしょうか。

    お手数ですがご教示願えませんか。

    ご提示頂いたページを英語が苦手なりに読んでみたところ、BindingSource (というより BindingManager)の仕様で、PropertyChanged を通知したプロパティ以外のプロパティも全体的に getter が呼び出されてしまうのは避けようがない

    という風にとれたのですが、重要な事を何か見落としておりますでしょうか。

    BindingSource というよりも BindingManager の問題(仕様)だよというご提言でしょうか?

  • 2012年6月22日 8:56
    モデレータ
     
     

    単純な話で、私が紹介したページではBindingSourceを使わず、DataBindingsプロパティでバインドしていますが、その状態でも1つのプロパティについてOnPropertyChangedを実行しているにも関わらず、もう一つのプロパティにバインドしているテキストボックスも更新されているからです。この場合は、2つともTextBoxですから、同じPropertyManagerを共有しているのだと思います。
    私がその前の投稿でうまく行ったと報告したケースは、TextBoxはPropertyManagerが管理し、DataGridViewはCurrencyManagerが管理しているはずで、それぞれ別なもので管理されているため、うまく行ったのだと想像しています。(時間が取れなくて検証できていません。ごめんなさい)


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

  • 2012年6月22日 15:28
     
     

    試しましたが確かにどうにもなりませんでした。

    初期化プロセスで BindingSource =  new Model(); とだけ実行すれば良いという明快さは失われますね。

    現状では2つに分けるくらいしか思いつきません。

    私はフォーム上のBindingSourceをスキャンしDataMemberと名前が一致するリストを結合するルーチンを作ってそれを呼び出しています。

    ですので集計部も件数1件のリストにしています。


    http://systemartlaboratory.com/

  • 2012年6月22日 18:20
     
      コードあり

    おはようございます。Fuda1です。

    チェックボタンを押下した場合、再表示すると位置がずれる
    のを、防止するだけであれば、フォーカスの位置を調整すると同じ
    表示位置を保つ事ができます。
    チェックボタン有りませんが、以下改造して見ました。

    1.表示行のトップ位置、最上行を取得する。
    2.コントロール内、表示可能行数を計算する。
    3.フォーカスを表示行の最下行に移動する。
    4.表示を更新する。

    namespace WindowsFormsApplication1 { public partial class Form1 : Form { int intTopval; public Form1() { InitializeComponent(); var list = new BindingList<Class1>(); for (int i = 0; i < 100; i++) list.Add(new Class1() { Value1 = "v1-" + i, Value2 = "v2-" + i }); this.class1BindingSource.DataSource = list; } // 表示を更新する private void button1_Click(object sender, EventArgs e) { //コントロールの枠内表示可能行数 int DispLine = (this.dataGridView1.Size.Height - 55) / this.dataGridView1.ColumnHeadersHeight; //カレントフォーカスを表示最下行に移動する this.dataGridView1.CurrentCell = dataGridView1[0, intTopval + DispLine]; this.class1BindingSource.ResetBindings(false); } } public class Class1 { public string Value1 { get; set; } public string Value2 { get; set; } } private void dataGridView1_Scroll(object sender, ScrollEventArgs e) { // 表示行のトップ位置、最上行を取得する。 intTopval = (int)e.NewValue; } }



    • 編集済み FUDA1 2012年6月22日 18:25
    • 編集済み FUDA1 2012年6月22日 18:34
    •  
  • 2012年6月23日 1:25
     
      コードあり

    おはようございます。Fuda1です。 スーパーコード、有難うございます。ボタンを押下した場合が何故かコメントになっておりましたので、追加してみました。

    public partial class BindingSourceReset : Form
     {
       省略
         private void button1_Click(object sender, EventArgs e)
        {
            //this.uIObjectBindingSource.ResetBindings(false);
            this.dataGridView1.DataSource= "";
            this.dataGridView1.DataSource = ui.UIObjects;
        }
        省略
    }
    
  • 2012年6月25日 3:54
     
     

    返信ありがとうございます。

    この問題はバインディング機構のコアな部分に関わるむずかしい問題なんですね。。。

    もう少しだけ調べてみたいと思いますが、原因わかったとしても根本的な対応はオーバーワークと判断して直接バインドするか BindingSource をわける方法に逃げてしまいそうです。

    trapemiya 様

    確かに BindingSource 使用していないですね。。。なぜか気づきませんでした。
    ご指摘の通り、BindingSource が問題ではなく、各種マネージャに原因がありそうですね。BindingSource はあくまでマネージャからリフレッシュ通知を受信しているだけといった感じでしょうか。
    もう少し調べてみたいと思います。

    三輪の牛 様

    返信ありがとうございます。
    仰るとおり、デメリットといえば初期化がシンプルでなくなる事と、Form デザイナ上に BindingSource コンポーネントが 2 つできてしまって気持ちが悪いくらいの事なので、バインディングのコアな部分 (CurrencyManager 等) に手を入れるのもオーバーワークかと、いまひとつ躊躇しているところです。
    本件、もう少し未解決のままとさせて頂き、解決方法を模索してみようと思います。

    FUDA1 様

    返信ありがとうございます。サンプルコード確認してみました。
    私が、返信の真意を読取れていないのかも知れませんが、本件はユーザーコードの裏で動いているバインドによるスクロールですので適用がむずかしい気がします。
    ※ボタンやチェックボックスのクリックが、スクロールの直接の契機ではないので、スクロール位置を調整するユーザコードを挿入することができません。
     BindingCompleted 等のバインドに関するイベントを捕捉することもできますが、バインド全般で発生しますので、そのイベントを捕捉した時にスクロールを調整すべきかどうかの判断が難しいです。
    今後の開発の参考にさせていただきます。ありがとうございました。

  • 2012年6月28日 4:46
     
     回答済み コードあり

    デザイナーの設定はそのままでDataGridViewのDataSourceを差し替えてはどうですか。

          var list = ListBindingHelper.GetList(dataGridView1.DataSource, dataGridView1.DataMember);
          dataGridView1.DataMember = null;
          dataGridView1.DataSource = list;
    

    これで集計部とは切り離されるので期待する動作になっています。

    直接モデルのサブリストを代入しても同じ結果になりますが汎用的に書いてみました。

    フォーム上のDataGridViewをスキャンしてすべてのDataGridViewに対して上記の処理を施せばさらに汎用性は得られると思います。


    http://systemartlaboratory.com/

  • 2012年6月28日 7:43
     
     

    三輪の牛 様

    返信ありがとうございます。

    これなら汎用的に DataSource 差し替えることができますね。見かけ(デザイナ)上の BindingSouce も一つで済みますし。

    ご提示頂いた方法で対応したいと思います。ありがとうございました。

    余談ですが・・・現状、私たちが開発しているアプリケーションでは親子関係のあるデータを表示するシナリオとして次の 2 つを予定しております。

     ・画面上部に単一の親レコードを詳細表示 + 下部に子レコードの一覧

     ・画面上部に親レコードの一覧 + 下部に現在選択されている親にひもづく子レコードの一覧

    後者の方は内部的にも BindingSource が 1 つでなければ、親レコード一覧の行選択時(カレントの変更)が子レコード一覧に反映されませんので

    できることならばスクロール(というよりもバインディングのリフレッシュ)の問題を根本的に解決したかったのですが難しそうです。

    当面はシナリオ毎のテンプレートを作成して、後者についてはイベントの捕捉等でリフレッシュするように対処しようと思います。

    ありがとうございました。

  • 2012年6月28日 9:06
     
     

    今回の動きはNetFramework内部で、親の変更を受けてBindingSourceのParentCurrencyManager_CurrentItemChangedが呼ばれ、その中でサブリストであればSetList→ResetBingdingsを呼ぶという流れになっていて、takahashi-h1219さんが推測なさっていたとおりの流れでした。これをどうにかするのは困難だと思いました。

    WPFだとDataContext にモデルのインスタンスを代入することでバインディングするらしいのですが、WindowsFormsでもそれに近いことができるとわかったことが収穫でした。

    http://systemartlaboratory.com/

  • 2012年9月23日 13:05
     
     

    最近開発したシステムでtakahashi-h1219さんが最初に書かれた方法を全面的に使わせてもらいました。いつもはデータベース相手にDataSet, DataTableに相当する物を使うのですが、今回はデータベースを使用しないシステムでしたのでちょうどぴったりはまりました。私自身が回答した内容も含め使いました。

    その結果、構造がすっきりして見通しの良い物になりました。


    http://systemartlaboratory.com/