Браузер кнопка назад вне зоны по страничной навигации
Примечание: статья посвящена обзору проблемы неработающей кнопки «назад» в браузере при использовании AJAX-методов для передачи содержания страниц от сервера к клиенту. В статье рассматриваются основные принципы работы AJAX и возможные пути решения заявленной проблемы. Курсивом даны мои комментарии.
Эта статья является первой из ряда материалов (вторая статья посвящена работе с закладками), направленных на устранение части критики, которую адресуют сейчас AJAX, и предоставляющих обзор полезных методов, которые помогут сделать ваши приложения и веб-страницы, использующие технику AJAX, немного лучше.
Суть проблемы
С самого начала в основе Веба лежала возможность поставить гиперссылку с одной страницы на другую. Ссылки между страницами была первично двунаправленными: это означало, что переход по ссылке со страницы А на страницу Б никаким образом не мог предотвратить ни переход по ссылке обратно на страницу А, ни использование кнопки «назад» в браузере. Цепочка таких страниц представляет собой историю посещения в браузере, с каждой из них связан уникальный URL. Если представить это с технической стороны, то такая цепочка реализуется в виде стека. В дальнейшем я буду использовать термин «горизонтальная» ссылка для обозначения связи между элементами такого стека.
Рисунок 1. Горизонтальные ссылки
Рисунок 2. Вертикальная ссылка между вторым URL'ом и сервером
Веб-сайт и веб-приложение
Подход, который стоит использовать для решения этой проблемы, напрямую зависит от того, что вы разрабатываете: веб-сайт или веб-приложение. Иногда тяжело разделить эти два понятия, но обычно веб-приложение можно отличить по следующим свойствам:
- Перед использованием требуется авторизация
- Серьезная работа с пользовательскими сессиями
- Процесс взаимодействия пользователя с приложением имеет вполне определенное начало и конец
В качестве примера веб-сайта можно рассмотреть Yahoo! Finance и E*TRADE — в качестве веб-приложения. С точки зрения пользователя я могу сказать, что не всегда возможно провести четкую границу между этими двумя типами. Однако, веб-разработчикам стоит с самого начала определиться, что же они собираются разрабатывать: сайт или приложения? Если вам это понять, то можно задать простой вопрос: в отсутствии интернета чем лучше всего описывается ваша разработка: это набор Word'овых документов или же настольное приложение? Рассматривайте веб-приложения наравне с настольными с той лишь разницей, что первым еще требуется браузер для нормальной работы. Другой вопрос, который можно себе задать, звучит так: «Вашей главной целью будет предоставление информации или обеспечение функциональности?».
Первое решение: не злоупотребляйте вертикальными ссылками
Если вы собираетесь создать веб-сайт для публичного доступа, я бы советовал вам использовать AJAX только в случаях крайней необходимости (не злоупотреблять им). Возможно, будет требоваться изменение URL'а страницы, чтобы обновить ее всю, но при этом, я полагаю, иделогия правильной работы кнопки «назад» в браузере будет соблюдена. Помните, что не все вызовы AJAX сильно связаны с вертикальными ссылками (прим.: как я понимаю, автор имеет в виду прежде всего изменение каких-либо малых частей страницы при неизменном основном содержимом).
Легче всего просто не принимать во внимание неработоспособность кнопки «назад» в браузере, но полностью этим пренебрегать нельзя. Вместо того, чтобы создавать целостное приложение, использующее большое количество вертикальных ссылок и привязанное к единственному URL'у, создайте некоторое количество горизонтальных ссылок в тех местах, где это действительно требуется. Другими словами, используйте горизонтальные ссылки для разделения частей вашего приложения, например, таким образом, как это делается в бумажной литературе (книга делится на части и главы). Используя разумную комбинацию традиционных горизонтальных ссылок с вертикальными, можно добиться баланса мощи AJAX и возможности использовать функционал перемещения по истории браузера.
Прим.: в качестве примера, пожалуй, можно привести некоторое количество современных сайтов, на которых AJAX используется только для предоставления некоторых дополнительных возможностей, в частности, это Хабрахабр и механизм голосования/добавления комментариев.
Второе решение: используйте специальную AJAX-библиотеку
Прим.: ниже сгруппированы основные методы создание псевдо-горизонтальных ссылок, я постарался дополнить их известными мне примерами, расширив список статьи-первоистоника.
-
Прим.: суть решения: при каждом вызове AJAX к URL'у страницы в качестве якоря добавляется текущий номер элемента в стеке «истории», фактически, просто увеличивающиеся числа. При нажатии кнопки «назад» в браузере, URL страницы меняется на предыдущий. Каждые 100 (200, 400, 1000) миллисекунд страница проверяет, не изменился ли у нее якорь в URL'е, если якорь изменился, то осуществляется подгрузка данных, соответствующих текущему якорю (=элементу в стеке «истории»).
Подход Brad Neuberg'а, эта библиотека пытается быть максимально кроссбраузерной без лишнего усложнения кода. Хорошо, что она доступна под BSD opensource лицензией. Brad опубликовал пошаговую инструкцию создания этой библиотеки, равно как и онлайн-демо.
Plex — AJAX framework с открытым кодом, который поддерживает очень много возможностей, в том числе, и эмуляцию кнопки «назад» в браузере.
Dojo — еще один AJAX framework с открытым кодом, обеспечивающий некоторую AJAX функциональность и эмуляцию кнопки «назад» в браузере.
Не могу также не упомянуть про неплохую обзорную статью Vladimira Kelmana, в которой обсуждаются часть вышеупомянутых решений.
Решение третье: обеспечьте пользователям удобную альтернативу кнопки «назад»
Традиционно, кнопка «назад» служила для реализации концепции: «Верните меня туда, откуда я пришел» Однако, при нажатии на нее многие пользователи, на самом деле, подразумевают «отмените то, что я только что сделал». Чтобы избавить их от сооблазна воспользоваться кнопкой «обратно» не по назначению, можно создать кнопки с функциями «Отменить» или «Шаг назад» в вашем веб-приложении, использующем AJAX. Например, если вы разрабатываете в Вебе расширенный текстовый редактор, создайте кнопку «Отменить», которая предотвратит потерю пользователями целого документа при неверном нажатии кнопки «назад» в браузере.
Однако, самим плохим решением из всех, которые я видел, является создание альтернативной кнопки «назад» в пределах самой веб-страницы, используя AJAX-методы. Многие пользователи с трудом смогли привыкнуть к границе между браузером и веб-страницей, смогли понять, где кончается браузер и начинается, собственно, сама страница. Нет никакой необходимости усиливать их неудобства. Ведь вы не можете гарантировать, что те пользователи, которые легко с этим справились, смогут привыкнуть еще к одной «инновации» и изменят свои привычки ради вашего сайта. Обеспечить пользователей альтернативной навигацией и функционалом будет вполне достаточно, но никак не создавать эту кнопку заново.
В заключении, если вы все же ограничиваете функционал стандартной истории в браузере при создании веб-приложения, пожалуйста, поставьте пользователей об этом в известность тем или иным способом. AJAX не является первым методом, который ограничивает этот функционал, и, скорее всего, пользователи впервые узнают об этом тоже не от вас. (Есть еще апплеты, Flash и eCommerce приложения, которые могут снять с кредитной карточки сумму еще раз, если нажать кнопку «назад» в браузере.) Вес ответственности, который вы, в конечном счете, возложите на пользователя за его действия на сайте, будет зависеть от вашей корпоративной культуры, но почему бы не сделать его чуточку легче?
Спасибо всем, что читал, читает и будет читать мои переводы и статьи. Заранее хочу поблагодарить всех тех, что укажет на фактические неточности или ошибки в статье, а может быть, даст ссылки на дополнительную информацию.
Примечание. Если в настройках в разделе Дополнительно включена опция Переключать вкладки свайпом от края экрана , то листание будет переключать вас не между страницами истории просмотра, а между открытыми вкладками.
Просмотреть список открытых вкладок
Закрыть вкладку
Увеличить масштаб страницы
Если вы не нашли информацию в Справке или у вас возникает проблема в работе мобильного Яндекс Браузера, опишите все свои действия по шагам. Если возможно, сделайте скриншот. Это поможет специалистам службы поддержки быстрее разобраться в ситуации.
Примечание. Для решения проблем в работе сервисов Яндекса обращайтесь в службу поддержки этих сервисов:
О проблемах Яндекс Браузера на компьютере пишите прямо из Браузера: → Дополнительно → Сообщить о проблеме или через форму.
Если вопрос касается главной страницы Яндекса (изменить тему оформления, настроить блоки главной страницы или иконки сервисов, найти Яндекс Деньги и т. д.), пишите через форму. Выберите опцию Вопрос о главной странице Яндекса .
О работе Почты (отключить рекламу, настроить сбор писем с других ящиков, восстановить удаленные письма, найти письма, попавшие в спам и т. д.) пишите через форму.
О работе Поиска и выдачи (ранжирование сайта в результатах поиска, некорректные результаты поиска и т. д.) пишите через форму.
Перейти на предыдущую или следующую страницу
Примечание. Если в настройках в разделе Дополнительно включена опция Переключать вкладки свайпом от края экрана , то листание будет переключать вас не между страницами истории просмотра, а между открытыми вкладками.
Просмотреть список открытых вкладок
Нажмите значок с количеством вкладок (, если открыто три вкладки).
Закрыть вкладку
Нажмите значок с количеством вкладок (, если открыто три вкладки).
Увеличить масштаб страницы
Если вы не нашли информацию в Справке или у вас возникает проблема в работе мобильного Яндекс Браузера, опишите все свои действия по шагам. Если возможно, сделайте скриншот. Это поможет специалистам службы поддержки быстрее разобраться в ситуации.
Примечание. Для решения проблем в работе сервисов Яндекса обращайтесь в службу поддержки этих сервисов:
О проблемах Яндекс Браузера на компьютере пишите прямо из Браузера: → Дополнительно → Сообщить о проблеме или через форму.
Если вопрос касается главной страницы Яндекса (изменить тему оформления, настроить блоки главной страницы или иконки сервисов, найти Яндекс Деньги и т. д.), пишите через форму. Выберите опцию Вопрос о главной странице Яндекса .
О работе Почты (отключить рекламу, настроить сбор писем с других ящиков, восстановить удаленные письма, найти письма, попавшие в спам и т. д.) пишите через форму.
О работе Поиска и выдачи (ранжирование сайта в результатах поиска, некорректные результаты поиска и т. д.) пишите через форму.
Сделайте работу с мобильным Яндекс Браузером удобнее: включите значок для быстрого возврата в Браузер из других приложений и добавьте значки любимых сайтов на домашний экран смартфона.
Где найти настройки?
Значок возврата в Браузер
Включите в настройках значок возврата в Браузер и при переходе в другое приложение достаточно будет нажать значок , чтобы вернуться. Для этого:
Значок сайта на домашнем экране
Добавьте на домашний экран смартфона значок любимого сайта и переходите на него одним нажатием. Сайт откроется в новой вкладке Яндекс Браузера вне зависимости от того, какой Браузер установлен по умолчанию.
Если вы не нашли информацию в Справке или у вас возникает проблема в работе мобильного Яндекс Браузера, опишите все свои действия по шагам. Если возможно, сделайте скриншот. Это поможет специалистам службы поддержки быстрее разобраться в ситуации.
Примечание. Для решения проблем в работе сервисов Яндекса обращайтесь в службу поддержки этих сервисов:
О проблемах Яндекс Браузера на компьютере пишите прямо из Браузера: → Дополнительно → Сообщить о проблеме или через форму.
Если вопрос касается главной страницы Яндекса (изменить тему оформления, настроить блоки главной страницы или иконки сервисов, найти Яндекс Деньги и т. д.), пишите через форму. Выберите опцию Вопрос о главной странице Яндекса .
О работе Почты (отключить рекламу, настроить сбор писем с других ящиков, восстановить удаленные письма, найти письма, попавшие в спам и т. д.) пишите через форму.
О работе Поиска и выдачи (ранжирование сайта в результатах поиска, некорректные результаты поиска и т. д.) пишите через форму.
Сделайте работу с мобильным Яндекс Браузером удобнее: включите значок для быстрого возврата в Браузер из других приложений и добавьте значки любимых сайтов на домашний экран смартфона.
Где найти настройки?
Нажмите → Настройки .
Значок возврата в Браузер
Включите в настройках значок возврата в Браузер и при переходе в другое приложение достаточно будет нажать значок , чтобы вернуться. Для этого:
Нажмите → Настройки .
Значок сайта на домашнем экране
Добавьте на домашний экран смартфона значок любимого сайта и переходите на него одним нажатием. Сайт откроется в новой вкладке Яндекс Браузера вне зависимости от того, какой Браузер установлен по умолчанию.
Нажмите → Добавить ярлык .
Если вы не нашли информацию в Справке или у вас возникает проблема в работе мобильного Яндекс Браузера, опишите все свои действия по шагам. Если возможно, сделайте скриншот. Это поможет специалистам службы поддержки быстрее разобраться в ситуации.
Примечание. Для решения проблем в работе сервисов Яндекса обращайтесь в службу поддержки этих сервисов:
О проблемах Яндекс Браузера на компьютере пишите прямо из Браузера: → Дополнительно → Сообщить о проблеме или через форму.
Если вопрос касается главной страницы Яндекса (изменить тему оформления, настроить блоки главной страницы или иконки сервисов, найти Яндекс Деньги и т. д.), пишите через форму. Выберите опцию Вопрос о главной странице Яндекса .
О работе Почты (отключить рекламу, настроить сбор писем с других ящиков, восстановить удаленные письма, найти письма, попавшие в спам и т. д.) пишите через форму.
О работе Поиска и выдачи (ранжирование сайта в результатах поиска, некорректные результаты поиска и т. д.) пишите через форму.
При переходе по ссылке из приложений система Android предлагает выбрать браузер, в котором будет открыт сайт. Также может осуществляться автоматический переход в программу, которая практически не используется по разным причинам. Установить или изменить обозреватель можно по умолчанию самостоятельно. В «Блоге системного администратора» разберем, как это можно сделать.
Браузер по умолчанию – что это такое
На всех устройствах с выходом в интернет есть предустановленный браузер. Если он один, то все ссылки на интернет-страницы будут открываться только в нем. Именно поэтому программа и считается по умолчанию.
Если на смартфоне установлено несколько обозревателей, то после нажатия на ссылку для перехода на сайт каждый раз отображается окно, где нужно выбрать приложение. Следует отметить, что некоторые приложения имеют интегрированный браузер, через который все ссылки открываются по умолчанию. Если его нет, то они запустятся через сторонний софт.
Как изменить или установить браузер по умолчанию
Изменить обозреватель по умолчанию можно через системные настройки операционной системы. Также это делается с помощью параметров браузера. Разберем, как это можно сделать несколькими способами.
Способ 1: системные настройки
Чтобы выбрать браузер, который будет использоваться по умолчанию, сделайте следующее:
1. Откройте главное меню Android, затем найдите пункт «Настройки». В 11 версии ОС открыть параметры системы можно через верхнюю шторку, если смахнуть ее вниз и выбрать иконку в виде шестеренки.
2. Откройте раздел «Приложения» в списке.
3. В перечне установленного софта отыщите предпочтительный браузер и тапните по его названию.
4. В блоке «Параметры по умолчанию» нажмите на пункт «Веб-браузер».
5. Откроется окно, где отобразятся браузеры, установленные на смартфоне.
6. Отметьте нужный пункт.
Таким же образом настраиваются и другие приложения. Например, для открытия мультимедийных файлов или документов.
Способ 2: всплывающее окно при переходе по ссылке
Еще один способ, позволяющий установить браузер по умолчанию – это его выбор во всплывающем окне при нажатии на ссылку. В этом случае появятся два варианта: «Только сейчас» или «Всегда». Чтобы все страницы, на которые совершается переход, запускались в выбранной программе, нужно выбрать второй вариант.
В некоторых версиях операционной системы Android открывается чек-бокс, где нужно отметить тот обозреватель. Если выбрать пункт «Запомнить этот выбор», то он всегда будет открываться при переходе по ссылке.
Способ 3: параметры браузера
Функционал мобильных обозревателей предполагает возможность смены приложения по умолчанию. На примере Яндекс.Браузера рассмотрим, как это сделать:
1. Запустите Яндекс.Браузер и тапните по иконке в виде трех вертикальных точек, которая находится слева от «умной адресной строки».
2. Выберите раздел «Браузер по умолчанию».
3. Во всплывающем окне выберите нужный вариант и подтвердите действие.
Поменять браузер по умолчанию на устройстве, работающем на базе Android, можно несколькими способами через системные настройки или с помощью параметров программы, если они предусмотрены. А ранее мы рассматривали, как записать видео с экрана Android в Google Play Игры. Напишите в комментариях, каким браузером пользуетесь вы.
Применение страничной навигации достаточно актуальная задача для настольных WPF-MVVM приложений.
Разномастных руководств по организации такой навигации в сети достаточно.
И, конечно, Хабрахабр не исключение (имеются статьи раз и два).
Взглянув на первую статью Вы узнаете про NavigationService и возможность пользоваться Hyperlink.
Если перейдете по второй ссылке, то узнаете как пользоваться NavigationService в так называемом «Code Behind».
Таким образом, решения полного в этих статьях не представлено (на мой взгляд).
Хочется заполнить пробел и представить Вашему вниманию, как мне кажется, вполне рабочее решение.
Абсолютно не претендую на законченный компонент для организации страничной навигации.
Буду благодарен за полезные комментарии, поправки и дополнения.
Рад буду, если кому-то моя реализация навигатора окажется полезной.
Введение
Все, кто уже работал с WPF, наверняка знакомы с паттерном MVVM (дал ссылки в конце статьи). Ведь концепция MVVM несложна и, как минимум, интуитивно должна быть понятной выгода от его использования. Если хотите, чтобы он проявил себя во всей красе, то как можно меньше логики помещайте в «Code Behind» пользовательских элементов управления (UserControl) и ни в коем случае не используйте прямые ссылки на UI внутри ViewModel’ей. Данный подход даст Вам большой профит в виде возможности тестирования ViewModel’ей отдельно от контролов. Еще одной хорошей практикой будет сведение к минимуму создания экземпляров ViewModel’ей напрямую в контролах. Нестрашно, если элемент управления сам для себя создает ViewModel конкретного типа – в этом случае просто труднее будет подложить контролу какую-нибудь тестовую куклу. Иначе будут обстоять дела, когда некий родительский контрол будет занят созданием ViewModel’ей для остальных экранов, ведь тогда код может превратиться в нетестируемую кучу спагетти. Если за создание ViewModel’ей будут отвечать другие ViewModel’и, то тестировать станет намного легче.
Давайте представим себе приложение с панелью навигации, несколькими экранами и диалоговыми окнами. Нечто подобное представлено ниже.
- В событии Frame.Navigated в главном окне приложения через Code Behind можно получить доступ к загруженному во фрейм содержимому и подложить туда созданную там же в Code Behind ViewModel. Таким образом, создание ViewModel’ей для всех страниц будет сконцентрировано в одном обработчике с использованием длинной портянки if…else if… либо switch. Про то, что тестирование такого «Hard Coded» процесса навигации крайне трудно автоматизировать, я молчу.
- Другим решением является создание экземпляра Page и ViewModel’и под нее, подкладывание ViewModel’и в DataContext экземпляра Page и вызов Navigate у фрейма с передачей созданного экземпляра Page. Это решение немного лучше предыдущего, но по-прежнему совсем не «MVVM-way».
- Третьим решением можно назвать использование библиотек PRISM. Она используется в секторе крупных корпоративных приложений для реализации композитного UI. Если знакомы с AngularJS, то поймете что это. Реализуется некий RegionManager, в котором регистрируются части UI. Потом через созданный менеджер вызывается инстанциирование контрола по некоемому псевдониму, также присвоение нужного контекста данных. Этот функционал похож на то, что уже реализовано в NavigationService WPF.
Какое простейшее решение могло бы более-менее гладко вписаться в контекст MVVM? У класса Page в Silverlight есть перегружаемый метод OnNavigatedTo. В этом методе было бы удобно принимать ViewModel, переданную в NavigationService.Navigate(Uri uri, object navigationContext) вторым параметром. Однако в WPF у Page такого метода нет. По крайней мере я не нашел его или чего-то эквивалентного. Нам нужен некий посредник или, если хотите, менеджер, который будет контролировать переходы по страницам и перекладывать из параметра метода в DataContext нужную ViewModel. О реализации такого менеджера навигации и пойдет речь в данной статье.
В следующем разделе я расскажу о реализации ядра решения, о менеджере навигации. Затем, будет рассказано о том, что нужно реализовать на UI и ViewModel слоях. Для экономии времени можно прочитать раздел «Менеджер навигации», а остальное додумать по ходу решения своих задач.
Кому интересно сразу взглянуть на код, может переходить в репозиторий на GitHub.
Менеджер навигации
Этот менеджер реализован в виде синглтона с двойной проверкой экземляра на null (так называемый Double-Check Locking Singleton, многопоточная версия синглтона). Использование синглтона — это мое предпочтение. Так мне проще контролировать жизненный цикл. Вам возможно хватило бы и простого статического класса.
Код реализации синглтона смотрите ниже.
В представленном выше коде Вы можете увидеть, что свойство Instance я сделал приватным. Так сделано для простоты, чтобы наружу не выглядывало ничего лишнего. Вам же на практике может потребоваться сделать его доступным публично. Вместо приватного свойства экземпляра синглтона я создал публичное свойство сервиса навигации Service (типа NavigationService), которое транслирует вызовы через приватный экземпляр синглтона. Можно было сделать наоборот, но тогда бы все вызовы снаружи приходилось делать через экземпляр, т.е.
Выбирайте вариант, который Вам больше нравится. Мне кажется последний вариант проще, но он требует дополнительной реализации статических свойств и методов. Поэтому с реализацией нового функционала может стать выгоднее открыть свойство экземпляра (Navigation.Instance).
Свойство Service в этом синглтоне будет хранить ссылку на NavigationService экземпляра Frame, в котором требуется выполнять страничные переходы. Присваивать актуальное значение этой ссылке можно как при старте приложения (в обработчике события Loaded главного окна), так и в любой другой более поздний момент до вызова одного из методов навигации.
В примере выше мы назначаем нашему навигатору NavigationService Frame главного окна. Вместо главного окна мог быть любой контрол, но забирать NavigationService нужно в событии Loaded данного контрола. До этого события можно получить null. Более детально жизненный цикл контролов и NavigationService я не изучал.
В качестве альтернативного сценария я мог бы предложить использование ChildWindow из WPF Toolkit Extended, в который встроен еще один Frame. Можно в таком случае временно подменить NavigationService в нашем навигаторе, чтобы совершить переход внутри такого диалога. Это позволит автоматизировать через биндинг подгрузку различных экранов в диалоговые окна. Но сценарий такого использования кажется весьма экзотичным, потому подробно расписывать не буду. Если такой сценарий интересен, то напишу отдельную статью.
По-хорошему, в сеттере (и публичных методах менеджера тоже) не хватает использования lock. Но вообще, если у Вас в приложении параллельно с вызовом какого-либо метода навигации будет производиться замена NavigationService, то, скорее всего, что-то реализовано некорректно. Пока для простоты обойдемся без lock, но я Вас предупредил.
В коде выше Вы можете заметить использование "_resolver". В разделе IoC я про него расскажу. Если кратко, то это наипростейшая реализация Контейнера для Инверсии Управления.
В менеджере навигации реализовано подмножество навигационных методов из NavigationService, которого вполне достаточно для большинства простых случаев. Остается только подложить передаваемую ViewModel в свойство DataContext целевой страницы. Это делается в обработчике события Navigated (см. код ниже).
В обработчике события Navigated делается попытка приведения контента Frame к типу Page. Таким образом, обработаны будут только переходы на Page. Все остальные отфильтруются. Если хотите, то можете убрать этот «железный занавес». В случае успешного приведения типов переданный в свойстве ExtraData аргументов события экземпляр ViewModel'и будет помещен в DataContext целевой страницы. Это все о менеджере навигации.
Осталось создать сборку с реализацией страниц и сборку ViewModel’ей. Еще я реализовал сборку Helpers, в которой разместил код реализации RelayCommand для ViewModel’ей. Если есть силы и время, переходите к следующим разделам с описанием реализации UI и ViewModel’ей. Если нет, то ниже кратко изложу, что еще нужно реализовать.
Для каждой страницы следует создать отдельную ViewModel. Эти, «частные», ViewModel’и инстанциируются в их родительской MainViewModel с использованием "Инверсии Управления" (см. раздел IoC). Главная ViewModel помещается в DataContext главного окна, но с таким же успехом ее можно было бы инстанциировать в виде статического ресурса XAML в словаре ресурсов главного окна или даже на уровне всего приложения. В таком случае в биндингах для DataContext придется указывать что-то типа Source=. Но зато можно будет не беспокоиться о том, наследуется ли в нужном месте DataContext логического родителя.
В MainViewModel я создал несколько команд. Одну для перехода по указанному в CommandParameter строковому псевдониму страницы (переход без передачи контекста данных). Другие команды содержат в их делегате Execute переход по какому-то конкретному псевдониму целевой страницы с приемем через CommandParameter контекста данных. За деталями можете перейти на GitHub или продолжить чтение данной статьи.
Сборка ViewModels
Остальные ViewModel’и наследуются от нее и на данный момент предельно просты. Они содержат одно строковое свойство с уникальным именем (см. пример ниже).
Обратите внимание, здесь свойство только для чтения и без вызова RaisePropertyChanged(…) где бы то ни было. В данном случае это было сделано для простоты. На практике такие свойства ViewModel’ей встречаются, но нечасто, т.к. Binding на таких свойствах сработает лишь раз. Даже если я добавлю сеттер без RaisePropertyChanged(…), то Binding все равно будет «одноразовым».
MainViewModel уже значительно сложнее. Как я написал кратко в предыдущем разделе, она будет хранить частные ViewModel’и и реализовывать команды навигации. В моем случае частные ViewModel’и создаются лишь раз с использованием так называемого "Resolver'а", при инициализации MainViewModel. Поэтому я реализовал только геттеры этих ViewModel’ей.
Поля инициализируются в конструкторе MainViewModel:
"_resolver" в данном случае — это еще один Контейнер Инверсии Управления, о которых пойдет речь в соответствующем разделе. На данный момент этот Resolver просто достает из словаря делегат, соответствующий переданному псевдониму ViewModel. Также стоит заметить, что на практике Вам может понадобиться сделать полную реализацию полей и свойств для частных ViewModel’ей. Это уже делается элементарно.
Команды в моем случае реализованы с указанием get и set, а инициализация их экземпляров помещена в отдельной функции. Наличие сеттеров команды позволяет мне подменить каждую команду снаружи текущей ViewModel. Такой подход позволяет, например, изменить реакцию диалогового окна на клик кнопки «ОК», если та привязана через Binding к соответствующей команде в его (диалога) внутренней ViewModel. Впрочем, такой сценарий весьма экзотичен и может быть реализован без сеттеров команд.
Обратите внимание, в качестве пути передается псевдоним целевой страницы. Эти псевдонимы я поместил в виде констант в менеджер навигации, но вообще лучшее место для них в XML-файле настроек или просто в каком-то текстовом словаре.
После выполнения команды GoToPage1Command будет осуществлен переход на страницу по указанному псевдониму, а в DataContext страницы будет положена ссылка на Page1ViewModel. Таким образом, нам не надо реализовывать дополнительную логику по получению данных обратно из целевой страницы. Она будет работать с хранилищем внутри нашей MainViewModel, поэтому все изменения мы будем получать автоматически еще до перехода обратно.
Вроде бы все с ViewModel’ями. Переходим к UI.
Главное окно и сборка Pages
Приведу снова для удобства вид тестового приложения.
Слева представлены четыре кнопки. Первая кнопка привязана к команде GoToPathCommand и выполняет переход на Page1 без контекста данных. После перехода на страницу без контекста данных вместо актуального значения из ViewModel'и будет подставлено значение из параметра FallbackValue объекта Binding. Остальные кнопки привязаны к «частным» командам с указанным в делегате команды псевдонимом требуемой страницы страницы.
Сборка Pages содержит четыре страницы: Page1, Page2, Page3, Page404. Первые две просто содержат текстовые блоки, привязанные к свойству соответствующей частной ViewModel. Третью я немного усложнил, чтобы реализовать еще одну проблему MVVM, а именно задачу привязки ListBox.SelectedItems к ViewModel. Это отдельная тема, которая на мой взгляд заслуживает отдельной статьи. Для интереса можете заглянуть под спойлер разметки ниже.
На этой странице я разместил простенький диалог с выбором элементов из определенной категории. Это пример, приближенный к реальности. Вид диалога был показан на снимке главного окна выше. Обратите внимание, текстовый блок, кажется, лежит в диалоговом окне, но биндится к DataContext’у страницы напрямую, без всяких ухищрений. Это заслуга ChildWindow из WPF Toolkit Extended. Этот контрол на самом деле лишь имитирует поведение диалогового окна и является прямым потомком своего родителя в разметке XAML. Таким образом, DataContext наследуется в ChildWindow от Grid, в который я его поместил.
Кратко о проблеме привязки множественного выбора в ListBox. Чтобы вернуть во ViewModel список выбранных элементов ListBox’a я не могу использовать биндинг напрямую, т.к. свойство ListBox.SelectedItems не поддерживает биндинг. Чтобы решить эту проблему, можно отнаследовать от ListBox свой элемент управления, в котором добавить DependencyProperty. Однако есть более гибкий подход в контексте MVVM, о котором я и собираюсь написать в отдельной статье, если Вам это будет интересно.
IoC (Инверсия Управления)
К сожалению не могу подробно описать этот подход в данной статье. Объем и так велик. Но Вы можете почерпнуть нужные знания, например, из статей на хабре. Также множество ресурсов можно нагуглить. Если коротко, то «Инверсия Управления» это способ устранить прямые ссылки в одной сборке на другую сборку. Инжекция зависимостей выполняется специальными "Контейнерами", которые из конфигурационных файлов узнают какие конкретно классы и из каких сборок инициализировать для указываемого интерфейса и имени секции в конфиге. Нужно признаться, что в моем коде IoC не реализована полностью. Если честно, то и цели такой не было. Разумеется, концепцию IoC в коде я попытался отразить и попытался показать каким образом можно сделать код менее связным.
Ниже представлены интерфейсы контейнеров и их реализации.
Эти интерфейсы играют роль неких контрактов для различных реализаций контейнеров страниц и ViewModel'ей. На данный момент я сделал две реализации, которые Вы ни в коем случае не должны использовать в реальных проектах.
Это просто «куклы» контейнеров, которые подлежать замене на нечто управляемое, например из библиотеки Unity. В качестве интерфейсов тоже лучше было бы использовать что-то наподобие IUnityContainer, но мне не хотелось утяжелять солюшен дополнительным референсом и усложнять восприятие своей реализации навигатора. Тем более Вы можете предпочесть любую другую библиотеку IoC вместо Unity.
Читайте также: