いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

DevExpress奮闘記-012 別スレッドでデータソースのデータを変更する(1) (XtraGrid, GridContorl, DataSource, Cross thread operation, DisableThreadingProblemsDetection)

DevExpressのコントロールでは、v8.3.2から以下の重要な制約が追加されました。


要約すると以下の感じ。

データソースとして設定されているオブジェクトのデータを別スレッドで変更すると例外が発生する。


これ、意外に結構はまります。
XtraGridやTreeListを利用している場合、データの件数が元々少ない場合は
いいのですが、件数が多い場合(特にグリッド)は、コントロールをすぐ描画させて
バックでデータを順次取得していくことが多いからです。つまり、対象のデータが
相手の場合は非同期で処理を行う事が多いからです。


で、本家の方でも結構話題になってましていろいろポストされてます。


んで、ここからどのようにしたら例外が発生するのかのケースと
それの解決方法を記述していこうと思います。


ちなみに、以下ではデータソースとしてDataTableを使っています。
XPCollectionではどうなるか試してません。(普段あまりXPO使いませんのですみません)


まず、例外が発生するパターン。
100万件のデータがあるとして、それを非同期で読み込むシナリオです。
以下のようなコード。

#region GridControlSample-01

    public class GridControlSamples01 : IExecutable{

        class TestForm : XtraForm{

            GridControl      grdMain;
            BackgroundWorker _worker;

            public TestForm(){
                InitializeComponent();
                InitializeEventSettings();
            }

            protected void InitializeComponent(){
                Text = "GridControlSamples-01";
                Size = new Size(800, 600);
                StartPosition = FormStartPosition.CenterScreen;

                SuspendLayout();

                grdMain = new GridControl();
                grdMain.Dock = DockStyle.Fill;
                grdMain.UseEmbeddedNavigator = true;

                Controls.Add(grdMain);
                ResumeLayout();
            }

            protected void InitializeEventSettings(){

                Load += (s, e) => {
                    DataTable table = new DataTable();

                    table.BeginInit();
                    table.Columns.Add("COLUMN-1");
                    table.Columns.Add("COLUMN-2");
                    table.EndInit();

                    grdMain.DataSource = table;
                };

                Activated += TestForm_Activated;
            }

            void TestForm_Activated(object sender, EventArgs args){
                Activated -= TestForm_Activated;

                //
                // データの取得は別スレッドにて行う。
                //
                _worker = new BackgroundWorker();

                _worker.DoWork += (s, e) => {
                    DataTable table = e.Argument as DataTable;

                    if(table == null){
                        throw new ArgumentNullException("table");
                    }

                    //
                    // 別スレッドで処理が開始されているのが分かりやすいように
                    // 少し待ってから行動開始.
                    //
                    Thread.Sleep(2000);

                    for(int i = 0; i < 1000000; i++){
                        table.LoadDataRow(new []{i.ToString(), i.ToString()}, true);
                    }

                    e.Result = table;
                };

                if(!_worker.IsBusy){
                    _worker.RunWorkerAsync(grdMain.DataSource as DataTable);
                }
            }
        }

        public void Execute(){
            Application.EnableVisualStyles();
            Application.Run(new TestForm());
        }
    }
#endregion

上記のコードで動かすと、フォームが表示されてから2秒後にデータの
取得を行いはじめます。が、即以下の例外が発生します。

System.InvalidOperationException: Cross thread operation detected. To suppress this exception, set DevExpress.Data.CurrencyDataController.DisableThreadingProblemsDetection = true


で、例外のメッセージに書いているようにフォームのコンストラクタ

DevExpress.Data.CurrencyDataController.DisableThreadingProblemsDetection = true

とやると、例外は発生しなくなります。
ですが、上記のプロパティは既にObsolate扱いとなっていますので警告が出るようになります。
つまり、DevExpress側では

どうしても例外を止めたい場合は、設定したらオッケイだが、DevExpress側では推奨しない。

という位置付けであると思われます。
なので、設定しないのに越した事はないでしょう。


で、最初のサンプルを例外が発生しないようにするにはどうすればいいかというと,先ほどのサンプルの
TestForm_Activatedメソッドを以下のように変更します。

            void TestForm_Activated(object sender, EventArgs args){
                Activated -= TestForm_Activated;

                //
                // データの取得は別スレッドにて行う。
                //
                _worker = new BackgroundWorker();

                _worker.DoWork += (s, e) => {
                    DataTable table = e.Argument as DataTable;

                    if(table == null){
                        throw new ArgumentNullException("table");
                    }

                    //
                    // 別スレッドで処理が開始されているのが分かりやすいように
                    // 少し待ってから行動開始.
                    //
                    Thread.Sleep(2000);

                    table.BeginLoadData();

                    for(int i = 0; i < 1000000; i++){
                        table.LoadDataRow(new []{i.ToString(), i.ToString()}, true);
                    }

                    e.Result = table;
                };

                _worker.RunWorkerCompleted += (s, e) => {

                    if(e.Error != null){
                        throw e.Error;
                    }

                    DataTable table = e.Result as DataTable;
                    table.EndLoadData();
                };

                if(!_worker.IsBusy){
                    _worker.RunWorkerAsync(grdMain.DataSource as DataTable);
                }
            }


上記の場合は、例外が発生しません。


修正したポイントは、以下の点です。

  • データを設定する前にBeginLoadDataメソッドを呼ぶようにした。
  • データ取得が完了後にEndLoadDataメソッドを呼ぶようにした。


つまり、データ通知機能を無効にしたという事です。
基本的にDataSourceを持つコントロールは、データソースの
データ変更通知を受け取るようになっています。


先にBeginLoadDataメソッドにてデータ通知機能を無効にしておいて
データを取得し、終了次第、UIスレッドにてEndLoadDataメソッドで
データ通知を再開させてます。EndLoadDataメソッドを別スレッドで
呼んでしまうと当たり前ですが、例外が発生します。
(EndLoadDataメソッドがコールされた瞬間にデータ通知が発生しますので)


これで、無事例外が出なくなってオッケイ!!って思ったら現実問題そうではないです。
このままだと、データ通知が走らないので取得が完了するまでグリッド上には
なんの変化も起こりません。確かにUIスレッドを独占することは無くなっていますが
やはり、設定したデータが順にグリッドに表示されていかないと非同期で処理を
行っている旨みがないです。


てことで、次はデータを非同期で取得・設定しながら、且つ、グリッドにも
反映させるようにしてみましょう。ってところなんですが
長文になってきたので一旦ここでこの記事は終わりにして、次の記事で
その方法について書きます。