Хронология страниц

86

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

Вам наверняка интересно узнать, как в действительности работают свойства вроде Application.StartupUri, Frame.Source и Hyperlink.NavigateUri. В приложении, которое состоит из несвязанных XAML-файлов и выполняется в браузере, этот процесс выглядит довольно просто: при щелчке на гиперссылке браузер интерпретирует ссылку на страницу как относительный URI-адрес и ищет указанную XAML-страницу в текущей папке. Но в скомпилированном приложении страницы перестают быть доступными в виде отдельных ресурсов: они компилируются в BAML (Binary Application Markup Language — двоичный язык разметки приложений) и вставляются в сборку. Так как же на них ссылаться с помощью URI?

Эта система работает благодаря способу, которым WPF обращается к ресурсам приложения. При выполнении щелчка на гиперссылке в скомпилированном XAML-приложении URI все равно интерпретируется как относительный путь. Однако он является относительным по отношению к базовому URI приложения. Поэтому гиперссылка, указывающая на Page1.xaml, фактически преобразуется в следующий упакованный URI:

NavigateUri="pack://application:,,,/Page3.xaml"

Может возникнуть вопрос: почему так важно знать, как работают URI-адреса гиперссылок, если весь процесс проходит столь гладко? Главная причина состоит в том, что может потребоваться создать приложение, позволяющее переходить на XAML-страницы, которые хранятся в другой сборке. На самом деле для принятия такого проектного решения имеются веские основания. Поскольку страницы могут применяться в разных контейнерах, может возникнуть желание повторно использовать один и тот же набор страниц как в приложении ХВАР, так и в обычном приложении Windows.

В таком случае можно развернуть просто две версии приложения — браузерную и настольную. Чтобы избежать дублирования кода, все страницы, которые планируется использовать повторно, следует поместить в отдельную сборку библиотеки классов (DLL), на которую затем можно сослаться в обоих проектах приложений.

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

pack://application:,,,/PageLibrary;component/Page1.xaml

Здесь компонент имеет имя PageLibrary, а путь ,,,PageLibrary;component/Page1.xaml указывает на скомпилированную и вставленную внутри него страницу Page1.xaml.

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

/PageLibrary;component/Page1.xaml

При создании сборки SharedLibrary для получения правильных ссылок на сборки, импортированных пространств имен и настроек приложения лучше использовать шаблон проекта Custom Control Library (WPF) (Библиотека специальных элементов управления (WPF)).

Хронология навигации

Хронология страниц в WPF работает точно так же, как и в браузере. При каждом переходе на новую страницу текущая страница добавляется в список предыдущих страниц. При щелчке на кнопке возврата страница добавляется в список следующих страниц. В случае возврата на одну страницу и перехода с нее на новую страницу список следующих страниц очищается.

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

Между возвращением на страницу через хронологию навигации и выполнением щелчка на ссылке, которая направляет на эту же самую страницу, существует большая разница. Например, при переходе со страницы Page1 на страницу Раде2 и затем снова на страницу Page1 с помощью соответствующих ссылок WPF создаст три отдельных объекта страницы. При втором отображении страница Page1 создастся как отдельный экземпляр с собственным состоянием. Однако в случае возврата к первому экземпляру Page1 за счет двукратного щелчка на кнопке возврата она будет видна в исходном состоянии.

Может показаться, что WPF поддерживает состояние ранее посещенных страниц за счет удержания объекта страницы в памяти. Проблема такого подхода состоит в том, что связанные с памятью накладные расходы в сложном приложении с множеством страниц в таком случае могут быть слишком большими. По этой причине удержание объекта страницы не считается безопасной стратегией и вместо него при покидании страницы сохраняется информация о состоянии всех элементов управления, а объект страницы уничтожается. При возврате на эту страницу WPF создает ее заново (из исходного XAML-файла) и восстанавливает состояние ее элементов управления.

Такая стратегия сопровождается меньшим количеством накладных расходов, поскольку для сохранения деталей о состоянии элементов управления требуется гораздо меньше памяти, чем для сохранения всей страницы вместе с визуальным деревом объектов.

