1

更新: MainForm のウィンドウ ハンドルの遅延インスタンス化と関係があると思いますが、ここで見られる動作がどのように発生するかを完全に理解することはできませんでした。

アプリケーションは、結果を処理するためのコールバックを提供するサードパーティの COM インターフェイスを介してデータを要求します。コールバックでは、UI を更新する必要がありますが、更新が期待どおりに機能しません。MainForm.DataReadyクロス スレッドで直接呼び出されるか呼び出されると、MainForm の値のコピーが作成されたかのようになりますが、イベント ハンドラーから実行されると、UI の更新は期待どおりに機能します。理由を説明できますか?

(注: MainForm または ClassB で検査されるかどうかAppDomain.CurrentDomain.Idは常にです。)1

初期コード- MainForm で InvokeRequred /Delegate /Invoke ロジックを使用せずに、ClassB インスタンスから DataReady を呼び出します。アプリケーション UI の変更は期待どおりに機能し、MainFormSomeListControl.EmptyListMsg = "Not Available"の変更は「定着」しません (あたかも MainForm の別のコピーに適用されたかのように)。



Module AppGlobals
  Public WithEvents A As ClassA
End Module

Partial Friend Class MyApplication
  Private Sub MyApplication_Startup(ByVal sender As Object,
                                          ByVal e As StartupEventArgs) Handles Me.Startup
    A = New ClassA()

  End Sub
End Class

Class MainForm

  private sub getData
    ToggleWait(True)
    SomeListControl.Clear()
    A.getData() 'Sets up the com object & callback
  end sub

  Public Sub DataReady()
    ToggleWait(False)
    ' Do something with the data
  End Sub

  Private Sub ToggleWait(toggle as Boolean)
    Application.UseWaitCursor = False
    if toggle then
      SomeListControl.EmptyListMsg = "Not Available"
    else
      SomeListControl.EmptyListMsg = "Please Wait"
    end if
  End Sub

End Class

Class ClassA

  public sub getData()
     Dim ComObj as New ComObject
     Call ComObj.setClient(New ClassB)
  End Sub

End Class

Class ClassB
  Implements IComObjectClient

  sub getdata_callback(results() as Object) handles IComObjectClient.getdata_callback
    ' Get the results
    MainForm.DataReady() 
  end sub

End Class

