Подпишись на наш Twitter

Быть в курсе появления новых статей!

В 2001 Microsoft опубликовала документ под названием Inductive User Interface Guidelines. Если вы еще не читали этот документ, то я советую сделать это. В этой статье я хочу рассмотреть построение вводного пользовательского интерфейса на WPF.

Приложения с навигацией

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

Страницы приложения могут быть обычно разбиты на две категории:

  1. Отдельные и независимые страницы.
  2. Страницы которые являются частью процесса.

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

  • Страница приветствия
  • Страница баланса аккаунта
  • Страница справки

Эти страницы зачастую только для чтения и на них можно перемещаться в любое время так как они не являются частью "задачи", которую выполняет пользователь.

Есть много задач которые пользователь может выполнять:

  • Подавать заявление на получение кредитной карты
  • Оплачивает счет
  • Запрашивает увеличение своих лимитов

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

Оболочка

Оболочка обычно является главным окном вашего приложения и содержит все ваши страницы. В WPF у вас есть две возможности:

  1. Если вы делаете XAML Browser Application или XBAP, оболочкой обычно является броузер, который содержит ваше приложение. Возможно сделать свою оболочку внутри броузера, но это может запутать пользователей.
  2. Если вы делаете отдельное приложение, вам надо сделать свою оболочку.

Создать свою оболочку просто как создать стандартное WPF окно, в обычном приложение WPF и использовать элемент WPF Frame. Задача Frame в разделении разделов окна, в котором размещаются ваши страницы и обеспечение сервиса навигации. Вот пример:


    
        
    

Из кода вы можете сказать фрейму сделать переход, как здесь:

_mainFrame.Navigate(new Page1());

Что является полезным сокращением для:

_mainFrame.NavigationService.Navigate(new Page1());

NavigationService

Навигация включает в себя много функций: переход назад, переход вперед, переход на новую страницу, обновление страницы и так далее. Объекты в экосистеме WPF навигации, такие как класс Fram, а так же класс Page который я опишу коротко, могут иметь доступ к этой функциональности через свойство NavigationService, которое неожиданно является типом NavigationService. Для примера:

_mainFrame.NavigationService.GoBack(); 
_mainFrame.NavigationService.GoForward(); 
_mainFrame.NavigationService.Refresh();

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

_mainFrame.NavigationService.Navigate(new Uri("http://www.google.com/"));
_mainFrame.NavigationService.Navigate(new Uri("Page1.xaml", UriKind.Relative));
_mainFrame.NavigationService.Navigate(new Page1());
_mainFrame.NavigationService.Navigate(new Button());
_mainFrame.NavigationService.Navigate("Hello world");

Во-первых загрузится полноценный контрол броузера с Google в фрейме. Во-вторых, укажет странице WPF найти XAML Uri в ресурсам приложения и отобразить страницу. В-третьих, отобразится XAML страница по прямой объектной ссылке. В-четвертых, это отобразится объект Button на экране без страницы вокруг. В конце отобразится строка в контроле TextBlock. Frame может отобразить все.

Фреймы делают свой контент доступным через dependency свойство Content, которое делает простым привязку данных. Например, при переходе на страницу, свойство Content даст вам объект Page. Показать заголовок текущей страницы в оболочке делается очень просто:


    
        
    

NavigationService так же предоставляет события, на которые вы можете подписаться, если хотите контролировать процесс навигации:

  • Navigating, когда фрейм начинает навигацию. Установите Cancel в true, если хотите остановить.
  • Navigated, когда навигация закончена, но до отображения.
  • NavigationFailed, когда что-то пошло не так.
  • NavigationProgress, когда блоки удаленной навигации начинают загружаться. Удобно для прогресс баров.
  • NavigationStopped, когда метод StopLoading вызван (аналогично нажатию "Stop" в IE) или новый запрос new Navigate сделан во время загрузки.
  • LoadCompleted, когда страница полностью отобразилась.

Настройка внешнего вида

Контрол Frame control поставляется со стандартным UI, которое предоставляет кнопки вперед и назад, когда вы прешли на вторую страницу. Из коробки это выглядит очень похожим на IE 7.0, хотя она и не учитывает текущую тему Windows:

К счастью, как и каждый контрол WPF, внешний вид фрейма зависит только от вас. Просто добавьте ControlTemplate и используйте ContentPresenter для показа содержимого страницы:


    
        
            

