トップ回答者
フォームクローズと更新が重なると異常終了する

質問
-
環境: Visual stdio 2015 C#
フォームアプリで、カウントダウン用の表示を行っていますが、表題のように、Form内のデータ表示と、フォームのクローズ重なるタイミングで異常終了する事があります。現象としては、ほぼ、把握できたのですが、どう対応すべきか困っています。
発生するパターン:
ClassA(コントロールするためのクラス): ここで、次のフォームを作成、制御する。
public event Action EventInterval;
定期的に、EventInterval?.Invoke(); で呼出し、下記フォーム表示の更新を行う。
FormA(カウントダウン表示用フォーム)
ClassA.EventInterval には、: TextBoxの表示更新(値設定)用メソッドを設定
Closed/Closing イベントで、ClassA.EventInterval から、設定メソッドを削除。
上記の設定で、画面更新(EventInterval?.Invoke()) と画面クローズが重なると、
異常終了する。 (異常終了の内容: TextBoxの参照が失敗)
ここで、呼出し元の EventInterval を見ると、nullになっている
大変、分かりにくく申し訳ないですが、このような場合の対応方法について分かる方がいましたら、教えてくださるようお願いします。
回答
-
SynchronizationContextの使い方がもう一つなのですが、このような場合に有効でしょうか?
Windows Forms の場合、「最初の Control 作成時」か、「Application.Run 実行時」に一度設定され、メッセージループを完全に抜けるまでは、同期用の Control を持った、WindowsFormsSynchronizationContext が設定されています。
このため、フォームを閉じる瞬間の同期の際、Invoke/BeginInvoke 内で例外が発生する…といったリスクは減ります。
もちろん、SynchronizationContext.Send/Post の先で IsDisposed チェックが必要なのは変わりません。今、ObjectDisposedException例外を読み捨てで暫定対応。
ところで、"無限待ち"とはどのような場合でしょうか?以前は Invoke の先の実態、MarshaledInvoke やその先で、処理完了待ちのところで PostMessage が呼ばれた後、ウィンドウの生死チェックがなかったので、タイミング次第では無限待ち(Invoke から帰ってこない)といったことがあったはずですが、最近のコード を見る限りは、大丈夫そうです。
なお、前述のハイパーリンク先を見るとわかりますように、ObjectDisposedException は Invoke の内部実装で発生するので、Control.IsHandleCreated や Control.InvokeRequired、Control.IsDisposed といった事前チェック方式では回避できませんので、try-catch は必要ですね。
(Invoke の内部実装で PostMessage して待ち受けループに入る前後で、相手先ウィンドウ(フォーム)の Destroy/Dispose に至ると起きるので、スレッドが並走する限りは、事前チェック方式では回避不可です)- 編集済み AzuleanMVP, Moderator 2020年2月18日 12:56
- 回答としてマーク pepperleaf01 2020年2月19日 12:34
すべての返信
-
いまいち何やっているのか説明不足気味なので推測しかできないですが…
更新が重なると書いてありますが、シングルスレッドならあり得ないことですから、マルチスレッドという事ですよね?
マルチスレッドだとして、「TextBoxの参照が失敗」ということは正常時にはControl.Invoke/BeginInvokeでUIスレッドに切り替えてTextBoxを操作しようとした処理でエラーが発生しているという事ですよね?
そうであればTextBoxは必ずUIスレッドから操作されることになるので、重なるという事はあり得ないことになります。にもかかわらずTextBoxの参照で失敗するという事は、TextBoxが削除された後にEventIntervalから呼びだしている処理を実行してしまっているという事です。
ならば、Formを閉じるよりも前に別スレッドからの呼び出しイベントをきちんと解除し、BeginInvokeを行っているならBeginInvokeで呼ばれる処理ではCloseされているなら余計な処理を実行しないようにすればいいです。
それでもエラーが出るなら、AppDomain.CurrentDomain.UnhandledExceptionで例外を捕まえて、その例外がどこでどうして発生しているのか調べましょう。
これらよりももっと簡単なのはマルチスレッドにせずにForms.Timerを使ってUIスレッド以外から更新操作をしないことです。
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
-
-
-
SynchronizationContextの使い方がもう一つなのですが、このような場合に有効でしょうか?
Windows Forms の場合、「最初の Control 作成時」か、「Application.Run 実行時」に一度設定され、メッセージループを完全に抜けるまでは、同期用の Control を持った、WindowsFormsSynchronizationContext が設定されています。
このため、フォームを閉じる瞬間の同期の際、Invoke/BeginInvoke 内で例外が発生する…といったリスクは減ります。
もちろん、SynchronizationContext.Send/Post の先で IsDisposed チェックが必要なのは変わりません。今、ObjectDisposedException例外を読み捨てで暫定対応。
ところで、"無限待ち"とはどのような場合でしょうか?以前は Invoke の先の実態、MarshaledInvoke やその先で、処理完了待ちのところで PostMessage が呼ばれた後、ウィンドウの生死チェックがなかったので、タイミング次第では無限待ち(Invoke から帰ってこない)といったことがあったはずですが、最近のコード を見る限りは、大丈夫そうです。
なお、前述のハイパーリンク先を見るとわかりますように、ObjectDisposedException は Invoke の内部実装で発生するので、Control.IsHandleCreated や Control.InvokeRequired、Control.IsDisposed といった事前チェック方式では回避できませんので、try-catch は必要ですね。
(Invoke の内部実装で PostMessage して待ち受けループに入る前後で、相手先ウィンドウ(フォーム)の Destroy/Dispose に至ると起きるので、スレッドが並走する限りは、事前チェック方式では回避不可です)- 編集済み AzuleanMVP, Moderator 2020年2月18日 12:56
- 回答としてマーク pepperleaf01 2020年2月19日 12:34
-
Azuleanさん、説明、ありがとうございます。
> もちろん、SynchronizationContext.Send/Post の先で IsDisposed チェックが必要
> Control.IsHandleCreated や Control.InvokeRequired、Control.IsDisposed といった事前チェック方式では回避できませんので、try-catch は必要ですね。
という事は、Try-catch で十分ということでしょうか?
クローズ時のTextBox更新は当然、意味が無く、読み捨てでよいので。色々とありがとうございます。