none
他のスレッドで動くControlへ向けてイベントを発する方法 RRS feed

  • 質問

  • 外池と申します。

     

    InvokeRequiredがTrueな状態のControl(あるいは派生したもの)オブジェクトへイベントを発したい場合、何らかの形でControl.Invokeを使わないといけないわけですが、VBでイベントを定義する方法が3種類ある(Delegateを使わず定義するもの、Delegateを使って定義するもの、Customに定義するもの)うちCustomではない(前者2つ)方法を使う場合、どうすればよいのでしょう?

     

    RaiseEventを含むメソッド全体をControl.Invokeで呼び出す・・・、ということも考えましたが、スレッドの違いを意識する必要がないイベントハンドラまで巻き込むことになり変ですよね。Customな定義しか方法はないという理解で良いのでしょうか?

     

     

     

     

    2008年10月8日 0:13

回答

  •  外池 さんからの引用

    そうしますとですね、Visual StudioにはFormに貼り付けることを前提にいろんなコントロールが附属していて、私は、そのコントロールが発するイベントを使って、Form自身や他のControlを無造作に操作しちゃっています。たぶんみなさんもそうだと思いますが。これは、Formも含めてこれらのコントロールの発するイベントが、特に大丈夫なようにプログラムされているから、ということですね? これは特殊なんだと。

    Windowsメッセージに基づいたイベントの場合、Windowsメッセージを受け取るのは原則的にそのウィンドウが属するスレッドなので、Controlが属するスレッドでイベントが実行されることになります。

    このため、大抵の場合はスレッドを意識する必要がありません。

    (Control.Invokeも内部的にWindowsメッセージを利用しているはずです)

     

    もちろん、BackgroundWorkerのDoWorkイベント、FileSystemWatcherの各イベントは同じように使えるように見えますが、違うスレッドである点を意識する必要があります。

    そのあたりはクラスの作成者であるMicrosoftが情報を公開しています。

     

    http://msdn.microsoft.com/ja-jp/library/system.componentmodel.backgroundworker(VS.80).aspx

    DoWork イベント ハンドラでユーザー インターフェイス オブジェクトを操作しないように注意する必要があります。

    http://msdn.microsoft.com/ja-jp/library/system.io.filesystemwatcher.synchronizingobject(VS.80).aspx

    SynchronizingObject が null 参照 (Visual Basic では Nothing) の場合は、Changed、Created、Deleted、Renamed の各イベントを処理するメソッドがシステム スレッド プールのスレッドで呼び出されます。システム スレッド プールの詳細については、ThreadPool のトピックを参照してください。

     

    FileSystemWatcherのドキュメントはやや不親切である点は否めませんが。

     

     外池 さんからの引用

    私、さらに別の会社が用意した計測器を制御するクラスライブラリーを使ってコールバックを伴う非同期実行をやったり、コールバックからイベントを発したりしていて、Form上のコントロールに測定結果を表示するようなことをしていますが、比較的頻繁に「コントロールと同一のスレッドで呼び出せ」の例外に遭遇して、Invokeを使うことを覚えました。この場合も、イベントを発する側でInvokeするのではなく、イベントを受ける側が、まずはイベントハンドラで受けて、その中でコントロールのプロパティーに書き込むための別のメソッドをInvokeするようにすべき・・・、

    ですね。

    コールバック関数の時点でUIに反映したいニーズが多いですが、もしかしたら、次の呼び出しだけをしたいのかもしれません。

    そのあたりは作り手に自由度を与えるべきかなと。

     

    ちなみに、クラスライブラリ側で判定可能というのは作り方次第です。

    Controlのインスタンスを持った独自のクラス(Controlj派生クラスではないもの)のメソッドがイベントハンドラとして登録されており、そのイベントハンドラでControlインスタンスに対して操作していた場合、クラスライブラリ側では判断できません。

    2008年10月9日 21:44
    モデレータ