В примере, TemplateBindings используется потому-что Frame (использует шаблон) предоставляет свойства CanGoBack и CanGoForward. NavigationCommands устанавливает статически направляемые команды UI, которые фрейм автоматически перехватывает - не нужно обработчиков событий на C# для вызова NavigationService.GoBack().

Объект NavigationService и Frame предоставляют много других свойств и событий; это только верхушка айсберга, которые вы зачастую используете для построения UI с помощью WPF.

Примечание: WPF включает класс NavigationWindow, который по существу является Window, который также служит как Frame, реализуя большую часть тех же интерфейсов. Это звучит хорошо сперва, но большинство вам нужно больше контроля над Window, так что мне нет никакой необходимости использовать этот класс. Я просто указал на это ради полноты.

Страницы

Страницы краеугольный камень навигационного приложения и основной интерфейс пользователя для взаимодействия. Аналогично Windows и UserControls, Page это файл с разметкой XAML и кодом. Если вы работаете с XBAP'ми, Page основа приложения.

Как и Window или UserControl, страница может только иметь одного потомка, который необходимо установить панелью разметки. Вы можете делать все что хотите в WPF Page - 3D, анимация, повороты, как и в других контролах.

Использование Navigation Service из Page

Каждая Page имеет ссылку на объект NavigationService предоставляемая контейнером, обычно это Frame. Обратите внимание, что Page и содержащий ее Frame имеют ссылку на один и тот же NavigationService, поэтому Page не может ссылаться на него в конструкторе.

Page может воспользоваться NavigationService событием Navigating для остановки навигации со страницы, установив свойство Cancel в аргументе события. Это полезно если у пользователя есть не сохраненные изменения на странице и вы хотите дать ему шанс сохранить их. Не беспокойтесь об отписке - WPF определяет, когда вы покидаете страницу и автоматически удаляет все обработчики события для избегания утечек памяти (поскольку объект NavigationService живет намного дольше чем любая страница).

Поскольку страница содержится в Frame в Visual Tree, вы так же можете использовать некоторые NavigationCommands команды, поскольку родительский Frame будет их автоматически обрабатывать:


    
        
    

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

Ссылки

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


    
        
            Go to page 2
        
    

Переход данных между страницами

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

Хотя это не очевидно, вы можете передать строковый запрос странице, и извлечь его из пути. Например, ваша гиперссылка может передать значение в URI:


    Go to page 2

После загрузки страницы, можно извлекать параметры через NavigationService.CurrentSource, который возвращает объект Uri. Затем он может исследовать Uri, вытащив значения. Тем не менее, я настоятельно против такого подхода, за исключением самых крайних обстоятельствах.

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

Customer selectedCustomer = (Customer)listBox.SelectedItem;
this.NavigationService.Navigate(new CustomerDetailsPage(selectedCustomer));

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

Жизненный цикл Page

Множество людей были введены в заблуждение насчет времени жизни страниц и как работает навигация Назад/Вперед. Простейший путь показать это является создание пару простых страниц, Page1 и Page2, которые содержат гиперссылки друг на друга. Я собираюсь написать тот же код отслеживания для каждой страницы:

public Page1()
{
    InitializeComponent();
    Trace.WriteLine("Page 1 constructed");
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 navigating to page 2");
    this.NavigationService.Navigate(new Uri("Page2.xaml", UriKind.Relative));
}

private void Page_Loaded(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 loaded");
}

private void Page_Unloaded(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 unloaded");
}

~Page1()
{
    Trace.WriteLine("Page 1 destroyed");
}

Я так же повесил обработчики на кнопки Back и Forward, таким образом можно отслеживать, когда они нажимаются и я добавил кнопку для принудительного запуска garbage collector. Вот что получилось:

Page 1 navigating to page 2
Page 2 constructed
Page 1 unloaded
Page 2 loaded
Garbage collector runs
Page 1 destroyed
Clicked Back
Page 1 constructed
Page 2 unloaded
Page 1 loaded
Garbage collector runs
Page 2 destroyed

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

public Page1(DateTime date)
{
    InitializeComponent();
    Trace.WriteLine("Page 1 constructed " + date.ToString());
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    Trace.WriteLine("Page 1 navigating to page 2");
    this.NavigationService.Navigate(new Page2(DateTime.Now));
}

