Элементы Frame и Page
191Silverlight 5 --- Элементы Frame и Page
Изменение пользовательского интерфейса вручную приемлемо, если приложение содержит не более нескольких страниц (например, в анимированной игре, состоящей из главного и конфигурационного окон).
Кроме того, оно имеет смысл, когда необходим полный контроль над процессами навигации (например, для реализации эффектов перехода). Однако в более традиционных приложениях, в которых пользователь должен переключать большое количество страниц, лучше применить специальные средства навигации, встроенные в Silverlight и значительно сокращающие время разработки.
Встроенные системы навигации основаны на двух элементах управления: Frame и Page. Из них более важен элемент Frame, потому что на его основе создается контейнер, содержащий средства навигации. Элемент Page — необязательное дополнение, предоставляющее удобный способ отображения разных частей содержимого в окне фрейма. Оба класса предоставляют свойства и методы, позволяющие управлять навигацией в коде.
Frame
Frame — это элемент управления, производный от класса ContentControl и содержащий единственный дочерний элемент, предоставляемый посредством свойства Content.
Класс ContentControl наследуют многие классы, например Button, ListBoxItem, ToolTip, ScrollViewer и т.д., однако элемент Frame особенный: при правильном использовании вам почти никогда не придется обращаться непосредственно к свойству Content. Для изменения содержимого фрейма предназначен высокоуровневый метод Navigate(). Он изменяет свойство Content и активизирует службы навигации, которые отслеживают историю пользовательских страниц и обновляют URI браузера.
В приведенной ниже разметке определен контейнер Grid, состоящий из двух строк. В верхней строке находится элемент Border, содержащий элемент Frame. Класс Frame имеет свойства BorderBrush и BorderThickness, однако свойств CornerRadius у него нет, поэтому для скругления рамки необходимо применять элемент Border. В нижней строке решетки Grid находится кнопка, запускающая процесс навигации:
<UserControl x:Class="SilverlightTest.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation">
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Border Margin="10" Padding="10" BorderBrush="DarkOrange" BorderThickness="2"
CornerRadius="4">
<navigation:Frame x:Name="mainFrame">
</navigation:Frame>
</Border>
<Button Grid.Row="1" Margin="5" Padding="5" Content="Переход на другую страницу"
HorizontalAlignment="Center" Click="cmdNavigate_Click" />
</Grid>
</UserControl>
Чтобы можно было применить класс Frame, нужно отобразить пространство имен System.Windows.Controls, определенное в сборке System.Windows.Controls.Navigation.dll, на префикс пространства имен XML. В данном примере используется префикс navigation.
Сейчас фрейм пустой. Однако после щелчка на кнопке обработчик щелчка вызывает метод Navigate() и передает ему объект Uri, указывающий на скомпилированный файл XAML:
private void cmdNavigate_Click(object sender, RoutedEventArgs e)
{
mainFrame.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
}
Конечно, нужно, чтобы существовал пользовательский элемент управления или страница Page1.xaml. Обратите внимание на дробную черту перед URI. Она обозначает корневую папку приложения.
В методе Navigate() нельзя применять URI, указывающие на содержимое других типов или на внешние страницы, определенные в других приложениях (например, размещенные на внешнем веб-сайте).
Ниже приведена разметка страницы Page1.xaml:
<Grid x:Name="LayoutRoot">
<TextBlock Text="Это содержание страницы Page1" FontSize="16"/>
</Grid>
При вызове метода Navigate() надстройка Silverlight создает экземпляр класса Page1 и применяет его для заполнения фрейма:
Если бы рассматриваемая система навигации создавалась вручную, вызов метода Navigate() можно было бы заменить следующим кодом:
private void cmdNavigate_Click(object sender, RoutedEventArgs e)
{
//mainFrame.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
// Создание страницы
Page1 page = new Page1();
// Вывод пользовательского элемента управления
mainFrame.Content = page;
}
Однако этот код всего лишь заменяет содержимое, а высокоуровневый метод Navigate() выполняет множество других полезных операций. В приведенном выше примере после щелчка на кнопке изменилась адресная строка URI браузера и стали активными кнопки перехода, что указывает на интеграцию навигационной системы в браузер.
Извлечь URI текущей страницы можно в любой момент с помощью свойства Frame.Source. Кроме того, установку свойства Source можно использовать вместо вызова метода Navigate().
Интеграция с адресной строкой браузера
При изменении содержимого элемента Frame с помощью метода Navigate() имя ресурса XAML добавляется в текущий URI после маркера фрагмента #. Следовательно, если приложение находится по адресу localhost://Navigation/TestPage.html и код выполняет операцию:
mainFrame.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
то в адресной строке браузера будет отображено следующее:
localhost://Navigation/TestPage.html#/Page1.xaml
Это имеет много последствий, одни из них благоприятные, другие — не очень. Существенно то, что при использовании навигационной системы Silverlight на основе фреймов каждая страница, загружаемая во фрейм, имеет отдельный URI, отмечается отдельным элементом в списке истории браузера и может служить новой точкой входа в приложение.
Например, если закрыть браузер, а потом снова открыть его, можно будет ввести навигационный адрес URI с маркером #/Page1.xaml в конце запроса страницы TestPage.html, загрузить приложение Silverlight и вставить содержимое страницы Page1.xaml во фрейм. Кроме того, пользователь может создать закладку с навигационным URI, позволяющую вернуться к странице, загруженной во фрейм. Полученный таким образом навигационный URI называется глубокой ссылкой, потому что позволяет обратиться не только к входной точке приложения, но и к другой точке в приложении.
Несомненно, интеграция URI с браузером — удобное средство, однако при ее использовании возникает ряд вопросов, которые рассматриваются ниже:
- Что произойдет, если страница имеет более одного фрейма?
Фрагмент URI указывает на страницу, которая должна появиться во фрейме, но в нем нет имени фрейма. Следовательно, система полностью работоспособна только в приложении с одним фреймом (приложения с двумя и более фреймами используются довольно редко).
Если в приложении более одного фрейма, все они будут иметь один и тот же навигационный маршрут. В результате этого, когда код вызывает метод Navigate() или пользователь вводит URI с именем страницы, одно и то же содержимое будет загружено в каждый фрейм.
Чтобы избежать этой проблемы, нужно выбрать один фрейм, представляющий главное содержимое приложения. Этот фрейм будет управлять адресами URI и списком истории браузера. Остальные фреймы будут неявно отслеживать навигацию без взаимодействия с браузером. Для реализации этого сценария присвойте свойству JournalOwnership каждого дополнительного фрейма значение OwnJournal. Тогда перейти к этим фреймам можно будет только путем вызова метода Navigate() в коде.
- Что произойдет, если начальная страница не содержит элемент Frame?
Страницы с многими фреймами — не единственная потенциальная проблема с навигационными системами, в которых используются адреса URI. Еще одна проблема возникает, когда приложение не может загрузить запрошенное содержимое, потому что в корневом визуальном элементе приложения нет фрейма. Это может произойти, например, при использовании кода для создания объекта Frame или замены страницы, содержащей фрейм. Приложение запускается нормально, однако, поскольку фрейм недоступен, маркер URI игнорируется.
Существуют два способа решения этой проблемы. Можно упростить приложение, сделав фрейм доступным в корневом визуальном элементе в момент запуска приложения. Кроме того, можно добавить код, реагирующий на событие ApplicationStartup и проверяющий маркер URI с помощью следующего кода:
string fragment = System.Windows.Browser.HtmlPage .Document.DocumentUri.Fragment;
Если обнаружится, что URI содержит маркер, код должен вернуть приложение в предыдущее состояние.
Поддержка истории браузера
Элемент Frame интегрирует средства навигации в браузер. При каждом вызове метода Navigate() надстройка Silverlight добавляет запись в список истории:
Сначала в списке истории появляется первая страница приложения с именем или заголовком входной страницы HTML. Каждая следующая страница выводится под ней (или над ней при переходе назад) с именем файла пользовательского элемента управления (например, Page2.xaml) или заголовком страницы.
Список истории браузера полностью соответствует ожиданиям пользователя. Он может щелкать на кнопках Forward (Вперед) и Back (Назад) и выбирать элемент списка истории для загрузки любой предыдущей страницы во фрейм. Существенное преимущество системы навигации состоит в том, что приложение при этом не запускается повторно. Пока остальные адреса URI остаются прежними (за исключением маркера), Silverlight всего лишь загружает соответствующую страницу во фрейм. Если же пользователь перейдет к другому веб-сайту, а затем щелкнет на кнопке Back, приложение Silverlight будет запущено повторно, возникнет событие Application.Startup и только после этого Silverlight попытается загрузить запрошенную страницу во фрейм.
Метод Frame.Navigate() можно вызвать несколько раз для разных страниц. В результате будет выведена последняя страница, а другие страницы будут добавлены в список истории. Если страница уже загружена, метод Navigate() не делает ничего, даже не добавляет элемент в список истории браузера.
Неуспешная навигация
При использовании кнопки Back (Назад) в Silverlight необходимо учитывать следующее. Если применить список истории браузера для возврата к странице, на которой нет маркера URI, система сгенерирует исключение. Эта проблема может возникнуть в предыдущем примере, если запустить приложение, перейти к новой странице и щелкнуть на кнопке Back, чтобы вернуться к исходной странице. Будет сгенерировано исключение ArgumentException, сообщающее о том, что содержимое URI не может быть загружено. Иными словами, URI не определяет содержимое для фрейма, и Silverlight не знает, что поместить в него.
Существуют два простых способа решения этой проблемы. Первый состоит в обработке события Frame.NavigationFailed. Закодируйте в обработчике проверку объекта исключения, предоставленного в свойстве NavigationFailedEventArgs.Exception, и присвойте свойству NavigationFailedEventArgs.Handled значение true, чтобы браузер проигнорировал исключение.
Второй способ состоит в использовании объекта UriMapper для установки начального содержимого фрейма. Свяжите пустой URI с действительной страницей, которая будет загружена во фрейм (впрочем, страница тоже может быть пустой). Более подробно этот способ рассматривается в следующем разделе.
Привязка адресов URI
Навигационная система применяет имя страницы в качестве маркера URI. В некоторых ситуациях связывать имя страницы нежелательно, например, чтобы не сбивать с толку пользователя непонятным расширением .xaml или если нужно применить маркер, который легче запомнить и ввести вручную. Средства привязки URI можно использовать для определения другого, более простого текста.
Чтобы применить средства привязки URI, нужно добавить в приложение объект UriMapper как ресурс XAML. Обычно объект UriMapper определяют в коллекции ресурсов главной страницы приложения или в файле App.xaml:
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SilverlightTest.App"
xmlns:navigation="clr-namespace:System.Windows.Navigation;assembly=System.Windows.Controls.Navigation">
<Application.Resources>
<navigation:UriMapper x:Key="PageMapper">
<navigation:UriMapping Uri="Home" MappedUri="/MainPage.xaml" />
</navigation:UriMapper>
</Application.Resources>
</Application>
Упрощенный URI можно использовать при вызове метода Navigate():
mainFrame.Navigate(new Uri("Home", UriKind.Relative));
Обратите внимание на то, что в начало связанного URI не нужно добавлять косую черту. После привязки работоспособны оба URI: связанный и реальный, причем оба указывают на одну и ту же страницу. Если в инструкции вызова метода Navigate(), в гиперссылке или в закладке применить реальный URI, пользователь увидит его в адресной строке браузера.
Объект UriMapper можно использовать также для установки начального содержимого фрейма. Для этого нужно связать Uri с пустой строкой:
<navigation:UriMapper x:Key="PageMapper">
<navigation:UriMapping Uri="" MappedUri="/StartUpPage.xaml" />
<navigation:UriMapping Uri="Home" MappedUri="/MainPage.xaml" />
</navigation:UriMapper>
Тогда при первом появлении страницы во фрейме будет выведено содержимое файла StartUpPage.xaml.
Если для установки начальной страницы объект UriMapper не используется, необходимо запрограммировать обработку события Frame.NavigationFailed. В противном случае пользователи будут получать сообщение об ошибке при попытке вернуться к первой странице с помощью кнопки Back (Назад).
Гиперссылки
В предыдущих примерах навигация выполнялась с помощью обычных кнопок. Однако в Silverlight для этого можно также использовать набор элементов HyperlinkButton. Благодаря системе URI применить гиперссылки для навигации даже легче чем кнопки. Нужно всего лишь присвоить свойству NavigateUri соответствующий адрес URI. Адрес может указывать непосредственно на страницу или связываться с ней посредством объекта UriMapper.
Ниже приведена разметка панели StackPanel, содержащей три навигационные гиперссылки:
<StackPanel Grid.Row="1" Margin="5" HorizontalAlignment="Center" Orientation="Horizontal">
<HyperlinkButton NavigateUri="/Page1.xaml" Content="Page 1" Margin="3"></HyperlinkButton>
<HyperlinkButton NavigateUri="/Page2.xaml" Content="Page 2" Margin="3"></HyperlinkButton>
<HyperlinkButton NavigateUri="Home" Content="Home" Margin="3"></HyperlinkButton>
</StackPanel>
В гиперссылках используется тот же принцип навигации, что и при использовании обычных кнопок. Преимущество гиперссылок состоит в том, что они позволяют задавать адреса URI в разметке XAML. Благодаря этому код остается простым и не засоряется дополнительными деталями:
Page
Довольно часто вместо пользовательских элементов управления загружаются классы, производные от Page, потому что он предоставляет удобные средства взаимодействия с навигационной системой и автоматически управляет состояниями навигации.
Для того чтобы добавить страницу Page в проект Visual Studio, щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer и выберите в контекстном меню команду Add --> New Item. Выделите шаблон Silverlight Page (Страница Silverlight), введите имя страницы и щелкните на кнопке Add. Разметка страницы, производной от Page, почти не отличается от разметки пользовательского элемента управления. Ниже приведена разметка страницы Page1.xaml на основе класса Page:
<navigation:Page x:Class="SilverlightTest.Page1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
Title="Страница Page1">
<Grid x:Name="LayoutRoot">
<TextBlock Text="Это содержание страницы Page1" FontSize="16"/>
</Grid>
</navigation:Page>
Обычно страницы, производные от Page, размещают в отдельной папке проекта, чтобы не смешивать их со страницами на основе пользовательских элементов управления. Например, можно разместить все страницы Page в папке Pages и применить навигационные URI вида /Pages/Page1.xaml.
Класс Page наследует класс UserControl, добавляя небольшой набор свойств и методов. Добавлены методы, переопределяя которые можно задать реакцию на операции, связанные с навигацией. Кроме того, добавлены четыре свойства: Title, NavigationService, NavigationContext и NavigationCacheMode. Свойство Title содержит текст, выводимый в списке истории браузера. Другие свойства и методы описаны далее.
Свойства навигации
Каждая страница Page имеет свойство NavigationService, предоставляющее точку входа в систему навигации. Объект NavigationService содержит те же методы и свойства, что и класс Frame, включая Navigate(), GoBack(), GoForward (), CanGoBack, CanGoForward и CurrentSource. Это означает, что запустить навигацию можно изнутри страницы, добавив следующий код:
this.NavigationService.Navigate(
new Uri("/Page2.xaml", UriKind.Relative));
Класс Page предоставляет также свойство NavigationContext, содержащее объект NavigationContext. Этот объект имеет два свойства: Uri, предназначенное для извлечения текущего URI страницы, и QueryString, из которого можно извлечь коллекцию аргументов строки запроса, расположенных в конце строки URI. С помощью этих свойств код, запускающий навигацию, может передать информацию результирующей странице.
Сохранение состояний
Обычно, когда пользователь перемещается по страницам с помощью кнопок Forward (Вперед) и Backward (Назад) или списка истории, страница создается заново с нуля. Когда пользователь покидает страницу, объект страницы уничтожается. Следовательно, если на странице есть элемент ввода данных (например, текстовое поле), при повторных посещениях этой же страницы выводится значение, установленное по умолчанию, а не введенное пользователем во время предыдущего посещения. Аналогично все переменные объекта страницы получают исходные значения.
В системе навигации, созданной вручную (т.е. без помощи объектов Frame и Page), указанную проблему можно устранить, кэшируя объект страницы в памяти. В системе навигации на основе объектов Frame и Page можно применить аналогичный метод сохранения с помощью свойства Page.NavigationCacheMode.
По умолчанию оно имеет значение Disabled, запрещающее кэширование. При значении Required объект Frame обязательно сохраняет объект страницы в памяти после перехода к другой странице. Когда пользователь возвращается к сохраненной странице, используется кэшированный экземпляр объекта страницы. Конструктор страницы не активизируется, однако событие Loaded генерируется.
Если свойству NavigationCacheMode присвоить значение Enabled, страницы будут кэшироваться, пока их количество не превысит значение свойства Frame.CacheSize. Например, если оно равно 10 (это значение установлено по умолчанию), объект Frame сохранит 10 последних страниц, у которых свойство NavigationCacheMode равно Enabled. При добавлении в кэш одиннадцатой страницы первая (самая старая) будет удалена из кэша. Страницы, у которых свойство Navigation CacheMode равно Required, при вычислении общего количества страниц (для сравнения со значением CacheSize) не учитываются.
Обычно свойству NavigationCacheMode присваивают значение Required, когда нужно кэшировать страницу для сохранения ее текущего состояния. Значение Enabled используется, если нужно сэкономить время и увеличить производительность, например если страница долго инициализируется или обращается к веб-службе. В этом случае разместите код инициализации в конструкторе страницы, а не в обработчике события Loaded, которое генерируется, даже когда страница извлекается из кэша.
Методы, используемые при навигации
В классе Page определен ряд методов, вызываемых на разных этапах навигации:
- OnNavigatedTo()
Вызывается, когда фрейм переходит к странице (в первый раз или возвращается к этой же странице с помощью списка истории).
- OnNavigatingFrom()
Вызывается, когда пользователь покидает страницу. Позволяет отменить навигацию.
- OnNavigatedFrom()
Вызывается, когда пользователь покинул страницу, но перед появлением следующей страницы.
Эти методы можно использовать для выполнения нужных операций при посещении страницы, например для отслеживания или инициализации страницы. В частности, они используются для экономии памяти при управлении состояниями, а именно — для сохранения только некоторых деталей, а не всего объекта страницы. Сохранить данные страницы можно при вызове метода OnNavigatedFrom(), а извлечь сохраненные данные — при вызове метода OnNavigatedTo(). Хранить состояние можно в любом месте, например в объекте App или в статическом поле объекта страницы, как в приведенном ниже коде:
public partial class CustomCachedPage : Page
{
...
public static string TextBoxState { get; set; }
}
Ниже приведен код, в котором свойство TextBoxState используется для хранения значения текстового поля. Сохраненное значение извлекается, когда пользователь возвращается к странице:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
// Сохранение значения текстового поля
CustomCachedPage.TextBoxState = txtCached.Text;
base.OnNavigatedFrom(e);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Извлечение сохраненного значения
if (CustomCachedPage.TextBoxState != null) txtCached.Text = CustomCachedPage.TextBoxState;
base.OnNavigatedTo(e);
}
Шаблоны навигации
Теперь вы знаете все, что необходимо для создания приложений с навигацией с помощью специальных классов Frame и Page. Однако между знанием этих средств и умением создать привлекательную и удобную систему навигации — огромная пропасть. Существуют два способа преодолеть ее. Первый состоит в постепенном повышении мастерства путем ознакомления с работами других людей, экспериментирования, чтения учебников, создания реальных приложений и т.п. Второй способ значительно легче: можно использовать готовый шаблон навигации в качестве начальной точки. С шаблонами можно работать в Visual Studio. Они содержат базовую структуру проекта и набор стилей, позволяющих сделать приложение привлекательным.
На рисунке ниже показано приложение, созданное программой Visual Studio в качестве шаблона навигации при выборе шаблона проекта Silverlight Navigation Application вместо стандартного Silverlight Application:
Базовая структура приложения довольно простая. В верхней части страницы расположена группа кнопок, служащих для навигации. Под ними расположен фрейм. Страницы отображаются через объект UriMapper и размещаются во вложенной папке Views.
Рабочая среда Silverlight в программе Visual Studio содержит только один шаблон. Однако команда разработчиков Silverlight создала ряд дополнительных шаблонов и поместила их на страницу "7 additional application themes". В них используются разнообразные визуальные стили и по-разному размещаются кнопки.