I have some VB6 code that instantiates a class which handles events that are being raised from a VB.NET component. The VB6 is pretty straightforward:
private m_eventHandler as new Collection
...
public sub InitSomething()
dim handler as EventHandler
set handler = new EventHandler
m_eventHandler.Add handler
...
m_engine.Start
end sub
Note that the event handler object has to live beyond the scope of the init method (which is why it is being stored in a Collection). Note also that m_engine.Start indicates the point in the program where the VB.NET component would start raising events.
The actual event handler (as requested):
Private WithEvents m_SomeClass As SomeClass
Private m_object as Object
...
Private Sub m_SomeClass_SomeEvent(obj As Variant)
Set obj = m_object
End Sub
Note that m_object is initialized when an instance of EventHandler is created.
The VB.NET code which raises the event is even simpler:
Public ReadOnly Property SomeProp() As Object
Get
Dim obj As Object
obj = Nothing
RaiseEvent SomeEvent(obj)
SomeProp = obj
End Get
End Property
My problem is that when I debug the VB6 program, the first time InitSomething gets called, the event will not be handled (the VB6 event handler is never entered). Subsequent calls to InitSomething does work.
Everything works as I would have expected when I run the program outside the debugger. At this point, I'm not even sure if this is something I should be worried about.
It may or may not be relevant but the VB.NET was converted from a VB6 using the Visual Studio code conversion tool (and subsequently manually cleaned up).
I've found that if you are writing .Net Components for Consumption in VB6 (or any other COM environment) the utilisation of Interfaces is absolutely criticial.
The COM templates that comes out of the box with VStudio leave a lot to be desired especially when you are trying to get Events to work.
Here's what I've used.
Imports System.Runtime.InteropServices
Imports System.ComponentModel
<InterfaceType(ComInterfaceType.InterfaceIsDual), Guid(ClientAction.InterfaceId)> Public Interface IClientAction
<DispId(1), Description("Make the system raise the event")> sub SendMessage(ByVal theMessage As String)
End Interface
<InterfaceType(ComInterfaceType.InterfaceIsIDispatch), Guid(ClientAction.EventsId)> Public Interface IClientActionEvents
<DispId(1)> Sub TestEvent(ByVal sender As Object, ByVal e As PacketArrivedEventArgs)
End Interface
<ComSourceInterfaces(GetType(IClientActionEvents)), Guid(ClientAction.ClassId), ClassInterface(ClassInterfaceType.None)> _
Public Class ClientAction
Implements IClientAction
Public Delegate Sub TestEventDelegate(ByVal sender As Object, ByVal e As PacketArrivedEventArgs)
Public Event TestEvent As TestEventDelegate
public sub New()
//Init etc
end sub
public sub SendMessage(theMessage as string) implements IClientAction.SendMessage
onSendMessage(theMessage)
end sub
Protected Sub onSendMessage(message as string)
If mRaiseEvents Then
RaiseEvent TestEvent(Me, New PacketArrivedEventArgs(theMessage))
End If
End Sub
end Class
I've been able to get COM and .Net consumers of the Assembly/Component to work properly with events and be able to debug in and out of the component.
Hope this helps.
Just something to try - I have an inherent distrust of "As New .."
Can you try
private m_eventHandler as Collection
public sub InitSomething()
dim handler as EventHandler
set handler = new EventHandler
If m_eventHandler Is Nothing Then
Set m_eventHandler = New Collection
End if
m_eventHandler.Add handler
...
m_engine.Start
end sub
Alas, I've got no idea why this works in normal execution and not in debug except some vague suspicions that it's to do with .NET being unable to instantiate the VBA.Collection object (MS recommends that you write a quick VB6 component to do so), but since you're not creating collections in .NET code, it is still just a vague suspicion.
Related
I have a weird problem that I can't wrap my head around.
I have the following code:
Public Class Form1
Public WithEvents MyClass1 As New MyClass
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
End Sub
Private Sub MyClass_UpdateListbox() Handles MyClass1.UpdateListbox
For Each sItem as String In MyClass1.Listbox
MsgBox(sItem) 'an MsgBox shows correct items each time.
Next sItem
Me.Listbox = Me.MyClass1.Listbox 'doesn't work and breaks listbox.
Me.Listbox.Items.Clear() 'listbox is empty anyway, but has no effect.
Me.Listbox.Items.Add("event triggered") 'does nothing.
End Sub
End Class
Public Class MyClass
Public Listbox as new Listbox
Public Event UpdateListbox()
Public Sub New()
'Constructor. sub.
Me.AddItem("Populating listbox")
End Sub
Public Sub AddItem(sItem as String)
Me.Listbox.Items.Add(sItem)
RaiseEvent UpdateListbox()
End Sub
End Class
If I comment the following lines in above code, the listbox keeps adding event triggered, as expected. Of course, I don't have to remove the clear one. It will work, but then it just adds the same item. If I use a command button and call MyClass.AddItem("Something") that is correctly added too as long as the below is commented out. But if not, then once the listbox is in broken state, nothing can be added anymore.
Me.Listbox = Me.MyClass1.Listbox 'doesn't work and breaks listbox.
Me.Listbox.Items.Clear() 'listbox is empty anyway, but has no effect.
How can I use a virtual listbox and assign it to my real listbox?
Also, instead of assigning one listbox to the other, I can of course use that for each loop and add each item one by one which works, but that for each look was for debugging purpose in the first place.
EDIT:
My goal with this application is to build a Todo list with features that are not in a todolist. This is a project I build for work because there I need a tool like this. I already have a todolist that I use but I built it wrong in the past. Everything was condensed in form1, no modules no extra classes. As a result I got weird bugs that I patched with workarounds. I am now rebuilding the application from the ground up, separating tasks in its own classes so I can apply business logic and have a true OOP application. The todo list will become its own class, and managing the list etc will be handeled by this class. It interacts with controls on the form, such as buttons and listboxes. If I just use form1.listbox from the class, things break at program start. I started another question and the below code was a now deleted answer. At first I did not get it working because I did not realize the listbox crashes if I assign it the virtual instance.
So my goal is to have the todolist be handled entirely by the todolist class. It does need a way to interact with controls on form1, and that is the puzzle I'm currently trying to solve.
In the original code, the main problem is that the Field that hold the instance of a Control shown if a Form is reassigned to the instance of another ListBox Control defined in a custom class:
Me.Listbox = Me.MyClass1.Listbox
From now on, Me.Listbox points another ListBox that is not show on screen, so any attempt to update the Form's child ListBox fails, except when Me.Listbox.Items.Clear() is called - in the same procedure - after it's being reassigned, because the handle of the Owner of the ObjectCollection (the object that holds the Items shown in the ListBox) has not been updated yet. It's going to fail after the current method exits nonetheless.
As noted in comments, this is a simplified method to handle a Form and its child Controls using a handler class. The contract between the class handler and a Form is sealed by an Interface (named IFormHandler here).
A Form that implements this Interface exposes the methods defined by the Interface that allow to trigger Actions and specific behaviors, depending on the Type of Control and the implementation.
I suggest to take a look at the MVP or ReactiveUI (MVVM-derived) for WinForms Patterns.
How too proceed:
Open up the ApplicationEvents class object.
If you don't have it already, select Project -> Properties -> Application and click the View Application Events button. It will generate ApplicationEvents.vb. Find it in Solution Explorer and open it up.
It should look like this (plus a bunch of comments that explain what it's for):
Imports Microsoft.VisualBasic.ApplicationServices
Namespace My
Partial Friend Class MyApplication
End Class
End Namespace
Paste into MyApplication these lines of code:
Imports Microsoft.VisualBasic.ApplicationServices
Namespace My
Partial Friend Class MyApplication
Public SomeFormHandler As MyFormHandler(Of SomeForm)
Protected Overrides Function OnStartup(e As StartupEventArgs) As Boolean
SomeFormHandler = New MyFormHandler(Of SomeForm)
Return MyBase.OnStartup(e)
End Function
End Class
End Namespace
Add an Interface that defines the Actions (or Behaviors) that a Form must implement.
Here, the GetUsersList() method specifies that a Form that implements this Interface must return the instance of a child ListBox Control.
(To add an Interface, select Project -> Add -> New Item... and select the Interface template. Name the file IFormHandler)
Extend this Interface as needed, to add more Methods or Properties that define actions and behaviors.
Public Interface IFormHandler
Function GetUsersList() As ListBox
End Interface
A Form that implements the IFormHandler Interface implements and exposes the GetUsersList() method, which returns the instance of a ListBox Control (named usersList here)
There's nothing else to do with this Form, the control is handed over to the MyFormHandler object that is initialized with this Type.
Public Class SomeForm
Implements IFormHandler
Public Sub New()
InitializeComponent()
End Sub
Public Function GetUsersList() As ListBox Implements IFormHandler.GetUsersList
Return Me.usersList
End Function
End Class
Now, to show SomeForm, you can use the MyFormHandler class object show below.
' Set the Owner if called from another Form
My.Application.SomeFormHandler.Show(Me)
' Or without an Owner
My.Application.SomeFormHandler.Show()
To close SomeForm, you can either use its handler:
My.Application.SomeFormHandler.Close()
or close it as usual:
[SomeForm Instance].Close()
If MyFormHandler determines that the instance of SomeForm has been disposed, it creates a new one when you call its Show() method again later.
To update the ListBox Control of SomeForm, use the public methods exposed by the MyFormHandler class:
' Add a new element
My.Application.SomeFormHandler.UpdateUsersList(UpdateType.AddElement, "Some Item")
' Remove an element
My.Application.SomeFormHandler.UpdateUsersList(UpdateType.RemoveElement, "Some Item")
' Replace an element
My.Application.SomeFormHandler.UpdateUsersList(UpdateType.ReplaceElement, "New Item", "Some Item")
' Clears the ListBox
My.Application.SomeFormHandler.ClearUsersList()
All these actions generate an event that you can subscribe to when needed.
See also the example that shows how to raise a custom event when the ListBox raises one of its stardard events; SelectedIndexChanged is handled here.
See the implementation of MyFormHandler.
Generic Form handler:
A Form needs to implement the IFormHandler Interface for the MyFormHandler class to accept it as valid.
You can of course extend the Interface, to add more Actions, or build a MyFormHandler class object that uses a different Interface, or more than one.
Public Class MyFormHandler(Of TForm As {Form, IFormHandler, New})
Implements IDisposable
Private formObject As TForm
Private IsInstanceSelfClosing As Boolean = False
Public Event UsersListUpdate(item As Object, changeType As UpdateType)
Public Event UsersListIndexChanged(index As Integer)
Public Sub New()
InitializeInstance()
Dim lstBox = formObject.GetUsersList()
AddHandler lstBox.SelectedIndexChanged, AddressOf OnUsersListIndexChanged
End Sub
Private Sub InitializeInstance()
formObject = New TForm()
AddHandler formObject.FormClosing, AddressOf OnFormClosing
End Sub
Private Sub OnFormClosing(sender As Object, e As FormClosingEventArgs)
IsInstanceSelfClosing = True
Dispose()
End Sub
Public Sub UpdateUsersList(updateMode As UpdateType, newItem As Object, Optional oldItem As Object = Nothing)
If newItem Is Nothing Then Throw New ArgumentException("New Item is null")
Dim lstBox = formObject.GetUsersList()
Select Case updateMode
Case UpdateType.AddElement
lstBox.Items.Add(newItem)
Case UpdateType.RemoveElement
lstBox.Items.Remove(newItem)
Case UpdateType.ReplaceElement
If oldItem Is Nothing Then Throw New ArgumentException("Replacement Item is null")
Dim index = lstBox.Items.IndexOf(oldItem)
lstBox.Items.Remove(oldItem)
lstBox.Items.Insert(index, newItem)
Case Else : Return
End Select
RaiseEvent UsersListUpdate(newItem, updateMode)
End Sub
Public Sub ClearUsersList()
formObject.GetUsersList().Items.Clear()
End Sub
Private Sub OnUsersListIndexChanged(sender As Object, e As EventArgs)
RaiseEvent UsersListIndexChanged(DirectCast(sender, ListBox).SelectedIndex)
End Sub
Public Sub Show(Optional owner As IWin32Window = Nothing)
If formObject Is Nothing OrElse formObject.IsDisposed Then InitializeInstance()
If formObject.Visible Then
formObject.WindowState = FormWindowState.Normal
formObject.BringToFront()
Else
formObject.Show(owner)
End If
End Sub
Public Sub Close()
If formObject IsNot Nothing AndAlso (Not formObject.IsDisposed) Then
RemoveHandler formObject.FormClosing, AddressOf OnFormClosing
IsInstanceSelfClosing = False
Dispose()
End If
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Overridable Sub Dispose(disposing As Boolean)
If disposing Then
If formObject Is Nothing OrElse formObject.IsDisposed Then Return
Dim lstBox = formObject.GetUsersList()
RemoveHandler lstBox.SelectedIndexChanged, AddressOf OnUsersListIndexChanged
RemoveHandler formObject.FormClosing, AddressOf OnFormClosing
If Not IsInstanceSelfClosing Then formObject.Close()
IsInstanceSelfClosing = False
End If
End Sub
End Class
Enumerator used in MyFormHandler:
Public Enum UpdateType
AddElement
RemoveElement
ReplaceElement
End Enum
I'm having trouble raising an event in a unit test using VB.NET and NSubstitute. The interface being mocked defines an event:
Event BlockOfVehiclesProcessed(source As Object, stats As ProcessingStats)
The class under test registers a handler for the event. In the unit test, I want to raise the event so the handler in the class under test gets invoked. Based on the NSubstitute documentation (all C#, unfortunately) and various answers on Stackoverflow, etc., I tried various permutations of this:
AddHandler mock.BlockOfVehiclesProcessed, Raise.EventWith(New ProcessingStats(50))
But I haven't found anything that compiles. One error message:
Value of type 'EventHandlerWrapper(...)' cannot be converted to '...BlockOfVehiclesProcessedEventHandler'
I tried passing 0 args and 2 args to EventWith(), I tried specifying the type arg explicitly for EventWith(), and I tried Raise.Event(), but I can't find the magic sequence to make the compiler happy. Does anyone have an example of a working VB unit test that raises an event?
Then problem is that NSubstitute doesn't support anonymous event handler types created by vb.net when event declared without explicitly provided event handler type.
If use of NSubstitute is mandatory (and as an answer for question) declaring event of explicitly provided event handler type will solve your problem.
' Example with Action as event handler type
Public Interface IVehicleProcessor
Event BlockOfVehiclesProcessed As Action(Of Object, String)
End Interface
Public Class System
Private ReadOnly _processor As IVehicleProcessor
Public Property ProcessedStats As String
Public Sub New(IVehicleProcessor processor)
_processor = processor
AddHandler _processor.BlockOfVehiclesProcessed, Sub(sender, stats) ProcessedStats = stats
End Sub
End System
' Test
Public Sub ShouldNotifyProcessedStats()
Dim fakeProcessor = Substitute.For(Of IVehicleProcessor)
Dim system As New System(fakeProcessor)
' Raise an event with known event handler type
AddHandler fakeProcessor.BlockOfVehiclesProcessed,
Raise.Event(Of Action(Of Object, String))(fakeProcessor, "test-stats")
system.ProcessedStats.Should().Be("test-stats") ' Pass
End Sub
Another approach is to create own fake implementation of interface with an event. I found this approach much better, only because you don't need to change your production code, because some testing framework not able to support vb.net language features.
Public Class FakeVehicleProcessor
Implements IVehicleProcessor
Public Event BlockOfVehiclesProcessed(source As Object, stats As String) Implements IVehicleProcessor.BlockOfVehiclesProcessed
' Use this method to raise an event with required arguments
Public Sub RaiseEventWith(stats As String)
RaiseEvent BlockOfVehiclesProcessed(Me, stats)
End Sub
End Class
' Test
Public Sub ShouldNotifyProcessedStats()
Dim fakeProcessor As New FakeVehicleProcessor()
Dim system As New System(fakeProcessor)
fakeProcessor.RaiseEventWith("test-stats")
system.ProcessedStats.Should().Be("test-stats") ' Pass
End Sub
Given two classes with identical events I'd like to be able to refer to them using one name. Similar to the following:
Public Class myclass1
Public event1()
End Class
Public Class myclass2
Public event1()
End Class
Here I'd like to be able to determine at run time which class to use:
Sub somefunction(select as integer)
Dim voidclass
if select = 1 then
voidclass = myclass1
else
voidclass = myclass2
end if
AddHandler voidclass.event1, AddressOf eventhappened
End Sub
Sub eventhappened()
msgbox("Event occured")
End Sub
Obviously there may be a better method for this example, but let's assume that class book1 already exists, and that I'm tasked with creating book2 and only modifying somefunction without modifying book1 itself.
The example above should result in the error event1 is not an event of 'Object'`.
It appears that properties and methods are fine with this, but events are not. How do I handle events in this situation?
The snippet does not match the question very well. It has an odd bug, the events in the classes are not declared Shared so the code must use a proper object reference. Not a type name. Maybe the answer is as simple as:
Private obj1 As myclass1
Private obj2 As myclass2
Sub somefunction(select as integer)
If select = 1 Then
AddHandler obj1.event1, AddressOf eventhappened
Else
AddHandler obj2.event1, AddressOf eventhappened
End If
End Sub
But presumably the real question is the same scenario, but now the variables declared as:
Private obj1, obj2
Untyped and thus Object. Yes, VB.NET does not support that. As with most quirks in VB.NET there is history behind this. VB never did support explicit late binding of events, only methods and properties. The scheme in the legacy versions was very quirky. You had to declare the variable with the WithEvents keyword and pick a specific name for the event handler. In other words:
Dim WithEvents obj1
Sub obj1_somethinghappened
'' handles the event1 event for obj1
End Sub
Or to put it another way, all events were late-bound. This scheme was not carried forward into VB.NET, too many practical problems with it. WithEvents still survives but now requires the Handles keyword on the method declaration. That doesn't do what you want it to do.
The VB.NET designers intentionally did not add late binding support to the AddHandler statement. I am not privy to the exact reason they decided this and can only guess. There is no technical reason it could not be added, it just requires the compiler to generate reflection code. One possible explanation is that they considered it too expensive. Another is that they deemed the runtime exceptions that are raised when there's a method signature mismatch too hard to interpret. I like that last one best, they are awfully ugly.
You'll have to use Reflection to get what you want. Use obj1.GetType().GetEvent("event1") to get the EventInfo, its Get/AddMethod() to add the event handler.
Fwiw, the C# language does support this in its dynamic keyword implementation. Maybe you can put a bug in their ear by asking for the feature. No real idea if this was requested before.
You can define an interface for the event and implement it in the classes then you can subscribe to this event through the interface reference. See below:
Public Interface INotifier
Event SomethingHappened()
End Interface
Public Class Class1
Implements INotifier
Public Event SomethingHappened() Implements INotifier.SomethingHappened
End Class
Public Class Class2
Implements INotifier
Public Event SomethingHappened() Implements INotifier.SomethingHappened
End Class
Module Module1
Dim notifiers As List(Of INotifier) = New List(Of INotifier) From
{
New Class1(),
New Class2()
}
Sub Main()
SubscribeToEventHandler(0)
End Sub
Private Sub SubscribeToEventHandler(ByVal index As Integer)
Dim notifier As INotifier = notifiers(index)
AddHandler notifier.SomethingHappened, AddressOf EventHandler
End Sub
Private Sub EventHandler()
End Sub
End Module
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.
I'm having some trouble getting cross-thread communication/field-updating working properly in my VB.NET 2010 program. I'm trying to update a field on my main form whenever a thread that I've started throws an event. Here's a simplified version of my code:
My main form:
Public Class Main
' stuff
' Eventually, startProcessing gets called:
Private Sub startProcessing()
Dim processingClass = New MyProcessingClass("whatever")
AddHandler processingClass.processStatusUpdate, AddressOf handleProcessStatusUpdate
Dim processingThread = New Thread(AddressOf processingClass.process)
processingThread.Start()
End Sub
Private Sub handleProcessStatusUpdate(statusUpdate As String)
txtMainFormTextBox.Text = statusUpdate ' InvalidOperationException
' "Cross-threaded operation not valid: Control 'txtMainFormTextBox' accessed from a thread other than the thread it was created on"
End Sub
End Class
The class which raises the event:
Public Class MyProcessingClass
Private whatever As String
Public Event processStatusUpdate(status As String)
Public Sub New(inWhatever As String)
whatever = inWhatever
End Sub
Public Sub process()
' do some stuff
RaiseEvent processStatusUpdate(whatever)
End Sub
End Class
As you can see, the handler in my main class doesn't have access to the TextBox I need since it was triggered by a different thread (I think). I've tried a number of other approaches to get this working, including:
Moving the event handler to MyProcessingClass, and passing txtMainFormTextBox by reference (ByRef) to the class.
Having the actual thread start inside of MyProcessingClass instead of Main.
None of these have worked. Clearly there's a concept that I'm missing here. What's the best way to get this done? Thanks!
You need to update the textbox on the UI thread by calling BeginInvoke.
You should use the BackgroundWorker component, which does all of this for you.
Simply handle the DoWork and ProgressChanged events.