自作プログラムでデータベースを検索する場合、特に何も考慮しなければ結果が返ってくるまで画面がフリーズしてしまいます。
検索中も画面を操作したい場合や、検索途中で強制中断したい場合などは、検索処理を別スレッドで実行することになりますが、別スレッドで得た検索結果をそのまま画面に表示しようとすると、エラーが発生してしまいます。
今回は、SQLiteのデータベースに対して別スレッドで検索し、検索結果をDataGridに表示するという一連の流れについて解説したいと思います。
やりたいこと
「データやファイルの読み込み中であっても、画面がフリーズすることなく操作ができるようにしたい」ということが目標です。
今回紹介するサンプルプログラムのは、「SQLiteに対して検索を行っている間、画面がフリーズすることなく、中止ボタンを押すことで検索処理を中断できる」といったものです。

実現方法
画面の「検索ボタン」のクリックイベントにて、SQLiteへの検索処理を別スレッドで実行します。
呼び出した画面側では、スレッドの終了を待たないようにすることで、検索中であっても画面操作が可能になります。
//HogeクラスのインスタンスのExecuteメソッドに引数10000を渡して、別スレッドで実行するサンプル Task.Run(() => Hoge.Execute(1000));

とまあ、ここまでは簡単なのですが、ひとつ考慮する点があって、別スレッドの検索が完了ことを画面側に知らせる必要があります。
これは、検索処理が完了した時点でイベントを発生させるようにすれば解決します。
public class Hoge
{
//イベントハンドラの定義
public event EventHandler<EventArgs> TaskCompleted = delegate { };
//メソッド
public void Execute(int val)
{
~中略~
TaskCompleted(this, new EventArgs());
}
}
画面側では、そのイベントを受取り、コントロールに検索結果をセットすればOKです。
var hoge = Hoge();
hoge.TaskCompleted += (sender,e) => { 処理;};
別スレッドからコントロールにアクセスする
ただし注意点があって、別タスクの検索結果をそのままコントロールにセットすると、下記のエラーが発生します。

これは、「別スレッドの中から画面のコントロールにアクセスできない」という制限があるためです。
イベントハンドラに記述した処理は別スレッドから呼び出されるので、結果的に上記の制限に引っかかってエラーが発生します。
このエラー回避の回避策として、 this.Dispatcher.Invoke を使って次のように記述する方法があります。
this.Dispatcher.Invoke((Action)(() => { 処理; }));
サンプルプログラムでは、この方法を使って別スレッドの中から画面のコントロールに検索結果をセットしています。
サンプルプログラムのソースコード
サンプルプログラムの概要
今回紹介するサンプルプログラムは次の様な構成になっています。
SQLiteLibには、SQLiteに対してSQLを実行したり、結果を受取ったりするメソッドが入っています。
TaskCompletedEventArgsは、SQLiteLibの検索メソッドで取得したデータ(検索結果はDataTableに格納)を、呼び出し元(MainWindow.xaml.cs)に返すためのクラスです。

