"COM object that has been separated from its underlying RCW cannot be used" error related to vb.net form event - vb.net

I'm hooking an arcobjects map event to a vb.net form to listen for map selection changes. This all works fine but users are reporting this error occassionally when opening the form. I can't see any pattern to reproduce the error and it seems to be random.
"COM object that has been separated from its underlying RCW cannot be used"
It originates from the form Load() method where I am hooking the event.
Can anyone help me understand what I've done wrong? I'm unhooking the map selection event in the FormClosing() event which I think is the correct approach.
Public Class MyForm
Private _activeViewEvents As IActiveViewEvents_Event
Private Sub FormLoad(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
_activeViewEvents = TryCast(pMxDoc.ActiveView.FocusMap, IActiveViewEvents_Event)
AddHandler _activeViewEvents.SelectionChanged, AddressOf SelectionChanged
End Sub
Private Sub SelectionChanged
'do something when selection is changed
End Sub
Private Sub FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
RemoveHandler _activeViewEvents.SelectionChanged, AddressOf SelectionChanged
End Sub
End Class

The approach you are taking to creating and destroying your handlers are valid. You can receive a RCW COM Exception when the map document is changed while your form is open. Since you are using the FocusMap to create the handles, when the document is changed, so is the FocusMap, which means you need to re-create your handlers for the new map document.

Ok so I think i've resolved this via use of the ActiveViewChanged event. Instead of rehooking the event on each form load or new document event, I tried listening for when the ActiveViewChanged event was fired and rehooking the SelectionChanged event each time. Turns out this is fired more than once each time a new document is opened (not sure why). Anyway, problem seems to have gone. Here's some example code:
Public Class MyForm
Private _activeViewEvents As IActiveViewEvents_Event
Private _docEvents As IDocumentEvents_Event
Private Sub FormLoad(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
AddHandler _docEvents.ActiveViewChanged, AddressOf ActiveViewChanged
End Sub
Private Sub ActiveViewChanged()
Dim maps = pMxDoc.Maps
For i = 0 to maps.Count - 1 'remove handlers from all maps
RemoveActiveViewEvents(maps.Item(i))
Next
SetupActiveViewEvent(pMxDoc.ActiveView.FocusMap) 'only add handler to active map
End Sub
Private Sub RemoveActiveViewEvents(map As IMap)
_activeViewEvents = CType(map, IActiveViewEvents_Event)
RemoveHandler _activeViewEvents.SelectionChanged, AddressOf SelectionChanged
End Sub
Private Sub SetupActiveViewEvents(map As IMap)
_activeViewEvents = CType(map, IActiveViewEvents_Event)
AddHandler _activeViewEvents.SelectionChanged, AddressOf SelectionChanged
End Sub
Private Sub SelectionChanged
'do something when selection is changed
End Sub
End Class

Related

How to raise event from user control to another form vb.net

I am not familiar with user controls. I have user control with OK and Cancel buttons, I add this user control to an empty form during run time, I have another form which I called "Main Form", In the code I open the empty form and add the user control, after that I want to raise an event (on the OK button) from the user control (or from the empty form I don't know!) to the Main form!
I searched the net and found way to create an event and raise the event in the same form. I tried to do the same thing between different forms but I don't know how to do that.
create event
Public Event OKEvent As EventHandler
raise event
Protected Overridable Sub OnbtnOKClick(e As EventArgs)
AddHandler OKEvent , AddressOf btnOKClickHandler
RaiseEvent OKEvent(Me, e)
End Sub
event Sub
Sub btnOKClickHandler(sender As Object, e As EventArgs)
'Event Handling
'My event code
End Sub
handle my event on btnOK.click event
Private Sub btnOK_Click(sender As Object, e As EventArgs) Handles btnOK.Click
OnbtnOKClick(e)
End Sub
Okey, that all code works in the same form, maybe its messy but that what I found on the net, I want to make similar thing but on different forms, how can I organize my code?
You don't raise an event "to" anywhere. The whole point of events is that you don't have to care who is listening. It's up to the whoever wants to react to the event to handle it and whoever raised the event never has to know about them.
So, all you need to do in this case is have the main form handle the appropriate event(s) of the user control. The user control is a control like any other and the event is an event like any other so the main form handles the event of the user control like it would handle any other event of any other control. Where you create the user control, use an AddHandler statement to register a handler for the event you declared in the user control.
EDIT:
The OnbtnOKClick method that you have shown above should not look like this:
Protected Overridable Sub OnbtnOKClick(e As EventArgs)
AddHandler OKEvent , AddressOf btnOKClickHandler
RaiseEvent OKEvent(Me, e)
End Sub
but rather like this:
Protected Overridable Sub OnbtnOKClick(e As EventArgs)
RaiseEvent OKEvent(Me, e)
End Sub
Also, the btnOKClickHandler method should be in the main form, not the user control. As I said in my answer, it's the main form that handles the event. That means that the event handler is in the main form. As I also said, when you create the user control in the main form, that is where you register the event handler, e.g.
Dim uc As New SomeUserControl
AddHandler uc.OKEvent, AddressOf btnOKClickHandler
As I hope you have absorbed in your reading, if you use AddHandler to register an event handler then you need a corresponding RemoveHandler to unregister it when the object is finished with.
If I'm understanding correctly, you have...
FormA creates FormB.
FormB creates UserControlB.
UserControlB raises "OK" event.
FormB receives "OK" event.
...but you want an additional step of:
FormA receives "OK" event.
The problem here is that FormA has no reference to UserControlB because FormB was the one that created the UserControl. An additional problem is that FormB may have no idea who FormA is (depending on how you've got it setup).
Option 1 - Pass References
If you want both FormB and FormA to respond to a SINGLE "OK" event (generated by the UserControl), then you'd have to somehow have a reference to all three players in the same place so that the event can be properly wired up. The logical place to do this would be in FormB as that is where the UserControl is created. To facilitate that, however, you'd have to modify FormB so that a reference to FormA is somehow passed to it. Then you can wire up the "OK" event directly to the handler in FormA when FormB creates its instance of UserControlB:
Public Class FormA
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim frmB As New FormB(Me) ' pass reference to FormA into FormB via "Me" and the Constructor of FormB
frmB.Show()
End Sub
Public Sub ucB_OKEvent()
' ... do something in here ...
Debug.Print("OK Event received in FormA")
End Sub
End Class
Public Class FormB
Private _frmA As FormA
Public Sub New(ByVal frmA As FormA)
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
_frmA = frmA ' store reference to FormA so we can use it later
End Sub
Private Sub FormB_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim ucB As New UserControlB
ucB.Location = New Point(10, 10)
AddHandler ucB.OKEvent, AddressOf _frmA.ucB_OKEvent ' wire up the "OK" event DIRECTLY to the handler in our stored reference of FormA
Me.Controls.Add(ucB)
End Sub
End Class
Public Class UserControlB
Public Event OKEvent()
Private Sub btnOK_Click(sender As Object, e As EventArgs) Handles btnOK.Click
RaiseEvent OKEvent()
End Sub
End Class
Option 2 - "Bubble" the Event
Another option is "bubble" the event from FormB up to FormA. In this scenario, FormB will have no idea who FormA is, so no reference will be passed. Instead, FormB will have its own "OK" event that is raised in response to receiving the original "OK" event from UserControlB. FormA will get the "OK" notification from the UserControl, just not directly:
Public Class FormA
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim frmB As New FormB
AddHandler frmB.OKEvent, AddressOf frmB_OKEvent
frmB.Show()
End Sub
Private Sub frmB_OKEvent()
' ... do something in here ...
Debug.Print("OK Event received from FormB in FormA")
End Sub
End Class
Public Class FormB
Public Event OKEvent()
Private Sub FormB_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim ucB As New UserControlB
ucB.Location = New Point(10, 10)
AddHandler ucB.OKEvent, AddressOf ucB_OKEvent
Me.Controls.Add(ucB)
End Sub
Private Sub ucB_OKEvent()
Debug.Print("OK Event received from UserControl in FormB")
RaiseEvent OKEvent()
End Sub
End Class
Public Class UserControlB
Public Event OKEvent()
Private Sub btnOK_Click(sender As Object, e As EventArgs) Handles btnOK.Click
RaiseEvent OKEvent()
End Sub
End Class
Design Decisions
You have to make a design decision here. Who is the source of the "OK" event? Should it be the UserControl or FormB? Will the UserControl ever be used in different forms (other than FormB)? Will FormB ever be used with Forms other then FormA? These answers may help you decide which approach is better...or may lead you to rethink how you've designed your current solution; you may need to change it altogether.

Handle an event of a control defined on another form

I have a form declared as s property WithEvents. If I add Handles formServers.FormClosing to a Sub declaration it works fine, but when I want to handle an event of a control within formServers I get the following error -
'Handles' in classes must specify a 'WithEvents' variable.
How do I correctly set this up? Thanks.
Private WithEvents formServers As New formServers
Private Sub txtServers_Closing(ByVal Sender As Object,
ByVal e As EventArgs) Handles formServers.txtServers.LostFocus
Me.SetServers()
If Me.ServersError Then
Dim Ex As New Exception("Error validating Servers.")
Dim ErrorForm = New formError(Ex, 101)
End If
End Sub
The error message is fairly misleading. The Handles keyword has several restrictions, it cannot work across different classes, it needs an object reference. You must use the more universal AddHandler keyword instead.
There are some additional problems in your scenario. Never use the LostFocus event, use Leave instead. And it is very important that you subscribe the event for the specific instance of the form, using As New gets you into trouble when you display the form multiple times, an ObjectDisposedException will be the outcome. Correct code looks like this:
Private formInstance As FormServers
Private Sub DisplayFormServer()
formInstance = new FormServers
AddHandler formInstance.txtServers.Leave, AddressOf txtServers_Closing
AddHandler formInstance.FormClosed, _
Sub()
formInstance = Nothing
End Sub
formInstance.Show()
End Sub
A much more elegant approach is to expose the event explicitly in your FormServers class. Make that look like this:
Public Class FormServers
Public Event ServersLeave As EventHandler
Private Sub txtServers_Leave(sender As Object, e As EventArgs) Handles txtServers.Leave
RaiseEvent ServersLeave(Me, EventArgs.Empty)
End Sub
End Class
The problem is that you do are not specifying WithEvents on the TextBox. Rather, you are specifying WithEvents on the Form. You can only use Handles on variables which you have declared directly with the WithEvents keyword. With the WithEvents being on the form, you will only be able to use Handles to handle events that are raised directly by the form itself. You will not be able to do so for events raised by any of its controls.
You can fix this in one of two ways. Either you can use AddHandler to register your event handler (rather than using the Handles keyword), or you can create a TextBox variable WithEvents and then set it to the appropriate TextBox object on the form, like this.
Private formInstance As New FormServers
Private WithEvents txtServers As TextBox
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
txtServers = formServers.txtServers
End Sub
Private Sub txtServers_LostFocus(Sender As Object, e As EventArgs) Handles txtServers.LostFocus
' ...
End Sub
The advantage of the latter approach, besides the more consistent, and possibly more elegant syntax, is that you don't have to remember to call RemoveHandler.

trigger an event from another control

I'm using vb.net and winform. I am coming across an issue which I'm pounding my head against for the past few hours.
I have a main usercontrol which I added a groupbox and inside that groupbox, added a control like this:
main usercontrol
Me.GroupBox1.Controls.Add(Me.ctlWithDropDown)
user control ctlWithDropDown
Me.Controls.Add(Me.ddList)
Private Sub ddlList_SelectionChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ddlList.SelectionChanged
'some simple logic here to check if value changed
End Sub
The main usercontrol inherits the base class which has an event to set a value to true or false like so:
Public Event SetFlag(ByVal value As Boolean)
I want to know how I can trigger/set this boolean value from the dropdownlist when the SelectionChanged event is trigger. Any help on this issue?
Wire up an event handler for the drop down list:
AddHandler Me.ctlDropDown.SelectedIndexChanged, AddressOf ddlSelectedIndexChanged
Me.GroupBox1.Controls.Add(Me.ctlDropDown)
Make sure you create ddlSelectedIndexChanged in your control and have it fire the SetFlag Event:
Protected Sub ddlSelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)
RaiseEvent SetFlag(True)
End Sub
I presume the me.ctlDropDown is something that you are making programmatically? If so then this sort of thing should work for you.
Public Sub Blah()
Dim ctlDropDown As New ComboBox
AddHandler ctlDropDown.SelectedIndexChanged, AddressOf IndexChangedHandler
Me.GroupBox1.Controls.Add(ctlDropDown)
End Sub
Private Sub IndexChangedHandler()
'Do whatever you need here.
End Sub
However, if this is not created at runtime should make an event handler like:
Private Sub IndexChangedHandler() Handles Me.ctlDropdown.SelectedIndexChanged
'Do whatever you need here.
End Sub

How do I fire an event in VB.NET code?

I have a form that has a start button (to allow users to run the processes over and over if they wish), and I want to send a btnStart.Click event when the form loads, so that the processes start automatically.
I have the following function for the btnStart.Click event, but how do I actually tell Visual Basic 'Pretend someone has clicked the button and fire this event'?
I've tried going very simple, which essentially works. However, Visual Studio gives me a warning Variable 'sender' is used before it has been assigned a value, so I'm guessing this is not really the way to do it:
Dim sender As Object
btnStart_Click(sender, New EventArgs())
I have also tried using RaiseEvent btnStart.Click, but that gives the following error:
'btnStart' is not an event of 'MyProject.MyFormClass
Code
Imports System.ComponentModel
Partial Public Class frmProgress
Private bw As BackgroundWorker = New BackgroundWorker
Public Sub New()
InitializeComponent()
' Set up the BackgroundWorker
bw.WorkerReportsProgress = True
bw.WorkerSupportsCancellation = True
AddHandler bw.DoWork, AddressOf bw_DoWork
AddHandler bw.ProgressChanged, AddressOf bw_ProgressChanged
AddHandler bw.RunWorkerCompleted, AddressOf bw_RunWorkerCompleted
' Fire the 'btnStart.click' event when the form loads
Dim sender As Object
btnStart_Click(sender, New EventArgs())
End Sub
Private Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
If Not bw.IsBusy = True Then
' Enable the 'More >>' button on the form, as there will now be details for users to view
Me.btnMore.Enabled = True
' Update the form control settings so that they correctly formatted when the processing starts
set_form_on_start()
bw.RunWorkerAsync()
End If
End Sub
' Other functions exist here
End Class
You should send a button as sender into the event handler:
btnStart_Click(btnStart, New EventArgs())
Steps in involved in raising an event is as follows,
Public Event ForceManualStep As EventHandler
RaiseEvent ForceManualStep(Me, EventArgs.Empty)
AddHandler ForceManualStep, AddressOf ManualStepCompletion
Private Sub ManualStepCompletion(sender As Object, e As EventArgs)
End Sub
So in your case, it should be as below,
btnStart_Click(btnStart, EventArgs.Empty)
Just Call
btnStart.PerformClick()
You are trying to implement a bad idea. Actually, you have to make a subroutine to accomplish these kind of tasks.
Private Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
call SeparateSubroutine()
End Sub
private sub SeparateSubroutine()
'Your code here.
End Sub
And then whereever you want to call the btnStart's click event, just call that SeparateSubroutine. This should be a correct way in your case.
You can subclass the button and make its OnClick Method public as I described here.

BackGroundWorker RunWorkerCompleted Event Not Called Sometimes

I'm seeing some strange behavior where the RunWorkerCompleted event for one of two threads I start isn't being called depending on how I call them. Check out the code below, and the two methods of firing the threads, good() and bad().
Public Class Form1
Private WithEvents bw As System.ComponentModel.BackgroundWorker
Private WithEvents bw2 As System.ComponentModel.BackgroundWorker
Private starts As Integer = 0
Private Sub bw_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bw.DoWork
starts += 1
Threading.Thread.Sleep(e.Argument)
End Sub
Private Sub bw_Completed(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles bw.RunWorkerCompleted
MessageBox.Show("Ending " + starts.ToString())
End Sub
Private Sub bad()
bw = New System.ComponentModel.BackgroundWorker()
bw.RunWorkerAsync(5000)
Threading.Thread.Sleep(500)
bw = New System.ComponentModel.BackgroundWorker()
bw.RunWorkerAsync(5)
End Sub
Private Sub good()
bw2 = New System.ComponentModel.BackgroundWorker()
AddHandler bw2.DoWork, AddressOf bw_DoWork
AddHandler bw2.RunWorkerCompleted, AddressOf bw_Completed
bw2.RunWorkerAsync(500)
bw2 = New System.ComponentModel.BackgroundWorker()
AddHandler bw2.DoWork, AddressOf bw_DoWork
AddHandler bw2.RunWorkerCompleted, AddressOf bw_Completed
bw2.RunWorkerAsync(5)
End Sub
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
'good()
bad()
End Sub
End Class
In both cases the DoWork event is called for both threads. But in the bad() case, only the second thread fires the RunWorkerCompleted event. This is obviously due to the two different ways I'm using VB to handle events here. I'm looking for an explanation of this behavior, preferably with a link to some documentation that could help me understand how these events are being handled in VB better. It seems strange to me that just reusing a variable name here seems to either dispose of the thread before it's done or else just make it stop firing events.
Where is this automatic unsubscribing documented?
In the Visual Basic Language Specification, a document you can download from Microsoft. Chapter 9.6.2 "WithEvents Variables" says this:
The implicit property created by a WithEvents declaration takes care of hooking and unhooking the relevant event handlers. When a value is assigned to the variable, the property first calls the remove method for the event on the instance currently in the variable (unhooking the existing event handler, if any). Next the assignment is made, and the property calls the add method for the event on the new instance in the variable (hooking up the new event handler).
The bolded phrase describes the behavior you see. It is rather important it works that way. If it didn't then you could never unsubscribe from an event and the event subscriptions would pile on without limit.