Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF application exits immediately when showing a dialog before startup

Update: I guess, what I need is to understand what is the "correct", "supported" way to show a dialog before application start in WPF.

Here's the code:

    public partial class App : Application
    {
        [STAThread]
        public static void Main()
        {
            var app = new App();
            app.InitializeComponent();

            new DialogWindow().ShowDialog();

            app.Run( new MainWindow() );
        }
    }

The DialogWindow shows up as expected.
But after closing it, the application exits immediately. MainWindow doesn't show up at all!

I have done some debugging and traced the problem to this:

  1. When the dialog is created, it becomes app's MainWindow, since there is no MainWindow at the moment.
  2. Therefore, closing the dialog causes the application to post ShutdownCallback on the dispatcher queue.
  3. However, the dispatcher doesn't run long enough to execute the callback.
  4. Therefore, once app.Run is called subsequently, the first thing on the queue is ShutdownCallback, which, naturally, causes the app to close immediately.

Given this analysis, there is an obvious workaround: create MainWindow right after App, thus making it app's MainWindow, which would prevent DialogWindow from causing application closure.

However, here is what bothers me.

First, this looks like a dirty hack to me. I mean, there is no explicit reason for creating windows in this order, and I have only found this through some debugging. This can't be the supported way.

Second, this is clearly a bug. I mean, if creating a second window after shutdown wasn't supported explicitly, it should've thrown some InvalidOperationException, right?

Thirdly, not only is this a bug, but it looks like a very naive one, something like a multithreading beginner would create.

All this leads me to believe that maybe I don't get something fundamental here? Maybe I don't make sense at all? Maybe it all should be done in some different fashion?

Here's some background:
The application has to do some bootstrapping on startup. Check this and that, setup exception handlers, logging - you know, the usual stuff. In this process, it may become necessary to ask the user for some help - which is what the dialog is for.

I absolutely don't want to put all that in some kind of state machine that executes on MainWindow.IsVisibleChanged or something like that. I would like to keep it really simple, short and straightforward - the way bootstrap code is supposed to be, so that it's easy to spot bugs with a naked eye.

like image 599
Fyodor Soikin Avatar asked Sep 13 '10 17:09

Fyodor Soikin


4 Answers

By default, the ShutdownMode of a WPF application is OnLastWindowClose. In your code you show a single window and then close it. So the last window is closed and the application shuts down. Then while shutting down, you show another window. Since the application is shutting down, the window is immediately closed.

So everything is working as designed and programmed by you.

However, you want to do something different: The window you show first as the only window is supposed to be a "special window", and after closing it you want to continue executing, show your "main window" and then exit the application once it (or all windows associated with the app) closes.

The easiest way: First set the shutdown mode to OnExplicitShutdown, then after showing the main window set it to OnLastWindowClose or OnMainWindowClose. In code:

public static void Main()
{
    var app = new App();
    app.InitializeComponent();

    app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
    new DialogWindow().ShowDialog();

    var mainWindow = new MainWindow();
    app.MainWindow = mainWindow;
    app.Run(mainWindow);
    // When the window has loaded, it should then set the app.ShutdownMode to what you actually want.
}

EDIT: I am not sure what exactly you are doing. The code you gave will not compile, since when properly using a WPF application class (with an App.xaml build-action as ApplicationDefinition), a Main method is already defined. If you just have a class derived from Application, you have no InitializeComponent() method. The only way to get you code to compile was by manually changing the build-action to Page. However, in that case, Application.Current == app.

