views:

728

answers:

4

Hi all, I cannot understand how this is possible. Please help!!

I have an app with a trayicon. I want a form to be show when the user double clicks the trayicon. I have a problem where it is possible to get 2 or more forms showing by quickly triple or quadruple clicking the trayicon. The reason I don't want a singleton is that I want the form to be released each time it is closed to save memory, maybe this is not a good idea?

I have a field called m_form1. I have a method called ShowForm1; I call the method ShowForm1 on the double-click of the TrayIcon.

        private Form1 m_form1;
        private void ShowForm1()
        {
            if (m_form1 == null)
            {
                Trace.WriteLine("*CREATE*" + Thread.CurrentThread.ManagedThreadId.ToString());
                m_form1 = new Form1();
                m_form1.FormClosed += new FormClosedEventHandler(m_form1_FormClosed);
                m_form1.Show();
            }
            m_form1.BringToFront();
            m_form1.Activate();
        }

So when Form1 takes a while to construct, then it is possible to create 2 because m_form1 is still null when the second call arrives. Locking does not seem to work as it is the same thread both calls (I'm guessing the UI thread) ie the trace writes out CREATE1 twice (below).

[3560] *CREATE*1 
[3560] *CREATE*1

Changing the code to include a lock statement does not help me.

    private Form1 m_form1;
    private object m_Locker = new object();
    private void ShowForm1()
    {
        lock (m_Locker)
        {
            if (m_form1 == null)
            {
                Trace.WriteLine("****CREATE****" + Thread.CurrentThread.ManagedThreadId.ToString());
                m_form1 = new Form1();
                m_form1.FormClosed += new FormClosedEventHandler(m_form1_FormClosed);
                m_form1.Show();
            }
        }
        m_form1.BringToFront();
        m_form1.Activate();
    }

How should I handle this situation?

Thanks guys

Tim.

+5  A: 

Have an additional boolean variable, "m_formUnderConstruction" which you test for before constructing the form, and which you set as soon as you've decided to construct it.

The re-entrancy makes all of this a little icky, unfortunately. I've removed the lock, as if this ever gets called from a different thread then you've got the nasty situation of trying to show a form from a different thread to the one it was constructed on.

private Form1 m_form1;
private bool m_underConstruction = false;

private void ShowForm1()
{
    if (m_underConstruction)
    {
        // We're about to show it anyway
        return;
    }
    m_underConstruction = true;
    try
    {
        if (m_form1 == null)
        {
            m_form1 = new Form1();
            m_form1.FormClosed += new FormClosedEventHandler(m_form1_FormClosed);
            m_form1.Show();
        }
    }
    finally
    {
        m_underConstruction = false;
    }
    m_form1.BringToFront();
    m_form1.Activate();
}
Jon Skeet
I will try now, but won't that cause a NullReferenceException? Ie call 1 will test m_Form1, its null, will set the flag, then start to construct the form. Then call 2 arrives, m_Form1 is still null, bu the flag is set, so tries to call BringToFront() on m_Form1 which is still null?
Tim Bailey
ok, Sorry. didn't see the rest of your answer!
Tim Bailey
Jon, do you have explanation why the lock statement does not work in his example?
Sunny
Sunny: Yes - it's re-entrant. In other words, the event handler is being called while you're trying to show the form - e.g. during the constructor. I wouldn't like to say *exactly* where, but if you log a stack trace at the start of ShowForm1() it'll probably make it clearer.
Jon Skeet
Is does work but Is the same problem still present? ie the check for m_UnderContruction. Much less likely to happen as setting the bool value is very quick compared to constructing the form?
Tim Bailey
No, because I don't believe the re-entrancy can occur between the check for m_underConstruction and the set of it. It will be in UI routines like Show() or the constructor that the event loop will be run. I'd still be interested to see the stack traces, mind you...
Jon Skeet
So, are the threads different really. Isn't it always happen on the same thread (I guess the tray icon raises the event on it's application thread). If this is true, then lock will not help. But there will be no race condition, so the flag will work even without lock.
Sunny
More than that - if this happens on a different thread, there'll be a different bug anyway. Editing answer...
Jon Skeet
Anyway, as Interlocked is atomic operation, this should be much better for tracking how many forms are started, and prevent a new ones to be created.
Sunny
But you'd still have the same problem of showing a form from the wrong thread. In other words, either this solution is fine, or interlocked would fail too :)
Jon Skeet
(I generally try to avoid lock-free coding. It makes me think too hard, and leaves opportunities for issues. For instance, you'd also have to make m_form1 volatile.)
Jon Skeet
This is really strange situation - how comes that re-entrance occur in the first place? Same thread in different parts of the code?
Sunny
Something in the UI construction stuff will be effectively calling Application.DoEvents, I suspect. That's why I've asked for the stack trace :)
Jon Skeet
hmm, then this is clearly a bug in the framework, would be nice if the OP posts a stack trace, so it can be properly reported.
Sunny
It *may* be a bug in the framework - but I don't think it "clearly" is at all.
Jon Skeet
I can't post more than 300 chars here. So will "answer" with the stack traces. In the stack trace ShowQuickView == ShowForm1 and frmQuickView = Form1
Tim Bailey
Okay, so this is where the re-entrancy happens: at Acclipse.InOut.Tray.frmQuickview.SetBounds(Rectangle r) That calls Application.DoEvents.
Jon Skeet
Presume that is normal?
Tim Bailey
Calling Application.DoEvents? It's best avoided for precisely this sort of re-entrancy issue. Why does frmQuickview do it?
Jon Skeet
Hmm, not sure why I'm doing that....Will remove and re-test. Thanks for yuor help.
Tim Bailey
A: 

