views:

277

answers:

4

Is there a better way to handle the task of doing something after the user has chosen to exit a WinForms program than this :

[edit 1 : in response to comment by 'NoBugz] In this case there is no ControlBox on the Form, and there is a reason for putting one level of indirection in what happens when the user chooses to close the Form [/edit 1]

[edit 2 : in response to all comments as of GMT +7 18:35 January 20 ] Perhaps using fading out the MainForm is a simple illustration of what you might want do as the Application is being closed : the user cannot interact with that : it is self-evidently related to the user's decision to terminate the application. [/edit 2]

(use some form of threading ?) (implications for a multi-threaded app ?) (does this code "smell bad" ?)

    // 'shutDown is an external form-scoped boolean variable
    //
    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        // make sure we don't block Windows ShutDown
        // or other valid reasons to close the Form
        if (e.CloseReason != CloseReason.ApplicationExitCall) return;

        // test for 'shutDown flag set here
        if (shutDown) return;

        // cancel closing the Form this time through
        e.Cancel = true;

        // user choice : default = 'Cancel
        if (MessageBox.Show("Exit Application ?", "", MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2) == System.Windows.Forms.DialogResult.OK)
        {
            // user says Exit : activate Timer, set the flag to detect Exit
            timer1.Enabled = true;
            shutDown = true;
        }
    }

Summary : In a very standard WinForms application (one MainForm launched in Program.cs in the standard way) : in the FormClosing Event handler of the MainForm :

  1. exit immediately (triggering the default behavior : which is to close the MainForm and exit the Application) if :

    a. the CloseReason is anything other CloseReason.ApplicationExitCall

    b. if a special boolean variable is set to true, or

  2. if no immediate exit : cancel the "first call" to FormClosing.

  3. the user then makes a choice, via MessageBox.Show dialog, to Exit the Application, or Cancel :

    a. if the user Cancels, of course, the Application stays "as is."

    b. if the user has chosen to 'Exit :

    1. set the special boolean flag variable to true

    2. run a Timer that does some special stuff.

    3. when the internal test in the Timer code detects the "special stuff" is done, it calls Application.Exit

+2  A: 

I don't see anything "wrong" with this. My only recommendation would be to let the user know that the program is "shutting down" by raising a non-modal window or perhaps a notification toaster above the system tray.

Dave Swersky
+1 Thanks for your comment ! I like the idea of system tray notification, and will explore that.
BillW
A: 

A couple of minor points:

  • I'd question the need for a timer: as the application is exiting anyway, and the user won't therefore expect the UI to be responsive, why not simply do the clean-up code on the UI thread? As suggested by @Dave Swersky, a little notification window during cleanup would be polite.

  • What happens if the application exit is triggered by a Windows shutdown or a user logoff?

Jeremy McGee
Thanks for your comment ! In this case the Timer is required, and the application does not exit until the Timer business is finished. What's being done by code exceuted in the Timer 'Tick event is not something the user can interact with, by design : there are some "visual side-effects" of the application closing process, but, again, the user can only "see" them, not interact with them.
BillW
+1  A: 

What is preventing doing the "special stuff" in the Form1_FormClosing function? Like Jeremy said, the user wouldn't expect to be using the form anyway. If you wish to hide immediately and do operations in the background, then it makes more sense to use the BackgroundWorker object (just drop it in from the toolbox). I would also recommend removing those immediate returns and structuring your logic a bit differently:

private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    //  do whatever logic here

    if (!e.Cancel)
    {
        backgroundWorker1.RunWorkerAsync();
            Hide();
    }
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    //  call other things here
    //    (but make sure you're not calling UI things -- they're disposed)
}

A bit of that is personal preference, but I think it's cleaner.

Travis Gockel
+1 Thanks for your comment ! I will explore the use of BackGroundWorker, as you suggest. In order for any code to run on exit from the FormClosing event, e.Cancel must be set to 'true. The immediate intecepts of certain conditions are required in order that the second time the FormClosing event is called (in the 'Timer 'Tick event code) the application really closes, so the user does not repeat seeing the MessageBox.Show presented Dialog.
BillW
+2  A: 

My suggestions, both as a developer and a user:

A very fast task

  • Just do the task in the Closing event handler.

A less fast, but not incredibly slow task

  • Create a non-background thread (so it's not shut down when the application exits) with the task in the Closing event handler.
  • Let the application exit. Forms will go away, et cetera, but the process will keep running until the task is done.
  • Just remember to handle exceptions and such in that worker thread. And make sure that things doesn't crash if the user reopens your application before that task is done.

Slower tasks

  • In the Closing event handler, open a shutting-down-form and let the form itself close.
  • Do the task in the/behind the shutting-down-form while displaying some friendly progress and information.
  • Exit application when task is done.

Some untested example code. We are doing something similar to this in one of our applications. The task in our case is to store window properties (location, size, window state, et cetera) to a database.

private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    // If it wasn't the user who asked for the closing, we just close
    if (e.CloseReason != CloseReason.UserClosing)
        return;

    // If it was the user, we want to make sure he didn't do it by accident
    DialogResult r = MessageBox.Show("Are you sure you want this?", 
                                     "Application is shutting down.",
                                     MessageBoxButtons.YesNo, 
                                     MessageBoxIcon.Question);
    if (r != DialogResult.Yes)
    {
        e.Cancel = true;
        return;
    }
}

protected override void OnFormClosed(FormClosedEventArgs e)
{
    // Start the task
    var thread = new Thread(DoTask)
    {
        IsBackground = false,
        Name = "Closing thread.",
    };
    thread.Start();

    base.OnFormClosed(e);
}

private void DoTask()
{
    // Some task to do here
}
Svish
+1 Thanks for your comment ! Really like the way you broke out strategies by time consumed. Very good point about preventing re-opening (will consider making the app a Singleton). In this case there are side-effects on the UI as the MainForm/Application is closed, and "extra step" of requiring the user to confirm shutdown is a requirement. But what happens does fall in the "fast" category.
BillW
@BillW: Don't prevent re-opening unless it is really necessary. There is almost nothing I hate more as a user than closing an app and then when trying to launch it again I get a message that it is already running. How to handle this depends a lot upon what that task of yours is though. (Will add some example code to my answer with what you can do in the closing event)
Svish
Thanks, Svish, for the example code ! I am curious what the advantages are of over-riding the FormClosed event, and launching your "finalizer" thread there, rather than in the "default" FormClosed event of the Form.
BillW
@BillW: Oh, that's just because those 3 methods in my code are actually spread across three classes. The MainForm_FormClosing event handler is in the actual MainForm. The OnFormClosed is in a different class that the MainForm inherits from and the DoTask method is in a totally different class. What it happens in our application is that when a form inherits from that special form, it will automatically save the forms location, size and state when it closes. The reason for the overriding is just because I think it is kind of recommended when dealing with form inheritance. I think :p
Svish