Во время навигации, если вы нажмете "Back", WPF не имеет возможности узнать какого типа значения переданы в конструктор; поэтому нужно оставлять страницы живыми. Вот вывод трассировки:

Page 1 navigating to page 2
Page 2 constructed 5/06/2008 12:23:19 PM
Page 1 unloaded
Page 2 loaded
Garbage collector runs
Clicked Back
Page 2 unloaded
Page 1 loaded

Обратите внимание, что, несмотря на то, что страницы загружаются и выгружаются много раз это те же экземпляры. Это значит что:

  • Если вы переходите использую URI, WPF создаст страницу вызывая конструктор каждый раз. История навигации содержит URI, но не объект.
  • Если вы переходите передавая объект напрямую, WPF будет поддерживать объект живым.

Это дает вам несколько вещей, для рассмотрения:

  • С течением времени, это может повлечь серьезное потребление памяти.
  • События Loaded и Unloaded вызываются каждый раз, когда страница показывается или исчезает, так что используйте их как шанс почистить все данные для уменьшения потребления памяти.
  • Режим навигации с помощью URI объясненный выше может быть полезен по этим причинам, но не злоупотребляйте ими :)

Наконец, каждая страница предоставляет boolean свойство KeepAlive boolean, которое по умолчанию false. Однако, оно применяется только при использовании URI навигации, что объясняет, почему многие люди пытались изменить его, но не увидели разницы. Back/forward попросту не могут работать, если объекты не остались живыми, так как сервис навигации не имеет возможности создать их. Как вариант, я бы хотел видеть, возможность передавать лямбда или делегат, который используется для перезагрузки страницы.

PageFunctions

WPF Pages служат для построения одиночных независимых страниц в нашем приложении - страницы, как Welcome, Help или View Account Balances. Однако когда дело доходит до завершения задач, они не отвечают в ряду требований. Рассмотрим сценарий, для оплаты счета:

Как только задача будет завершена, вы должны вернуться к странице Home. Если страница Confirm была написана для перехода непосредственно Home при нажатии кнопки? Что делать, если задача Pay Bill была вызвана с другой страницы? И если пользователь нажимает "Назад" после возвращения на странице Home, они должны получить отмену платежа после того как они уже подтвердили? А что, если мы не хотим, позволить им вернуться?

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

Это заставляет нас внимательно подумать о нашей навигационной модели:

  • Будут ли Verify и Confirm страницы жестко закодированы для перехода к деталям транзакции?
  • Когда они увидят страницу Transaction Details, что будет если в оболочке нажать кнопку "Back"?
    • Если отправятся на Verify и Confirm, то у нас будут проблемы, так как выставленный счет уже будет сохранен

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

  • В рамках задачи, вы можете нажать вперед или назад так, как вам нравится, пока не вернется.
  • Когда задача выполнена, история браузера "Стек" разматывается, где она была, когда задача началась.

Это полезно проиллюстрировать кодом:

PaidBill PayBill()
{
    var biller = SelectBiller();
    EnterTransactionDetails(biller);
    return Confirm();
}
Biller SelectBiller()
{
    if (BillerAlreadyExists)
        return SelectExisting();
    else
        return CreateNew();
}

Где каждая функция представляет собой задачу, которая может ответвиться, а затем вернуться к исходной задаче.

Введение в PageFunctions

Теперь вы можете начать понимать, где Page Functions получили свое название. WPF Page Functions представляют собой особый тип Page, которые наследуют от класса PageFunction <T>. Они являются стандартными объектами Page, но с одним отличием: они предоставляют событие Return, которое позволяет не напрягать журнал навигации.

Возврат

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

public partial class VerifyAndConfirmNewBillerPage : PageFunction<Biller>
{
    public VerifyAndConfirmNewBillerPage(Biller newBiller)
    {
        InitializeComponent();
        this.DataContext = newBiller;
    }

    private void ConfirmButton_Click(object sender, RoutedEventArgs e)
    {
        this.OnReturn(new ReturnEventArgs<Biller>((Biller)this.DataContext));
    }
}

Обратите внимание, что класс наследует от generic типа. Когда пользователь нажимает "Подтвердить" на этой странице, это вызывает событие Return для всех страниц этого экземпляра и возвращает выставленный счет, который был создан.

