本文介绍了为什么在调用或调用方法时会创建MainForm的值副本?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

更新:我认为它与MainForm的窗口句柄的懒惰实例有关,但是还是无法弄清楚这将导致这里看到的行为。 / p>

应用程序通过第三方COM接口提供数据,提供回调处理结果。在回调中,UI需要更新 - 但更新不能按预期方式运行。就像MainForm的值拷贝一样,当 MainForm.DataReady 被直接调用或调用交叉线程,但UI更新如从事件处理程序执行时那样工作。你能解释一下为什么吗



(注意: AppDomain.CurrentDomain.Id 总是 1 无论是在MainForm还是ClassB中检查。)



初始代码 - 从ClassB实例调用DataReady,在MainForm中InvokeRequred / Delegate / Invoke逻辑。应用程序UI更改按预期工作,MainForm SomeListControl.EmptyListMsg =不可用更改不会粘贴(如果应用于MainForm的单独副本)

  

模块AppGlobals
公开WithEvents A As ClassA
结束模块

部分朋友类MyApplication
Private Sub MyApplication_Startup(ByVal sender As Object,
ByVal e As StartupEventArgs)处理Me.Startup
A =新的ClassA()

End Sub
结束类

类MainForm

private sub getData
ToggleWait(True)
SomeListControl.Clear()
A.getData()'设置com对象和回调
end sub

Public Sub DataReady()
ToggleWait(False)
'数据
End Sub

Private Sub ToggleWait(toggle as Boolean)
Application.UseWaitCursor = False
如果切换
SomeListControl.EmptyListMsg =不可用
else
SomeListControl.EmptyListMsg =Please Wait
end if
End Sub

结束类

ClassA

public sub getData()
Dim ComObj作为新ComObject
调用ComObj.setClient(新ClassB)
End Sub

结束类

类ClassB
实现IComObjectClient

sub getdata_callback(results()as Object)处理IComObjectClient.getdata_callback
'获取结果
MainForm.DataReady()
end sub

结束类