サンプル プログラムのダウンロードURL
ソースコードはこちらからダウンロードできます。
尚、SQLiteのライブラリは含んでいませんので、ビルド&実行したい場合は、NuGetからパッケージをインストールして下さい。
インストール方法の詳細についてお知りになりたい方は、こちらの記事をどうぞ。
ソースコードの紹介
まず最初にMainWindow.xamlです。
<Window x:Class="TaskSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TaskSample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<DataGrid x:Name="uxDataGrid" ItemsSource="{Binding}" Margin="0,59,0,0"/>
<Button Content="検索" HorizontalAlignment="Right" Margin="0,15,132,0" VerticalAlignment="Top" Height="30" Width="80" Click="FindButton_Click"/>
<Button Content="中止" Margin="0,15,17,0" VerticalAlignment="Top" Height="30" HorizontalAlignment="Right" Width="80" Click="StopButton_Click"/>
</Grid>
</Window>
次に、 MainWindow.xaml.cs です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
namespace TaskSample
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
//テストデータの件数
int COUNT = 1000000;
//テストDBのファイル名
string DB_NAME = "test.db";
//SQLiteLibのインスタンス格納用変数
SQLiteLib _sqlite;
/// <summary>
/// コンストラクタ
/// </summary>
public MainWindow()
{
InitializeComponent();
//インスタンス生成
_sqlite = new SQLiteLib(DB_NAME);
//DBの作成とテストデータの登録
init_db();
//処理完了のイベントハンドラを登録
_sqlite.TaskCompleted += (sender, e) =>
this.Dispatcher.Invoke((Action)(() => {
//検索結果をDataGridにセット
uxDataGrid.DataContext = e.Result;
//マウスカーソルを元に戻す
this.Cursor = Cursors.Arrow;
}));
}
/// <summary>
/// 検索の実行
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FindButton_Click(object sender, RoutedEventArgs e)
{
//マウスカーソルを待機アイコンにする
this.Cursor = Cursors.Wait;
//DataGridの表示をクリアする
uxDataGrid.DataContext = null;
//DBの検索処理を別スレッドで実行
Task.Run(() => _sqlite.ExecuteQuery("select * from Test"));
}
/// <summary>
/// 処理の中断
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void StopButton_Click(object sender, RoutedEventArgs e)
{
_sqlite.Stop();
}
/// <summary>
/// テストDBの初期化
/// </summary>
private void init_db()
{
//テスト用DBが無ければ作成する
if (! File.Exists(DB_NAME))
{
//マウスカーソルを待機アイコンに変更
this.Cursor = Cursors.Wait;
//テーブル削除、テーブル作成、テストデータ登録様SQLを生成
List<string> sqls = new List<string>();
sqls.Add("drop table if exists Test");
sqls.Add("create table Test(name text, val1, real val2)");
for (int i = 0; i < COUNT; i++)
{
sqls.Add(string.Format("insert into Test values('Item_{0}',{1},{2})", i, i, i * i));
}
//
_sqlite.ExecuteNoneQuery(sqls.ToArray());
//マウスカーソルを元に戻す
this.Cursor = Cursors.Arrow;
}
}
}
}
最後に、SQLiteLibとTaskCompletedEventArgs のクラスです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.SQLite;
using System.Data;
namespace TaskSample
{
/// <summary>
/// 呼び出し元に検索結果を返すためのデータ保持クラス
/// </summary>
public class TaskCompletedEventArgs : EventArgs
{
public object Result { get; set; }
}
public class SQLiteLib
{
/// <summary>
/// 完了イベントハンドラ
/// </summary>
public event EventHandler<TaskCompletedEventArgs> TaskCompleted = delegate { };
/// <summary>
/// 読み込み中止フラグ
/// </summary>
private bool _stopFlag = false;
/// <summary>
/// 接続文字列
/// </summary>
public string ConnectString { get; set; }
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="connectString"></param>
public SQLiteLib(string connectString)
{
ConnectString = "DataSource="+ connectString;
}
/// <summary>
/// SQLの実行
/// </summary>
/// <param name="sqls"></param>
public void ExecuteNoneQuery(string[] sqls)
{
using (SQLiteConnection connection = new SQLiteConnection(ConnectString))
{
connection.Open();
using (SQLiteTransaction trans = connection.BeginTransaction())
{
try
{
foreach (string sql in sqls)
{
using (SQLiteCommand cmd = connection.CreateCommand())
{
cmd.Transaction = trans;
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
cmd.Dispose();
}
}
trans.Commit();
//処理完了イベント
TaskCompleted(this, new TaskCompletedEventArgs() { Result = true });
}
catch
{
trans.Rollback();
throw;
}
}
}
}
/// <summary>
/// Queryの実行
/// </summary>
/// <param name="sql"></param>
/// <param name="param"></param>
/// <returns></returns>
public DataTable ExecuteQuery(string sql, Dictionary<string, object> param = null)
{
DataTable dt = null;
_stopFlag = false;
using (SQLiteConnection connection = new SQLiteConnection(ConnectString))
{
connection.Open();
using (SQLiteCommand cmd = connection.CreateCommand())
{
//SQLの設定
cmd.CommandText = sql;
//パラメータの設定
if (param != null)
{
foreach (KeyValuePair<string, object> kvp in param)
{
cmd.Parameters.Add(new SQLiteParameter(kvp.Key, kvp.Value));
}
}
//レコードの読み出し
using (SQLiteDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
if (_stopFlag)
{
break;
}
if(dt == null)
{
dt = new DataTable();
Enumerable.Range(0,reader.FieldCount)
.Select(i=>dt.Columns.Add(reader.GetName(i),reader.GetFieldType(i))).ToArray();
}
else
{
var dr = dt.NewRow();
dr.ItemArray = Enumerable.Range(0, reader.FieldCount)
.Select(i => reader.GetValue(i)).ToArray();
dt.Rows.Add(dr);
}
}
}
}
}
//処理完了イベント
TaskCompleted(this, new TaskCompletedEventArgs() { Result = dt });
return dt;
}
/// <summary>
/// 読み込みの強制中止
/// </summary>
public void Stop()
{
_stopFlag = true;
}
}
}
まとめ
今回は、WPFの画面から別タスクでデータベースに検索を行い、結果を画面に表示するという一連の流れについて紹介いたしました。
題材はデータベース検索ですが、ここで紹介した方法はCSVの読み書きやデータの入力待ちなど、時間の掛かる処理を行わせた時に、画面をフリーズさせたくない場合に使える方法です。
サンプルプログラムは必要最小限の機能しかなく、例えば「検索」ボタンを押してから検索が終わるまで「検索」ボタンの色を変えておくとか、処理完了メッセージを表示するとかは行っていません。
これを参考にするか、あるいはカスタマイズしてお役立ていただければと思います。
この記事が皆様のプログラミングの一助になれば幸いです。