Search code examples
c#winformswindows-7timerscreensaver

App Freezes When Screensaver Settings Changed In Windows 7 - System.Threading.Timer The Culprit?


I have a C#/TCP/Winform app that's been running for quite some time, but one of my users has discovered an issue that I can replicate easily, but that I can't seem to solve.

If the app is open on Windows 7 and the user then changes any option in their screensaver settings - be it the screensaver being used or the time - the app freezes and the UI becomes non-responsive. Clicking anywhere on the form just gets the system "ding."

When the app is run through Visual Studio in debug mode, the problem ceases to exist, but once out of the confines of VS, goes back to freezing.

After some tinkering and testing, I seem to have narrowed down my problem to a System.Threading.Timer that runs once every second. I've been steadily paring down what the timer does and discovered that even if the timer's event does nothing, it still locks the app. If I disable the timer in the code or cancel the timer in the app prior to changing the Screensaver settings, then the app will return to functional after a screensaver change (though it still seems to freeze for about 2-3 seconds).

This code here, seems to be all that is necessary to make the app freezable (within the WinForm code):

    /// <summary>
    /// Starts the countdown timer for unit alerts
    /// </summary>
    private void startCountDownTimer()
    {
        Object timeState = new object();
        this._timerCall = new System.Threading.TimerCallback(this.countDown);
        this._countdownTimer = new System.Threading.Timer(_timerCall, timeState, 0, 1000);
    }

    /// <summary>
    /// Invokes the countdown logic for unit alerts
    /// </summary>
    /// <param name="state"></param>
    private void countDown(Object state)
    {
        // REMOVED AND STILL FREEZING
    }

Note that this freeze happens even with the contents of countDown commented out so that the timer is doing nothing other than firing once per second.

If I launch process and then attach a remote debugger to it, there is nothing in the output that indicates that anything is wrong with the application. The form actually still fires events like "Activated":

*** MAIN WINDOW ACTIVATED ***
*** MAIN WINDOW ACTIVATED ***

However nothing else seems to be firing and the application has to be either killed or shut down via EndTask. If EndTask is used while the debugger is still attached, I suddenly get errors:

A first chance exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll
===================================
ERR: Invoke or BeginInvoke cannot be called on a control until the window handle has been created.
       at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
   at System.Windows.Forms.Control.BeginInvoke(Delegate method, Object[] args)
   at System.Windows.Forms.Control.BeginInvoke(Delegate method)
   at Wcsg.UI.Windows.CadClient.client_MessageReceived(TcpMessageReceivedEventArgs mrea) in C:\Users\---------\Documents\Visual Studio 2010\Projects\Dispatch-DEVELOPMENT\CadClient\Forms\CadClientForm.cs
   at Wcsg.Net.Tcp.WcsgTcpClient.processStream(Int32 count)
   at Wcsg.Net.Tcp.WcsgTcpClient.performSocketRead(IAsyncResult ar)
---------------------

The errors that I do finally get would seem to be linked to the closing of the form while it finally gets around to processing the messages on the socket.

I'm looking for any sort of direction to look here.

* EDIT *

I was asked about the Main method in the program when the expected workaround (see answer) didn't work. Here is the Main code:

[STAThread]
    static void Main(String[] args)
    {
        // check for other running CadClients
        bool createdNew = _mutex.WaitOne(TimeSpan.Zero, false);

        if (createdNew) // first-run, launch Status Monitor on load
        {
            List<String> newArgs = new List<string>(args);
            newArgs.Add("-SM");
            args = newArgs.ToArray();
        }

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        AppDomain currentDomain = AppDomain.CurrentDomain;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

        // try to catch issue 13445 -- see testing notes
        CadClient cc = new CadClient(args);
        CadExceptionHandler ceh = new CadExceptionHandler(ref cc);
        currentDomain.UnhandledException += new UnhandledExceptionEventHandler(ceh.CurrentDomain_UnhandledException);
        Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(ceh.Application_ThreadException);

        Application.Run(cc);
    }

* EDIT * I should probably add the Splashscreen code as well:

    private void showSplashScreen()
    {
        WaitCallback wcb = new WaitCallback(doSplashScreen);
        ThreadPool.QueueUserWorkItem(wcb);
    }

    private void doSplashScreen(object state)
    {
        if (this._splash == null)
            this._splash = new SplashForm(this);

        this._splash.FormClosed += new FormClosedEventHandler(_splash_FormClosed);
        this._splash.Show();

        while (this._splash != null && !this._splash.WorkDone)
            Application.DoEvents();
        this._splash.Close();
    }

The method showSplashScreen() is called within the main form's constructor - originally before and now after the InitializeComponents() call. The splash screen displays updates its display while the main app validates some security. Then it turns into a login screen and then displays more udpates while the main app loads data from the server. Once the main form indicates (via event) that it has completed its work, the SplashScreen is closed.


Solution

  • Used the comment from @HansPassant and found that the showSplash method for our splash/login screen was not being called before Application.Run, but WAS being called before our main form's InitializeComponent method. Moved this.showSplash to the line immediately below InitializeComponent() and recompiled and the problem seems to be gone.