InvokeRequred ロジックを DataReady に追加しましたが、引き続き ClassB から直接呼び出されます。InvokeRequired が true になることはありません。アプリケーション UI の変更は期待どおりに機能し、MainFormSomeListControl.EmptyListMsg = "Not Available"の変更は「固定」されません (MainForm の別のコピーに適用されたかのように)。


  Class MainForm
    Public Delegate Sub DataReadyDelegate(ByVal toggle As Boolean)
    ...
    Public Sub DataReady()
        If InvokeRequired Then
            Invoke(New DataReadyDelegate()
        Else
          ToggleWait(False)
          ' Do something with the data
        End If
    End Sub
    ...
  End Class

MainForm.DataReadyClassB から直接呼び出された例外が発生しました:「ウィンドウ ハンドルが作成されるまで、Invoke または BeginInvoke をコントロールで呼び出すことはできません。」ウィンドウハンドルの作成を強制するまで。次に、以前と同じ動作です。つまり、InvokeRequired が true になることはなく、アプリケーション UI の変更は期待どおりに機能し、MainFormSomeListControl.EmptyListMsg = "Not Available"の変更は「固定」されません (MainForm の別のコピーに適用されたかのように)。


Class ClassB
  Implements IComObjectClient
  Public Delegate Sub DataReadDelegate()

  sub getdata_callback(results() as Object) handles IComObjectClient.getdata_callback
    ' Get the results 
    If Not MainForm.IsHandleCreated Then
      ' This call forces creation of the control's handle
      Dim handle As IntPtr = MainForm.Handle
    End If
    MainForm.Invoke(New DataReadyDelegate(AddressOf MainForm.DataReady))
  end sub

End Class

イベント ハンドラから実行ClassA および ClassB で定義されたカスタム 'データ取得' イベント。ClassA は ClassB.got_data_event をリッスンして ClassA.got_data_event を発生させます。MainForm は ClassA.got_data_event をリッスンし、DataReady() を呼び出して処理します。これは機能します - InvokeRequired が true で、Invoke が実行され、アプリケーション UI と MainForm UI の変更が意図したとおりに機能します。


  Class MainForm
    Public Delegate Sub DataReadyDelegate()
    ...
    Public Sub DataReady()
        If InvokeRequired Then
            Invoke(New DataReadyDelegate()
        Else
          ToggleWait(False)
          ' Do something with the data
        End If
    End Sub

    Public Sub _GotData_HandleEvent(ByVal resultMessage As String)
        DataReady()
    End Sub

    Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles Me.Load
        ...
        ToggleWait(False)
        AddHandler A.GotData, AddressOf _GotData_HandleEvent
        ...
    End Sub
    ...
  End Class
4

1 に答える 1

3

対比:

  A.getData() 

と:

  If Not MainForm.IsHandleCreated Then

最初のステートメントで適切なオブジェクト指向プログラミング構文を使用しています。A はオブジェクトです。Form.IsHandleCreated プロパティはインスタンスプロパティであり、左側にオブジェクト名が必要です。ただし、タイプ名を使用しました。MainForm はオブジェクトではなく、コード内の型です。

これが可能であることは、非常に厄介な VB.NET 機能です。これは、VB6 プログラマーが VB.NET コーディングに移行するのを支援するために存在し、VB6 はフォームの型名を使用することを強く推奨しています。VB4 がオブジェクトに似たものを実装する前に、VB1 から継承された構文。

これは確かに便利です。タイプ名を使用するだけで、別のクラスのフォーム オブジェクトを参照できます。A オブジェクトではそのような利便性が得られなかったことに注意してください。グローバル変数にしてモジュールに格納することで解決しました。それも価格に勝るものはありませんが、どのクラスでも A を参照できました。

問題は、別のスレッドで偽のフォーム オブジェクトを使い始めると、この便利さが致命的になることです。あなたが期待していなかったのは、このオブジェクトに<ThreadLocal>スコープがあるということです。つまり、ワーカー スレッドで使用すると、クラス MainFormの新しいオブジェクトが取得されます。このフォーム オブジェクトは表示されません。その Show() メソッドを呼び出していません。これが機能するとは限りません。スレッドはメッセージ ループをポンプしないため、フォームが適切に描画されません。あなたが観察したもう 1 つの副作用は、その InvokeRequired プロパティが動作しないことです。False を返します。正しくは、ワーク スレッドでフォームが作成されたので、BeginInvoke() を実際に使用する必要はありません。これもうまくいくとは限りません。ユーザーが見ているオブジェクトではなく、まだ間違ったオブジェクトです。

したがって、Q&D の回避策の 1 つは、A オブジェクトで行ったのと同じことを form オブジェクトで行い、それをグローバル変数に格納することです。

Module AppGlobals
  Public WithEvents A As ClassA
  Public MainWindow As MainForm
End Module

そして、クラス コンストラクターから初期化します。

Class MainForm
    Sub New()
        InitializeComponent()
        MainWindow = Me
    End Sub
'' etc..
End Class

これで、クラスで MainWindow を参照できます。そして、ユーザーが見ている MainForm クラスの実際のインスタンスへの参照を取得します。MainWindow.InvokeRequired から適切な戻り値を取得します。

これで問題は解決しますが、それでも見苦しく、エラーが発生しやすくなります。正しい方法は次のようになります。

Public Class MainForm
    Private Shared MainWindow As MainForm

    Public Shared ReadOnly Property Instance() As MainForm
        Get
            '' Return a reference to the one-and-only instance of MainForm
            If MainWindow Is Nothing Then
                '' It doesn't exist yet so create an instance 
                '' Creating one on a worker thread will never work, so complain
                If System.Threading.Thread.CurrentThread.GetApartmentState() <> Threading.ApartmentState.STA Then
                    Throw New InvalidOperationException("Cannot create a window on a worker thread")
                End If
                New MainForm()
            End If
            Return MainWindow
        End Get
    End Property

    Protected Overrides Sub OnFormClosed(ByVal e As System.Windows.Forms.FormClosedEventArgs)
        '' Ensure that the one-and-only instance is now Nothing since it closed
        MyBase.OnFormClosed(e)
        MainWindow = Nothing
    End Sub

    Sub New()
        '' Creating more than once instance of this form can't work, so complain
        If MainWindow IsNot Nothing Then Throw New InvalidOperationException("Cannot create more than one instance of the main window")
        InitializeComponent()
        '' We need to keep track of this instance since the Instance property returns it
        MainWindow = Me
    End Sub

    '' etc...
End Class

MainForm.Instance.InvokeRequired のように、クラス内の任意の場所で MainForm.Instance を使用できるようになりました。また、例外で間違った場合に通知されます。

于 2013-01-11T22:51:13.490 に答える