- PR -

イベントハンドラーの中でさらにイベントを起こすと、再帰的に呼び出されてしまう

投稿者投稿内容
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-13 19:33
引用:

れいさんの書き込み (2008-04-13 05:23) より:
Win32ではユーザーメッセージをPostMessageするというのが普通でした。
.NetではおそらくControl.BeginInvokeを使うことを想定していたのだと思います。
残念ながらうまく動きませんので、私はTimerやApplication.Idleを使います。


Application.Idle を使ってみました。イベントハンドラーは Application_Idle というメソッドです。自前でキューを持っており、イベントをレイズしたいときには、一旦キューに追加し、その後 Application_Idle が動いたときに、実際にイベントがレイズされます。
この動きは、button1 を押した場合は、私の目論見どおり動きました。

一方、button2 も作り、これを押すと、タイマーを起動して、3秒後にキューに入れるようになっています。この場合、問題があって Application_Idle が呼ばれなくなるのです。フォームの上でマウスを動かしていたり、なにかキーを押せば Application_Idle が頻繁に起こるのですが、完全にじっとしていると Application_Idle が呼ばれません。(タイマーを使ったのは、テスト用に、ボタンを押した後にじっとするまでの余裕を作るためです。)

要は、好きなタイミングで Application_Idle が呼ばれるようにしたいのですが、その手段が分かりません。
Application クラスに RaiseIdle という名前のメソッドがあるのをたまたま見つけて、とりあえず呼んでみましたが、変化はないようでした。
まあ、なにかフォーム上のコントロールをプログラムからごにょごにょいじれば Application_Idle が起こるとは思いますが、なんだかスマートではないなあと思いました。
なお、タイマーのプロパティー SynchronizingObject にフォームのインスタンスを指定して、UI のスレッドでタイマーを動かせば Application_Idle は起きました。

コード:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace HogeApp
{
public partial class Form1 : Form
{
private Hoge hoge = new Hoge();
private System.Timers.Timer timer = new System.Timers.Timer();

public Form1()
{
InitializeComponent();
hoge.HogeHandler += new HogeEventHandler(hoge_HogeHandler);
timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
}

private void hoge_HogeHandler(object sender, HogeEventArgs e)
{
Console.WriteLine("hoge_HogeHandler at " + DateTime.Now);
Thread.Sleep(1 * 1000);
hoge.foo();
}

private void button1_Click(object sender, EventArgs e)
{
hoge.foo();
}

private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
Console.WriteLine("timer_Elapsed at " + DateTime.Now);
hoge.foo();
}

private void button2_Click(object sender, EventArgs e)
{
timer.AutoReset = false;
// SynchronizingObject プロパティーを設定すると、タイマーが発火するスレッドを選べる。
//timer.SynchronizingObject = this;
timer.Interval = 3 * 1000;

timer.Start();
}
}

class HogeEventArgs : EventArgs
{
public HogeEventArgs()
{
}
}

delegate void HogeEventHandler(object sender, HogeEventArgs e);

class Hoge
{
public event HogeEventHandler HogeHandler;
// イベントキュー
private List<HogeEventArgs> eas = new List<HogeEventArgs>();

public Hoge()
{
Application.Idle += new EventHandler(Application_Idle);
}

private void Application_Idle(object sender, EventArgs e)
{
Console.WriteLine("Application_Idle at " + DateTime.Now);
// イベントキューが空になるまで繰り返す。
while (eas.Count > 0)
//if (eas.Count > 0)
{
HogeEventArgs ea = eas[0];
eas.RemoveAt(0);
if (HogeHandler != null)
{
HogeHandler(this, ea);
}
}
}

public void foo()
{
eas.Add(new HogeEventArgs());
// Application.RaiseIdle(new EventArgs());
}
}
}



(以下、追記。)
話がややこしくなってすみません。ここでのタイマーは、マウスイベントやキーイベントのほとぼりが冷めるまで待つまでのためだけの、一時的なテスト用のものです。
キューの List に要素を追加しただけでは、当然のことながらだれも感づいてくれず、したがって Application_Idle が呼ばれないわけですが、この辺の仕組みをどう解決すればよいかで悩んでいます。

[ メッセージ編集済み 編集者: unibon 編集日時 2008-04-13 19:43 ]
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2008-04-13 20:28
引用:

渋木宏明(ひどり)さんの書き込み (2008-04-12 23:50) より:
引用:

こういうアプリケーションをイベントを使って作ると、私のような再帰の問題が出てくると思います。



そういう場合、イベント引数とかに「誰が発したイベントなのか」を示すヒントを入れておいて、それを元にイベントハンドラの先頭で再帰しないようにガードするもんじゃないですか?



すみません。自分でも話がややこしくなって良く理解できていないのですが、先日挙げたAさん、Bさん、Cさん、の例は適切ではなかったかもしれません。
私のアプリケーションでは、Aさん、Bさん、Cさん、の3つのインスタンスを区別したいのではないと思います。マルチキャストはしたいのですが。
れい
ぬし
会議室デビュー日: 2005/11/01
投稿数: 346
投稿日時: 2008-04-13 22:59
引用:

unibonさんの書き込み+(2008-04-13+19:33)+より:
要は、好きなタイミングで+Application_Idle+が呼ばれるようにしたいのですが、その手段が分かりません。


Idleはその名の通り、Idleになったとき=メッセージが空になったときに
一度だけ起きるイベントです。

定期的にイベントを発生させたいならTimerを、
全てのイベントが終了して暇になったのを知りたいときにはIdleを。

よく考えればわかると思いますが、
イベントドリブン的には十分なイベントがそろっています。

内部でループで条件待ちをしたり、
WaitHandleを使ったり、
Thread.Sleepで他のアプリに時間の解放をしたり、
DoEventsを使って危険なメッセージポンピングをしたり…

そういったことは全く必要ありません。
TimerやApplication.Idleを使うことで
レスポンスもパフォーマンスもよく、無駄のない、
「よいアプリケーション」を組むのは理論上可能です。

問題は、イベントの使い方、Idleの使い方、
そういったパラダイムが逐次実行の概念と合わず、
少しわかりづらいという点です。

引用:

要は、好きなタイミングで+Application_Idle+が呼ばれるようにしたいのですが、その手段が分かりません。


最初に言ったように、
イベントドリブンというものをもう一度よく考え、
「好きなタイミング」というのが「いつ」なのか、
よく考えてみるとよいでしょう。
未記入
大ベテラン
会議室デビュー日: 2008/02/07
投稿数: 115
投稿日時: 2008-04-14 07:24
Application.Idle というものを知りませんが…。wikipediaの説明を読むと興味深いことが書いてありますね。シングルスレッドなのにハンドラのコード実行中に他に制御が移るってことなのかな…。よく分かりません。今度、暇を見つけて試してみよう。。

で、unibon さんの書いてる症状を見ると、なんか GetMessage() なり、WaitMessage() なりでメッセージを待っているときと似ているように思います。これって Windows アプリケーションとして当たり前のことですよね。(マウス操作など)何もしなければ、なにも起こらない。でも、Application.Idle というハンドラが発生しなくなっても良いものなのかと疑問に思います。

Application.Idle って内部的にはどうやって実装されてるんでしょうね。メッセージループで、メッセージが着信したあとで、GetQueueStatus() でキューの状態を調べて空なら Application.Idle ハンドラを呼んでるとか? それで、メッセージが着信しない状況では動かなくなってしまうのかなあと予想。

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