-
さて、Part 1~3 の解説で、Windows フォームにおけるマルチスレッドアプリケーションをスクラッチで開発する方法について述べてきました。結論としては、実は Windows フォームにおけるマルチスレッドアプリケーション開発は恐ろしく厄介で面倒である、ということになると思うのですが;、とはいえ
- 長時間を要する処理があるため、どうしてもマルチスレッドアプリにしなければならない。
ということも当然あると思います。幸い、.NET Framework 2.0/Visual Studio 2005 以降では、BackgroundWorker コンポーネントをはじめとして、マルチスレッドアプリを比較的簡単に書けるようにするための各種のコンポーネントやツールセットがいくつか追加されました。最後にこれらについて解説して、4 回にわたるエントリを締めくくっていきたいと思います。
今回のエントリで解説する内容は以下の 3 つです。
- XML Web サービス呼び出しの非同期処理化
- WCF サービス呼び出しの非同期処理化
- BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化
なお、本エントリでは基本的な XML Web サービスの作り方・使い方に関する解説は行いません。*.asmx による XML Web サービス開発をご存じない方は、一般的な書籍や Web の情報などを参照してみてください。また、今回のサンプルコードはこちらになります。
では、順番に解説していきましょう。
[XML Web サービス呼び出しの非同期処理化]
ASP.NET 2.0 XML Web サービス(*.asmx)に対する Web サービス参照(.NET Framework 2.0 ベースのプロキシクラス)には、非常に簡単に XML Web サービス呼び出しを非同期処理化できる機能が備わっています。ここではこの機能を使って、長時間を要する XML Web サービス呼び出しを行う Windows フォームアプリケーションを開発してみます。
まず、新規に Windows フォームアプリケーションを作成し、そこに Web サイトプロジェクトを追加します。
次に、*.asmx ファイルを使って XML Web サービスを作ります。長時間呼び出しをシミュレートしたいので、Thread.Sleep() 命令を使って 5,000msec だけ待機するように実装しておきます。
1: <%@ WebService Language="C#" Class="WebService" %>
2:
3: using System;
4: using System.Web;
5: using System.Web.Services;
6: using System.Web.Services.Protocols;
7:
8: [WebService(Namespace = "http://tempuri.org/")]
9: [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
10: public class WebService : System.Web.Services.WebService {
11:
12: [WebMethod]
13: public string GetMessage(string name) {
14: System.Threading.Thread.Sleep(5000);
15: return "Hello World " + name;
16: }
17: }
18:
実装が終わったら、Windows フォームアプリケーション側で XML Web サービス参照を作成します。なお、Visual Studio 2008 を利用している場合、既定では .NET Framework 3.0 ベースの WCF サービスプロキシが作成されてしまいます。このため、「サービス参照の追加」→「詳細設定」→「Web 参照の追加」を選択しして、.NET Framework 2.0 ベースのプロキシクラスを作成する画面を表示し、ここでプロキシクラスを作成してください。
プロキシクラスを作成したら、ボタンやテキストボックスなどを貼り付けて画面を作成し、いったんコンパイルを行います。すると、ツールボックス上に、XML Web サービスプロキシのクラスが現れますので、これを当該画面上に貼り付けます。
そののち、以下の 2 つのイベントハンドラを記述します。
- button1_Click イベントハンドラ
ボタンが押されたときに発生するイベントハンドラ。画面に貼り付けた Web サービスプロキシ(webService1)を使って、XML Web サービスの非同期呼び出しを開始させる。
- webService1_GetMessageCompleted イベントハンドラ
非同期呼び出しが終了した場合に呼び出されるイベントハンドラ。ここに、XML Web サービス呼び出しが終わったときの処理(画面表示など)を書く。
※ 後者のイベントハンドラについては、プロパティ画面の上の方にあるイナズママークをクリックした上で、プロパティ画面内の「GetMessageCompleted」の項目をダブルクリックすると、作成することができます。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: // 二重押し防止のためのコード
4: button1.Enabled = false;
5: textBox1.Enabled = false;
6: // XML Web サービスの非同期呼び出し
7: webService1.GetMessageAsync(textBox1.Text);
8: }
9:
10: private void webService1_GetMessageCompleted(object sender, WindowsFormsApplication1.localhost.GetMessageCompletedEventArgs e)
11: {
12: // 終了結果取り出し(XML Web サービス呼び出し中に例外が発生した場合には、e.Resultプロパティにアクセスした際に例外が発生)
13: string result = e.Result;
14: // 結果表示
15: label1.Text = result;
16: button1.Enabled = true;
17: textBox1.Enabled = true;
18: }
さらに、Program.cs ファイルに集約例外ハンドラを記述します。
1: static class Program
2: {
3: [STAThread]
4: static void Main()
5: {
6: Application.EnableVisualStyles();
7: Application.SetCompatibleTextRenderingDefault(false);
8:
9: Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
10: Application.Run(new Form1());
11: }
12:
13: static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
14: {
15: MessageBox.Show("システムエラーが発生しました。アプリケーションを終了します。");
16: // 通常は例外ロギングも併せて実施
17: Application.Exit();
18: }
19: }
以上で作業は終了です。実行してみて、以下のような挙動をすることを確認してみてください。
- XML Web サービスの呼び出し中に、ウィンドウがフリーズしない(きちんとドラッグできる)。
- アプリケーション起動後に ASP.NET 開発サーバを終了させて、ボタンを押下すると、ちゃんと集約例外ハンドラがフックされる。

← XML Web サービスを呼び出せなかった場合
内部動作の概念図を下に示します。この処理のキーポイントは、UI スレッドへの戻りが自動的に行われる、という点です。webService1.GetMessageAsync() メソッドにより、Web サービス呼び出し自体は背後のスレッド(具体的にはプールスレッド)上で行われますが、
- 呼び出しが正常終了した場合に呼び出される webService1_GetMessageCompleted() イベントハンドラは、UI スレッド上で呼び出される。このため、このイベントハンドラ内では自由に UI コントロールを操作してよい。
- 呼び出しが正常終了せず例外が発生した場合でも、webService1_GetMessageCompleted() イベントハンドラが UI スレッド上で呼び出される。そして、e.Result プロパティにアクセスした瞬間に、発生した例外がリスローされる。
という挙動をします。
この挙動の中でも後者は非常に上手いところで、このような機能があるため、特に追加のコードを書かなくても、XML Web サービス呼び出し中に発生した例外を、Application.ThreadException 集約例外ハンドラで捕捉することができます。よって、上記のようなコードだけで、XML Web サービス呼び出しを非同期化することができる(背後のタスクスレッド上で動かすことができる)のです。
※ (注意) ただし、この実装方法では、XML Web サービス呼び出しをキャンセルすることはできません。一応 XML Web サービスプロキシには .CancelAsync() というメソッドがあるものの、これは「まだ未送信状態だったら呼び出しを取り消す」というものです。このため、実際にタスクスレッドで XML Web サービス呼び出しが行われてしまった後に .CancelAsync() したところで、行われてしまった呼び出しは取り消せません(=確実な呼び出し取り消しができるメソッドではありません)。もともとこの問題は、タスクスレッドを使っている以上は原理的に発生するものなので、設計時に注意しておくことが必要です。
※ (注意&参考) また、本題からは若干それますが、プロキシクラスを画面に貼り付けて利用する場合は、URL プロパティを構成設定ファイルから自動的に読み取らなくなってしまうため、下図のようにして明示的に紐付けを行ってください。(プロキシクラスのコード生成ツールとの兼ね合いで発生するトラブルのようです。明示的に紐付けすればきちんと読み取るようになります。)
[WCF サービス呼び出しの非同期処理化]
では今度は、同じことを .NET Framework 3.0 ベースの WCF プロキシクラスで行ってみましょう。話を簡単にするために、サーバ側は上記のサンプルと同じく、*.asmx を使うことにして、クライアント側に(サービス参照の追加機能を利用して) WCF のプロキシクラスを作成します。

