Подтвердить что ты не робот

Приложение WPF выходит сразу при показе диалога перед запуском

Обновить. Я думаю, мне нужно понять, что такое "правильный", "поддерживаемый" способ показать диалог перед запуском приложения в WPF.

Здесь код:

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

            new DialogWindow().ShowDialog();

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

DialogWindow отображается как ожидалось.
Но после закрытия приложения приложение немедленно завершает работу. MainWindow не отображается вообще!

Я выполнил некоторую отладку и проследил эту проблему:

  • Когда диалог создается, он становится app MainWindow, так как на данный момент MainWindow отсутствует.
  • Поэтому закрытие диалога заставляет приложение отправлять ShutdownCallback в очередь диспетчера.
  • Однако диспетчер не работает достаточно долго, чтобы выполнить обратный вызов.
  • Поэтому, как только app.Run вызывается впоследствии, первая вещь в очереди ShutdownCallback, что, естественно, заставляет приложение немедленно закрыть.

При таком анализе существует очевидное обходное решение: создайте MainWindow сразу после app, тем самым сделав его app MainWindow, что предотвратит закрытие приложения DialogWindow.

Однако, вот что меня беспокоит.

Во-первых, это похоже на грязный хак для меня. Я имею в виду, что нет явной причины для создания окон в этом порядке, и я нашел это только через некоторую отладку. Это не может быть поддерживаемый способ.

Во-вторых, это явно ошибка. Я имею в виду, что если создание второго окна после выключения не поддерживалось явно, оно должно было бросить несколько InvalidOperationException, правильно?

В-третьих, это не только ошибка, но она выглядит очень наивной, что-то вроде начинающего многопоточности.

Все это заставляет меня поверить, что, может быть, я не понимаю здесь ничего принципиального? Может быть, я не имею никакого смысла? Может быть, все это должно быть сделано по-разному?

Вот какой фон:
При запуске приложение должно выполнить загрузку. Проверьте это и все, установите обработчики исключений, выполните регистрацию - вы знаете, обычный материал. В этом процессе может возникнуть необходимость запросить у пользователя некоторую помощь - для чего это диалоговое окно.

Я абсолютно не хочу вкладывать все это в какой-то конечный автомат, который выполняется на MainWindow.IsVisibleChanged или что-то в этом роде. Я бы хотел, чтобы это было очень просто, коротко и прямолинейно - как должен выглядеть код начальной загрузки, так что он легко обнаруживает ошибки невооруженным глазом.

4b9b3361

Ответ 1

По умолчанию ShutdownMode приложения WPF является OnLastWindowClose. В вашем коде вы показываете одно окно, а затем закрываете его. Таким образом, последнее окно закрывается, и приложение отключается. Затем при выключении вы показываете другое окно. Поскольку приложение закрывается, окно немедленно закрывается.

Итак, все работает так, как запроектировано и запрограммировано вами.

Однако вы хотите сделать что-то другое: окно, которое вы показываете первым, поскольку единственным окном должно быть "специальное окно", а после его закрытия вы хотите продолжить выполнение, показать свое "главное окно", а затем выйти приложение, когда оно (или все окна, связанные с приложением) закрывается.

Самый простой способ: сначала установите режим выключения на OnExplicitShutdown, затем после показа основного окна установите его в OnLastWindowClose или OnMainWindowClose. В коде:

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: Я не уверен, что именно вы делаете. Код, который вы указали, не будет компилироваться, так как при правильном использовании класса приложений WPF (с созданием App.xaml как ApplicationDefinition) уже определен метод Main. Если у вас просто есть класс, полученный из приложения, у вас нет метода InitializeComponent(). Единственный способ получить код для компиляции - это вручную изменить действие сборки на страницу. Однако в этом случае Application.Current == app.

