VB.NET: Add Cancel Event Handler From Other Class - vb.net

I am trying to figure out a way to use events, handlers - to cancel an operation (a progress bar) from an asynchronous operation.
I have a class, ProgressBar, that is displaying the progress of an operation. It has a Cancel button (which is Friend WithEvents, typically generated, and has a Private Sub ButtonCancel_click). Nothing special.
The Cancel sets a public CancelButtonHasBeenPressed.
I want to be able to react to pressing Cancel, in another class.
I read about adding a handler to react to an event, and removing the handler at the end of the operation, which is what I should be doing.
Something like:
Public Function Mine(ByRef myProgress As ThatProgressWindow)
' some setup
AddHandler CancelEvent, AddressOf myProgress.ButtonCancel_Click
' create and call workers
RemoveHandler CancelEvent, AddressOf myProgress.ButtonCancel_Click
End Function
Public Event CancelEvent()
Private Function worker(ByVal state As Object) As Object
' do work
If ' how do I check for the event ? Then drop everything and run
End Function
The problems with the code/pseudocode above:
1) in the AddHandler, I should be adding an event that exists, something connected to the actual cancellation - but there is nothing in the ProgressBar, as is, to do that - how do I add a cancel ? I only have a Cancel button... So I had to make the button_click Public which is probably wrong...
Somehow, I was thinking that the user pressing Cancel would trigger that event... Is that not true ? I am truly new to this, and the web seems to assume, in all the examples, that the people looking at those examples are already experts.
2) How do I check that the event has been raised ?
As I tried to type "If" followed by an event name, in any form, Intellisense kept telling me that it was wrong.
Please, help me get started !
Thank you.

Have you considered using a BackgroundWorker? It supports progress events and cancellation.
As an aside, don't use ByRef in your Mine function. It is not required and could lead to subtle bugs down the road if someone maintaining your code doesn't realize the parameter is ByRef.

Related

Issues with Event Handlers not being removed (using RemoveHandler) when sender is part of another class

I have a system where a Class of "Automation Providers" is working with some Control objects to provide some advanced monitoring functionality to them (by dynamically monitoring events).
A part of my code; A class called Automation_Provider contains a function called Browser_Navigate that takes in a reference to a System.Windows.Forms.WebBrowser instance and performs a .Navigate operation with a URL.
The special functionality it provides is that it sets an Event Handler to the Browser.DocumentCompleted to perform some actions when the event is raised.
This part actually works. What doesn't work, is I'm trying to dynamically remove the handler that causes the Subroutine to be called in the first place, but it doesn't seem to remove the Handler and if I try to call the function again, it fires twice.
The code looks like this:
Public Class Automation_Functions
Public Function Browser_Navigate(ByRef Browser As WebBrowser, ByVal Address As String) As Function_Status
'-----------------------------------------------------------------------------------
' A bunch of URL checks are performed here to make sure the "Address" is a valid URL
'-----------------------------------------------------------------------------------
AddHandler Browser.DocumentCompleted, AddressOf Browser_Navigation_Callback
Browser.Navigate(Address)
End Function
Private Sub Browser_Navigation_Callback(sender As Object, e As System.Windows.Forms.WebBrowserDocumentCompletedEventArgs)
'Get the browser object from the sender and remove the handler that initially called this function
Dim Browser As WebBrowser = CType(sender, WebBrowser)
RemoveHandler Browser.DocumentCompleted, AddressOf Browser_Navigation_Callback
'------------------------------------------------------------------------------------------
' A bunch of operations are performed here related to telling the caller of the original
' function about the performance of the web page - stuff like load time, etc.
'------------------------------------------------------------------------------------------
End Sub
End Class
However the Past Handlers are never actually removed from the Browser_Navigation_Callback function - I know this because calling the Browser_Navigate function a second time results in Browser_Navigation_Callback function being called twice, then if you call it a third time, it gets called Three times! - The Handlers just compound on top of each other since they never actually get removed.
I've been unable to find any reason as to why this is happening - I'm assuming it might have something to do with the fact that the WebBrowser object is actually inside of another class and casting the sender as a WebBrowser object doesn't refer it back to the original WebBrowser instance, but creates a new instance. However I have no idea on how to confirm that this is actually what is happening since I can't really see the attached event handlers in debug mode.
Other than that - this code should be working. Any help would be appreciated!
One of the features of VB when Option Strict is not set is that it will use "relaxed delegate conversion". This means that when a delegate argument does not exactly match the required signature but is compatible with it, VB will automagically insert an anonymous wrapper that converts from the provided delegate to the required signature.
This has a negative interaction with AddHandler and RemoveHandler. VB creates a new anonymous wrapper on each call, and each instance is distinct. As a practical matter, that means that it is impossible to remove a handler that has been added when relaxed delegate conversion is used.
With Option Strict on, relaxed delegate conversion is disabled, so it turns into a compiler error when the delegate argument does not exactly match the required signature. I have not found an individual compiler switch that will turn off relaxed delegate conversion, so the only way to deal with this is to turn on Option Strict for the file.