作成したプロキシクラスは(先と異なり)フォーム上に貼り付けることはできません。しかし、以下のようなコードを書くことで、先ほどと同じようにコーディングすることができます。
このように、WCF プロキシクラスの場合には、画面上に貼り付けることはできないものの、きちんと UI スレッド上で呼び出し終了イベントハンドラを呼び出してもらうことができます。
※ (注意) .NET Framework 2.0 ベースの ASP.NET XML Web サービスプロキシの場合には、画面上に貼り付けなければなりません。コード上で Completed イベントハンドラの登録を行うと、UI スレッドへの戻りが発生しないため、注意してください。
さて、ここまで Web サービス呼び出しを非同期化する方法について解説してきましたが、最後に、より一般的なタスクを簡単に非同期化する方法について解説します。
[BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化]
ここまでの解説からわかるように、Windows フォームにおけるマルチスレッドアプリケーションの難しさは、UI スレッドとタスクスレッド間での連携によるところが大きいです。この連携処理を簡素化するために .NET Framework 2.0 で導入されたのが、ここで解説する BackgroundWorker コンポーネントです。この BackgroundWorker コンポーネントは、UI スレッドとタスクスレッド(プールスレッド)との間の協調連携動作を支援するコンポーネントとして機能します。概念図を下に示します。
この概念図だけだとわかりにくいと思いますので、実際に BackgroundWorker コンポーネントを使って、長時間処理を背後で行う以下のようなアプリケーションを作ってみることにしたいと思います。
具体的な実装手順は、以下の通りです。(何をやっているのかをわかりやすく示すため、Step by Step で実装していきます。)
① UI の作成
- まずは画面上に 2 つのボタン、ラベル、プログレスバーを置きます。
- それぞれのボタンに、btnStart, btnCancel と名前をつけ、キャンセルボタンの Enable プロパティを false にしておきます。
- 画面上に、BackgroundWorker コンポーネントを貼り付けます。
② 長時間処理の作成
- btnStart_Click() イベントハンドラを作り、ここに、BackgroundWorker コンポーネントに対して非同期処理を開始する指示を出すコードを記述します。
- 次に、backgroundWorker1_DoWork() イベントハンドラを作り、ここに実際の長時間処理を記述します。
- 最後に、backgroundWorker1_RunWorkerCompleted() イベントハンドラを作り、ここに終了後の処理を記述します。
実際の処理の流れを以下に示します。重要なのは、UI スレッド → プールスレッド → UI スレッドの流れが自動的に制御される、という点です。従来だと、自力で .BeginInvoke() などを記述しなければなりませんでしたが、そうした処理はすべて BackgroundWorker が肩代わりしてくれます。
③ 起動パラメータと処理結果の引き渡し
さて、上記のサンプルだと、タスクスレッドの起動パラメータの受け渡しや、タスクスレッドの処理結果の受け取りがありません。これらのコードを追加すると、以下のようになります。
(30msec × 321 回なので 10 秒ぐらいかかります)
④ 進捗状態表示機能の追加
では次に、進捗状態を UI 上に表示する機能を追加します。進捗状態は、プールスレッドから UI スレッドへの通知が必要ですが、これを行うために、以下の 2 つの作業を行います。
- backgroundWorker1 の WorkerReportsProgress プロパティを true に変更する。
- backgroundWorker1_DoWork() メソッドの中に、進捗報告のためのコードを追加する。(backgroundWorker1.ReportProgress() メソッド)
- backgroundWorker1_ProgressChanged() イベントハンドラを追加し、UI に表示する。
このようにすると、進捗状態が UI に表示されるようになります。
ここで注意していただきたいのは、プールスレッドで動作している backgroundWorker1_DoWork() メソッドから、UI 更新を行う backgroundWorker1_ProgressChanged() メソッドを直接呼び出しているわけではない、という点です。
- プールスレッドからは、backgroundWorker1 の .ReportProgress() メソッドを叩き、backgroundWorker1 にスレッド同期を依頼する。
- backgroundWorker1 は、UI スレッド上で backgroundWorker1_ProgressChanged イベントハンドラを呼び出すように、内部で .BeginInvoke() 命令を利用する。
ここでもう一度、最初に示した内部動作の模式図を示します。
最初からの流れをもう一度追いかけてみると、
- BackgroundWorker コンポーネントを用いたタスクスレッドの起動
① UI スレッドから BackgroundWorker コンポーネントの RunWorkerAsync() を叩く
② BackgroundWorker が自動的にプールスレッドに処理開始要求を投入する
③ その結果、DoWork イベントハンドラに登録されたメソッド(backgroundWorker1_DoWork() メソッド)が、プールスレッド上で起動する
- BackgroundWorker コンポーネントを用いた進捗状況の UI への通知
① プールスレッドから適当なタイミング(なるべく頻繁に)で BackgroundWorker コンポーネントの ReportProgress() メソッドを叩く
② BackgroundWorker は、内部で .BeginInvoke() を行い、メッセージキューにメッセージを投入
③ その結果、ProgressChanged イベントハンドラに登録されたメソッド(backgroundWorker1_ProgressChanged() メソッド)が、UI スレッド上で起動する
- BackgroundWorker コンポーネントを用いた終了通知
① プールスレッド上で、backgroundWorker1_DoWork() メソッドが終了する
② BackgroundWorker は、内部で .BeginInvoke() を行い、メッセージキューにメッセージを投入
③ その結果、RunWorkerCompleted イベントハンドラに登録されたメソッド(backgroundWorker1_RunWorkerCompleted() メソッド)が、UI スレッド上で起動する
となります。つまり、UI スレッドとプールスレッドの橋渡しを、BackgroundWorker コンポーネントが行ってくれている、ということになるわけです。
改めて、どの処理がどのスレッド上で動作するのかをまとめてみると、
- BackgroundWorker コンポーネント上の各メソッドをどのスレッド上で叩くか?
① BackgroundWorker.RunWorkerAsync() メソッドは、UI スレッド上から叩く。
② BackgroundWorker.ReportProgress() メソッドは、プールスレッドから叩く。
③ BackgroundWorker.CancelAsync() メソッド(後述)は、UI スレッド上から叩く。
- BackgroundWorker コンポーネントに登録したイベントハンドラはどのスレッド上で動くか?
① DoWork イベントに登録したハンドラは、プールスレッド上で動く。(=UI 操作不可)
② ReportProgress イベントに登録したハンドラは、UI スレッド上で動く。(=UI 操作可)
③ RunWorkerCompleted イベントに登録したハンドラは、UI スレッド上で動く。(=UI 操作可)
ということになります。
では最後に、キャンセル処理についても実装してみましょう。
⑤ キャンセル機能の追加
Part 3. で述べたように、UI スレッドからタスクスレッドを強制的に停止させることはできないため、キャンセル処理は「UI スレッドからフラグを立てる」「タスクスレッドからフラグをチェックして自主的に止まる」ことになります。具体的には、以下の実装を行います。
- backgroundWorker1 の WorkerSupportsCancellation プロパティを true に変更する。
- btnCancel_Click() メソッドに、backgroundWorker1.CancelAsync() メソッドを呼び出す処理を記述する。
- backgroundWorker1_DoWork イベントハンドラ内(タスクスレッドの長時間処理の中)に、キャンセルフラグを(なるべく頻繁に)チェックする処理を入れる。
追加されたコードは赤字部分です。ここまでの解説が理解できていれば、容易に理解できるのではないかと思います。
※ ちなみに実際に実行すると、キャンセルボタンを押した直後にプログレスバーが停止しませんが、これは Vista 以降でのコントロールのアニメーションの変更によるもの(アニメーションの遅延により発生する)です。XP などで実行すると、停止したタイミングでぴたっと止まります。
※ あと、書き忘れましたが、タスクスレッド上の例外処理についても書く必要がありません。タスクスレッド上で未処理例外が発生した場合には、RunWorkerCompleted イベントハンドラにて、e.Result で結果を取り出す際に例外がリスローされるため、特に例外処理のコードを追加しなくても、上のコードのままで集約例外ハンドラで例外を捕捉することができます。
このように、BackgroundWorker コンポーネントを利用すると、UI スレッド ⇔ タスクスレッドのスレッドスイッチに関連する処理を書く必要がなくなり、コードもかなりすっきりします。しかし、どの処理がどのスレッド上で動作しているのかを正確に理解しないと、非常に危険であるのも確かです。先に示した動作模式図を意識しながら、アプリケーションコードを記述するようにしてください。
[本エントリのまとめ]
では最後に、本エントリのまとめです。
- ASP.NET 2.0 XML Web サービスのプロキシクラスは、フォーム画面上に貼り付けて使うことにより、Web サービス呼び出し処理を非同期処理化できる。
- WCF サービスのプロキシクラスは、非同期処理メソッドを追加して使うことにより、呼び出し処理を非同期処理化できる。
- 一般的なタスクについては、BackgroundWorker コンポーネントを使うことで非同期処理化ができる。
というわけで、4 回に渡ってマルチスレッドアプリケーションの開発手法について解説してきましたが、総じて言えば、
マルチスレッドアプリケーションを書くのはかなり難しい;。
ということになります。正しい知識を持って記述しないと、とにかくトラブルを引き起こしがちな技術になりますので、記述するのであれば十分な知識を持った上で、正しく記述するように心掛けていただければと思います。
※ なお、今回は Windows フォームに限定して解説を進めてきましたが、WPF や Silverlight にも同様な UI スレッド制限があります。WPF などを利用する場合には、こちらの MSDN マガジンのエントリなどを参考にしながら開発を進めていただければ幸いです。
-
さて、前回の Part 2. のエントリでは、タスクスレッド(UI の背後で動作させる処理を動作させるスレッド、すなわちマニュアルスレッドやプールスレッドの総称)の様々な起動方法について解説しました。主な方法として、以下の 4 つの方法がありました。
- マニュアルスレッドの新規作成
- プールスレッドの利用
- 非同期デリゲートの利用
- タイマの利用
さて、いずれの方法を利用する場合であっても守る必要のあるルールとして、UI スレッド以外から UI コントロールの読み書きをしてはならない、というものがありました。
ここまで実際にプログラミングをしてみた方は感じられていると思うのですが、実はこの制限はかなり厄介です。例えば、以下のような処理を簡単に書くことはできません。
- 背後で行っているタスク処理の進捗状況や完了結果を、UI 上に表示する。
(これはすでに解説済み。.BeginInvoke() 処理が必要。) - 背後で行っているタスク処理から、UI 上のコントロールのプロパティを読み出す。
(タスクスレッドから、textBox1.Text プロパティを読み出すことは NG だがどうすればよい?) - 背後で行っているタスク処理を、UI 上のキャンセルボタン押下により中断させる。
(どうやって UI のイベントハンドラから背後のスレッドに通知を行えばよいのか?) - 背後で行っているタスクスレッド上で発生した例外の情報を、UI 上に表示する。
(どうやってタスクスレッドで発生した未処理例外を UI に通知すればよいのか?)
マルチスレッド処理を行う Windows フォームアプリケーションを書く上では、上記のような問題が発生したときの対処方法を知っておく必要があります。本エントリでは、これらについて解説します。
- タスクスレッドからの UI 画面の更新方法
- タスクスレッドからの UI 画面上のデータの読み取り方法
- UI 画面からのタスクスレッドの制御方法
- タスクスレッド上で発生した未処理例外の取り扱い方法
本エントリのサンプルコードはこちらになります。
では、以下に順に解説していきます。
[タスクスレッドからの UI 画面の更新方法]
さて、まずはおさらいです。タスクスレッド(マニュアルスレッドやプールスレッド)から UI 画面を更新する際には、タスクスレッドからコントロールのプロパティを直接いじってはいけません。このような場合には、BeginInvoke() 命令を利用し、メッセージキューに処理要求を投入する必要があります。具体的な作業手順は以下の通りです。
- UI 画面更新用の private メソッドを作成する。
画面更新に必要なデータをパラメータとして受け取るように書いておきます。(注意点は後述) - それにあわせた形で private デリゲートを定義します。
名称は任意ですが、××Delegate といった名前で定義しておくと便利です。 - タスクスレッドから BeginInvoke() 命令でメソッド呼び出しを実施する。
パラメータは object[] 配列として引き渡します。
なお、.BeginInvoke() 命令はどのコントロールに対して発行しても OK ですが、通常は親フォームオブジェクト(= this)に対して発行するのが便利です。
なお、関連する注意点として、以下についても知っておくと便利です。
① 現在のスレッドが UI スレッドかどうかは、InvokeRequired プロパティでチェックすることができる。
例えば上記の LongTask() メソッドは、マニュアルスレッド上だけでしか動作させられないかというとそんなことはなく、書き方や呼び方次第では、プールスレッド上や UI スレッド上で動作させることもできます。このため、あるメソッドが UI スレッド上で呼び出されることも、マニュアルスレッドやプールスレッド上で呼び出されることも、どちらもありうるような場合には、.InvokeRequired プロパティを確認することで、UI スレッド上で動作しているか否かを確認することができます。
ただし実際には、そもそも上記のようなチェックが必要とならないようにすることが望ましいです。ここまで取り扱ってきたサンプルは、必ず、あるメソッドはマニュアル/プール/UI のどれかの上でしか動作しないように設計してきました。実際、UI スレッドで行うべき処理とタスクスレッドで行うべき処理は異なっているのが当然で、UI スレッド/タスクスレッド両用になるようなメソッドというのは、(ユーティリティ的な処理を除けば)あまりないはずです。
UI フォームを設計・実装する際には、どのメソッドがどのスレッド上で動くのかを明確に意識しながら作業することが非常に重要です。InvokeRequired プロパティを使わなければならなくなった場合には、そもそもアプリケーションの設計としての是否をきちんと確認するようにしてください。
② 基本データ型及びイミュータブルオブジェクト以外を引き渡す場合には、オブジェクトの同期制御が必要になる。
UI スレッドからタスクスレッドを起こす場合でも、またタスクスレッドから UI スレッドへ制御を戻す場合でも、どちらでも共通する内容ですが、一般に、オブジェクトを引数として渡す場合には、参照渡しが行われます。例えば、下図のように、タスクスレッドから UI スレッドに StringBuilder の変数を引き渡すと、同一インスタンスがマニュアルスレッドと UI スレッドの両方から操作されることになります。
当然ですが、このような処理を行うと、同一インスタンスを同時に複数スレッドから操作することになり、データが破損することになります。よって、このようなコードは書いてはいけません。StringBuilder のインスタンスに対して同期制御を行うか、インスタンスをコピー(ディープコピー)して渡す必要があります。
このことからわかるように、UI スレッドとタスクスレッド間で、引数として基本データ型やイミュータブルオブジェクト以外のデータを引き渡す場合には、オブジェクトの同期制御が必要になります。(データ変数の同期制御に関しては、こちらとこちらのエントリに詳しく書いてありますので、よくわからないという方は確認してみてください。)
では引き続き、タスクスレッドからの UI 画面上のデータの読み取り方法について解説します。
[タスクスレッドからの UI 画面上のデータの読み取り方法]
上記では、タスクスレッドから UI 画面上へのデータ表示について考えたわけですが、実は、タスクスレッドから UI 画面上のデータを読み取ることも NG です。
UI コントロールを操作するためには、UI スレッドからの操作が必要になり、そのためにはメッセージキューへのメッセージ投入が必要....なのですが、ここまで解説してきた BeginInvoke() 命令では、メッセージを投入した後「やりっぱなし」の状態になってしまい、コントロールから値を読み取るなどした後の結果値を受け取ることができません。このような場合には、.Invoke() 命令を利用します。これを利用すると、UI スレッド上で行われた処理の結果を、タスクスレッド側で受け取ることができます。具体的な実装例を以下に示します。
なお、この .Invoke() 命令による、「UI スレッド上での処理結果の受け取り」はむやみに利用しないようにしてください。.BeginInvoke() 命令と異なり、.Invoke() 命令は、投入したメッセージが UI スレッド上で処理されるのを同期的に待つ(=タスクスレッド側は UI スレッド上での処理の終了を待機する)形になります。このため、以下のようなコードを書くとハングアップする危険性があります。
- 例1. Invoke() 命令で呼び出したメソッド内から、さらに別のスレッドを起こす ⇒ スレッドプールの枯渇や無限ループの発生
- 例2. 共有データに対して lock を取得している最中に Invoke() 命令を利用し、呼び出し先で当該データを操作 ⇒ デッドロックの発生
このようなことから、以下のルールを守ることをおすすめします。
- .Invoke() 命令は極力使わないようにする。
- 使う場合であっても、UI 上のプロパティを読み取ったらすぐに終了する、といった極力単純なコードのみを書くようにする。
- ロックを取得している最中に .Invoke() を呼び出さない。
では引き続き、UI 画面からのタスクスレッドの制御方法について解説します。
[UI 画面からのタスクスレッドの制御方法]
タスクスレッド上で時間を要する処理を起動した場合、UI からそのタスクを制御したいと思うことがよくあります。例えば、
- キャンセルボタンを押したら、背後で行っている処理を中断したい。
- チェックボックスやラジオボタンなどを押すと、背後で行っている処理が変更されるようにしたい。
といったことはしばしばあります。しかし、このような処理を作り込むのは意外に厄介です。なぜなら、UI からタスクスレッドで行われている処理に対して、割り込みをかけるような形で強制通知を行うことはできないからです。
例えば、以下のような画面を作る場合を考えてみます。
タスクスレッドでの処理を止めるため、キャンセルボタンを押したら、背後で行っている処理が中断されるように、Thread.Abort() 命令を使ったとします。この機能を使うと、タスクスレッドの処理を強制的に中断することができますが、そのスレッドがどんな状態であるかを全く無視して強制中断してしまうため、場合によっては共有データ変数の破損などの問題を引き起こすリスクがあります。このため、タスクスレッドへ強制的に割り込みをかけるような形で処理をキャンセルさせることはできないのです。
このような場合には、以下のような解決が必要になります。
- 状態を保存しておく共有データ変数を用意しておく。
- UI からタスクスレッドへ通知したい場合、共有データ変数に格納する。
- タスクスレッドが定期的に(=タスクの節目節目で)共有データ変数をチェックし、それに従った動作をする。
具体的な内部設計図を下図に示します。
実装コードを以下に示します。
コード上、特に注目していただきたいのが、共有データ領域の使い方です。UI スレッドから、強制的にタスクスレッドの処理に割り込みをかけることはできません。このため、button2_Click() メソッドでは、フラグ情報を立てておくだけにとどめます。一方、タスクスレッド側は、なるべく頻繁にこのフラグをチェックし、そのフラグに応じて処理をキャンセルしたり挙動を変更したりします。
このように、UI からタスクスレッドへ何かしらの通知を行う、というのは意外に実装が厄介なものである、ということを覚えておいてください。再度ポイントをまとめると、以下のようになります。
- 状態を保存しておく共有データ変数を用意しておく。
- UI からタスクスレッドへ通知したい場合、共有データ変数に格納する。
- タスクスレッドが定期的に(=タスクの節目節目で)共有データ変数をチェックし、それに従った動作をする。
なお、UI からタスクスレッドへの通知に関してもう一点注意していただきたいのが、Windows フォームのクローズ(アプリケーションの終了)に伴う動作です。Windows フォームの代表的なプロセス終了命令には、以下の 2 種類があり、それぞれ内部動作が異なります。
- Application.Exit() 命令
すべてのメッセージループを停止し、アプリケーションウィンドウを閉じ、メインスレッドの Application.Run() 命令を終了する。この際、フォアグラウンドスレッドが残留しているとプロセスが終了しない。 - Environment.Exit() 命令
強制的にプロセスを切り落とす。この際、フォアグラウンドスレッド、バックグラウンドスレッドの残留状態を問わず、プロセスが強制終了する。
後者の Environment.Exit() 命令はアプリケーションの強制終了に相当するものですから、通常、Windows フォームのアプリケーションを穏やかに終了させるには、前者の Application.Exit() 命令を利用します(この動作はフォームのクローズ時も同様です)。
しかし、Application.Exit() 命令でアプリケーションを終了させる際に問題になるのが、以下の 2 つです。
- フォアグラウンドスレッドが残っていると、プロセスが終了しない。
- バックグラウンドスレッドについては、強制終了となる。
ここまで、フォアグラウンドスレッドとバックグラウンドスレッドについての解説をしてきませんでしたので、まずこれについて解説します。.NET アプリケーションで取り扱う各スレッドには、フォアグラウンドスレッドか、バックグラウンドスレッドかのマーキングを行うプロパティ .IsBackground が存在します。そして、
- フォアグラウンドとマークされたすべてのスレッドが終了すると、CLR はバックグラウンドスレッドが残っていても当該プロセスを終了します。
- フォアグラウンドとマークされたスレッドが一つでも残っていると、Main() メソッドが終了しても CLR は当該プロセスを終了しません。
という挙動をします。例えば、下記のコンソールアプリケーションのサンプルを見てください。
このサンプルにおいて、
- バックグラウンド化した上でマニュアルスレッドを起動すると、Main() 関数が終了した直後にプロセスが終了します。
- しかし、バックグラウンド化せずに(t.IsBackground = false; として)マニュアルスレッドを起動すると、Main() 関数が終了しても、プロセスが終了しません。
両者の動作を以下に示します。
バックグラウンド化した場合
フォアグラウンドで動作させた場合
デフォルト状態での各スレッドの状態は、以下の通りです。
- Main() 関数を動作させるスレッド(メインスレッド) → フォアグラウンド
- マニュアルスレッド → フォアグラウンド
- プールスレッド → バックグラウンド
つまり、Windows フォームアプリケーションでマルチスレッドアプリを作成する場合、うかつにマニュアルスレッドを使うと、Application.Exit() 命令ではプロセスが終了しなくなります。
例えば、以下のようなアプリケーションを作ってみます。
※ ここでは、UI がなくなってもプロセスが残留することを簡単に示すために、DebugView を使います。このツールの詳細はここでは解説しませんが、簡単にいうと、System.Diagnostics.Debug.WriteLine() 命令によって出力したデータを、外部で簡単に参照できるようにしたツールです。Web アプリケーションなどの UI を持たないツールにおいて、内部動作を簡単に表示・モニタできるというメリットがあって便利です。ここからダウンロードできます。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: Thread t = new Thread(new ThreadStart(Task));
4: t.Start(); // バックグラウンド化せずに起動する
5: }
6:
7: private void Task()
8: {
9: while (true)
10: {
11: Thread.Sleep(300);
12: System.Diagnostics.Debug.WriteLine("タスク実行中...");
13: }
14: }
15:
16: private void button2_Click(object sender, EventArgs e)
17: {
18: Application.Exit();
19: }
フォアグラウンドでマニュアルスレッドを起動しておくと、フォームを閉じてもアプリケーションプロセスは終了せず、デバッガに情報を出力し続けます。(タスクマネージャを見ると、フォーム画面は消えているのにフォームの .exe プロセスが残っていることが確認できます。)
つまり、Windows フォームを適切に終了させるためには、残留しているフォアグラウンドスレッドをすべて終了させなければならない、ということになります。このことから、Windows フォームアプリケーションの開発に関する推奨事項として、次のようなことが言えます。
- 基本的に、タスクスレッドは「いつ強制終了されても困らない」ような防御的プログラミングをしておいた上で、さらにバッグラウンド化(.IsBackground = true)しておくことが望ましい。
「防御的プログラミング」というのは、「強制終了などのトラブルが起こったとしても、問題が起こらないようにプログラミングしておく」ことを指します。具体的には、以下のようなプログラミングの工夫を指します。
- ローカルキャッシュファイルが破損していた場合には、自動的に最新データをサーバから取り寄せる。
- ローカル設定ファイルが破損していた場合は、デフォルト値を利用する。
※ なお、このような防御的プログラミングの考え方は、Windows フォームアプリケーションでは特に重要です。というのも、Windows フォームアプリケーションは、「閉じる」ボタンなどで簡単に終了できるのはもちろんのこと、タスクマネージャからの強制終了や Windows OS シャットダウンなど、「完全な後片付け」ができないまま強制的に切り落とされるケースが多数考えられるからです。このため、Windows フォームアプリケーションでは『強制的に切り落とされても問題なく再起動ができる』形で実装することが望ましいといえます。タスクスレッド上の処理に限らず、「再起動時に何らかのローカルキャッシュデータファイルなどが破損していても自動復旧できる」ような設計をしておくことは非常に重要である、ということを覚えておいてください、とつぶやいておく^^。
では最後に、タスクスレッド上で発生した未処理例外の取り扱い方法について解説します。
[タスクスレッド上で発生した未処理例外の取り扱い方法]
一般的に、Windows フォーム上で発生した未処理例外に関しては、集約例外ハンドラを利用した後処理を行います。Windows フォームにおける集約例外ハンドラの書き方についてはこちらのエントリに記述しましたが、Application.ThreadException イベントハンドラで捕捉される例外は、メッセージループが捕捉した例外、すなわち UI スレッド上で発生した例外に限定されます。このため、マニュアルスレッドやタスクスレッド上で発生した未処理例外については、適切なハンドリングが必要です。これを怠ると、以下のような問題が発生します。
- タスクスレッドが例外によって終了してしまったにもかかわらず、タスクスレッドの正常終了を UI 側が永遠に待ち続けてしまう。
- タスクスレッド上で発生した例外情報がロギングされなかったため、障害解析ができなくなってしまう。
これらの問題を解決するため、以下のような対処を行います。
- AppDomain に対して集約例外ハンドラを仕掛けておく。
- タスクスレッド上で発生した例外を UI スレッド上でリスローする。
これらについて解説します。
AppDomain に対して集約例外ハンドラを仕掛けておく
CLR 上でアプリケーションを動作させる場合、プロセス内にはアプリケーションドメイン(AppDomain)と呼ばれる論理区画領域が作成されます。この AppDomain の UnhandledException イベントをフックすると、マニュアルスレッド上の未捕捉例外をすべて捕捉することができます。通常は、Application.ThreadException イベントハンドラも利用しますので、以下のようなコードを書くことになります。
ただし、この方法には以下の 2 つの難点があります。
- UI 通知ができない。 (タスクスレッド ≠ UI スレッドであるため)
- プールスレッド上で発生した例外を補足できない。
後者は特に大きな問題です。このため、実際には以下に述べる例外のリスローを使って、問題を解決することが望ましいです。これについて解説します。
タスクスレッド上で発生した例外を UI スレッド上でリスローする
そもそも UI 表示は UI スレッド上でしか行えない、という前提条件を考えると、タスクスレッドで発生した例外を UI 上で通知するためには、例外オブジェクトを UI スレッド側に伝搬する必要があります。そこで、タスクスレッド上で例外が発生した場合には、この例外オブジェクトを .BeginInvoke() 命令で UI スレッド側に伝搬し、UI スレッド上でそれをリスローします。
もともと UI スレッド上で発生する例外に関しては、Application.ThreadException イベントハンドラの集約例外ハンドラで捕捉することができますので、この方法を利用すれば、すべての例外を UI 側の集約例外ハンドラで補足することができます。
例えば、XML Web サービスの呼び出し処理をタスクスレッドに切り出す場合について考えてみます。XML Web サービス呼び出しに失敗すると、例外が発生します。このような例外をキャッチし、UI スレッドに伝搬すれば、集約例外ハンドラ(Application.ThreadException イベントハンドラ)でまとめて処理することができます。
# ちなみに、この場合の例外処理(try-catch)は、タスクスレッド上で発生したすべての例外をことごとく捕捉する必要がありますので、全体を大きく囲むことになります。この try-catch は、「例外→業務エラーの変換のための try-catch」でも「リソース解放のための try-finally」でもないので、注意してください。
[今回のエントリのまとめ]
というわけで、今回のエントリのキーポイントをまとめると、以下のようになります。
- タスクスレッドから UI 画面を更新したい場合には、.BeginInvoke() 命令を使って、メッセージキューにメッセージを投入する。
- タスクスレッドから UI 画面上のデータを読み取りたい場合には、データを読み取って返すメソッドを作成しておき、これを .Invoke() 命令でタスクスレッドから同期的に呼び出す。ただし気をつけてプログラミングしないと、スレッドプールの枯渇やデッドロック問題を引き起こすので注意が必要。
- UI 画面からタスクスレッドを直接操作することはできない。共有データ変数領域を用意しておき、① UI スレッドからはこの共有データ変数領域にフラグを立てる。② タスクスレッド側ではこの共有データ変数領域を頻繁にチェックする。という方法で間接的に制御を行う。
- タスクスレッド上で発生した未処理例外を取り扱うため、タスクスレッド全体を try-catch で囲む必要がある。捕捉した例外は、.BeginInvoke() 命令で UI スレッドに搬送し、リスローする。
さて、ここまで読んでみていただいて、
ちょーめんどい;。
と思われた方も多いと思います。はい、私もそう思います;;;。だったら説明するなー!と言われそうですが;、実際、マルチスレッドアプリケーションというのは、思っているよりも遥かに実装が厄介なものです。例えばボタンが押されたら XML Web サービスを呼び出すアプリケーションを作ろう、と思った場合、
同期型で実装する場合には、たったの 4 行で話が済みます。
しかし、これをタスクスレッドに切り出す場合、以下のようなポイントに対する考慮が必要になります。
- ボタンの二重押しの防止
- ステータスの表示
- タスクスレッドから UI スレッドへの書き戻し
- 集約例外処理
結果、以下のような膨大なコードになります。
なので、やはり個人的におすすめしたいのは、
マルチスレッドアプリケーションはなるべく作らない。
これに尽きます。って、まるでここまでの解説すべてを放り出すような発言ですが;;;、ここまでの延々とした説明をきちんと理解した上でコーディングをしようと思うと、相当大変であることは容易にご理解いただけるのではないでしょうか。(というか正直言ってこのエントリ書いてる自分もサクサク書けません、とても;。)
Visual Studio 2008 を利用すると、こうした問題を多少緩和するために、いくつかの機能が利用できるようになるのですが、そうはいってもここまでの解説をきちんと理解した上でないと、やはりそうしたウィザード類の利用もやはり危険です。Part 4. では Visual Studio 2008 の機能について解説しますが、Part 3. までの内容をよく理解した上で利用していただくことをお勧めします。
-
さて、前回のエントリでは、Windows フォーム内部におけるスレッドの構成や、メッセージループの働きなどについて解説しました。中でも重要なこととして、以下のようなキーポイントがありました。
- UI スレッド上で、長時間処理を動かしてはならない。
長時間処理は、マニュアルスレッドやプールスレッドなどの、他のスレッドに切り出す。 - UI スレッド以外から、UI コントロールを触ってはいけない。
マニュアルスレッドやプールスレッド上から、UI コントロールを読み書き・操作してはいけない。
上記の 2 つの重要ルールについて、Part 2~4 にてより実践的な解説を行っていきます。
- Part 2. タスクスレッドの起動方法
まず、マニュアルスレッドやスレッドプールの起動方法について解説します。 - Part 3. タスクスレッドと UI の協調動作
マニュアルスレッドやプールスレッドから UI コントロールを操作したり読み書きしたりすることはできないため、その回避方法について解説します。 - Part 4. Visual Studio を使ったマルチスレッドアプリケーション開発
上記 Part 2, 3 の作業を簡素化するために用意されている、Visual Studio の機能について解説します。
まず本エントリでは、UI スレッドから切り離した処理を動かすために利用するマニュアルスレッドやプールスレッドのことを、タスクスレッドと呼ぶことにし、その作成方法について解説します。
- デリゲートとは何か
- マニュアルスレッドの新規作成
- スレッドプールへのワークアイテムの追加
- 非同期デリゲートの利用
- タイマの利用
なお、以降の説明では様々なスレッドの起動方法を解説していきますが、突き詰めると、タスクを動かすスレッドには、マニュアルスレッドかプールスレッドかのどちらかを使っています。ただ、その起動方法が様々な種類がある、というだけの話ですので、見かけの多様性に惑わされず、しっかり学習していただければと思います。(今回はサンプルらしいサンプルはないですが、一応くっつけておきます。)
では、順番に解説していきます。
[デリゲートとは何か]
マニュアルスレッドやプールスレッドの起動処理を記述する上で欠かせない技術のひとつが、デリゲートです。デリゲートとは、オブジェクトに対して、「関数や処理ロジック」を引数として渡す際に利用される技術であり、.NET Framework の基盤技術の一つになっています。
そもそも「関数」や「処理」を引き渡すイメージがわかない、という方も多いと思いますので、まずここで簡単に解説します。以降の解説は、スレッディングの話からはちょっとそれますが、非常に重要なので必ず理解してください。(※ すでにデリゲートをご存じの方は、この項目は飛ばして先へ進んでください)
まず、なぜ「関数」を引数として引き渡す必要があるのかを理解するために、「コレクションから、ある条件を満たすものだけを抽出する処理」を考えてみることにします。例えば、List<int>型(動的配列)に含まれる整数値から、3で割り切れるものだけを取り出す処理を行う場合、おそらく多くの方は以下のようなコードを記述すると思います。
1: List<int> data1 = new List<int>();
2: for (int i = 0; i < 100; i++) data1.Add(i);
3:
4: // 自力抽出方式
5: List<int> data2 = new List<int>();
6: foreach (int i in data1)
7: {
8: if (i % 3 == 0)
9: {
10: data2.Add(i);
11: }
12: }
13:
14: foreach (int i in data2) Console.WriteLine(i);
この処理方式は、イラストであらわすと次のように示されます。
この方式は、「ユーザーがコレクション内のデータを1つずつ取り出しては吟味し、手作業で移し変えていく」ようなモデルです。もちろん、この処理自体は正しく動作するのですが、そもそも「何らかの条件に基づいてデータの抽出を行う」という処理自体、非常によく出てくる処理です。
そこで、以下のようなモデルを取ることができないか否かを考えてみます。
つまり、List<int> 型のコレクションに対して、「3で割り切れるか否かを確認するロジック」(=抽出条件のロジック)を外部から与えて、これに基づいて、コレクションクラス自身がデータを自動抽出するようなモデルで実装できないか、と考えるわけです。
実は、.NET Framework の「デリゲート」と呼ばれる仕組みを利用すると、これが実現できます。具体的には、以下の通りです。
- List<int> 型には .FindAll() というメソッドが備わっている。
- このメソッドには、引数として、関数(ロジック)そのものを渡すことができる。
実装方法を以下に示します。
- まず、CheckData() 関数(引き渡すことになる抽出条件判定関数)を作成しておく。
- これを「デリゲート」と呼ばれるオブジェクト(ここではPredicateオブジェクト)にラッピングして 、引数として .FindAll() メソッドに引き渡す。
このようにすると、List<int> 型コレクション(data1)自身が、渡されたロジックに基づいて抽出処理を行い、抽出結果を返します。
1: static void Main(string[] args)
2: {
3: List<int> data1 = new List<int>();
4: for (int i = 0; i < 100; i++) data1.Add(i);
5:
6: // デリゲート(関数ポインタ)方式
7: List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
8:
9: foreach (int i in data2) Console.WriteLine(i);
10: }
11:
12: static bool CheckData(int i)
13: {
14: return (i % 3 == 0);
15: }
ここで重要なのは、以下のポイントです。
- 本来、引数として渡せるものは、文字列や数値といった「具体的なモノ」。
- しかし、デリゲートのインスタンスでラッピングすると、処理(=関数)を引き渡すことができる。
また、デリゲートで重要なもう一つのポイントは、あるデリゲートクラスがラッピングできる関数は、そのデリゲートが定義している引数/戻り値と完全に一致していなければならない、という点です。上記のサンプルの場合、Predicate<int> というデリゲートでラッピングできる関数は、int 引数ひとつを取り、bool 型を返すような関数に限られています。引数や戻り値の型などがひとつでもずれていると、そのデリゲートでのラッピングはできませんので、注意してください。
※ (注意) 上記のコードでは少し簡略化して書いていますが、デリゲートインスタンス(関数をラッピングしたもの)はそれ自体を変数によって保持することができます。例えば、上記のコードは以下のようにも書けます。(普通は面倒なのでまとめて一行で書いてしまいますが)
List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
↓
Predicate<int> d = new Predicate<int>(CheckData);
List<int> data2 = data1.FindAll(d);
※ (注意) .NET Framework 2.0 以降では、下記のように、デリゲートを使わずに、直接、関数名をメソッド引数として渡すことができるようなコードを書くことができるようになっています。しかし、これはコンパイラが自動的にコードを補正してくれるためで、内部的にはデリゲートにラッピングされたコードとしてコンパイルが行われます。この機能は実装時には便利なこともあるのですが、今回は簡略化コードを使わずに、きちんとデリゲートでラッピングしたコードを書いていくことにしたいと思います。
1: // .NET Framework 2.0 以降のコンパイラでは、下記のコードでもコンパイルが通るが...
2: List<int> data2 = data1.FindAll(CheckData);
3:
4: // 内部的には、下のようなコードに変換された上で、コンパイルされている
5: List<int> data2 = data1.FindAll(new Predicate<int>(CheckData));
では引き続き、タスクスレッドの作成方法について順番に解説していきます。
[マニュアルスレッドの新規作成]
まず、最も基本となる、マニュアルスレッドの新規作成方法について解説します。マニュアルスレッドを新規で作成するには、System.Threading 名前空間にある、Thread クラスのインスタンスを作成し、これを起動します。基本的な実装方法は、以下の通りです。
- マニュアルスレッド上で動作させたい処理を、引数なし、戻り値なしのメソッドとして作成する。
- このメソッドを ThreadStart デリゲートにラッピングして、Thread オブジェクトのコンストラクタに引渡す。
- スレッドインスタンスの .Start() メソッドをたたくと、新規にマニュアルスレッドが起動し、引き渡しておいたメソッドが起動する。
コードサンプルを以下に示します。
※ (注意) スレッドを起動する前に、t.IsBackground = true; という設定をしていますが、この設定を行うとこのスレッドがバックグラウンドスレッドとしてマークされます。Windows フォームアプリケーションを終了する際に利用する Application.Exit() 命令は、「すべてのメッセージループを停止し、アプリケーションウィンドウを閉じ、メインスレッドの Application.Run() 命令を終了する」というものですが、フォアグラウンドスレッドが残留しているとプロセスが終了しません。このため、マニュアルスレッドは、バックグラウンドスレッド設定をしてから起動することが望ましいと言えます。
なお、この ThreadStart デリゲートは、System.Threading 名前空間の下側に定義されており、引数なし、戻り値が void 型のメソッドをラッピングすることができるデリゲートになっています。よって、この方法では、UI スレッドからマニュアルスレッドへとデータを直接引き渡すことができません。もちろん、上記のコードに示したように、共有変数領域を作成しておき、この領域を使ってデータを引き渡すこともできますが、この方法の場合、UI スレッドとマニュアルスレッドが同時にこのデータを操作する危険性があるため、排他制御が必要になります。(スレッド間での処理競合については、以前のエントリ(こちらとこちら)を参考にしてください。)
この問題を解決するために、.NET Framework 2.0 で導入されたのが、ParameterizedThreadStart デリゲートです。以下に具体的な実装方法を示します。
- マニュアルスレッド上で動作させたい処理を、引数 object 型ひとつ、戻り値なしのメソッドとして作成する。
- このメソッドを、ParameterizedThreadStart デリゲートにラッピングして、Thread オブジェクトのコンストラクタに引き渡す。
- スレッドインスタンスの .Start() メソッドに、object 型のパラメータを一つ渡して叩くと、新規にマニュアルスレッドが起動する。
この方法を利用すれば、明示的にデータ変数を引き渡すことができます。なお、この方法で認められている、UI スレッドからマニュアルスレッドへ引き渡せるパラメータは object 型変数 1 つだけですが、object 型ですのでなんでも渡すことが可能です。(複数のデータ項目を引き渡したい場合には、構造体にまとめたり object[] 配列などにして、これを引き渡せばよい) データを受け取ったメソッド側では、これを元のデータ型にキャストしてから利用してください。
では次に、スレッドプールの使い方について解説します。
[スレッドプールへのワークアイテムの追加]
スレッドプールは、マニュアルスレッドと異なり、自力でスレッドを新規に作成して利用するというものではありません。以前のエントリで解説したように、すでに起動しているスレッドに対して、メソッドを引き渡して処理してもらう、という形になります。
具体的には、以下の作業を行います。
- まず、引数として object 型変数を一つ、戻り値として void 型となるメソッドを用意する。
- これを WaitCallback デリゲートに包んで、プールのキューに追加する。(ThreadPool クラスの QueueUserWorkItem() メソッドを利用する)
実装コードサンプルは以下の通りです。
なお、スレッドプールのワークアイテムキューに追加できるデリゲートは、WaitCallback デリゲートのみになっています。WaitCallback デリゲートは、object 型引数ひとつ、void 型戻り値を持つメソッドしかラッピングできませんので、逆に言うと、スレッドプールによって非同期化できるメソッドのパラメータは object 型一つに限られる、ということになります。複数のパラメータを引き渡したい場合には、
- object 型の配列を使って一個の引数にまとめる。
- 構造体のようなクラスを使って一つの引数にまとめる。
- 後述する非同期デリゲートを使う。
のいずれかの方法を使う必要があります。
[ここまでのまとめ]
さて、ここまでの解説を一度まとめておきます。
- マニュアルスレッドを作成するためには、Thread クラスを作成する。
Thread クラスによりマニュアルスレッド上で起動できるメソッドは、引数なし/戻り値 void 型のメソッドか、引数 object 型ひとつ/戻り値 void 型のメソッドに限定される。
- プールスレッドを利用して処理を行わせるには、ThreadPool クラスを利用する。
ThreadPool クラスの .QueueUserWorkItem メソッドを使って、処理を投入する。投入できるメソッドは、引数 object 型ひとつ/戻り値 void 型のメソッドに限定される。
なお重要な注意点ですが、マニュアルスレッドやプールスレッド上で動作しているメソッドから、UI コントロールを読み書き・操作しては絶対にいけません。当然、マニュアルスレッドやプールスレッド上で長時間処理が終わったら、それをエンドユーザに通知したりする必要はあるのですが、そのためにうかつに button1.Enabled = true; とか label1.Text = “処理が完了しました”; とか MessageBox.Show(“Congulaturations!”); とか書いてはいけません。うっかりこれをやってしまうと、アプリケーションがクラッシュする危険性がありますので、十分に注意してください。
※ (参考) なお、マニュアルスレッドとプールスレッドの使い分けに関しては、Windows フォームアプリケーションの場合にはそれほどシビアになる必要はありません。スレッドプールのスレッドには上限があるため、スレッドプールが枯渇しないよう、長時間処理についてはマニュアルスレッドを使う、というのが一般的なベストプラクティスです。しかし多くの業務アプリケーションでは、そもそもそんなにたくさんの処理を同時に走らせるわけではありません。ASP.NET ランタイムなどでは内部的に大量のプールスレッドが利用されますが、Windows フォームでは、背後で走らせたいタスク処理はせいぜい数個程度でしょうから、プールスレッドを使ったところで枯渇現象を起こすことはないでしょう。このため、Windows フォームアプリケーションの場合には、長時間処理であってもプールスレッド上で動作させてしまうことがしばしばあります。
さて、ここまで解説してきた方法だと、マニュアルスレッドやプールスレッドに引き渡せるデータ変数にかなりの制限があることがわかると思います。もちろん object 型なのでなんでも渡せるといえばその通りなのですが、もうちょっと柔軟に引数として好きなものを渡す方法はないのか....と思うのも人情というものでしょう。実は、以下に解説する非同期デリゲートと呼ばれる機能を利用すると、複数の引数を持つメソッドを、プールスレッド上で処理することができるようになります。これについて解説します。
[非同期デリゲートの利用]
ここまでマニュアルスレッドやプールスレッドの使い方について解説してきましたが、これらに対して引き渡すために利用したデリゲートは、すべて .NET Framework 側で定義されているものであり、我々がデリゲートを定義することはしていませんでした。しかし、デリゲートは我々自身で定義することもできます。例えば、string 型引数と int 型引数を 1 つずつ取り、string 型を返すようなメソッドをラップするデリゲートは、以下のように定義することができます。
1: public delegate string MySampleDelegate(string a, int b);
このデリゲートを使うと、string 型引数と int 型引数を 1 つずつ取り、string 型を返すようなメソッドをラッピングしたオブジェクトを作ることができます。
1: // 以下のようなメソッドを作っておいて...(※ パラメタ名は一致していなくても OK)
2: public string MySampleMethod(string x, int y)
3: {
4: ... (何らかの処理) ...
5: }
6:
7: // このメソッドをラッピングしたデリゲートインスタンスを作る
8: MySampleDelegate del = new MySampleDelegate(MySampleMethod);
9:
さて、このデリゲート(上の例では MySampleDelegate)のインスタンスには、.BeginInvoke() というメソッドが定義されています。この .BeginInvoke() メソッドを叩くと、デリゲートがラップしているメソッドに対する呼び出し要求が、スレッドプールのワークアイテムキューにキューイングされます。そしてその結果、ラップしているメソッドが、プールスレッド上で呼び出されることになります。
1: MySampleDelegate del = new MySampleDelegate(MySampleMethod);
2: del.BeginInvoke("Nobuyuki", 123, null, null); // 後ろ 2 つのパラメータはコールバックに利用するもの(今回は解説しません。)
もう少し具体的な使い方として、Windows フォーム上で、非同期デリゲートを使って、ある長時間処理メソッドをプールスレッド上で動かすための手順を示すと、以下のようになります。
- プールスレッド上で実行したいメソッドを作成する。
このときのメソッドパラメータは任意ですが、戻り値は void としてください。
- 作成したメソッドの引数・戻り値に併せた形でデリゲートを定義する。
デリゲートの名称は、メソッド名 + Delegate とすると良いでしょう。(例: MethodX() に対して、MethodXDelegate とする) このデリゲートはここでしか利用しないので、public 宣言する必要はありません。private 宣言で十分です。
- UI スレッドからスレッドプールのワークアイテムキューにワークアイテムを投入する。
デリゲートインスタンスを作成し、BeginInvoke() 命令を発行します。これにより、当該メソッドへの呼び出しがワークアイテムキューに投入され、プールスレッドにより処理されます。(なお、呼び出しの際には、引数の末尾に null を二つつけてください。これらのパラメータは、コールバック処理や戻り値のハンドリングのために利用されますが、複雑なので今回は解説しません。)
具体的なコード例は以下の通りです。
さて、ここで解説したデリゲートが持っている .BeginInvoke() メソッドと、前回のエントリで解説した Windows フォームのコントロールが持っている .BeginInvoke() メソッドは、名前こそ同じですが全く別物であることに注意してください。
- デリゲートが持っている .BeginInvoke() メソッドは、スレッドプールのワークアイテムキューへの投入である。
- Windows フォームのコントロールが持っている .BeginInvoke() メソッドは、メッセージキューへのメッセージ投入である。
この違いをはっきりさせるため、以下のようなアプリケーション(ボタンを押すとプログレスバーが進んでいき、終了するとメッセージが表示される)を作成してみることにしましょう。

