views:

2778

answers:

4

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!

+2  A: 

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...
Hans Passant
In return, I've updated my question with the solution I came up with based on your clues. Thanks for the steering!
rwmnau
A: 

Use Control.InvokeRequired to determine if you're on the proper thread, then use Control.Invoke if you're not.

ctacke
A: 

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.

pipTheGeek
A: 

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
        }
    }
Brian Rudolph