So what occurs is the following:

  1. The application starts. Since no WPF-application has been created so far, Application.Current is null. This also means no dispatcher-loop is running and dispatcher messages are unhandled (note that the dispatcher loop also handles windows messages).
  2. A new App-object is created. Since Application.Current is null, it sets itself as Application.Current.
    • Application.Current.MainWindow is null and Application.Current.Windows is an empty list.
    • Since ShutdownMode is OnLastWindowClose, once the last window of the current application (i.e. app) closes, shutdown starts.
  3. The DialogBox is shown modally. Since no dispatcher-loop is running, the ShowDialog() itself runs a "local" dispatcher-loop.
    • Actually this is two parts: First the window is created. It belongs to the current application, so it adds itself to Application.Current.Windows. Since it is the first window shown and Application.Current.MainWindow is null, it also sets itself as main window. Secondly, the window is shown modally.
    • Since Application.Current.Windows is now non-empty, once it is empty, shutdown will start.
  4. The user closes the dialog window. As part of being closed, the window removes itself from Application.Current.Windows. Also, since it is the MainWindow, this is set to null. Since Application.Current.Windows is now empty, shutdown starts. However, since there is no dispatcher-loop running, nothing is done yet (only an internal flag or similar is set).
    • If you had used app.Run(new DialogWindow()); app.Run(new MainWindow());, you would have an exception while creating the MainWindow, since in this case the dispatcher-loop is running properly. Thus, it can actually shutting itself down, so when the MainWindow is created, it throws an exception since the dispatcher-loop is already shut down.
  5. MainWindow is created. As above, it adds itself to Application.Current.Windows and sets itself as Application.Current.MainWindow.
    • However, the condition for shutting down the application has already been reached. But, so far, the application had no chance to do something.
  6. Now Run() is called. The dispatcher-loop starts again and now has a chance to shutdown the application. So it shuts down the application and closes any open windows.

So again, no bug.

So one way to solve this is to change to OnExplicitShutdown. Then in step 4, no reason for shutting down is reached. Better (as in more like a normal WPF application) would be to have a proper ApplicationDefinition. Remove the StartupUri from the App.xaml and instead handle the Startup event:

private void OnStartup(object sender, StartupEventArgs e)
{
    this.ShutdownMode = ShutdownMode.OnExplicitShutdown;
    new DialogWindow().ShowDialog();

    var mainWindow = new MainWindow();
    this.ShutdownMode = ShutdownMode.OnLastWindowClose; // or OnMainWindowClose
    mainWindow.Show();
}

Since we have OnExplicitShudown while closing the dialog window, there is no reason for the application to start shutting down at that point. Then, after creating the MainWindow, we again have a window as main window and as (one of the) application windows. So then we can switch to the shutdown mode we actually want and show the main window.

like image 179
Daniel Rose Avatar answered Nov 20 '22 00:11

Daniel Rose


It does seem buggy.

I usually don't put anything in Main(), I let the no-arg app.Run() get called and call everything I need in the OnStartup method, but this wouldn't change the behaviour you're seeing.

Whenever I need to show something before my main application window is ready to be displayed, such as gathering input or showing a splash screen, I do it on a second thread.

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    // show splash
    var thread = new Thread(() =>
    {
        Dispatcher.CurrentDispatcher.BeginInvoke
            ((Action)(() => new MySplashWindow().Show()));
        Dispatcher.Run();
    }
    thread.SetApartmentState(ApartmentState.STA);
    thread.IsBackground = true;
    thread.Start();

    // run configuration steps

    // instantiate and show main window
}

Obviously, if you're calling ShowDialog() on a second thread, you'll need to make sure you get an answer before showing the main window. It does have the advantage that you can run other bootstrapping tasks while waiting for user input on a particular part; it really depends on how sequential your various tasks are.

Maybe that helps in your case; maybe not -- just a thought.

like image 2
Jay Avatar answered Nov 19 '22 23:11

Jay


If you set the Application.ShutdownMode to OnExplicitShutdown can you avoid putting the ShutdownCallback on the dispatcher queue and keep doing whatever you want without regard to windows? I have not tested this but it seems like a potential solution as well as using the Application.MainWindow property which can be changed on the fly.

like image 1
Jon Comtois Avatar answered Nov 19 '22 22:11

Jon Comtois


Very informative post thanks to all that contributed. I used a placebo window set it to the main window but did not show it. I also set the shutdown mode to OnLastWindow After all the setup dialogs where opened and closed I replaced the placebo window with the real main window and called App.Run(). This is probably not a best practice but it works and it works fast.

Application app = new App();

MainWindow y = new MainWindow();
app.MainWindow = y;
y.WindowStartupLocation = WindowStartupLocation.CenterScreen;
app.ShutdownMode = ShutdownMode.OnLastWindowClose;
//do lots of setup work to include authentication
MainWindow x = new MainWindow(containerdata)
app.MainWindow = x;
App.Run()
like image 1
cnotpc Avatar answered Nov 20 '22 00:11

cnotpc