In VB.NET how do I get a button's text to change in its own event handler?

I'm trying to change the text drawn on a Button at the beginning of the click event handler. No matter what I do the text doesn't change until after the event handler method finishes. I've tried calling the button's update, refresh, and invalidate methods and also tried calling Application.DoEvents afterward.
The only thing that works, which I don't want to do, is to set up a timer so that the click event handler ends, the button text updates, then the timer ticks and THEN I do everything else I wanted to do in the click handler in the timer tick handler instead...
The problem is that your user interface "hangs" while your long operation is executed on the UI thread. You could solve this by running your long operation in a background thread, for example, by using a BackgroundWorker in your button event handler:
Dim bw As New BackgroundWorker()
AddHandler bw.DoWork,
Sub(sender, args)
' Do your lengthy stuff here -- this will happen in a separate thread.
' If you want to do UI operations here, you need to use Control.Invoke.
End Sub
AddHandler bw.RunWorkerCompleted,
Sub(sender, args)
' We are back in the UI thread here.
If args.Error IsNot Nothing Then ' If an exception occurred during DoWork,
MsgBox(args.Error.ToString()) ' do your error handling here
End If
' Re-enable your button and change the text back here:
...
End Sub
' Disable your button and change the text here:
...
bw.RunWorkerAsync() ' Start the Background Worker
(Of course, if you don't like AddHandler, using an instance WithEvents variable for the BackgroundWorker and the Handles keyword is just as fine and more typical for VB.)
Note that, in this case, the user can interact with your user interface while the operation is running. This can be a good thing (you can add an "interrupt" button that sets a flag which is checked in your long operation), but you need to make sure that no bad things can happen, e.g. deactivate the button, so that the same operation cannot be started twice.
All UI updates have to come from the UI thread. The event handler is called (or invoked) by the UI thread when it detects an event (such as button pressed) occurs. The UI thread takes care of updating the UI after the events.

How to update a rich text box from a background worker using a delegate

Ok I'm pretty new to using threads but so far I've managed to get the following:
Private Delegate Sub dlgUpdateText(text as string)
Private Sub UpdateStatus(text as string)
If rtxStatus.InvokeRequired then
Dim dlg as new dlgUpdateText(AddressOf UpdateStatus)
Me.Invoke(dlg, text)
Else
rtxStatus.text = text
End If
End Sub
and from my Async BackgroundWorker I call
UpdateStatus("Some text")
which seems to work ok however in my original code (which generates errors because I'm updating the control from the wrong thread) I used the following code to append to the (rich)textbox:
rtxStatus.Select(rtxStatus.TextLength, 0)
rtxStatus.SelectionColor = Color.Red
rtxStatus.AppendText("Some error occurred gathering data")
My question is how should I modify my new code to allow me to do this rather than just replace the text? I have read several guides on using delegates but I'm still lost on a few points so I don't really know what's going on with the code I have.
Bonus questions (which probably serve best to show what needs explaining to me!):
What does the Delegate Sub actually do? It doesn't seem to serve any purpose other than hold the property (text) that was already passed to the main Sub (UpdateStatus)?
What is happening when Me.Invoke is called? Me is the current form so when I pass the Delegate Sub and the text where is it specified that the text should be passed to the rtxSTatus.Text property?
UpdateStatus runs in the main thread (?) and is called from one of the background worker threads so why is the Invoke even necessary or is the UpdateStatus Sub run under the background thread that called it? When I altered the text that is applied in the Else statement to see which was run it seems that Invoke is never used to change the text in the box.
Any help would be really appreciated, I'm completely baffled by this - thanks!
Rather than creating a delegate I would suggest using the existing methods offered from a backgroundworker. The backgroundworker provides two methods to access the main thread:
The ProgressChanged event to update the main thread during backgroundworker processing and the RunWorkerCompleted event to update the main thread once the backgroundworker process is complete.
You can find this information and how to implement it from the following link:
http://msdn.microsoft.com/en-us/library/ywkkz4s1.aspx

How to clone subscribtions to object's events is vs.net?

I have a custom datagridviewcolumn in which i've added an event.
The problem is, I can't work out how to see who has subscribed to the current column object's event and add those subscriptions to the cloned column object.
I get around this problem by asking the calling program to pass the address of the handler to a delegate in the custom column, instead of adding a handler to the event.
Forgive my terminology, I hope you understand what i'm trying to say!
By receiving the reference to the method, the datagridviewcolumn now has control and can then easily clone that reference.
This is fine, but users of controls expect to be able to suscribe to events by selecting the event in visual studio - which creates a template of the method.
At least in C# you can have "adders" and "removers" for events, like getters and setters for properties.
Maybe you can use that to do some custom processing during the process of somebody adding an event handler to the event?
EDIT
I don't know much about VB.NET, but I googled a little and found the following snippet:
Public Delegate Sub WorkDone(ByVal completedWork As Integer )
Private handlers As New ArrayList()
Public Custom Event WorkCompleted As WorkDone
AddHandler (ByVal value As WorkDone)
If handlers.Count <= 5 Then
handlers.Add(value)
End If
End AddHandler
RemoveHandler(ByVal value As WorkDone)
handlers.Remove(value)
End RemoveHandler
RaiseEvent (ByVal completedWork As Integer)
If completedWork > 50 Then
For Each handler As WorkDone In handlers
handler.Invoke(completedWork)
Next
End If
End RaiseEvent
End Event
This should help you customize your event handler, so that you can "see" the delegates being added to the event from within your class.
For anyone interested, I chanced upon a way to perform this without the need for a custom event.
By suffixing the event name with Event, you have access to some properties and methods.
So in Thorsten's example, I would refer to the event as WorkCompletedEvent.
Specific to this question, there is the method GetInvocationList that returns a list of delegates attached to the event.
In addition to this, checking if WorkCompletedEvent IsNot Nothing, will tell you whether there are handlers for the event without having to retrieve the invocation list.

Force multi-threaded VB.NET class to display results on a single form

I have a windows form application that uses a Shared class to house all of the common objects for the application. The settings class has a collection of objects that do things periodically, and then there's something of interest, they need to alert the main form and have it update.
I'm currently doing this through Events on the objects, and when each object is created, I add an EventHandler to maps the event back to the form. However, I'm running into some trouble that suggests that these requests aren't always ending up on the main copy of my form. For example, my form has a notification tray icon, but when the form captures and event and attempts to display a bubble, no bubble appears. However, if I modify that code to make the icon visible (though it already is), and then display the bubble, a second icon appears and displays the bubble properly.
Has anybody run into this before? Is there a way that I can force all of my events to be captured by the single instance of the form, or is there a completely different way to handle this? I can post code samples if necessary, but I'm thinking it's a common threading problem.
MORE INFORMATION: I'm currently using Me.InvokeRequired in the event handler on my form, and it always returns FALSE in this case. Also, the second tray icon created when I make it visible from this form doesn't have a context menu on it, whereas the "real" icon does - does that clue anybody in?
I'm going to pull my hair out! This can't be that hard!
SOLUTION: Thanks to nobugz for the clue, and it lead me to the code I'm now using (which works beautifully, though I can't help thinking there's a better way to do this). I added a private boolean variable to the form called "IsPrimary", and added the following code to the form constructor:
Public Sub New()
If My.Application.OpenForms(0).Equals(Me) Then
Me.IsFirstForm = True
End If
End Sub
Once this variable is set and the constructor finishes, it heads right to the event handler, and I deal with it this way (CAVEAT: Since the form I'm looking for is the primary form for the application, My.Application.OpenForms(0) gets what I need. If I was looking for the first instance of a non-startup form, I'd have to iterate through until I found it):
Public Sub EventHandler()
If Not IsFirstForm Then
Dim f As Form1 = My.Application.OpenForms(0)
f.EventHandler()
Me.Close()
ElseIf InvokeRequired Then
Me.Invoke(New HandlerDelegate(AddressOf EventHandler))
Else
' Do your event handling code '
End If
End Sub
First, it checks to see if it's running on the correct form - if it's not, then call the right form. Then it checks to see if the thread is correct, and calls the UI thread if it's not. Then it runs the event code. I don't like that it's potentially three calls, but I can't think of another way to do it. It seems to work well, though it's a little cumbersome. If anybody has a better way to do it, I'd love to hear it!
Again, thanks for all the help - this was going to drive me nuts!
I think it is a threading problem too. Are you using Control.Invoke() in your event handler? .NET usually catches violations when you debug the app but there are cases it can't. NotifyIcon is one of them, there is no window handle to check thread affinity.
Edit after OP changed question:
A classic VB.NET trap is to reference a Form instance by its type name. Like Form1.NotifyIcon1.Something. That doesn't work as expected when you use threading. It will create a new instance of the Form1 class, not use the existing instance. That instance isn't visible (Show() was never called) and is otherwise dead as a doornail since it is running on thread that doesn't pump a message loop. Seeing a second icon appear is a dead give-away. So is getting InvokeRequired = False when you know you are using it from a thread.
You must use a reference to the existing form instance. If that is hard to come by (you usually pass "Me" as an argument to the class constructor), you can use Application.OpenForms:
Dim main As Form1 = CType(Application.OpenForms(0), Form1)
if (main.InvokeRequired)
' etc...
Use Control.InvokeRequired to determine if you're on the proper thread, then use Control.Invoke if you're not.
You should look at the documentation for the Invoke method on the Form. It will allow you to make the code that updates the form run on the thread that owns the form, (which it must do, Windows forms are not thread safe).
Something like
Private Delegate Sub UpdateStatusDelegate(ByVal newStatus as String)
Public sub UpdateStatus(ByVal newStatus as String)
If Me.InvokeRequired Then
Dim d As New UpdateStatusDelegate(AddressOf UpdateStatus)
Me.Invoke(d,new Object() {newStatus})
Else
'Update the form status
End If
If you provide some sample code I would be happy to provide a more tailored example.
Edit after OP said they are using InvokeRequired.
Before calling InvokeRequired, check that the form handle has been created, there is a HandleCreated property I belive. InvokeRequired always returns false if the control doesn't currently have a handle, this would then mean the code is not thread safe even though you have done the right thing to make it so. Update your question when you find out. Some sample code would be helpful too.
in c# it looks like this:
private EventHandler StatusHandler = new EventHandler(eventHandlerCode)
void eventHandlerCode(object sender, EventArgs e)
{
if (this.InvokeRequired)
{
this.Invoke(StatusHandler, sender, e);
}
else
{
//do work
}
}