Use Interlocked.Increment to change the nr of the tries. If it is 1, open the form, otherwise, don't. And use Interlocked.Decrement after the test or on form's close.

private int openedForms = 0;
private Form1 m_form1;
private void ShowForm1()
{

    if (Interlocked.Increment(ref openedForms) = 1)
    {
       m_form1 = new Form1();
       m_form1.FormClosed += new FormClosedEventHandler(m_form1_FormClosed);
       m_form1.Show();
    }
    else
    {
       Interlocked.Decrement(ref openedForms);
    }
    if (m_form1 != null)
    {
       m_form1.BringToFront();
       m_form1.Activate();
    }
}

private void m_form1_FormClosed(object Sender, EventArgs args)
{
   Interlocked.Decrement(ref openedForms);
}
Sunny
A: 
[3056]    at Acclipse.InOut.Tray.TrayManager.ShowQuickView() 
[3056]    at Acclipse.InOut.Tray.TrayManager.notifyIcon_DoubleClick(Object sender, EventArgs e) 
[3056]    at System.Windows.Forms.NotifyIcon.OnDoubleClick(EventArgs e) 
[3056]    at System.Windows.Forms.NotifyIcon.WmMouseDown(Message& m, MouseButtons button, Int32 clicks) 
[3056]    at System.Windows.Forms.NotifyIcon.WndProc(Message& msg) 
[3056]    at System.Windows.Forms.NotifyIcon.NotifyIconNativeWindow.WndProc(Message& m) 
[3056]    at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam) 
[3056]    at System.Windows.Forms.UnsafeNativeMethods.PeekMessage(MSG& msg, HandleRef hwnd, Int32 msgMin, Int32 msgMax, Int32 remove) 
[3056]    at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData) 
[3056]    at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context) 
[3056]    at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context) 
[3056]    at System.Windows.Forms.Application.Run(Form mainForm) 
[3056]    at Acclipse.InOut.Tray.Program.Main(String[] args) 
[3056]  
[3056] ****CREATE****1 
[3056]    at Acclipse.InOut.Tray.TrayManager.ShowQuickView() 
[3056]    at Acclipse.InOut.Tray.TrayManager.notifyIcon_DoubleClick(Object sender, EventArgs e) 
[3056]    at System.Windows.Forms.NotifyIcon.OnDoubleClick(EventArgs e) 
[3056]    at System.Windows.Forms.NotifyIcon.WmMouseDown(Message& m, MouseButtons button, Int32 clicks) 
[3056]    at System.Windows.Forms.NotifyIcon.WndProc(Message& msg) 
[3056]    at System.Windows.Forms.NotifyIcon.NotifyIconNativeWindow.WndProc(Message& m) 
[3056]    at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam) 
[3056]    at System.Windows.Forms.UnsafeNativeMethods.PeekMessage(MSG& msg, HandleRef hwnd, Int32 msgMin, Int32 msgMax, Int32 remove) 
[3056]    at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData) 
[3056]    at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context) 
[3056]    at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context) 
[3056]    at System.Windows.Forms.Application.DoEvents() 
[3056]    at Acclipse.InOut.Tray.frmQuickview.SetBounds(Rectangle r) 
[3056]    at Acclipse.InOut.Presentation.QuickViewPresenter.Initialise() 
[3056]    at Acclipse.InOut.Tray.frmQuickview..ctor(InOutManager mgr, TrayManager tray) 
[3056]    at Acclipse.InOut.Tray.TrayManager.ShowQuickView() 
[3056]    at Acclipse.InOut.Tray.TrayManager.notifyIcon_DoubleClick(Object sender, EventArgs e) 
[3056]    at System.Windows.Forms.NotifyIcon.OnDoubleClick(EventArgs e) 
[3056]    at System.Windows.Forms.NotifyIcon.WmMouseDown(Message& m, MouseButtons button, Int32 clicks) 
[3056]    at System.Windows.Forms.NotifyIcon.WndProc(Message& msg) 
[3056]    at System.Windows.Forms.NotifyIcon.NotifyIconNativeWindow.WndProc(Message& m) 
[3056]    at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam) 
[3056]    at System.Windows.Forms.UnsafeNativeMethods.PeekMessage(MSG& msg, HandleRef hwnd, Int32 msgMin, Int32 msgMax, Int32 remove) 
[3056]    at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData) 
[3056]    at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context) 
[3056]    at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context) 
[3056]    at System.Windows.Forms.Application.Run(Form mainForm) 
[3056]    at Acclipse.InOut.Tray.Program.Main(String[] args) 
[3056]  
[3056] ****CREATE****1
Tim Bailey
A: 

Please see this, it handles all mouse event combinations for NotifyIcon as well as Form1.

More here: http://code.msdn.microsoft.com/TheNotifyIconExample