Страницы, которые были до этого нужно было бы подписаться на событие для того, чтобы ждать его. Вот как это будет выглядеть:

public partial class DefineNewBillerPage : PageFunction<Biller>
{
    public DefineNewBillerPage()
    {
        InitializeComponent();
        this.DataContext = new Biller();
    }

    private void SubmitButton_Click(object sender, RoutedEventArgs e)
    {
        var nextPage = new VerifyAndConfirmNewBillerPage((Biller)this.DataContext);
        nextPage.Return += new ReturnEventHandler<Biller>(BillerVerified);
        this.NavigationService.Navigate(nextPage);
    }

    private void BillerVerified(object sender, ReturnEventArgs<Biller> e) 
    {
        this.OnReturn(e);
    }
}

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

Back на странице Select Existing Biller, здесь показано как этот код будет выглядеть:

public partial class SelectExistingBillerPage : PageFunction
{
    public SelectExisingBillerPage()
    {
        InitializeComponent();
        this.DataContext = GetAllExisingBillers();
    }

    private void AddNewButton_Click(object sender, RoutedEventArgs e)
    {
        var nextPage = new DefineNewBillerPage();
        nextPage.Return += new ReturnEventHandler<Biller>(NewBillerAdded);
        this.NavigationService.Navigate(nextPage);
    }

    private void NewBillerAdded(object sender, ReturnEventArgs<Biller> e) 
    {
        // A biller has been added - we can pluck it from the event args
        var nextPage = new EnterTransactionDetailsPage(e.Result);
        this.NavigationService.Navigate(nextPage);
    }

    private void SelectExisingButton_Click(object sender, RoutedEventArgs e)
    {
        var nextPage = new EnterTransactionDetailsPage((Biller)listBox1.SelectedItem);
        this.NavigationService.Navigate(nextPage);
    }
}

Обратили внимание, как в обработчике события нашей страницы идет переходит на страницу Transaction Details? Таким образом, наша навигация теперь такая:

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

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

Разметка

PageFunctions наследуются от базового класса Page, и кроме концепции возвращения, они работают так же, как и любые другие страницы. Поскольку базовый класс является универсальным, XAML описывающий PageFunction действительно немного выглядит по другому:

domain="clr-namespace:DomainModel" 
    x:Class="NavigationSample.DefineNewBillerPage"
    x:TypeArguments="domain:Biller"
    Title="Define a new Biller">
    
        <... />
    

Жизненный цикл PageFunction

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

К счастью, в момент возврата PageFunction, она удаляется из журнала навигации и может быть собран мусор. Это означает, что, хотя страницы, которые составляют задачу, будут потреблять память, но как только задача выполнится, страницы будут уничтожены. Таким образом, в значительной степени в задаче ориентированных UI можно использовать богатую модель навигации без снижения производительности.

Итоги

Навигационные приложения сделаны из множества страниц, которые могут быть отнесены либо к автономным страницам или к страницам, которые представляют шаг в задаче пользователя. Автономные страницы, как правило, представлены в виде объектов Page, а шаги в рамках задачи представляются в виде PageFunctions. Страницы любого вида можут быть размещены в окне WPF используя Frame контрол или в XBAP приложении размещенного в браузере.

Объект NavigationService расшарен между Page и ее хостом, который предоставляет множество связок с навигационной системой. Жизненный цикл страницы может быть более сложным, что логично, когда вы подумаете о них. Обратите особое внимание на срок жизни страниц и найдите способы уменьшения потребления памяти, когда он не на виду (удалить все данные во время события Unloaded, и восстановить их во время события Loaded, например). Как обычно, используйте профилировщики памяти, чтобы обнаружить серьезные протечки.

Навигационные приложения позволяют легко создавать пользовательские интерфейсы, которые ориентированные на задачу, и направлены на простоту в использовании с большой производительностью. Этот стиль пользовательского интерфейса, очень трудно достигается, используя Windows Forms или другой системный пользовательский интерфейс. Даже в Интернете, который имеет подобные стили навигации, такие понятия, как журнал размотки, возврат к вызовающей странице и сохранение информации о состоянии между страницами может быть крайне сложно реализовать. Полноценные фреймворки как Struts в Java, созданы для обеспечения именно этих сценариев.

Оригинал: WPF Navigation




Дата публикации: 02.10.2009 18:02

Ярлыки: WPF