すべての返信

  • Custom が何に関係してくるのか良く分かりませんが。
    ざっくりこんなんでは?

    Code Snippet

    Public Event Hoge As EventHandler
    Private Delegate Sub EventHandlerCallback(e As EventArgs)
    Protected Overridable Sub OnHoge(e As EventArgs)
        RaiseEvent Hoge(Me, e)
    End Sub
    Private Sub Work()  ' 別スレッドで実行されるメソッド
        Thread.Sleep(10000)
        Me.Invoke(New EventHandlerCallback(AddressOf Me.OnHoge))
    End Sub


    それともなにか別のことでしょうか?
      2008年10月8日 0:54
    1. 外池です。

       

      Hongliangさん、お返事ありがとうございます。お示し頂いたアプローチは私も既にやってまして、限定された条件下では問題なく動いてます。「RaiseEventを含むOnHoge自体をInvokeしてやる」という発想ですよね。

       

      しかし、イベントを発する汎用のクラスライブラリーを作る場合、イベントを受け取る側、つまりイベントハンドラーは様々です。インスタンス・クラスのメンバーのメソッドだったり共有メソッドだったり。インスタンス・クラスもコントロールのようにスレッドを意識しないといけなかったり意識する必要がなかったり。その上、イベントを受け取る側は、AddHandlerで複数のイベントハンドラーをひとつのイベントに結び付けられ、その複数のイベントハンドラーは上述のようなさまざまなタイプのものが混在し得ます。

       

      このような場合、「RaiseEventを含むOnHoge自体をInvokeしてやる」と、結び付けられている様々なイベントハンドラーが、一斉にこのInvokeの下で呼び出されることになりますが・・・、

       

      これで、ええのかな? と疑問に思って質問させて頂いた次第。

       

      Customにすれば、結び付けられるイベントハンドラーをひとつひとつ独自に管理して(AddHandlerを独自に実装して)、RiaseEventに際しては、個々のイベントハンドラーについて、そのまま呼び出せば良いか、Invokeすべきか判断しながら、呼び出し方を使い分けることができるので、上述の私の疑問は解決できるのですが、ただ、AddHandlerを独自に実装することがかなり面倒に感じられて、Custom以外の方法で何かいい方法があるかな? 思ったわけです。

       

       

       

       

       

       

       

       

      2008年10月8日 1:29
    2. 外池です。すいません、自己レス&自己解決です。

       

      VBの仕様書(Ver8.0)をよく読んでいたら、結び付けられているイベントハンドラーを個別に読み出す方法があることがわかりました。Hogeというイベントを定義したら、HogeEventというDelegateが暗黙に宣言されるのだそうで、このDelegateを通じて読み出せるし、個別にイベントハンドラーをコール(直接コールあるいはInvoke)できそうです。

       

      2008年10月8日 1:42
    3.  外池 さんからの引用

      しかし、イベントを発する汎用のクラスライブラリーを作る場合、イベントを受け取る側、つまりイベントハンドラーは様々です。インスタンス・クラスのメンバーのメソッドだったり共有メソッドだったり。インスタンス・クラスもコントロールのようにスレッドを意識しないといけなかったり意識する必要がなかったり。その上、イベントを受け取る側は、AddHandlerで複数のイベントハンドラーをひとつのイベントに結び付けられ、その複数のイベントハンドラーは上述のようなさまざまなタイプのものが混在し得ます。

      スレッドを意識して個別に、呼び出し(イベントソース)側でフォローするのはあまり上策ではないかなと思います。

      本当にInvokeが必要な呼び出しか、クラスライブラリの設計者は知りようがありません。

      呼び出される方がきっちりと認識して使い分ける方が、クラスライブラリに手を入れる必要もなく、流用性が高まるんじゃないかなと考えました。

       

      ところで、標準クラスライブラリに含まれるFileSystemWatcherは、「そのスレッドで呼んで欲しいのであればInvokeするためのインスタンスをよこせ、そうでないならThreadPoolスレッドで呼び出す」みたいな設計がされています。

      http://msdn.microsoft.com/ja-jp/library/system.io.filesystemwatcher.synchronizingobject(VS.80).aspx

       

      どうしてもクラスライブラリ側でフォローする仕組みを入れたいのであれば、こういったパターンも参考になるのではないでしょうか。

      2008年10月8日 13:57
      モデレータ
    4. 外池です。

       

       Azulean さんからの引用

      スレッドを意識して個別に、呼び出し(イベントソース)側でフォローするのはあまり上策ではないかなと思います。

      本当にInvokeが必要な呼び出しか、クラスライブラリの設計者は知りようがありません。

       

      いや・・・、そこがですね、Invokeが必要な呼び出しであることを判別する方法があるんですよ。少なくとも呼び出す相手がControl(あるいはその派生オブジェクト)の場合は。

       

      クラスライブラリ側でイベントを発する際に、クラスライブラリ側でも結び付けられているイベントハンドラーの一覧をDelegateの配列の形で取得することができます。個々のDelegateのTargetを見れば、呼び出そうとしているメソッドを含むオブジェクトが何かわかります。そこで、TargetのInvokeRequiredを調べてやればInvokeすべきかどうか判明するわけです。

       

      そんなわけで、とりあえず、私としては次のようなコーディングのパターンを「標準」にしようかな、と思っている次第。なお、ここで、StoppedEventという名前がOnStoppedのFor Eachループの中に出てきますが、これは、コンパイラーが暗黙のうちに生成してくれる名前で、宣言なしに使ってもコンパイルは通ります。これが、VBの仕様書を読んでて気付いた点です。さらに、この着想は、C#の記事

      http://forums.microsoft.com/MSDN-JA/ShowPost.aspx?PostID=3961782&SiteID=7

      を非常に参考にさせてもらってます。

       

      Code Snippet

      Public Class MyEasyTimer
          Public Event Stopped As EventHandler
          Public Sub Start()
              Dim th As New Threading.Thread(AddressOf Ticking)
              th.Start()
          End Sub
          Protected Overridable Sub OnStopped()
              Dim ea As New EventArgs
              For Each dlg As [Delegate] In StoppedEvent.GetInvocationList
                  If TypeOf dlg.Target Is Control Then
                      Dim ctrl As Control = DirectCast(dlg.Target, Control)
                      If ctrl.InvokeRequired Then
                          Dim args() As Object = {Me, ea}
                          ctrl.Invoke(dlg, args)
                      Else
                          DirectCast(dlg, EventHandler)(Me, ea)
                      End If
                  Else
                      DirectCast(dlg, EventHandler)(Me, ea)
                  End If
              Next
          End Sub
          Private Sub Ticking()
              Threading.Thread.Sleep(1000)
              OnStopped()
          End Sub
      End Class

       

       

       

       

      2008年10月9日 0:15
    5.  外池 さんからの引用

       

       Azulean さんからの引用

      スレッドを意識して個別に、呼び出し(イベントソース)側でフォローするのはあまり上策ではないかなと思います。

      本当にInvokeが必要な呼び出しか、クラスライブラリの設計者は知りようがありません。

       

      いや・・・、そこがですね、Invokeが必要な呼び出しであることを判別する方法があるんですよ。少なくとも呼び出す相手がControl(あるいはその派生オブジェクト)の場合は。

       

      それは「Invokeが必要」という点のみの話ですよね。

      本当にイベントハンドラ内の処理がInvokeを必要としてるかは別ですよね。

      #プロパティを参照しなければ必要ありません。

      そういうわけでAzuleanさんの言われていることは、クラスが外の世界との結合度ができてしまうということでしょう。

      イベントはRaiseするだけにこしたことはないと思います。

      #拾いたきゃ拾えの世界

       

      物によってはありと思いますが、標準となると速度を犠牲にしてしまうのでどうかと思います。

      #Controlから絶対に呼ばれないとしたら最大のロスになる

       

       外池 さんからの引用

      Code Snippet

          Protected Overridable Sub OnStopped()
              Dim ea As New EventArgs
          End Sub
          Private Sub Ticking()
              OnStopped()
          End Sub
      End Class

       

       

      念のため。

      ガイドライン的には

      Code Snippet

      Protected Overridable Sub OnEvent(ByVal e As EvenArgs)

      End Sub

      Private Sub Hoge()

          OnEvent(EventArgs.Empty)

      End Sub

       

       

      の形だと思います。

      でないと派生先でイベント引数にアクセスできませんね。

      2008年10月9日 12:18
    6. まどかさんにフォローいただいていますが、一応。

       

       外池 さんからの引用

      いや・・・、そこがですね、Invokeが必要な呼び出しであることを判別する方法があるんですよ。少なくとも呼び出す相手がControl(あるいはその派生オブジェクト)の場合は。

      イベントハンドラを持つクラスがControlの派生クラスであるということと、Invokeが必要だということはイコールではありません。

      Invokeが必要なのはControlクラスが実装しているプロパティ・メソッドに対する操作です。(全てではありません)

      自分が作成した派生クラスで追加したメンバーは自分で保証(スレッド間排他等)すれば、違うスレッドで呼び出しても特に問題ありません。

       

      利用側のコードが一律、Controlが属するスレッドで呼んで欲しいとは限らないので、クラスライブラリの思想としては利用側が制御できる余地を持たせるべきかなと考えています。

      (スレッドを意識せずに呼び出すか、FileSystemWatcherみたいにインスタンスを要求するか)
      2008年10月9日 14:01
      モデレータ
    7. Control.Invoke 等のメモに、次のように書いてありますが、

      その他全てのメソッドの呼び出しについては、Invoke ... する必要があります

      その他全てのメソッドというのは、スレッド セーフでない全てのメソッドのことで、

      スレッド セーフであることが保証されている or

      自分で保証できるメソッドならば、普通に呼び出すことができます。

      もしかして、呼び出し可能なのは Invoke 等だけであると、

      解釈なされておられるのではと思いまして。私の思い過ごしでしたらすみません。

      2008年10月9日 14:21
    8. 外池です。みなさんのコメント、ありがたく拝承しました。で・・・、おそらく私がおおきな勘違い(プログラミングのスタイルについて)をしているわけですね。

       

      皆さんが仰っていることは、総じて、イベントを発する側は受け取る側のスレッドの問題を気にせずにとにかく出せ、と。受け取る側でスレッドの問題が出そうなら対処せよと。それが可能なことはわかります・・・。

       

      そうしますとですね、Visual StudioにはFormに貼り付けることを前提にいろんなコントロールが附属していて、私は、そのコントロールが発するイベントを使って、Form自身や他のControlを無造作に操作しちゃっています。たぶんみなさんもそうだと思いますが。これは、Formも含めてこれらのコントロールの発するイベントが、特に大丈夫なようにプログラムされているから、ということですね? これは特殊なんだと。

       

      私、さらに別の会社が用意した計測器を制御するクラスライブラリーを使ってコールバックを伴う非同期実行をやったり、コールバックからイベントを発したりしていて、Form上のコントロールに測定結果を表示するようなことをしていますが、比較的頻繁に「コントロールと同一のスレッドで呼び出せ」の例外に遭遇して、Invokeを使うことを覚えました。この場合も、イベントを発する側でInvokeするのではなく、イベントを受ける側が、まずはイベントハンドラで受けて、その中でコントロールのプロパティーに書き込むための別のメソッドをInvokeするようにすべき・・・、

       

      という理解でよろしいでしょうか?

       

       

       

      2008年10月9日 14:56
    9.  外池 さんからの引用

      そうしますとですね、Visual StudioにはFormに貼り付けることを前提にいろんなコントロールが附属していて、私は、そのコントロールが発するイベントを使って、Form自身や他のControlを無造作に操作しちゃっています。たぶんみなさんもそうだと思いますが。これは、Formも含めてこれらのコントロールの発するイベントが、特に大丈夫なようにプログラムされているから、ということですね? これは特殊なんだと。

      Windowsメッセージに基づいたイベントの場合、Windowsメッセージを受け取るのは原則的にそのウィンドウが属するスレッドなので、Controlが属するスレッドでイベントが実行されることになります。

      このため、大抵の場合はスレッドを意識する必要がありません。

      (Control.Invokeも内部的にWindowsメッセージを利用しているはずです)

       

      もちろん、BackgroundWorkerのDoWorkイベント、FileSystemWatcherの各イベントは同じように使えるように見えますが、違うスレッドである点を意識する必要があります。

      そのあたりはクラスの作成者であるMicrosoftが情報を公開しています。

       

      http://msdn.microsoft.com/ja-jp/library/system.componentmodel.backgroundworker(VS.80).aspx

      DoWork イベント ハンドラでユーザー インターフェイス オブジェクトを操作しないように注意する必要があります。

      http://msdn.microsoft.com/ja-jp/library/system.io.filesystemwatcher.synchronizingobject(VS.80).aspx

      SynchronizingObject が null 参照 (Visual Basic では Nothing) の場合は、Changed、Created、Deleted、Renamed の各イベントを処理するメソッドがシステム スレッド プールのスレッドで呼び出されます。システム スレッド プールの詳細については、ThreadPool のトピックを参照してください。

       

      FileSystemWatcherのドキュメントはやや不親切である点は否めませんが。

       

       外池 さんからの引用

      私、さらに別の会社が用意した計測器を制御するクラスライブラリーを使ってコールバックを伴う非同期実行をやったり、コールバックからイベントを発したりしていて、Form上のコントロールに測定結果を表示するようなことをしていますが、比較的頻繁に「コントロールと同一のスレッドで呼び出せ」の例外に遭遇して、Invokeを使うことを覚えました。この場合も、イベントを発する側でInvokeするのではなく、イベントを受ける側が、まずはイベントハンドラで受けて、その中でコントロールのプロパティーに書き込むための別のメソッドをInvokeするようにすべき・・・、

      ですね。

      コールバック関数の時点でUIに反映したいニーズが多いですが、もしかしたら、次の呼び出しだけをしたいのかもしれません。

      そのあたりは作り手に自由度を与えるべきかなと。

       

      ちなみに、クラスライブラリ側で判定可能というのは作り方次第です。

      Controlのインスタンスを持った独自のクラス(Controlj派生クラスではないもの)のメソッドがイベントハンドラとして登録されており、そのイベントハンドラでControlインスタンスに対して操作していた場合、クラスライブラリ側では判断できません。

      2008年10月9日 21:44
      モデレータ
    10. 外池です。

       

       Azulean さんからの引用

      Controlのインスタンスを持った独自のクラス(Controlj派生クラスではないもの)のメソッドがイベントハンドラとして登録されており、そのイベントハンドラでControlインスタンスに対して操作していた場合、クラスライブラリ側では判断できません。

       

      確かに、そのとおりですね。最初に仰っていただいたことと内容は同じだと思いますが、今、ようやく納得できました。

      考え方、スッキリ整理できました。ありがとうございます。

       

      2008年10月9日 22:13