- PR -

ShowDialogでハングする。

投稿者投稿内容
COPECHAN
会議室デビュー日: 2006/06/21
投稿数: 12
投稿日時: 2006-07-08 21:38
お世話になっております。

随分と期間が開いてしまいましたが、現象の再現性が確認できました。

下記のアドレスに現象再現の環境一式を提示させていただきます。
(一応、ウイルスチェックも行っております。)
http://www.copechan.com/~copechan/jp/upload/TestApp.zip

原因:
ポップアップ画面のLoadイベントが発生する前に、
ポップアップ画面のタイマオブジェクトがタイムアップし、
且つ、そのイベントハンドラ内で、ポップアップ画面のTextBoxに対して、
SelectAllメソッドを呼ぶと発生します。(タイムアップ+SelectAllの複合条件)
また、ハングは約5分ほどで解けます。(←環境によって異なる可能性あり)

この現象に伴い、画面の描画もおかしくなります。
本来表示されるべきTextBox4ヶが、3もしくは2ヶしか表示されません。(←実行する環境によって、発生する現象は異なります。)
また、プロジェクトの大きさ環境によっても、ハングする箇所が異なります。
こちらで再現したShowやShouDialogメソッド以外のプロパティでも、
ハングします。

現象の回避:
タイマイベントのEnableメソッドはdefaultでfalseにし、
Loadイベント内で、trueにすること。

上記のことから、バグの可能性があると思われるのですが、
皆様のご意見はいかがでしょうか?

よろしくお願いいたします。
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-07-09 03:24
System.Timers.Timer はドキュメントにもありますが別スレッドで動作します。
しかし、ウィンドウはウィンドウ(のハンドル)を作成したスレッド上で操作しなければならないという制限があります。
そのために Control クラスにはコントロールを作成したスレッドに処理を委譲させる Invoke / BeginInvoke というメソッドが用意されています。
毎回このメソッドを呼び出すのは面倒なので、System.Timers.Timer は SynchronizingObject というプロパティが用意されています。このプロパティにコントロールを追加しておけば、Elapsed イベントのイベントハンドラは自動的に必要に応じて Invoke / BeginInvoke 越しに実行され、イベントハンドラ側で Invoke / BeginInvokeを呼び出す必要が無くなります。
Invoke / BeginInvoke の呼び出しはコストが多少なりとかかるため、普通は InvokeRequired プロパティを参照して必要な時だけ Invoke / BeginInvoke を呼び出し、そうでない場合は普通にタイマの動作するスレッドでイベントハンドラが実行されます。
ここで、Invoke / BeginInvoke はコントロールのハンドルが作成されていない間は呼び出す事ができないという事実を知っておく必要があります。InvokeRequired も false を返します。
そのため、COPECHAN さんのサンプルコードを実行すると、Elapsed イベントのハンドラはタイマが動作するスレッドで実行され、そこでコントロールのメソッド呼び出し(今回の場合は TextBox.SelectAll ですね)の結果テキストボックスのハンドルが作成されます。
その後フォームを表示するために Show / ShowDialog を呼び出しますが、この時のスレッドは当然メインスレッドですので、フォーム(のハンドル)が作成されるスレッドはメインスレッドという事になります。
つまり別スレッドのコントロールがフォームに混入してるわけですね。
ハングするのは多分お互いにメッセージ処理の完了を待ち合ってデッドロックしてるんでしょう。

どうしてもタイマが必要なら System.Windows.Forms.Timer を使えばスレッドには悩まなくて済みます。これはメインスレッド上で実行されるタイマですので。
まあそもそも何故タイマなのかさっぱりわかりませんが。
nanbu
大ベテラン
会議室デビュー日: 2004/08/19
投稿数: 178
投稿日時: 2006-07-09 15:31
南部です。
ソースを拝見しました。

引用:

Hongliangさんの書き込み (2006-07-09 03:24) より:

そのため、COPECHAN さんのサンプルコードを実行すると、Elapsed イベントのハンドラはタイマが動作するスレッドで実行され、そこでコントロールのメソッド呼び出し(今回の場合は TextBox.SelectAll ですね)の結果テキストボックスのハンドルが作成されます。


テキストボックスのハンドルが作成されるのは、
親ウィンドウの表示時だと思います。

1. ExpTextBox構築
2. コンストラクタ内、タイマースレッド開始(AllSelectTimer_Elapsed)
3. Show・ShowDialog() ExpTextBoxのハンドル作成
4. AllSelectTimer_Elapsed内ExpTextBox.SelectAll()
この順序で実行された時、クロススレッド呼び出しになります。