UI の内部設計図 は、以下の通りです。
具体的な実装方法は、以下のようになります。
UI スレッドからの、処理タスクの起動
- まず長時間を要する処理を、LongTask() メソッドとして定義します。ここでは例のため、名前と処理回数最大値を引数として取るようにしておくことにしましょう。
- 次に、LongTask() メソッドに対して、引数や戻り値を合わせたデリゲートを定義します。メソッドのすぐ上に定義しておくと都合がよいでしょう。名前は任意ですが、ここではメソッド名+Delegate という名前をつけることにします。
- ボタンが押されたら、デリゲートを使って、プールスレッド上でこの LongTask() メソッドを動作させるようにします。
プールスレッドからの UI の更新
- ここまで解説してきたように、プールスレッドから直接 UI を更新することはできません。そこで、UI を更新したい処理(プログレスバーへの表示とラベルへの表示処理)を、メソッドとして定義し(UpdateProgressBar(), UpdateLabel() メソッド)、それぞれに対してデリゲートを作っておきます。
- さらにメッセージキューへメッセージを投入するため、コントロールの .BeginInvoke() メソッドを使い、これらの処理を UI スレッド上で動作させます。

このように、UI スレッドとプールスレッドの連携協調動作には、デリゲートや .BeginInvoke() メソッド(2 種類)が利用されることを覚えておいてください。
なお、上記のサンプルでは、プールスレッドから UI スレッドへ処理を移す際に、this.BeginInvoke() メソッドを叩いていますが、この “this” はフォームそのもの(Form1)を示しています。実は、通常の Windows フォームアプリケーションでは、すべてのコントロールが同一の UI スレッドに属しており、そのような場合には、どのコントロールの .BeginInvoke() メソッドを叩いても同じ結果となります。ですので、上記のサンプル中の this.BeginInvoke() メソッドは、button1.BeginInvoke(), label1.BeginInvoke(), progressBar1.BeginInvoke() などと書いても同じ結果となります。
※ 参考(ちょっと難しいので、わからない人は読み飛ばしてください。)
デリゲートが持っている .BeginInvoke() メソッドを利用する場合は、本来のメソッド引数の後ろにさらに 2 つの引数を付与する必要があり、上記のサンプルでは null をつけていました。この 2 つの引数をうまく使うと、戻り値を持つメソッドへの呼び出しを非同期化したり、その結果を取り出したりすることができます。しかし、特殊な理由がなければ、Windows フォームのプログラミングではこの機能は利用する必要はありません。
例えば、非同期デリゲートを利用して、戻り値を持つメソッドへの呼び出しを非同期化する例を考えてみます。この場合には、以下のような設計と実装になります。
上記のコードを見てみると、確かに、後ろ 2 つのパラメータを使うことにより、プールスレッド上で開始した非同期処理の戻り値を受け取るメソッド(これをコールバック関数といいます)を作ることができます。しかし、この処理は UI スレッド上では動作していないため、結局、ここから UI の更新を行うことができません。結果として、上記のような面倒なコーディングが必要になってしまいます。これならいっそ、下に示すコードのように、コールバック関数を使わず、普通にメッセージキューへメッセージを投入するようなプログラムを書いた方が単純です。
このように、デリゲートが持つコールバック機能を利用すると、
- 戻り値を持ったメソッドへの呼び出しを非同期化する(プールスレッド上で動作させる)。
- その戻り値を、別のメソッド(コールバック関数)で受け取る。
ということが可能になるのですが、どちらかというと、コールバック関数を使わずに済ませるプログラミングの方が素直でしょう。(もともとコールバック関数は UI を持たない通常のマルチスレッドプログラミングで使うものなので、UI を持つアプリの場合には、コントロールの .BeginInvoke() だけを使った方が簡単なのですね^^。) なので、この機能については忘れてしまって OK です。
では、最後にちょっとした応用として、タイマの使い方について解説します。
[タイマの利用]
定期的に何らかのタスク処理を行いたい場合、タスクスレッドを起こしてそこでビジーループを作って待機することは、リソース利用上望ましくありません。むしろこのような場合には、.NET Framework 内に用意されているタイマオブジェクトを利用すると便利でしょう。
ただし注意したいのは、.NET Framework の中には 3 種類のタイマがあり、適切な使い分けが必要になる、という点です。具体的には、以下の 3 種類のタイマを使い分けていただく必要があります。(いずれも名称は “Timer” クラスですが、中身や機能は全くといっていいほど異なります。)
- System.Windows.Forms.Timer クラス (Windows アプリ向け)
定期的に Windows メッセージキューにメッセージを投入してくれるもの。
- System.Timers.Timer クラス (汎用タイマ)
定期的にスレッドプールのワークアイテムキューにワークアイテムを投入してくれるもの。
- System.Threading.Timer クラス (低水準タイマ)
低水準 API を提供するタイマ。1. や 2. の内部で使われている。
このうち、3. の System.Threading.Timer クラスは低水準タイマであるため、ほとんど使う必要はありません。基本的には 1. を中心に使い、場合によって 2. を併用する、という形になります。それぞれについて、具体的なコード例を示します。
1. System.Windows.Forms.Timer クラス (Windows アプリ向け)
まず、System.Windows.Forms.Timer クラスは、定期的な UI 更新処理に利用するタイマです。Windows フォームのツールボックス一覧に表示されている部品になりますので、画面に貼り付けて利用します。
利用する際は、timer1_Tick イベントハンドラの記述と、Interval プロパティへのタイマ発生間隔(msec)の設定を行います。このようにすると、System.Windows.Forms.Timer クラスは、定期的にメッセージキューにメッセージを投入してくれます。コード例と、内部動作の概念図を以下に示します。
1: private void timer1_Tick(object sender, System.EventArgs e)
2: {
3: // ここでは長時間処理は絶対にしないこと!(0.1sec ルール)
4: label1.Text = DateTime.Now.ToString(); // BeginInvoke()は不要
5: }
6:
7: private void button1_Click(object sender, EventArgs e)
8: {
9: timer1.Enabled = !timer1.Enabled;
10: }
この System.Windows.Forms.Timer コントロールは、メッセージキューに定期的にメッセージを投入するコントロールです。このため、動作上、以下の特徴や制約があります。
- イベントハンドラ(timer1_Tick)は UI スレッド上で動作するため、直接、UI を更新してよい。(.BeginInvoke() メソッドを使う必要はない。)
- イベントハンドラ(timer1_Tick)は UI スレッド上で動作するため、長時間処理をしてはならない。
よって、この Timer コントロールは、時計の表示などの単純な画面更新タスクの非同期化に利用するのが都合がよいでしょう。
2. System.Timers.Timer クラス (汎用タイマ)
次に、System.Timers.Timer クラスについて解説します。こちらは、定期的な業務処理を行うために利用するタイマで、スレッドプールのワークアイテムキューにワークアイテムを定期的に投入してくれるものです。
先ほどの Windows フォームの System.Windows.Forms.Timer クラスとは異なり、こちらは画面に貼り付けて利用する部品(コントロール)ではなく、通常のオブジェクトになります。実装例を以下に示します。
この System.Timers.Timer クラスのタイマーには、以下のような特徴があります。
- スレッドプールのワークアイテムキューに定期的に処理を投入する。
- UI スレッドをブロックしないため、時間のかかる処理も実施できる。
- 半面、UI 更新のためには BeginInvoke() を利用する必要がある。
特に、最後のポイントについては注意してください。このタイマーのイベントハンドラ(上記のコード例の場合には t_Elapsed() メソッド)は、プールスレッド上で動作しますので、直接、UI コントロールを操作してはいけません。必ず、UI コントロールの BeginInvoke() メソッドにより、UI スレッドへの処理投入を行う必要があります。
(参考&応用) なお、少し裏ワザ的な機能になりますが、System.Timers.Timer クラスの Synchronized プロパティを使うと、t_Elapsed イベントハンドラを UI スレッド上で動作させることができます。しかし、この機能を使うぐらいなら最初から System.Windows.Forms.Timer コントロールを使った方がラクなので、そちらをお勧めします。
3. System.Threading.Timer クラス (低水準タイマ)
上記 2 つのタイマは、それぞれ
- 定期的な UI 更新タスクを動かしたい → System.Windows.Forms.Timer コントロール
- 定期的な業務処理を動かしたい → System.Timers.Timer クラス
というように使い分けますが、これらの内部で低水準 API として利用されているのが、ここで解説するSystem.Threading.Timer クラスになります。ただし、こちらは低水準 API であるため、基本的に使いません。参考までに実装例を以下に示しますが、通常は使わない、ということを覚えておいてください。
以上で解説した 3 種類のタイマの使い分け・比較をすると、以下のようになります。実際に利用するのは、1. と 2. のタイマである、ということを覚えておいていただければと思います。
[今回のエントリのまとめ]
というわけで、ここまで様々なタスクスレッドの起動方法について解説してきましたが、それぞれのタスクスレッドの起動方法には様々なトレードオフがあります。
- 起動パラメータ引渡し可否
- 動作スレッドの種類
- 記述できる処理の長さ
- UI オブジェクト操作時のスレッド同期要否、etc…
これらを比較表としてまとめると、以下のようになります。
もちろん、いずれも一長一短があるわけですが、基本的には以下のように使い分けるとよいでしょう。
- 通常のタスクスレッドの起動には、非同期デリゲートを使う。
非同期デリゲートは最も汎用性が高く、制限が少ないためです。
- 定期的な UI 更新処理については、System.Windows.Forms.Timer コントロールを使う。
ただし、イベントハンドラでは XML Web サービス呼び出しなどの長時間処理は行ってはいけません。
- 定期的な業務処理については、System.Timers.Timer クラスを使う。
ただし、イベントハンドラでは UI を直接操作してはいけません。
- プールの枯渇を考えなければならないような場合は、マニュアルスレッドの利用を検討する。
タスクスレッドの起動については、実装コードを見て「なるほど」と思っても、実際に自分でプログラミングしてみると意外に手詰まりしてしまうことが多いです。今回示したサンプルコードを実際に一度手を動かして組んでみると、なるほどと納得できるところも多いと思いますので、ぜひ一度トライしてみてください。
-
さて、Windows フォームは、Windows OS が持つ様々なウィンドウ制御の仕組みに基づいて開発されている UI 技術です。このため、Windows フォームのマルチスレッド処理を理解するためには、まず Windows OS がどのようにして Windows フォームアプリケーションを動作させているのかについて理解する必要があります。その中でも特に重要なのが、メッセージキューとメッセージループです。これらを理解することで、なぜ UI が固まるのか、また固まることを防ぐにはどうしたらよいのか、といったことが理解できるようになります。これについて解説します。
- メッセージキューとメッセージループ
- UI フリーズの発生理由
- Windows フォーム上でのマルチスレッド処理の基本ルール
- BeginInvoke() 命令
- 最も簡単なマルチスレッドアプリケーションの例
- Windows フォームにおけるスレッドの種類
なお、今回のサンプルは以下の通りです。ご活用ください。
[メッセージキューとメッセージループ]
Windows フォームは、メッセージループと呼ばれる仕組みを使うことにより、イベント駆動型のプログラミングモデルを実現しています。まず、概略図を以下に示します。

