Состояние приложения в WinRT

180

Если приложение - такое, как VisitedPageSave - завершается и перезапускается, скорее всего, вы захотите, чтобы все выглядело так, словно приложение не завершалось: ранее созданные страницы восстанавливаются с предыдущим содержимым, а в приложении отображается последняя страница, открытая пользователем.

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

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

Что это за строка? Посмотрите, если интересно. Вы увидите, что она содержит имена классов страниц из стека возврата, а также какие-то числа. Смысл этих чисел не документирован и может измениться в будущем, так что строку следует использовать только для передачи информации от GetNavigationState к SetNavigationState.

GetNavigationState в действительности не ограничивается простым возвращением строки, в которой закодировано состояние стека возврата. Вызов этого метода приводит к тому, что текущая страница получает вызов OnNavigatedFrom со значением NavigationMode, равным Forward. Это позволяет текущей странице сохранить свое состояние, но также означает, что вы уже не можете просто вызвать GetNavigationState в любой момент по своему усмотрению. Метод должен вызываться только при приостановке приложения. Уместнее всего делать это в обработчике события OnSuspending в файле App.xaml.cs. Вот как это делается в программе ApplicationStateSave:

private void OnSuspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();
    //TODO: Сохранить состояние приложения и остановить все фоновые операции

    // Добавленный код
    ApplicationDataContainer appData = ApplicationData.Current.LocalSettings;
    appData.Values["NavigationState"] = 
        (Window.Current.Content as Frame).GetNavigationState();
    // Конец добавленного кода

    deferral.Complete();
}

Я оставил версию OnSuspending, сгенерированную Visual Studio, без изменений и только добавил две строки кода, заключенные в комментарии. Код получает строку GetNavigationState от Frame и сохраняет ее в конфигурации приложения с именем «NavigationState».

Некоторые программы, приводившиеся ранее, сохраняют конфигурацию приложения из MainPage. Почему бы не сделать то же самое здесь? Вспомните, что классы, производные от Page, в многостраничной конфигурации должны назначать необходимые обработчики событий в OnNavigatedTo и Loaded и отменять их назначение в OnNavigatedFrom или Unloaded. Таким образом, каждый класс вашего приложения, производный от Page, должен назначать обработчик Suspending для решения этой задачи. Но на самом деле эта задача не для классов, производных от Page. Операция подразумевает сохранение состояния навигации, определяющего навигационные связи между несколькими страницами, поэтому отвечать за нее должно само приложение.

Это одна из причин для выделения этого кода в класс App. Другая причина состоит в том, что восстановление состояния навигации также должно вынолняться в классе App и даже в конкретном месте класса App, потому что фактически речь идет о переопределении логики по умолчанию.

Чтобы восстановить стек возврата, следует вызвать метод SetNavigationState с сохраненной строкой, полученной при вызове GetNavigationState. Вызов SetNavigationState осуществляет переход к странице, которая ранее была текущей. Метод OnNavigatedTo этой страницы вызывается с режимом NavigationMode, равным Back; это позволяет странице перезагрузить свою конфигурацию, не считая, что это происходит при создании новой страницы.

Очень важно, чтобы метод SetNavigationState вызывался в конкретной точке метода OnLaunched в файле App.xaml.cs. В следующем примере из проекта ApplicationStateSave весь сгенерированный код и комментарии в OnLaunched оставлены без изменений:

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
    Frame rootFrame = Window.Current.Content as Frame;

    // Не повторять инициализацию приложения, если содержимое уже есть.
    // Просто убедиться в том, что оно активно.
    if (rootFrame == null)
    {
        // Создание объекта Frame, который используется как контекст
        // навигации, и переход к первой странице
        rootFrame = new Frame();

        if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
        {
            //TODO: Загрузка состояния ранее приостановленного приложения

            // Добавленный код
            ApplicationDataContainer appData = ApplicationData.Current.LocalSettings;

            if (appData.Values.ContainsKey("NavigationState"))
                rootFrame.SetNavigationState(appData.Values["NavigationState"] as string);
            // Конец добавленного кода
        }

        // Назначить объект Frame содержимым окна
        Window.Current.Content = rootFrame;
    }
    
    // ...
}

Я снова использовал комментарии для пометки кода, добавленного в проект. (Обратите вниманне на многоточие в конце - дополнительный код, добавленный в App.xaml.cs, будет рассмотрен в следующем позже.)