将InvokeRequred逻辑添加到DataReady,仍然直接从ClassB调用。 InvokeRequired从不是真的,应用程序UI更改按预期工作,MainForm SomeListControl.EmptyListMsg =不可用更改不会粘(如果应用于单独的副本MainForm)

  
类MainForm
公共委托Sub DataReadyDelegate(ByVal toggle As Boolean)
...
Public Sub DataReady()
如果InvokeRequired然后
调用(New DataReadyDelegate()
Else
ToggleWait(False)
'数据
结束If
End Sub
...
结束类

直接从ClassB调用 MainForm.DataReady 获取异常:调用或BeginInvoke不能在控件上调用,直到窗口句柄已经创建了,直到我强制窗口句柄创建,然后与以前一样的行为,即InvokeRequired永远不会是真的,应用程序UI改变rks如预期的那样,MainForm SomeListControl.EmptyListMsg =不可用更改不会粘(如同应用于MainForm的单独副本)

  
Class Class B
实现IComObjectClient
公共委托Sub DataReadDelegate()

sub getdata_callback (result()as Object)处理IComObjectClient.getdata_callback
'获取结果
如果不是MainForm.IsHandleCreated然后
'此调用强制创建控件的句柄
Dim handle As IntPtr = MainForm.Handle
End If
MainForm.Invoke(New DataReadyDelegate(AddressOf MainForm.DataReady))
end sub

结束类

从事件处理程序执行在ClassA和ClassB中定义了获取数据事件。 ClassA侦听ClassB.got_data_event并引发ClassA.got_data_event,MainForm侦听ClassA.got_data_event,并通过调用DataReady()来处理它。这个工作 - InvokeRequired是真的,Invoke被排除,应用程序UI和MainForm UI按照预期改变工作。

  
类MainForm
公共委托Sub DataReadyDelegate()
...
Public Sub DataReady()
如果InvokeRequired然后
调用(New DataReadyDelegate()
Else
ToggleWait(False)
'使用数据
执行某些操作End If
End Sub

公共子_GotData_HandleEvent(ByVal resultMessage As String)
DataReady()
End Sub

Private Sub MainForm_Load(sender As Object,e As EventArgs)处理Me.Load
...
ToggleWait(False)
AddHandler A.GotData,AddressOf _GotData_HandleEvent
...
End Sub
...
结束类


解决方案

对比度:

  A.getData()

与:

 如果不是MainForm.IsHandleCreated然后

您在第一个语句中使用了正确的面向对象编程语法。 A是一个对象。 Form.IsHandleCreated属性是一个实例属性,它需要在左侧的对象名称。但是,您使用类型名称。 MainForm不是一个对象,它是你的代码中的一种类型。



这是可能的一个非常讨厌的VB.NET功能。它存在帮助VB6程序员移动到VB.NET编码,VB6强烈鼓励使用表单的类型名称。在VB4实现类似对象之前,从VB1继承的语法。



现在这当然是一个方便。您可以通过简单的使用类型名称来引用另一个类中的表单对象。请注意,您如何使用不具有A对象的方便。通过使其成为一个全局变量,将其存储在模块中来解决它。这不会赢得任何价格,但是允许您在任何课程中引用A.



问题是,这种方便会变得致命的您在另一个线程中开始使用假表单对象。你没有指望的是这个对象有< ThreadLocal> 范围。换句话说,当您在工作线程中使用它时,您将获得MainForm类的新的对象。这个表单对象不可见,你从来没有调用它的Show()方法。不是这样会有效,线程不会引发消息循环,因此表单不会正确绘制。您观察到的另一个副作用是其InvokeRequired属性不起作用。它返回False。正确地,表单是在工作线程上创建的,所以你实际上不必使用BeginInvoke()。不是这样也可以,这仍然是错误的对象,而不是用户正在查看的对象。



所以一个Q& D的解决方法是做同样的事情与Form对象一样,将其存储在全局变量中:

 模块AppGlobals 
Public WithEvents A As ClassA
公共MainWindow作为MainForm
结束模块

并初始化它来自类构造函数:

 类MainForm 
Sub New()
InitializeComponent()
MainWindow = Me
End Sub
''etc ..
结束类

现在,您可以在类中引用MainWindow。并且您可以引用用户正在查看的MainForm类的实际实例。并从MainWindow.InvokeRequired获取正确的返回值。



这将解决您的问题,但它仍然是丑陋和容易出错的。正确的方式如下所示:

 公共类MainForm 
私有共享MainWindow作为MainForm

公共共享ReadOnly属性实例()作为MainForm
获取
''返回对MainForm的唯一实例的引用
如果MainWindow不是,然后
''它不存在,所以创建一个实例
''在工作线程上创建一个将永远不会工作,所以抱怨
如果System.Threading.Thread.CurrentThread.GetApartmentState()<> Threading.ApartmentState.STA然后
抛出新的InvalidOperationException(无法在工作线程上创建一个窗口)
如果
结束新MainForm()
结束如果
返回MainWindow
结束获取
结束属性

受保护的覆盖Sub OnFormClosed(ByVal e As System.Windows.Forms.FormClosedEventArgs)
''确保one-现在没有什么,因为它关闭
MyBase.OnFormClosed(e)
MainWindow = Nothing
End Sub

Sub New()
''Creating不止一次这种形式的实例不能工作,所以抱怨
如果MainWindow IsNot Nothing然后抛出新的InvalidOperationException(不能创建多个主窗口的实例)
InitializeComponent()
我们需要跟踪这个实例,因为Instance属性返回
MainWindo w = Me
End Sub

''等...
结束类

现在,您可以在类中的任何位置使用MainForm.Instance,如MainForm.Instance.InvokeRequired。当您遇到异常时,您会被提醒。


Update: I think it has something to do with lazy instantiation of the window handle for MainForm - but haven't been able to work out quite how that would result in the behavior seen here.

The application requests data via 3rd party COM interface providing a callback to process the results. In the callback, the UI needs to be updated - but the update doesn't work as expected. It's as if a value copy of MainForm had been created, when MainForm.DataReady is called or invoked directly cross thread, but UI update works as expected when executed from an event handler. Can you explain why?

(Note: AppDomain.CurrentDomain.Id is always 1 whether examined in MainForm or in ClassB.)

Initial Code - call to DataReady from ClassB instance without InvokeRequred /Delegate /Invoke logic in MainForm. Application UI change works as expected, MainForm SomeListControl.EmptyListMsg = "Not Available" change doesn't 'stick' (as if applied to a separate copy of 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

Added InvokeRequred logic to DataReady, still called directly from ClassB. InvokeRequired is never true, Application UI change works as expected, MainForm SomeListControl.EmptyListMsg = "Not Available" change doesn't 'stick' (as if applied to a separate copy of 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

Invoked MainForm.DataReady directly from ClassB Got exception: "Invoke or BeginInvoke cannot be called on a control until the window handle has been created." until I forced the window handle creation. Then it's the same behavior as before, namely, InvokeRequired is never true, Application UI change works as expected, MainForm SomeListControl.EmptyListMsg = "Not Available" change doesn't 'stick' (as if applied to a separate copy of 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

Executed from Event Handler Defined custom 'got data' events in ClassA and ClassB. ClassA listens for ClassB.got_data_event and raises ClassA.got_data_event, MainForm listens for ClassA.got_data_event and handles it by calling DataReady(). This works - InvokeRequired is true, Invoke is excuted, Application UI and MainForm UI changes work as intended.


  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
解决方案

Contrast:

  A.getData()

with:

  If Not MainForm.IsHandleCreated Then

You are using proper object-oriented programming syntax in the first statement. A is an object. The Form.IsHandleCreated property is an instance property, it requires an object name at the left side. You however used a type name. MainForm is not an object, it is a type in your code.

That this is possible is a very nasty VB.NET feature. It exists to help VB6 programmers move to VB.NET coding, VB6 strongly encouraged using the form's type name. Syntax inherited from VB1 before VB4 implemented anything resembling objects.

Now this is most certainly a convenience. You can refer to the form object in another class by simply using the type name. Note how you did not have that convenience with the A object. You solved it by making it a global variable, storing it in a Module. That doesn't win any prices either, but did allow you to reference A in any class.

Problem is, this convenience turns deadly when you start using the fake form object in another thread. What you didn't count on is that this object has <ThreadLocal> scope. In other words, when you use it in a worker thread then you get a new object of class MainForm. This form object is not visible, you never called its Show() method. Not that this would work, the thread does not pump a message loop so that form won't paint itself properly. Another side effect you observed is that its InvokeRequired property doesn't behave. It returns False. Correctly so, the form was created on the work thread so you don't actually have to use BeginInvoke(). Not that this would work either, it is still the wrong object, not the one that the user is looking at.

So one Q&D workaround is to do the same thing with the form object as you did with the A object, store it in a global variable:

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

And initialize it from the class constructor:

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

Now you can refer to MainWindow in your classes. And you get a reference to the actual instance of MainForm class that the user is looking at. And get the proper return value from MainWindow.InvokeRequired.

This will solve your problem, but it is still ugly and error prone. The right way looks like this:

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

Now you can use MainForm.Instance anywhere in your classes, like MainForm.Instance.InvokeRequired. And you'll be reminded when you get it wrong with an exception.

这篇关于为什么在调用或调用方法时会创建MainForm的值副本?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-22 20:58
查看更多