2.と4.の間にExpTextBoxのハンドル作成が来ないタイミングの場合は、
ハンドル作成前 - SelectAll()が実行できない?(と思う)
ハンドル作成後 - タイマースレッドがメインスレッドにマーシャリングされる
となり、問題はありません。

***********************************************
Hongliangさんのご指摘のとおり、
上記内容は、.NET 2.0で確認したものであり、
本スレッドでの環境(.NET 1.1)とは異なっており、
混乱を招いたことをお詫びします。
***********************************************


[ メッセージ編集済み 編集者: nanbu 編集日時 2006-07-09 21:52 ]
Hongliang
ぬし
会議室デビュー日: 2004/12/25
投稿数: 576
投稿日時: 2006-07-09 20:02
引用:

nanbuさんの書き込み (2006-07-09 15:31) より:

テキストボックスのハンドルが作成されるのは、
親ウィンドウの表示時だと思います。



んー、こんな実証コードはどうでしょう。
コード:
class Sample {
    [STAThread] public static void Main() {
        TextBox tb = new TextBox();
        tb.HandleCreated += new EventHandler(OnCreated);
        Console.WriteLine("Selecting");
        tb.SelectAll();
        Console.WriteLine("Selected");
    }
    private static void OnCreated(object sender, EventArgs e) {
        Console.WriteLine("Handle was created.");
    }
}


.NET 1.1 では Handle was created が Selecting と Selected の間に表示されます。
// .NET 2.0 ではこれだけでは OnCreated は呼び出されませんが。
確かに通常は親ウィンドウが表示される時に子ウィンドウのハンドルが作成されますが、例えば子ウィンドウの Handle プロパティを参照したら、その時点で強制的に子ウィンドウのハンドルが作成されます。SelectAll も内部でやってるんでしょうね。
Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2006-07-09 20:13
コードよりも『手順』を出して欲しい。

他人のコードから手順にするのは労力がかかるし、
何より、コードを手順に自分で戻せば、
コード化した手順と見比べることで、
バグっている箇所を特定できる、、、という特典付き!!
(勉強会で言った、『逆設計』)

2006-06-22 22:02 で指摘した分もあわせて、再設計が必要じゃないかなぁ?
nanbu
大ベテラン
会議室デビュー日: 2004/08/19
投稿数: 178
投稿日時: 2006-07-09 21:46
引用:

Hongliangさんの書き込み (2006-07-09 20:02) より:
引用:

nanbuさんの書き込み (2006-07-09 15:31) より:

テキストボックスのハンドルが作成されるのは、
親ウィンドウの表示時だと思います。



.NET 1.1 では Handle was created が Selecting と Selected の間に表示されます。
// .NET 2.0 ではこれだけでは OnCreated は呼び出されませんが。
確かに通常は親ウィンドウが表示される時に子ウィンドウのハンドルが作成されますが、例えば子ウィンドウの Handle プロパティを参照したら、その時点で強制的に子ウィンドウのハンドルが作成されます。SelectAll も内部でやってるんでしょうね。


ご指摘ありがとうございます。
先程の私の投稿を撤回致します。
申し訳ございませんでした。

COPECHAN
会議室デビュー日: 2006/06/21
投稿数: 12
投稿日時: 2006-07-10 08:42
お世話になっております。

Windowsフォーム・コンポーネントの両方にタイマがあることは認識していましたが、
別物という認識がありませんでした。
VB同様に同一スレッドで動作しているものと勝手に思い込んでおりました。

Win32のネイティブでは、別スレッドで作成したウィンドウハンドルに対してアクセスした場合、無視されていたように思います。
C#では、デッドロックするのですね。
また、デッドロックから解放された時に、TextBoxオブジェクトが描画されないのは、「Windowハンドルが別スレッドで生成されてしまった為」という事なのですね、納得しました。


タイマを使用していたかについては、テキストボックスをマウスクリックした時に内容を全選択するためです。
TABキーでフォーカスを当てるとTextBoxの内容が全選択されますが、マウスでフォーカスを当てると全選択されないので、Enterハンドラにタイマをはって、一定時間経過後SelectAll()を呼び出すようにしていました。(これしか思いつかなかったので・・・)


P.S.
C#でスレッドの呪縛から解放されると思っていたのですが、そうではないのですね。

色々と勉強になりました、ありがとうございました。
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2006-07-10 08:51
引用:

Win32のネイティブでは、別スレッドで作成したウィンドウハンドルに対してアクセスした場合、無視されていたように思います。
C#では、デッドロックするのですね。



「アクセス」の仕方によります。

.NET では Invoke() で、Win32 の場合は SendMessage() で、デッドロックするような操作をするとデッドロックします。

スキルアップ/キャリアアップ(JOB@IT)