いろいろ備忘録日記

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

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


前回、例外が発生しないように実装する事が出来たので、今回は
それに加えて、さらにデータを順次表示していくようにしてみます。


前回の日記は以下から参照できます。


で、データを順次表示していく方法ですがこれは結構簡単です。
但し、常にデータソースにはデータが設定されていっているので
データ変更通知機能をONにするわけにはいきません。


てことで、今度はGridControl側の以下のメソッドを呼びます。

GridControl.RefreshDataSource()

このメソッドを呼ぶとグリッド側にてデータソース情報をリフレッシュ
してくれます。その際に現在のデータや件数がリフレッシュされるので
これを利用することで、順次データを表示していけます。


てことで、前回のサンプルを修正した版です。
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);

                        if((table.Rows.Count % 100000) == 0){
                            grdMain.Invoke(new MethodInvoker(() => {
                                grdMain.RefreshDataSource();
                            }));
                        }
                    }

                    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);
                }
            }


追加した部分は以下の部分です。

if((table.Rows.Count % 100000) == 0){
    grdMain.Invoke(new MethodInvoker(() => {
        grdMain.RefreshDataSource();
    }));
}


基準となる件数に達している場合に、RefreshDataSourceメソッドを呼んでます。
その際、現在処理が行われているのは別スレッドとなっていますのでInvokeを付けて呼んでます。


このようにすると、順に件数が更新されていきます。
また、もうすこし良くするなら、最初の一部分のデータに関してはすぐにリフレッシュを掛けて
画面に表示してしまい、その後は定期的に更新していくほうがユーザ側からすると体感的に
早く感じます。


てことで、更新タイミングを少し変更。

if((table.Rows.Count < 50000 && (table.Rows.Count % 10000) == 0) || (table.Rows.Count % 100000) == 0){
    grdMain.Invoke(new MethodInvoker(() => {
        grdMain.RefreshDataSource();
    }));
}


上記のように、最初の5万件の部分は1万件毎に更新を行い、それ以降は定期的に更新という
風にすると、使う側からは、データがとりあえずすぐに表示され始めるので体感的に速く感じるように
なったりします。


実際はデータベースのデータが表示対象となることがほとんどのはずですので
後は更新を掛けるタイミングを調整したりすればいい感じになります。


通常、このような処理はカスタムコントロール化してしまい内部に閉じてしまうのが一般的だと思います。
(私の場合も、自分用のカスタムコントロールとして作ってます。)
後は、データ処理中であることを示すようにしたり、いろいろ装飾をつければ尚いい感じになります。


最後に今回の最終的なソースです。

// vim:set ts=4 sw=4 et ws is nowrap ft=cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Common;
using System.Drawing;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting;
using System.Threading;
using System.Windows.Forms;

using DevExpress.XtraEditors;
using DevExpress.XtraGrid;

namespace Gsf.Samples{

    public class SampleLauncher{

        static void Main(string[] args){

            string className = typeof(Dummy).Name;
            if(args.Length != 0){
                className = args[0];
            }

            if(!string.IsNullOrEmpty(className)){
                className = string.Format("{0}.{1}", typeof(SampleLauncher).Namespace, className);
            }

            try{
                Assembly     assembly = Assembly.GetExecutingAssembly();
                ObjectHandle handle   = Activator.CreateInstance(assembly.FullName, className);
                if(handle != null){
                    object clazz = handle.Unwrap();

                    if(clazz != null){
                        (clazz as IExecutable).Execute();
                    }
                }
            }catch(Exception ex){
                Console.WriteLine(ex.Message);
            }
        }
    }

    interface IExecutable{
        void Execute();
    }

    class Dummy : IExecutable{

        public void Execute(){
            Console.WriteLine("THIS IS DUMMY CLASS.");
        }
    }

#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;

                //
                // データの取得は別スレッドにて行う。
                //
                // データ通知が行われる状態にしていると以下の例外が発生する。
                // (System.InvalidOperationException: Cross thread operation detected. To suppress this exception, 
                //  set DevExpress.Data.CurrencyDataController.DisableThreadingProblemsDetection = true)
                //
                _worker = new BackgroundWorker();

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

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

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

                    table.BeginLoadData();

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

                        if((table.Rows.Count < 50000 && (table.Rows.Count % 10000) == 0) || (table.Rows.Count % 100000) == 0){
                            grdMain.Invoke(new MethodInvoker(() => {
                                grdMain.RefreshDataSource();
                            }));
                        }
                    }

                    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);
                }
            }
        }

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


ちなみに、DevExpress側ではデータソースオブジェクトをコピー(クローン)して
そのコピーしたオブジェクトを別スレッドで処理し、更新するタイミングでコピーオブジェクトを
再度グリッドのデータソースに設定する方法を記述してくれていますが、これだと件数が
多い場合にコピー処理にすごい時間がかかります。件数が少ない場合は問題ないですが
件数が多い場合は、なかなか利用しづらいと思います。
(しかし、取得データを順次表示しなければ全然問題ない方法です。例えばダイアログや
 ステータスバーなどに現在処理中である表示を行えばよい場合など)