エンドユーザがマウスやキーボードによって Windows フォームのアプリケーションを操作した際に Button_Click などのイベントが発生するのは、以下のようなメカニズムによります。
- マウスやキーボードからの入力は、まず Windows OS が受け取る。
- Windows OS は、その操作内容(キーが押された、マウスが動いた、マウスのボタンがクリックされた、etc)を、メッセージ構造体(MSG 構造体)に固め、それを各アプリケーション用のメッセージキューに放り込む。
- 各アプリケーション内部では、メッセージループと呼ばれる処理が走っている。
- メッセージループは、自分用のメッセージキューからメッセージ構造体をひとつずつ取り出し、そのデータを解析し、イベントハンドラ呼び出し(Button_Click 呼び出しなど)を行う。
※ なお、ここでいうメッセージキューとは、MSMQ (Microsoft Message Queue)のことではありません。Windows OS が持っている、GUI 処理のための特殊なキューです。
C# で Windows フォームのアプリケーションを記述した方であれば、Main 関数の中に、以下のような Application.Run() 命令を記述したことがあると思います。この命令は、メインスレッド上でメッセージループを起動するためのものです。(VB だと内部的にこの処理が隠ぺいされるためこのコードが見えませんが、内部的には同じ処理が行われています。)
このメッセージループには、以下のような特徴があることを覚えておいてください。
- メッセージループは、一種の無限ループです。つまり、メッセージキューからメッセージを取り出して処理し、次のメッセージを取り出して処理し、...をひたすら繰り返します。メッセージがなくなると、次のメッセージが届くまで待機しますが、いずれにしてもこのメッセージループは終了しません。メッセージループのコードは .NET Framework 内部に実装されているため見ることができませんが、コードイメージとして、以下のような処理が行われていると思っていただけるとわかりやすいでしょう。
while (true)
{
メッセージを取り出す処理(); // (取りだせなかった場合は待機する)
メッセージの内容を解析する処理();
メッセージの内容に基づいて、イベントハンドラを呼び出したりする処理();
} - メッセージループから、(開発者が記述した)イベントハンドラが呼び出されるまでの流れは、スタックトレースを見てみるとわかります。
さて、このメッセージループによるメッセージの取り出しにおいて重要なことは、メッセージの取り出し作業がシングルスレッド処理である、という点です。つまり、
- ひとつのメッセージを取り出して、イベントハンドラ処理(Button_Click 処理など)を行っている最中は、次のメッセージが取り出されることはありません。
ということになります。実はこれが、UI フリーズが発生する主な原因になります。
[UI フリーズの発生理由]
では次に、UI のフリーズ(UI が固まって操作できなくなる現象)がなぜ発生するのかについて解説します。先ほど、メッセージキューに OS が投入する代表的なメッセージとして、以下のようなものを挙げました。
- キーが押された
- マウスが動いた
- マウスのボタンがクリックされた
しかし、実は OS が投入するメッセージには、これ以外にも次のようなものがあります。
例えば画面上で、最小化されていたフォームがタスクバーからクリックされ、非アクティブだったフォームがアクティブ化されたとします。この場合、OS は、当該アプリに対して「UI を描画しなさい」という命令(メッセージ)を、(メッセージキューを介して)送ります。これを受け取った Windows フォームアプリは自分を描画することで、フォームを表示することになります。
つまり、メッセージキューに投入されるメッセージの中には、Windows OS からの再描画要求やサイズ変更要求などもあります。こうしたメッセージをすぐに処理できないと、UI が固まったり、正しくウィンドウが表示されなくなったりするように見える、ということになるわけです。
ところが先ほど述べたように、メッセージループによるメッセージキューからのメッセージの取り出しは、ひとつずつ順次行われます。このため、メッセージループを持つスレッド上で時間のかかる処理を行ってしまうと、再描画処理が即座に行われず、UI がフリーズします。
例えば、以下のようなコードを書いたとします。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: System.Threading.Thread.Sleep(5000);
4: }
このようなコードを書いて実行すると、ボタン押下中はウィンドウがうんともすんとも言わなくなり(=ウィンドウを移動することなどができなくなり)、UI がフリーズします。理由は簡単です。
- メッセージループを動作させているメインスレッドが、button1_click() を処理している最中は、次のメッセージを処理できない。
- このため、OS からウィンドウの移動や再描画要求メッセージが送られても、このアプリケーションはそれに反応できない。
つまり、
- Windows フォームのイベントハンドラ(Button_Click や TextBox_TextChanged など)は、メッセージループから同期的に呼び出される。
- これらの中で時間のかかる処理を行ってしまうと、簡単に UI がフリーズする。
ということになります。言い換えれば、
- UI を全くフリーズさせないためには、メッセージループから呼び出されるイベントハンドラで、時間のかかる処理(具体的には 0.1 sec 以上かかる処理)を行わないようにすればよい。
ということになります。
なお、イベントハンドラ内では何秒程度までの処理なら認められるのか? については、なんとも言い難いものがあります。一般的に、人間の視覚速度は 30fps (秒間 30 フレーム、1 フレームあたり約 30msec)と言われており、これ以下であればスムーズに動作しているように見える、と言われています。とはいえ 30msec はかなり厳しい制限です。現実的には、各イベントハンドラ内の処理を 3 フレーム程度、つまり 100msec 程度以内に収まるように設計・実装すれば、ほとんどフリーズが感じられない、応答性の高いアプリになります。(もっとも、これはアプリの特性などによっても変わりますので、一概に言える数字ではありませんが。)
逆に言えば、次のようなことがいえます。
- UI がフリーズしないアプリを作るためには、時間のかかる処理(具体的には 0.1sec 以上かかる処理)を別スレッドに切り離して実行しなければならない。
業務アプリケーションには、多かれ少なかれ、こうした「時間のかかる処理」があります。典型的なものとしてはネットワークアクセス、具体的にはデータベースアクセスや XML Web サービスへのアクセスがあります。こうした時間のかかる処理をうかつにイベントハンドラに記述してしまうと、メッセージループをブロックし、UI がフリーズすることになります。今回のエントリで解説するのは、このような処理をいかにして別スレッドに切り出すのか、についてです。
では引き続き、別スレッドへの処理の切り出し方の具体的な説明を....と言いたいところですが、その前にもうひとつ説明すべきことがあります。それは、Windows フォーム上てのマルチスレッド処理に関する基本ルールです。
[Windows フォーム上でのマルチスレッド処理の基本ルール]
マルチスレッド処理を行う Windows フォームアプリケーションを開発する際には、必ず以下のルールを守る必要があります。
- 親子関係を持つコントロールは、必ず同一スレッドに所属させること。
Windows フォームのコントロール(UI 部品)は、インスタンス生成時に、それが生成されたスレッドに自動的に紐付けられるように設計されています。このため、フォーム上にテキストボックスやラベルなどがある場合、それらはすべてフォームのインスタンスを生成したスレッドと同一スレッド上で生成しなければなりません。
通常は、すべての UI 部品をメインスレッド上で生成し、ここでメッセージループを起動します。(このため、メインスレッドは UI スレッドとも呼ばれます。以降の解説は、すべてこの前提条件に基づいて解説します。)
- コントロールを生成したスレッド以外から、コントロールを操作しないこと。
Windows フォームのコントロールは、スレッドセーフではありません。このため、当該コントロールを作成したスレッド(通常は UI スレッド)以外から直接プロパティなどを操作してはいけません。
特に後者は非常に重要です。Windows フォーム内でバックグラウンドタスクを実行するために背後のスレッドを起動した場合、そこから UI 上に進捗状況を表示したり、処理結果を表示したりしたいことが多々あります。しかし、このような際に、背後のスレッドから直接 label1.Text = “…(処理結果)…”; といった具合に直接コントロールを操作すると、最悪の場合、アプリケーションがクラッシュします。
この問題を解決するために用意されているのが、BeginInvoke() 命令です。
[BeginInvoke() 命令]
先に述べたように、UI 部品はそれを作成したスレッド、通常は UI スレッド(=メッセージループを動作させているスレッド)から操作しなければなりません。では、上図のように独自に起動した処理スレッドから UI を更新したい場合にはどのようにすればよいかというと、BeginInvoke() 命令を利用します。この命令は、すべての Windows フォームコントロールが備えているメソッドで、簡単にいうと、「特定のメソッドを呼び出せ」という命令を、メッセージ構造体としてメッセージキューに投入するためのものです。
具体例を出しながら解説した方がわかりやすいと思いますので、一例として、以下のような画面(バックグラウンドで処理を進めている際に、進捗状況を ProgressBar に表示する)を考えてみます。
まず、バックグラウンドスレッドから、直接 ProgressBar を操作するのは厳禁です。つまり、progressBar1.Value = 87; といった具合に、ProgressBar コントロールを UI 以外のスレッドから直接操作する(上図の青いスレッドから操作する)ことは厳禁です。これを避けるために、以下の作業を行います。
- まず、画面更新用のメソッドをコードビハインド中に作成します。
private void UpdateProgressBar(int val)
{
progressBar1.Value = val;
}
- 次に、このメソッドのポインタ情報をラップするための、デリゲートクラスを定義します。(UpdateProgressBar メソッドのすぐ上に、UpdateProgressBarDelegate などの名前で配置するとわかりやすいです。なおデリゲートの詳細は、Part 2 にて解説します。)
private delegate void UpdateProgressBarDelegate(int val);
private void UpdateProgressBar(int val)
{
progressBar1.Value = val;
}
- 最後に、背後で動作するバックグラウンドタスクのスレッドから、BeginInvoke() 命令を利用してメッセージを投入します。(コードの全体像は後で示します。なお、通常はすべてのコントロールが UI スレッドに所属しているため、どのコントロールの BeginInvoke() 命令を利用しても同じ結果となります。通常は Form クラスの BeginInvoke() 命令を叩くとよいでしょう。)
// 進捗状態として画面を更新
this.BeginInvoke(new UpdateProgressBarDelegate(UpdateProgressBar),
new object[] {i});
このようにすると、バックグラウンドタスクのスレッドから、メッセージキューに UpdateProgressBar() メソッドを呼び出すメッセージが投入され、これが処理されると画面が更新されます。BeginInvoke() 経由で投入されたメッセージに含まれるメソッド呼び出しは、UI スレッド上で処理されることになるので、このメソッドでは安全に UI を更新することができます。
[最も簡単なマルチスレッドアプリケーションの例]
では上記のコードサンプルを利用して、以下のような「進捗表示アプリケーション」を作ってみることにしましょう。
具体的な作業手順は以下の通りです。まず、フォーム上にボタンとプログレスバーを貼り付けます。
次に、ボタンのクリックイベントハンドラに以下のようなコードを書き、実行してみてください。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: button1.Enabled = false; // いったんボタンを無効化
4: for (int i = 0; i < 100; i++)
5: {
6: // 何らかのタスクを実施...
7: Thread.Sleep(100);
8: // 進捗状態として画面を更新
9: progressBar1.Value = i;
10: }
11: button1.Enabled = true; // 再びボタンを有効化
12: }
実際に実行してみると、(プログレスバーの表示は一部きちんと動いてくれるものの)ウィンドウを動かそうとすると UI がフリーズしてしまったりします。これは、この button1_Click() メソッドが UI スレッド(メッセージループの処理)を占有してしまっており、OS からの再描画要求に応答できなくなってしまっているためです。
このような形になるのを避けるためには、この処理をバックグラウンドのスレッドに切り離す必要があります。バックグラウンドのスレッドとしては、プールスレッドとマニュアルスレッドの 2 種類がありますが、比較的長時間を要する処理(ここに示したような数秒以上かかるような処理)については、マニュアルスレッドを使う方がよいでしょう。まず、上記の for ループ処理をバックグラウンド処理に切り離します。
1: private void button1_Click(object sender, EventArgs e)
2: {
3: button1.Enabled = false; // いったんボタンを無効化
4: Thread t = new Thread(new ThreadStart(LongTask));
5: t.IsBackground = true; // バックグラウンド化してから起動
6: t.Start();
7: }
8:
9: private void LongTask()
10: {
11: for (int i = 0; i <= 100; i++)
12: {
13: // 何らかのタスクを実施...
14: System.Threading.Thread.Sleep(100);
15:
16: // 進捗状態として画面を更新
17: progressBar1.Value = i;
18: }
19: button1.Enabled = true; // 再びボタンを有効化
20: }
上記のコードの 17 , 19 行目に着目してください。
- button1_Click() は、メッセージループから呼び出されます。つまり UI スレッド上で動作します。
- しかし、上記に示した LongTask() は、新規に作成されたマニュアルスレッド上で動作します。
つまり、この 17 行目や 19 行目のコードは、UI スレッド以外からコントロールを操作しているので、アプリケーションをクラッシュさせる危険性があります。この問題を避けるためには、17 行目や 19 行目の処理を UI スレッド上で動作させるように、BeginInvoke() 命令を使う必要があります。
具体的には、まず UI 更新を行うためのメソッドと、そのメソッド呼び出しをラッピングするためのデリゲートを定義します。
1: private delegate void UpdateProgressBarDelegate(int val);
2: private void UpdateProgressBar(int val)
3: {
4: progressBar1.Value = val;
5: if (val == 100) button1.Enabled = true;
6: }
次に、LongTask() メソッド内の UI 更新処理を、BeginInvoke() によるメッセージ投入処理に切り替えます。
1: private void LongTask()
2: {
3: for (int i = 0; i <= 100; i++)
4: {
5: // 何らかのタスクを実施...
6: System.Threading.Thread.Sleep(100);
7:
8: // 進捗状態として画面を更新
9: this.BeginInvoke(
10: new UpdateProgressBarDelegate(UpdateProgressBar),
11: new object[] { i });
12: }
13: }
以上により、フリーズしない UI を持った進捗状況表示画面が作成されます。
完成したソースコードと、それぞれのメソッドがどこで動作するのかを下図に示します。
このようにすることで、時間のかかる処理を背後のスレッドに分離し、フリーズしない UI を作成することができます。
[Windows フォームにおけるスレッドの種類]
さて、上記では処理の切り離しにマニュアルスレッド(新規に作成したスレッド)を使いましたが、スレッドプールを使って処理を別スレッド化することもあります。(※ スレッドプールがどのようなものであるかについては、以前に記述したエントリを参照してください。)
つまり、Windows フォームでは、UI スレッド(メインスレッド)から処理を切り離す方法として、マニュアルスレッドとプールスレッドの 2 種類がある、ということになります。結果として、Windows フォームアプリケーション内部では、主に以下のようなスレッドが利用されることになります。
- メインスレッド (UI スレッド)
当該プロセス内に最初に作られ、Main() メソッドを呼び出すスレッド。このスレッド上で UI コントロールを作成し、Application.Run() メソッドを呼び出し、メッセージループを動作させる。メッセージキュー内のメッセージ処理や、各種のイベントハンドラ呼び出しはこのスレッド上で発生する。
- マニュアルスレッド
非同期処理(バックグラウンドタスク)を行うために、自力で Thread オブジェクトを生成することにより作ったスレッド。
- プールスレッド
非同期処理(バックグラウンドタスク)を行う際、CLR の機能であるスレッドプール機能を用いる場合に利用されるスレッド。
- その他のスレッド
ファイナライザスレッドやアンマネージスレッドなどが動作するスレッド。通常は気にしなくて OK。
まとめると、下図のようになります。注意すべき点は、マニュアルスレッドやプールスレッドから UI コントロールを直接更新してはならないという点です。UI を更新したい場合には、BeginInvoke() 命令を使って、メッセージキューにメソッド呼び出し要求を投入してください。
[今回のエントリのまとめ]
というわけで、今回のエントリでは Windows フォームのマルチスレッド処理の基礎について解説してきました。キーポイントは以下の通りです。
- Windows OS は、メッセージキューにメッセージ(MSG 構造体)を投入することによって、マウスの移動やキーボードの押下を通知している。
- Windows フォームアプリケーションは、内部でメッセージループを使い、メッセージキューから一件ずつ、逐次でメッセージを取り出していくことで処理を進める。
- UI フリーズが発生する原因は、メッセージループ上(UI スレッド上)で長時間処理を行ってしまうことである。イベントハンドラなどに長時間処理を記述すると、OS からの再描画要求が実行されなくなり、UI がフリーズする。
- 通常、UI コントロールはすべてメインスレッド上でインスタンス化し、このスレッド上から操作する。このメインスレッドのことを、別名で UI スレッドと呼ぶ。
- マニュアルスレッドやプールスレッドといった、メイン以外のスレッドから UI コントロールを操作してはならない。このような場合には、BeginInvoke() 命令を使って、UI スレッド上で画面描画更新処理を動作させる。
以上が基本的な Windows フォームのマルチスレッド処理の大原則です。しかしこれだけではまだマルチスレッド動作する Windows フォームアプリケーションを開発するにはやや知識が足りません。次回のエントリでは、上記の大原則に従った、より詳細なマルチスレッドアプリの開発方法について解説してきます。
-
というわけでまたしてもかなり日にちが空いてしまいました;。年度末ということもあって仕事が立て込んでいたのですが、ほぼ一段落したので久しぶりにエントリを。どうしてもまとまった話題を書こうとすると時間がかかっちゃいますね....
今回の話題は、Windows フォームにおけるマルチスレッド処理の正しい書き方です。以前、マルチスレッドアプリケーションにおけるデータ変数の排他制御(スレッドセーフか否かの判定)についてこちらとこちらのエントリに書きましたが、さらにもう少し応用的なトピックとして、Windows フォームにおけるマルチスレッド処理について解説したいと思います。
この辺の話は、C++ でアプリケーションを作られている方には当たり前の話(らしい)のですが、私のような pure .NET デベロッパーな人にはあまり馴染みのない話だと思います。しかし、C# や VB でしか Windows フォームのアプリケーションを書いたことがない人であっても、うまくコードを書かないと、下図のような「フリーズしてしまう」アプリケーションができあがってしまいます。
今回のエントリでは、以下について解説します。
- そもそもなぜ UI のフリーズ現象は発生するのか? それはどうすれば回避できるのか?
- Windows フォームにおける UI スレッドの位置づけ
- Windows フォーム上で、背後にスレッドを起こすための適切な方法
では、以降のエントリでこれらについて解説していきます。
[Part 1. Windows フォームのマルチスレッド処理の基礎]
- メッセージキューとメッセージループ
- UI フリーズの発生理由
- Windows フォーム上でのマルチスレッド処理の基本ルール
- BeginInvoke() 命令
- 最も簡単なマルチスレッドアプリケーション
- Windows フォームにおけるスレッドの種類
[Part 2. タスクスレッドの起動方法]
- マニュアルスレッドの新規作成
- スレッドプールへのワークアイテムの追加
- 非同期デリゲートの利用
- タイマの利用
[Part 3. タスクスレッドと UI の協調動作]
- タスクスレッドからの UI 画面の更新方法
- タスクスレッドからの UI 画面上のデータの読み取り方法
- UI 画面からのタスクスレッドの制御方法
- タスクスレッド上で発生した未処理例外の取り扱い方法
[Part 4. Visual Studio によるマルチスレッドアプリの開発]
- XML Web サービス呼び出しの非同期処理化
- WCF サービス呼び出しの非同期処理化
- BackgroundWorker コンポーネントによる一般的なタスクの非同期処理化
-
さて、前回のエントリでは Windows フォームにおける双方向データバインドの基本的な使い方を解説しました。要点をまとめると、以下の通りとなります。
- 双方向データバインドを用いると、データソースから UI コントロールへ値を表示するだけでなく、UI コントロールからの入力をデータソースに反映できる。
- データバインドには、2 種類のデータバインドがある。
① 単一値データバインド(単票形式データバインド)
② コレクションデータバインド(グリッド形式データバインド)
- どちらの場合も、BindingSource コントロールを介して、UI コントロールとデータソースを紐づける。
さて前回のエントリでは、 双方向データバインドにより、テキストボックスから入力された値がデータソースオブジェクトに反映されることを確認しました。しかし、これらのデータはそのまま使えるとは限りません。例えば配達希望日を入力するテキストボックスの場合、
- "ABCDE" や "2008/14/63" といった、そもそも日付ではない(型変換できない)データが入力された場合はどうすればよいか?
- "1973/06/07" のように、日付としては有効でも、未来の日付ではないデータが入力された場合はどうすればよいか?
といった問題があるため、入力されたデータ値は検証を行った上で利用する必要があります。しかし、こうしたデータ入力検証を場当たり的に実装すると、コード量が膨大に膨れ上がり、アプリケーションの保守性も極端に悪化します。これを避けるためには、データ入力検証に関して、アプリケーション全体で一貫した考え方を使う必要があります。
本エントリでは、スマートクライアント(Windows フォーム)における、業務アプリケーションを想定した入力データ検証の考え方と、その実装方法について解説します。
- エラーの分類
- 単体入力エラーの分類
- 単体入力チェックとエラーメッセージの関係
- 双方向データバインドにおける値の同期の考え方
- IDataErrorInfo インタフェースとは何か
- 具体的な単体入力チェックの実装方法
- DTO と UI バインドオブジェクトの違い
なお、今回の実装サンプルコードはこちらになります。
では、以下に解説していきましょう。
[エラーの分類]
Web アプリ、Windows アプリすべてに共通する考え方ですが、業務アプリケーションにおける「エラー」は、以下の 3 種類に分類されます。
- 単体入力エラー : UI 内部のみで単体で正誤判定できるもの。
例) 入力された電子メールアドレスが「nakama@ms」だった、入力された価格が 0 未満だった、生年月日として未来の日付を入力された、日付入力欄に “AAA” が入力されている、etc.
- 業務エラー : サーバやデータベースまで連携しないと正誤判定できないもの。
例) 登録しようとした希望ユーザ ID がすでに他のユーザにより使われていた、入力された商品 ID がすでに廃番のものだった、etc.
- システムエラー : システムインフラの不具合やアプリケーションバグで発生するもの。
例) メモリ不足、DB 破損、ネットワークエラー、etc.
スマートクライアントの場合を考えてみると、単体入力エラーに関してはサーバ側へ通信を行うまでもなく、UI 部(Windows フォームアプリケーション)の中で即時にチェックを行い、すぐさまエンドユーザにエラー情報通知を行うのが望ましいでしょう。
[単体入力エラーの分類]
さて、UI 部のみで単体チェックが可能な「単体入力エラー」ですが、実はこの単体入力エラーや単体入力チェックは、さらに 3 種類に細分化することができます。例えば下図のような、新規顧客登録画面を考えてみましょう。
この画面において実施する必要のある単体入力チェックは、以下の 3 種類に分類できます。
- データ型変換チェック
例) 入力された生年月日が、DateTime? 型に変換できるか?
- フィールド単位の有効性チェック
例) ID や電子メールアドレス、電話番号などが適切なフォーマットか?
例) 入力された生年月日が、未来の日付ではないか?
- インスタンス(レコード)単位の有効性チェック
例) 連絡先として、電話番号か電子メールアドレスかの少なくとも一方が入力されているか?
そして単体入力チェックでは、これらのチェックを、場当たり的ではない考え方で実装する必要があります。
[単体入力チェックとエラーメッセージの関係]
またもう一点重要なポイントとして、UI 部における単体入力エラーチェックでは、エラー発見時に即時にユーザに対するガイダンスメッセージ表示を行う必要があります。
このエラーメッセージ表示に関しては、以下のポイントに留意する必要があります。これらが満たされていないと、エンドユーザにとって使いにくい画面になってしまいます。
- 入力エラーを修正してもらえるようなガイダンス的なメッセージであること。
- 入力エラーは、可能な限り即座にユーザに通知すること。
- 入力エラーの通知が、ユーザ操作を妨げないこと(例:メッセージダイアログを出すと、ユーザにとって非常に煩わしい)
- 入力エラーがある状態でも、他の入力欄にフォーカスを移せること。
こうした入力データ検証を実装しやすくするための機能として、.NET Framework 3.5 から追加されたのが、IDataErrorInfo インタフェースと呼ばれる機能(と、それに関連する BindingSource クラスの機能強化)です。が、これを説明する前にもうひとつ押さえておくべきことがあります。それは、双方向データバインドにおける値の同期の考え方です。
[双方向データバインドにおける値の同期の考え方]
もともとデータバインドというのは、二点間の値を常に同じに保つという意味を持っています。そして双方向データバインドの場合には、テキストボックスから入力された値をデータソースに反映することで、二点間のデータ値をリアルタイムに同期しようとします。
ここで、年齢を入力できるよう、テキストボックスと int 型のデータとを双方向データバインドする場合を考えてみます。この場合、まず初期表示ではデータソース→UI にデータが表示されるので特に問題は生じません。しかし、テキストボックスから数値以外の文字列が入力された状態でロストフォーカスを認めてしまうと、テキストボックス上のデータと、データソースの値とにずれが生じてしまいます。このようなデータずれが生じないよう、Windows フォームのデータバインドでは、データずれが生じるようなロストフォーカスを認めないようになっています。このようにすることで、(入力仕掛かりの状態を除けば)二点間のデータ同期を保つわけです。
さて、そもそも int 型に変換できない文字列がテキストボックスから入力された場合にロストフォーカスを認めない、という挙動は至極当然でしょう。しかし、単体入力エラーとなる値、たとえば “-5” を入力した場合はどうなるでしょうか? ここで問題になるのは、単体入力エラーとなる値を、データソースオブジェクトが受け取るかどうか、というポイントです。データソースとなるオブジェクトにビジネスルールを直接実装してしまうと、単体入力エラーとなる値を受け取れなくなるため、双方向データバインドでデータの同期をうまく保つことができなくなります。下の例を見てください。
1: public class Author
2: { 3: private string _au_id;
4: public string au_id
5: { 6: get { return _au_id; } 7: set
8: { 9: if (value == null) throw new ArgumentException("au_id は null を設定できません。"); 10: if (Regex.IsMatch(value, @"^\d{3}-\d{4}$)") == false) throw new ArgumentException("著者IDは123-4567のような形式です。"); 11: _au_id = value;
12: }
13: }
14:
15: private string _au_name;
16: public string au_name
17: { 18: get { return _au_name; } 19: set
20: { 21: if (value == null) throw new ArgumentException("名前は null にできません。"); 22: if (value == "") throw new ArgumentException("名前は空文字にできません。"); 23: if (value.Length > 20) throw new ArgumentException("名前は20文字以内である必要があります。"); 24: _au_name = value;
25: }
26: }
27:
28: private int _age;
29: public int age
30: { 31: get { return _age; } 32: set
33: { 34: if (value < 0) throw new ArgumentException("年齢は 0 未満にはできません。"); 35: _age = value;
36: }
37: }
38: }
一般的に、ビジネスオブジェクトは上記のような実装をするのが望ましい、と言われますが、このような Author オブジェクトを UI に双方向データバインドすると、非常に使いづらい UI になります。下図を見てみてください。
テキストボックスから –5 を入力した場合、これは当然 Author オブジェクトに反映できません。となると、ロストフォーカス時に、テキストボックスとデータソースの値の同期を取るためには、
- 年齢入力テキストボックスからのロストフォーカスを認めない(入力しかけの状態で別のフィールドに移れない)(既定の挙動)
- しれっとテキストボックスの表示を元に戻してしまう(入力したつもりがいつの間にか取り消されている) (※ こらちは作り込みが必要ですが)
のどちらかを取る必要があります。しかし、これでは使いやすい UI を実現することはとても不可能です。このような問題を解決するのが、IDataErrorInfo インタフェースです。
[IDataErrorInfo インタフェースとは何か]
IDataErrorInfo インタフェースは、簡単に言うと、以下のような特性を持ったオブジェクトのクラスを作成するために使うインタフェースです。
- UI からの入力値を、エラーがあろうとなかろうとそのまま受け取る。
- そのかわりに、内部データにエラーが含まれる場合には、エラーに関する情報を文字列データで返すようにする。
1: public interface IDataErrorInfo
2: { 3: public string Error { get; } 4: public string this[string propertyName] { get; } 5: }
このインタフェースを実装する前の Author オブジェクトと、実装した後の Author オブジェクトの比較コードを下記に示します。
IDataErrorInfo インタフェースを実装する前の Author オブジェクト
1: public class Author
2: { 3: public string au_id { get; set; } 4: public string au_name { get; set; } 5: public int age { get; set; } 6: }
IDataErrorInfo インタフェースを実装した Author オブジェクト
1: public class Author : System.ComponentModel.IDataErrorInfo
2: { 3: public string au_id { get; set; } 4: public string au_name { get; set; } 5: public int? age { get; set; } 6:
7: public string Error
8: { 9: get { return null; } 10: }
11:
12: public string this[string columnName]
13: { 14: get
15: { 16: switch (columnName)
17: { 18: case "au_id":
19: if (au_id == null) return "au_id は null にできません。";
20: if (Regex.IsMatch(au_id, @"^\d{3}-\d{4}$") == false) return "著者IDは123-4567のような形式です。"; 21: return null;
22: case "au_name":
23: if (au_name == null) return "名前は null にできません。";
24: if (au_name == "") return "名前は空文字にできません。";
25: if (au_name.Length > 20) return "名前は20文字以内である必要があります。";
26: return null;
27: case "age":
28: if (age == null) return "年齢は必須入力です。";
29: if (age < 0) return "年齢は 0 未満にはできません。";
30: return null;
31: default:
32: throw new ArgumentException("不明なプロパティです。" + columnName); 33: }
34: }
35: }
36: }
実装上のポイントは以下の通りです。
- UI に双方向バインドするオブジェクトは、とりあえずどんな入力値でも受け付けるように実装する。(age プロパティが、int 型から int? 型に変更されているのは、テキストボックスが未入力状態(null 状態)でロストフォーカスすることを認めるための措置です。)
- データに誤りがある場合には、IDataErrorInfo インタフェースにより、エラーメッセージを返すようにする。(エラーがない場合には null を返す)
- エラーメッセージは以下の 2 種類を返せるようにする。
① プロパティ単位のチェック(public string this[string columnName])
② データ全体のチェック(public string Error)
このようなオブジェクトを UI にバインドした上で、さらに ErrorProvier コントロールを画面に貼り付けると、ErrorProvider コントロールが自動的に IDataErrorInfo インタフェースからエラー情報を取り出し、ツールチップ形式でエラーメッセージを表示してくれるようになります。
具体的な実装例を以下に示します。(ErrorProvider コントロールの DataSource プロパティに、BindingSource オブジェクトを割り当てることを忘れずに。)
1: public partial class AuthorForm : Form
2: { 3: public AuthorForm()
4: { 5: InitializeComponent();
6: }
7:
8: private Author _author;
9:
10: private void AuthorForm_Load(object sender, EventArgs e)
11: { 12: _author = new Author();
13: bindingSource1.DataSource = this._author;
14: }
15:
16: private void bindingSource1_BindingComplete(object sender, BindingCompleteEventArgs e)
17: { 18: lblError.Text = _author.Error;
19: }
20: }
このようにすれば、以下のことが実現できます。
- 使いやすい UI 入力制御を容易に実現できる。
入力仕掛の状態で他のフィールドにフォーカスを移動できると同時に、エラー情報をツールチップ形式で簡単に表示できます。
- UI フォームの実装コードから、単体入力データ検証のためのコードを切り離すことができる。 UI のコードビハインドは一般的に非常に汚くなりがちですが、この方法を利用すれば、単体入力チェックにかかわるコードを別クラスに切り離すことができます。
[具体的な単体入力チェックの実装方法]
ではもうひとつの具体的な実装例として、最初に挙げた顧客データ入力フォームの例を採り上げてみましょう。
このようなアプリケーションは、以下の手順で実装していきます。
① データバインド用の IDataErrorInfo オブジェクトの実装
まずは、IDataErrorInfo インタフェースを実装した、UI 双方向データバインド用のクラスを作成します。なお、実装上、以下の点にも気を付けるとよいでしょう。
- 前述のサンプルでは this[columnName] インデクサ内で毎回チェックをしていましたが、実際には毎回チェックする必要はないので、エラー情報を蓄積しておくディクショナリを使うと便利。
- (フィールド単位ではなく)オブジェクト全体(入力全体)の整合性チェックをしたい場合には、public string Error プロパティのところに記述する。(この例では、「電子メールと電話番号の少なくとも片方は入力が必要」というオブジェクト全体の整合性チェックロジックを実装しています。)
- int 型や DateTime 型の入力フィールドを、int? 型や DateTime? 型で定義するのか、string 型で定義するのか、どちらにするのかはよく考えた方がよい。例えばこの例の場合、生年月日(Birthday)プロパティを DateTime? 型で定義しているが、この場合、UI から “1973/56/21” といった具合に、そもそも日付ではないデータが入力された場合にはロストフォーカスができなくなる(が、後続の処理を作る上では便利になる)。しかし、Birthday プロパティを string 型として定義した場合には、日付ではないデータ(例えば “19aA/s2/32f” などといった文字)が入力されていても、ロストフォーカスして別のテキストボックスに移動できる。(どちらも一長一短です。)
- オブジェクトに一つもエラーがないか否かを確認するための public bool HasErrors; というプロパティを作っておくと便利。(ボタン押下時のイベントハンドラで、入力エラーが一つもないか否かを一発で確認できるためです。)
- このクラスは、単体機能テストで動作確認を行うことを推奨。(UI から切り離されているクラスなので、実は単体機能テストが非常に書きやすいのです。)
1: public class CustomerInput : IDataErrorInfo
2: { 3: private Dictionary<string, string> _errors = new Dictionary<string, string>();
4:
5: private string _id;
6: public string ID
7: { 8: get { return _id; } 9: set
10: { 11: _id = value;
12: if (_id == null)
13: { 14: _errors["ID"] = "ID は必須入力項目です。";
15: }
16: else if (Regex.IsMatch(value, @"^[0-9A-Z]{4}$") == false) 17: { 18: _errors["ID"] = "ID は半角英数大文字 4 文字です。";
19: }
20: else
21: { 22: _errors.Remove("ID"); 23: }
24: }
25: }
26:
27: private string _name;
28: public string Name
29: { 30: get { return _name; } 31: set
32: { 33: _name = value;
34: if (_name == null || _name == "")
35: { 36: _errors["Name"] = "名前は必須入力項目です。";
37: }
38: else
39: { 40: _errors.Remove("Name"); 41: }
42: }
43: }
44:
45: private string _email;
46: public string Email
47: { 48: get { return _email; } 49: set
50: { 51: _email = value;
52: if (value == null || Regex.IsMatch(value, @"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"))
53: { 54: _errors.Remove("Email"); 55: }
56: else
57: { 58: _errors["Email"] = "電子メールアドレスとして有効な値を入力してください。";
59: }
60: }
61: }
62:
63: private string _phone;
64: public string Phone
65: { 66: get { return _phone; } 67: set
68: { 69: _phone = value;
70: if (value == null || Regex.IsMatch(value, @"(0\d{1,4}-|\(0\d{1,4}\) ?)?\d{1,4}-\d{4}")) 71: { 72: _errors.Remove("Phone"); 73: }
74: else
75: { 76: _errors["Phone"] = "電話番号は (03)1234-5678 のように入力してください。";
77: }
78: }
79: }
80:
81: public DateTime? Birthday { get; set; } 82:
83: // 全体整合チェック
84: public string Error
85: { 86: get
87: { 88: if (_email == null && _phone == null)
89: { 90: return "電子メールアドレスか電話番号かのいずれか一方は必須入力です。";
91: }
92: else
93: { 94: return null;
95: }
96: }
97: }
98:
99: public bool HasErrors
100: { 101: get { return (_errors.Count != 0 || Error != null); } 102: }
103:
104: public string this[string columnName]
105: { 106: get
107: { 108: return (_errors.ContainsKey(columnName) ? _errors[columnName] : null);
109: }
110: }
111: }
② データソースの登録と UI の構築
BindingSource コントロールと ErrorProvider を貼り付け、さらにテキストボックスなどを貼り付けていって UI を構築します。なお、オブジェクト全体に関するエラー(.Error プロパティの情報)は、ErrorProvider コントロールによる自動表示ができません(多分....)。このため、余白領域に全体エラー表示用のラベルを貼り付けておいてください。
③ コードビハインドの実装
あとは、bindingSource1 の .DataSource プロパティに実際のインスタンスを割り当てて、データ入力画面を作成します。全体エラーをリアルタイムに表示するために、bindingSource1 の BindingComplete イベントハンドラを利用していることに注意してください。
1: public partial class Form3 : Form
2: { 3: public Form3()
4: { 5: InitializeComponent();
6: }
7:
8: private CustomerInput _data;
9:
10: private void Form3_Load(object sender, EventArgs e)
11: { 12: _data = new CustomerInput();
13: bindingSource1.DataSource = _data;
14: }
15:
16: private void bindingSource1_BindingComplete(object sender, BindingCompleteEventArgs e)
17: { 18: lblError.Text = _data.Error;
19: }
20:
21: private void button1_Click(object sender, EventArgs e)
22: { 23: if (_data.HasErrors)
24: { 25: MessageBox.Show("入力データに誤りがあります。修正してください。"); 26: return;
27: }
28:
29: // 単体入力チェックを通過したデータを使って
30: // XML Web サービス呼び出しなどを実施
31: MessageBox.Show("OK"); 32: }
33: }
実行結果を以下に示します。
なお、以上は単票形式データバインドに関しての実施方法を示しましたが、グリッド形式データバインドの場合でも同様の方法で実装することができます。以下の点に気をつけて実装してみてください。
- ErrorProvider コントロールを貼り付ける必要はありません。(フィールド単位のエラー、インスタンス単位のエラーを自動的にアイコン表示してくれるようになっています。)
- パースエラー(型変換エラー)に関してはフォーカス移動が抑止されず例外メッセージが表示されてしまう、という仕様になっているため、型変換エラー時のフォーカス移動を防止するイベントハンドラを組み込んでください。
1: private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e)
2: { 3: // パースできない状態で他のセルに移動することは禁止する
4: if (e.Context == DataGridViewDataErrorContexts.Parsing) e.Cancel = true;
5: }
ここまでに解説してきた内容を用いれば、単票形式およびグリッド形式で、単体入力エラーチェックをかけながらデータ入力を行わせる Windows フォームを開発していくことができるはずです。というわけで以上で解説はおしまい……としたいところなのですが、もうひとつ解説しておかなければならないことがあります。それは、DTO (Data Transfer Object)と UI バインドオブジェクトの違いです。最後に、このことについて解説します。
[DTO と UI バインドオブジェクトの違い]
一般に、マスタメンテナンスのように、
- サーバから一括してデータを取り寄せ、
- クライアント内でデータをまとめて書き換えて、
- サーバ側にそれを再度アップロードして一括データ更新を行う。
といったタイプのスマートクライアントアプリケーションでは、通常、型付きデータセットを使ったデータのやり取りが行われます。
一般に、クラス間やプロセス間でデータをごそっと引き渡すために使う BEC(ビジネスエンティティクラス)のことを、DTO (データトランスファオブジェクト)と呼びます。.NET Framework によるアプリケーション開発では、DTO として使えるオブジェクトとして、データセット及び型付きデータセットが用意されており、これを使うと、一括してデータを引き渡せる上に、楽観同時実行制御に基づくデータ更新処理も作りやすくなるというメリットがあります(これについての詳細は、拙著「Visual Studio 2005によるWebアプリケーション構築技法」の 第13章「楽観同時実行制御による対話型トランザクション処理の開発」を見てください)。
が、ここで重要なのは、だからといって Windows フォーム上で、サーバから取り寄せた型付きデータセットを直接 DataGridView にバインドしてはいけない(しない方がよい)、という点です。
例えば、データベース上の書籍マスタを編集する、下図のような画面を考えてみてください。
この例の場合、XML Web サービスから書籍データを含む型付きデータセットを取り寄せてデータバインドしたくなる……と思うのですが、データの更新処理を行うことを念頭に置いた場合、データセットを直接グリッドにバインドしてしまうと、入力エラー制御のコードを場当たり的に書かざるを得なくなってしまいます。(表示するだけであればデータセットをバインドしてもよいのですが、データを入力させることを考えた場合、単体入力チェックのコードを一か所に固めることが難しい)
このような場合には、XML Web サービスから取り寄せた型付きデータセットのデータを、UI バインド用の IDataErrorInfo オブジェクトに移し替えてバインドした方が、むしろコードがすっきりします。
なぜこのようなことが起こるのかというと、DTO と UI バインド用オブジェクトには以下のような特性の違いがあり、DTO を UI バインド用オブジェクトにそのまま転用できないことがほとんどだからです。
DTO (データトランスファオブジェクト)
- 「データベーススキーマ」と同様の検証ロジックが搭載されていることが多い
例) データベース上で必須なら、null 値を受け付けない
- 入力値にエラーがある場合は例外を発生させ、値そのものを受け付けないことが多い
例) 単体入力エラーを含むデータを設定した場合、ArgumentException 例外が発生する
- その際、懇切丁寧なガイダンスメッセージも表示しない
UI バインド用オブジェクト(IDataErrorInfo オブジェクト)
- UI 上の表示との同期を常に取るために、不正な入力値も受け付けなければならないこともある
例) 必須入力フィールドであっても、一時的に null を受け付ける
- 入力値にエラーがある場合には、エラーメッセージをどこかで作成しなければならない
つまり、データ参照(表示)だけなら DTO を直接バインドしても問題はありませんが、更新処理を含む場合には、DTO は UI バインドオブジェクトとしての要件を満たさないのです。このため、更新系アプリケーションでは、DTO と UI バインドオブジェクトを分けなければならないことが多い、ということになります。
なお実際のアプリケーションでは、データを取り寄せてきて表示するタイミングでは DTO → UI バインドオブジェクトへのデータコピーを、またサーバへの書き戻しのタイミングでは UI バインドオブジェクトから DTO へのデータコピーを行う必要があります。この処理に関しては、LINQ to Objects などを使うと便利だと思います。(for ループ回してもたかがしれていますが^^)
1: TitlesMaintenance.EditTitlesService.EditTitlesDataSet _originalData = null;
2: List<TitleInput> _titleInputs;
3:
4: private void btnGetData_Click(object sender, EventArgs e)
5: { 6: EditTitlesService.EditTitlesWebService proxy = new EditTitlesService.EditTitlesWebService();
7: _originalData = proxy.GetData();
8: _titleInputs =
9: (from t in _originalData.titles.Cast<EditTitlesService.EditTitlesDataSet.titlesRow>()
10: select new TitleInput
11: { 12: title_id = t.title_id,
13: title = t.title,
14: price = (t.IspriceNull() ? (decimal?)null : t.price),
15: pubdate = t.pubdate
16: }).ToList();
17: bindingSource1.DataSource = _titleInputs;
18: }
19:
# 実はこの点は、私自身が書籍を読んで勉強していて昔からずーっとひっかかっていたことでした。
# そもそも DB 上の非 NULL フィールドでも、UI 上から入力される場合には一時的に空欄にする
# ことが当然あるわけで、このようなケースをどうハンドリングすればいいのか? ……というのを
# 突き詰めて考えていって、要するに、DTO と UI バインドオブジェクトは本質的に違うものだ、という
# 結論に自分は達しました。そのことに気付いたのは IDataErrorInfo インタフェースの仕様。
# 値にエラーがあるときに string 型を返す、という設計は一般的にはよくないはず(コードを返して
# UI 部でメッセージに変換すべき)なのですが、string 型にしているのは、UI 部でしか使わない、
# という前提条件に立っているから、なんですよね。実際に、DTO と UI バインドオブジェクトを分けて
# 実装してみると、データの移し替えの手間はかかるものの、UI 部のコードビハインドのコードが
# ものすごくすっきりするのでかなりびっくりしました。……とつぶやいてみる。
[今回のエントリのまとめ]
というわけで、今回のエントリをまとめると、以下のようになります。
- スマートクライアントアプリケーションなどにおけるエラーは、以下の 3 種類に大別される。
① 単体入力エラー : UI 内部のみで単体で正誤判定できるもの
② 業務エラー : サーバやデータベースまで連携しないと正誤判定できないもの
③ システムエラー : システムインフラの不具合やアプリケーションバグで発生するもの
このうち、①を実装する際には、Windows フォームの双方向データバインドを活用すると便利。
- 単体入力チェックは、さらに以下の 3 つに分類することができる。
1. データ型変換チェック
2. フィールド単位の有効性チェック
3. インスタンス(レコード)単位の有効性チェック
このうち、1. についてはロストフォーカスを認めず、2., 3. については即座にエラーアイコンやツールチップを使って通知を行う UI を作りたいと思った場合には、IDataErrorInfo オブジェクトによる双方向データバインドを行うとよい。
- IDataErrorInfo とは、以下のような特性を持つ UI バインド用オブジェクトを作るためのインタフェースである。
A. UI からの入力値を、エラーがあろうとなかろうとそのまま受け取る。
B. そのかわりに、内部データにエラーが含まれる場合には、エラーに関する情報を文字列データで返すようにする。
- IDataErrorInfo インタフェース、BindingSource コントロール、ErrorProvider コントロールによる即時エラー通知は、単票形式データバインドでもグリッド形式データバインドでも利用することができる。
- DTO (データ転送用オブジェクト)と、UI バインド用オブジェクトとを混同してはならない。.NET Framework での開発の場合には、前者に型付きデータセットが、後者に IDataErrorInfo オブジェクトが利用される。
というわけで、Windows フォームにおける双方向データバインドを活用した単体入力データ検証の実装方法について解説してきましたが、ここで解説した IDataErrorInfo を使う方式は、WPF でもほぼ同じになります。今回は WPF の場合についての解説は割愛しますが、興味がある方は以下の記事を参照することをおすすめします。
何かと場当たり的な実装がされることが多い Windows フォームのデータ入力検証ですが、.NET Framework 3.5 で導入された IDataErrorInfo インタフェースを使うと、コードをかなりきれいな形に持っていくことができると思いますし、さらにひと工夫を行えば、IDataErrorInfo インタフェースを実装するクラスを作ることもより容易化できると思います。ぜひ本エントリを活用して、Windows フォームの実装コードを少しでも美しい形にしていただければと思います。
-
Windows フォームにおけるデータ入力において、スマートに単体入力エラーチェックを行いたい場合には、双方向データバインドと IDataErrorInfo インタフェースを利用する方式が便利です。ただし、この Windows フォームのデータバインドは Web フォームのデータバインドとは大きく異なる点があります。
- 双方向データバインドが利用できる。(※ Web フォームでも使えますが、極めて限定的です。)
- 入力データのデータソースへの反映がリアルタイムに行われる。(リアルタイムにデータが同期。)
このため、ASP.NET Web アプリケーションでのデータバインドにすでに慣れ親しんでいる方でも、Windows フォームのデータバインドについては、全く新しい技術として学習しなおす、ぐらいの感覚で取り組むことが重要です。今回のエントリでは、まず基本的な Windows フォームのデータバインドの実装方法を解説したいと思います。具体的な解説トピックは以下の通りです。
- 双方向データバインドとは何か
- 2 種類のデータバインド
- データバインドの基本的な使い方 : データソースとなりうるオブジェクト
- データバインドの基本的な使い方 : 単票形式データバインド
- データバインドの基本的な使い方 : グリッド形式データバインド
- null 値の入力制御方法(パース処理のカスタマイズ方法)
なお、今回のサンプルを最後まで実装したサンプルコードはこちらになります。併せてご利用ください。
では、以下に解説していきます。
[双方向データバインドとは何か]
双方向データバインドとは、UI コントロール(DataGrid や TextBox, Label コントロールなど)の特定のプロパティと、データソースとなるオブジェクト(型付きデータセットやカスタムオブジェクト)とを紐づけておき、
- データソースから自動的にデータを抽出して UI コントロールに表示する。
- UI コントロールから入力された値を、自動的にデータソースに反映する。
という動作をさせるものになります。
ASP.NET Web フォームなどのデータバインドでは、ほとんどの場合、データソース内のデータを UI コントロールに表示するという目的のみで利用されます。このため、更新系アプリケーションにおけるデータ入力シートを作成する目的でデータバインドを使うことはまずありません、しかし、Windows フォームや WPF などでは、更新系アプリケーションにおけるデータ入力画面を作成する際に、今回解説する双方向データバインドを利用します。
例えば、下図のような新規顧客登録画面を作ろうと思った場合を考えてみてください。ASP.NET Web フォームの場合には、通常のテキストボックスと検証コントロール(Validation Control)によりこのような画面の単体入力チェック機能を作るはずです。しかし、Windows フォームの場合には、検証コントロールは使いません(というより存在しません)。このような場合に、双方向データバインドと IDataErrorInfo インタフェースを利用して、単体入力チェック機能を実装します。
- ASP.NET Web フォームの場合 : 検証コントロールで実装
- Windows フォームの場合 : 双方向データバインドと IDataErrorInfo で実装
この双方向データバインドによるエラーチェックは、単票形式でもグリッド形式でもどちらでも利用可能ですが、いきなりこれらの解説に入る前に、まずは Windows フォームによるデータバインドの基本についてもう少し解説しましょう。
[2 種類のデータバインド]
上にちょろっと書いたように、データバインドには、単票形式のデータバインドと、グリッド形式のデータバインドの 2 種類があります。(ちなみにこの分類方法は、Windows フォームだけでなく WPF でも同じです。)
- 単一値データバインド (単票形式のデータバインド)
データソースとして、「あるオブジェクトインスタンス(の一つ)」を使うもの
- コレクションデータバインド (グリッド形式のデータバインド)
データソースとして、「オブジェクトインスタンスのコレクション」を使い、一括表示するもの
基本的に単一値データバインド(単票形式データバインド)は、1 レコード分のデータを保有するオブジェクトインスタンスを、単票形式に表示するためのものです。そしてこれを n 回、縦方向に繰り返したものがコレクションデータバインド(グリッド形式データバインド)になります。このため、まずは単一値データバインド(単票形式データバインド)のことを正しく理解することが重要です。これを正しく理解すれば、コレクションデータバインドもおのずと理解できるようになります。
[データバインドの基本的な使い方 : データソースとなりうるオブジェクト]
さて、データバインドは、UI コントロール(バインドされたデータを表示する UI 部品) とデータソースオブジェクト(実際のデータとなるもの)とが、BindingSource オブジェクト(データバインドを制御するもの)により連結制御されることにより実現されます。
よって、このような連結関係を組み立て上げればデータバインドが使えるようになるのですが、このデータバインドのデータソースとして利用できる代表的なオブジェクトとしては、以下の 2 つがあります。これについて解説しておきましょう。
- カスタムオブジェクトおよびそのコレクション
- 型付きデータ行および型付きデータテーブル
① カスタムオブジェクトおよびそのコレクション
public フィールドや public プロパティを持つオブジェクトは、そのまま単票形式にバインド(単一値データバインド)することができます。具体的には、下図のサンプルに示すようなクラスのインスタンスを単票形式データバインドのデータソースに、またそのコレクションをグリッド形式データバインドのデータソースに利用できます。
② 型付きデータ行および型付きデータテーブル
また、.xsd ファイルを用いて作成できる型付きデータセットの中に含まれる、型付きデータテーブルと型付きデータ行は、それぞれグリッド形式データバインドと単票形式データバインドに利用できます。
②の型付きデータセットや型付きデータテーブルを利用するデータバインドについては、ASP.NET Web フォームにおけるデータ表示用データバインドなどでもお馴染みだと思います。しかし、Windows フォームでデータバインドを行う場合、
- データ表示しか行わない場合には、②を利用する。
- 表示したデータをユーザに更新させる場合には、①を利用する。
という使い分けを行います。特に後者が重要で、Windows フォームで更新系アプリケーションを作る場合には、型付きデータセットを使った双方向データバインドは行いません。このことを頭の片隅に置いておいてください。(理由はおいおいわかってくると思いますが、簡単に記述すると、型付きデータセットを使うと単体入力データ検証ロジックの実装が分かりづらくなるからです。)
では引き続き、具体的なデータバインドの実装方法(Visual Studio 2008の使い方)について解説していきます。今回の blog エントリのゴールは単体入力チェック機能を持つデータ入力ページを作成することであるため、①の方法を中心に解説します。また、ここではまだ単体入力チェック機能は実装しません。まず、双方向データバインドの組み立て方(BindingSource オブジェクトによる連結方法)に絞って解説します。
[データバインドの基本的な使い方 : 単票形式データバインド]
まずは単票形式のデータバインドから解説していきましょう。具体的には、以下の 4 つの作業を行います。
- データバインドするオブジェクトの準備
- データソースの登録
- BindingSource コントロールの貼り付け
- データの紐付け
ここでは、以下のような画面を双方向データバインドで組み上げる方法について解説します。
Step 1. データバインドするオブジェクトの準備
まず、UI とバインドを行うためのクラスを作成します。具体的には、新規に Windows フォームアプリケーションプロジェクトを作成し、TitleInput.cs という名前でクラスを追加します。