Итак, что происходит, это следующее:

  • Запускается приложение. Поскольку WPF-приложение пока не создано, Application.Current имеет значение NULL. Это также означает, что ни один диспетчерский цикл не запущен, а сообщения диспетчера не обрабатываются (обратите внимание, что цикл диспетчера также обрабатывает сообщения Windows).
  • Создается новый объект App. Так как Application.Current имеет значение null, он устанавливает себя как Application.Current.
    • Application.Current.MainWindow имеет значение null и Application.Current.Windows - пустой список.
    • Так как ShutdownMode - OnLastWindowClose, после закрытия последнего окна текущего приложения (например, приложения) завершается выключение.
  • Диалоговое окно отображается модально. Так как никакой диспетчер-цикл не запущен, сам ShowDialog() запускает "локальный" диспетчер-цикл.
    • На самом деле это две части: сначала создается окно. Он принадлежит текущему приложению, поэтому он добавляет себя в Application.Current.Windows. Поскольку это первое показанное окно и Application.Current.MainWindow имеет значение null, оно также устанавливает себя как основное окно. Во-вторых, окно показано модально.
    • Так как Application.Current.Windows теперь не является пустым, как только он пуст, завершение работы будет завершено.
  • Пользователь закрывает диалоговое окно. Как часть закрытия, окно удаляется из Application.Current.Windows. Кроме того, поскольку это MainWindow, это значение равно null. Так как Application.Current.Windows теперь пуст, запускается shutdown. Однако, поскольку нет цикла диспетчерского цикла, ничего еще не сделано (установлен только внутренний флаг или аналогичный параметр).
    • Если вы использовали app.Run(new DialogWindow()); app.Run(new MainWindow());, у вас возникло бы исключение при создании MainWindow, так как в этом случае цикл диспетчера работает правильно. Таким образом, он может фактически отключиться, поэтому, когда MainWindow создан, он генерирует исключение, поскольку цикл диспетчера уже отключен.
  • Создается MainWindow. Как и выше, он добавляет себя в Application.Current.Windows и устанавливает себя как Application.Current.MainWindow.
    • Однако условие завершения приложения уже достигнуто. Но до сих пор приложение не имело шансов что-то сделать.
  • Вызывается Run(). Контур диспетчера снова запускается и теперь имеет возможность отключить приложение. Поэтому он закрывает приложение и закрывает все открытые окна.

Итак, нет ошибок.

Таким образом, один из способов решения этой проблемы - изменить OnExplicitShutdown. Затем на шаге 4 нет причин для отключения. Лучше (как в обычном приложении WPF) было бы иметь надлежащее ApplicationDefinition. Удалите StartupUri из App.xaml и вместо этого обработайте событие Startup:

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();
}

Так как у нас OnExplicitShudown при закрытии диалогового окна, нет никаких оснований для того, чтобы приложение запустилось в этот момент. Затем, после создания MainWindow, мы снова имеем окно как главное окно и как (одно из) окон приложения. Итак, мы можем переключиться в режим выключения, который мы действительно хотим, и показать главное окно.

Ответ 2

Это похоже на ошибку.

Я обычно ничего не помещаю в Main(), я позволю no-arg app.Run() получить вызов и вызвать все, что мне нужно в методе OnStartup, но это не изменит поведение, которое вы видите.

Всякий раз, когда мне нужно что-то показывать, прежде чем мое главное окно приложения будет готово к отображению, например, сбор ввода или отображение заставки, я делаю это во втором потоке.

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
}

Очевидно, что если вы вызываете ShowDialog() во второй поток, вам нужно убедиться, что вы получите ответ, прежде чем показывать главное окно. Преимущество состоит в том, что вы можете запускать другие задачи начальной загрузки, ожидая ввода пользователем определенной части; это действительно зависит от того, насколько последовательны ваши различные задачи.

Может быть, это поможет в вашем случае; может быть, не просто мысль.

Ответ 3

Очень информативный пост, благодаря всем, что было сделано. Я использовал окно плацебо, которое установило его в главное окно, но не показывало его. Я также установил режим выключения на OnLastWindow. После того, как все диалоговые окна установки были открыты и закрыты, я заменил окно плацебо реальным основным окном и назвал App.Run(). Это, вероятно, не лучшая практика, но она работает, и она работает быстро.

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()

Ответ 4

Если вы установите Application.ShutdownMode на OnExplicitShutdown, вы можете не помещать ShutdownCallback в очередь диспетчера и продолжать делать все, что хотите независимо от окон? Я не тестировал это, но это похоже на потенциальное решение, а также с помощью свойства Application.MainWindow, которое можно изменить "на лету".