В конце метода OnLaunched вызывается метод Navigate с классом MainPage. Этот вызов не должен происходить при восстановлении состояния стека возврата, потому что вызов Navigate приведет к уходу с текущей страницы и, возможно, удалению части стека возврата в методе OnNavigatedTo класа MainPage. По этой причине стек возврата должен быть восстановлен до этого вызова; тем самым гарантируется, что свойству Content объекта Frame будет задана прежняя текущая страница, а переход к MainPage будет пропущен.

Код, приведенный выше, сохраняет и восстанавливает стек возврата. Вторая часть операции требует сохранения и восстановления всех состояний страниц. В предыдущем проекте я определил класс с именем SaveStatePage, который поддерживал два словаря (экземплярный и статический) для сохранения состояния страницы. Как MainPage, так и SecondPage являются производными от этого класса.

Я сохранил эту архитектуру в текущей программе; более того, классы MainPage и SecondPage идентичны классам из предыдущего проекта. Но класс SaveStatePage был усовершенствован для сохранения всех настроек всех страниц в локальном хранилище приложения и их восстановления.

Если конкретный стек возврата содержит ссылки на четыре экземпляра MainPage и три экземпляра SecondPage, появляются семь строк с ключом «TextBoxText». Их необходимо как-то отличать друг от друга. К счастью, контейнер ApplicationDataContainer, используемый для хранения данных конфигурации приложения, поддерживает функциональность, сходную с иерархией папок или подкаталогов. Она идеально подходит для изоляции настроек отдельных страниц. Контейнер идентифицируется по имени; имя, которое я выбрал для каждого экземпляра Page, идентифицирует местонахождение этого экземпляра в стеке возврата, то есть является эквивалентом целочисленного ключа словаря pages, преобразованного в строку.

Ниже приводится статический конструктор и обработчик Suspending в усовершенствованной версии SaveStatePage. Обработчик события Suspending назначается в статическом конструкторе, чтобы он выполнялся только один раз для сохранения настроек всех страниц и конечно, при этом он не располагает никакой информацией об этих настройках:

namespace WinRTTestApp
{
    public class SaveStatePage : Page
    {
        static protected Dictionary<int, Dictionary<string, object>> pages =
                                new Dictionary<int, Dictionary<string, object>>();
                                
        protected Dictionary<string, object> pageState;

        static SaveStatePage()
        {
            // Назначение обработчика для событий Suspending
            Application.Current.Suspending += OnApplicationSuspending;

            ApplicationDataContainer appData = ApplicationData.Current.LocalSettings;

            // Перебор контейнеров (по одному для каждой страницы в стеке возврата)
            foreach (ApplicationDataContainer container in appData.Containers.Values)
            {
                // Создание словаря состояния для страницы
                Dictionary<string, object> pageState = new Dictionary<string, object>();

                // Заполнение словаря сохраненными значениями
                foreach (string key in container.Values.Keys)
                {
                    pageState.Add(key, container.Values[key]);
                }

                // Сохранение в статическом словаре
                int pageKey = Int32.Parse(container.Name);
                pages[pageKey] = pageState;
            }
        }

        static void OnApplicationSuspending(object sender, SuspendingEventArgs args)
        {
            ApplicationDataContainer appData = ApplicationData.Current.LocalSettings;

            foreach (int pageKey in pages.Keys)
            {
                // Создание контейнера по позиции в состоянии возврата
                string containerName = pageKey.ToString();

                // Получение контейнера с заданным именем и его очистка
                ApplicationDataContainer container = 
                    appData.CreateContainer(containerName, 
                                            ApplicationDataCreateDisposition.Always);
                container.Values.Clear();

                // Сохранение настроек для каждой страницы в этом контейнере
                foreach (string key in pages[pageKey].Keys)
                    container.Values.Add(key, pages[pageKey][key]);
            }
        }

        protected override void OnNavigatedTo(NavigationEventArgs args)
        {
            // ...
        }
    }
}

К моменту завершения статического конструктора в словаре pages содержится один элемент для каждой страницы в стеке возврата. Ни один экземпляр конкретной страницы при этом еще не создан. Однако при создании экземпляра каждого класса, производного от SaveStatePage, он получает собственный словарь pageState в переопределении OnNavigatedTo - либо читает его из словаря pages, либо создает заново.

Пройди тесты
Лучший чат для C# программистов