ここに、データ 1 レコード分に相当するオブジェクトのクラスを定義します。なお、null 入力値を受け付けるフィールドについては、Nullable<T> 型で定義をしてください。
1: namespace WindowsFormsApplication1
2: { 3: public class TitleInput
4: { 5: public string title_id { get; set; } 6: public string title { get; set; } 7: public decimal? price { get; set; } 8: public DateTime? pubdate { get; set; } 9: }
10: }
Step 2. データソースの登録
次に、Visual Studio でビジュアルデザイン機能を利用できるようにするため、データソース登録を行います。以下の作業を行ってください。
- プロジェクト全体をまずリビルドする。
- ツールバーの「データ」→「データソースの表示」を行って、データソースウィンドウを表示する。
- データソースウィンドウ内の「新しいデータソースの追加」をクリックする。
- データソース構成ウィザードが起動するので、TitleInput オブジェクトを選択する。
これにより、データソースウィンドウに、TitleInput クラスのデータ構造情報が取り込まれ、GUI からのデータバインド設定操作ができるようになります。
Step 3. BindingSource コントロールの貼り付け
次に、単票形式の画面を作成します。具体的には、以下の作業を行います(が、できればウィザードを使わないでできるようになってほしいので、まずは後述する方法を使ってください)。
- Form1.cs のデザイン画面を開く。
- データソースの TitleInput の貼り付け方式を「DataGridView」から「詳細」に切り替えた上で、フォーム上にドラッグ&ドロップする。
- 単票形式のデータバインドの場合、BindingNavigator コントロールは不要なので削除する。
- あとは適宜、画面を編集する。(ラベルの追加やボタンの追加など)
実際の開発では上記の作業を行っていくのですが、これだとウィザードがかなりの部分を自動的にやってしまうので、何をやっているのかがわからないと思います。以下のようにすると、上記の作業をきちんと一つずつ手作業で行うことができます。最初のうちはこちらの方法でやってください。
- BindingSource コントロールを画面に貼り付ける。
- BindingSource コントロールの DataSource プロパティに、作業 2. でデータソース登録したクラス(この例の場合は TitleInput クラス)を指定する。(この作業により、以降のステップで、データソースのプロパティとの紐づけが GUI から簡単に設定できるようになる)
- 画面上に、ラベルやテキストボックスを貼り付けて並べていく。
- データバインド対象となる UI コントロール(例えばテキストボックスなど)の DataBindings プロパティを開き、データソースオブジェクトのターゲットプロパティとの紐づけ関係を指定する。
Step 4. データの紐付け
最後に、フォームのロードイベントなどにデータの紐づけコードを作成します。具体的には、以下の作業を行います。
- フォームの private メンバ変数として、データバインドするオブジェクトインスタンスの変数を宣言しておく。(下記コードの _data 変数)
- Form_Load() メソッド内で、データバインドするオブジェクトインスタンスを作成し、BindingSource コントロールに割り当てる。
1: public partial class Form1 : Form
2: { 3: public Form1()
4: { 5: InitializeComponent();
6: }
7:
8: private TitleInput _data;
9:
10: private void Form1_Load(object sender, EventArgs e)
11: { 12: this._data = new TitleInput()
13: { 14: title_id = "BU1032",
15: title = "Book of .NET",
16: price = 30,
17: pubdate = new DateTime(1973, 6, 7)
18: };
19: this.bindingSource1.DataSource = this._data;
20: // ※ ウィザードで作成した場合は
21: // this.titleInputBindingSource.DataSource = this._data;
22: }
23: }
以上の作業により、下図のようなデータバインドが構成され、画面上にデータの内容が自動的に表示されるようになります。
- UI 画面と BindingSource コントロールとデータソースオブジェクトの関係
- 実行結果
なお、注意していただきたいのは Step 3 と Step 4 で行うプロパティへの設定の違いです。
- Step 3 では、BindingSource コントロールの DataSource プロパティに、データソースウィンドウで設定した TitleInput クラスを設定している。
- Step 4 では、BindingSource コントロールの DataSource プロパティに、実際に Form_Load() イベント内で作成した TitleInput オブジェクトのインスタンスを割り当てている。
実際に UI 画面上にデータを表示するためには、DataSource プロパティにオブジェクトのインスタンスを割り当てなければなりません(Step 4)。しかしながら、UI 画面のデザイン時には、まだオブジェクトインスタンスが作成されていません。そこで、UI 画面のデザイン時(Step 3)には、インスタンスのかわりにクラスを割り当てておくことで、デザイン機能を有効化する、というわけです。
現時点での実装上の問題
ここまでで、_data 変数の内容が自動的に UI 上に表示されることが確認できましたので、今度は UI からの入力がデータソースである _data 変数に反映されることを確認してみましょう。まず、button1_Click() メソッドに以下のような処理を組み込んでおきます。
1: private void button1_Click(object sender, EventArgs e)
2: { 3: MessageBox.Show(
4: "書籍ID : " + _data.title_id + "\n" +
5: "書籍名 : " + _data.title + "\n" +
6: "価格 : " + _data.price + "\n" +
7: "出版日 : " + _data.pubdate);
8: }
その上で、以下のような作業をしてみてください。
- 不適切なデータの入力
書籍 ID に不適切なコードを入力したり、価格にマイナスの値を入力しても、問題なく入力が通り、そのまま背後の _data 変数に入力内容が反映されてしまいます。
- フォーカス制御(フォーカス移動の抑止)
例えば、価格欄に “ABC” などの型変換ができないデータを入力した場合には、フォーカス移動ができなくなります。
- null 値の入力
価格や出版日は null 値が設定できるはずですが、入力を空欄(空文字 “”)にしても、null 値が設定されません。
確かに、UI からの入力内容は BinidngSource コントロールによりデータソースに反映されています。しかしながら、空文字として入力された値を null 値に変換したり、不適切なデータ入力を抑止させたりするといった制御が行われておらず、このままでは業務アプリケーションのデータ入力フォームとしては利用できません。(これらの問題の解決方法については後述します。)
では引き続き、グリッド形式のデータバインドの実施方法の基本を解説します。
[データバインドの基本的な使い方 : グリッド形式データバインド]
グリッド形式のデータバインドには、通常 DataGridView コントロールを利用します。具体的には、以下の作業を行います。
- データバインドするオブジェクトの準備
- データソースの登録
- DataGridView コントロールの貼り付けとデータソースの指定
- DataGridView の表示列のカスタマイズ
- データソースへのコレクションの割り当て
これについて解説します。
1. データバインドするオブジェクトの準備、2. データソースの登録
先に解説したように、コレクションデータバインドは単一値データバインドの n 件繰り返しになります。このため、データソースとして利用するオブジェクトは、先の単票形式データバインドの場合とまったく同じになります。1 レコード分に相当するオブジェクトとして TitleInput クラスを作成し、データソースとして登録を行っておいてください。
3. DataGridView コントロールの貼り付けとデータソースの指定
次に、コレクションデータバインドを行う画面を作ります。アプリケーションにもうひとつフォームを追加し、以下の作業を行います。
- BindingSource コントロールと DataGridView コントロールを貼り付ける。
- bindingSource1 の DataSource プロパティに、TitleInput クラスを指定する。
- dataGridView1 の DataSource プロパティに、bindingSource1 インスタンスを指定する。
以上の作業により、下図のようなプレビュー画面が表示されます。
4. DataGridView の表示列のカスタマイズ
次に、DataGridVIew のタスクペイン(コントロールの右上に表示される小さな矢印から開かれるタスク一覧)から列の編集を行います。これにより、表示列の絞り込みや、列ヘッダーや列幅、表示フォーマットなどの調整ができます。(※ 価格列については、DefaultCellStyle プロパティを変更すると通貨表示ができます。)
5. データソースへのコレクションの割り当て
最後に、Form_Load() イベントなどで、bindingSource1 の DataSource プロパティにて、実際に画面上に表示するデータコレクションを割り当てます。(なお、今回は簡単のため、データをその場で作成して割り当てますが、実際の業務アプリケーションでは、XML Web サービスなどからデータを取り寄せて、そのデータを紐づけ表示することになります。)
1: public partial class Form2 : Form
2: { 3: public Form2()
4: { 5: InitializeComponent();
6: }
7:
8: private List<TitleInput> _dataItems;