Глядя на эту систему, возникает интересный вопрос: как WPF решает, какие детали нужно сохранять? WPF анализирует все дерево элементов страницы и просматривает имеющиеся у этих элементов свойства зависимости. Свойства, которые должны быть сохранены, имеют небольшой фрагмент дополнительных метаданных — журнальный флаг, который указывает, что они должны помещаться в журнал навигации. Этот флаг устанавливается с помощью объекта FrameworkPropertyMetadata при регистрации свойства зависимости.

Присмотревшись к системе навигации поближе, можно будет заметить, что у многих свойств нет журнального флага. Например, если установить свойство Content элемента управления содержимым или свойство Text элемента TextBlock с помощью кода, ни одна из этих деталей не будет восстановлена при возврате на страницу. То же самое будет и при динамической установке свойства Foreground или Background. Однако если установить свойство Text элемента TextBox, свойство IsSelected элемента CheckBox или свойство SelectedIndex элемента ListBox, то все эти детали сохранятся.

Так что же можно предпринять, если такое поведение не подходит? Как быть в случае установки множества свойств динамическим образом и желании, чтобы вся эта информация сохранялась на страницах? Существует несколько возможных вариантов.

Самый мощный предполагает применение свойства Page.KeepAlive, которое по умолчанию имеет значение false. Когда это свойство устанавливается в true, WPF не применяет описанный выше механизм сериализации. Вместо этого WPF оставляет объекты всех страниц в действующем состоянии. Благодаря этому, при возврате обратно страница оказывается в том же виде, в котором была ранее. Разумеется, у этого варианта есть один недостаток, заключающийся в увеличении накладных расходов, связанных с памятью, поэтому его следует применять только для нескольких страниц, которые действительно в нем нуждаются.

В случае использования свойства KeepAlive для сохранения страницы в действующем состоянии, при следующем возврате к ней событие Initialized генерироваться не будет. (Для страниц, которые не сохраняются в действующем состоянии, а "возвращаются к жизни" с помощью системы журнализации WPF, такое событие будет инициироваться при каждом их посещении пользователем.) Если такое поведение не подходит, тогда следует обработать события Unloaded и Loaded, которые генерируются всегда.

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

С хронологией навигации WPF связан еще один недостаток. Можно написать код, который будет динамически создавать объект страницы и затем переходить на нее. В такой ситуации обычный механизм сохранения состояния страницы работать не будет. У WPF нет ссылки на XAML-документ страницы, поэтому ей не известно, как реконструировать страницу. (А если страница создается динамически, у нее может вообще не быть соответствующего XAML-документа.) В такой ситуации WPF всегда будет сохранять объект страницы в памяти, каким бы ни было значение свойства KeepAlive.

Добавление специальных свойств

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

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

Чтобы определить свойство зависимости, необходимо создать статическое поле, такое как показанное ниже:

private static DependencyProperty MyPageDataProperty;

По соглашению поле, определяющее свойство зависимости, должно обязательно иметь то же имя, что и обычное свойство, но со словом Property в конце.

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

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

static PageWithPersistentData()
{
   FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata();
   metadata.Journal = true;
   
   MyPageDataProperty = DependencyProperty.Register(
      "MyPageDataProperty", typeof(string),
      typeof(PageWithPersistentData), metadata, null);
}

Затем можно создавать обычное свойство, упаковывающее данное свойство зависимости. Однако при написании процедур извлечения и установки следует использовать методы GetValue() и SetValue(), определенные в базовом классе DependencyObject:

private string MyPageData
{
   set { SetValue(MyPageDataProperty, value); }
   get { return (string)GetValue(MyPageDataProperty); }
}

Осталось только добавить все эти детали в одну страницу (в данном примере это PageWithPersistentData). После этого значение свойства MyPageData будет автоматически сериализироваться, когда пользователь покидает страницу, и восстанавливаться при его возвращении.

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