В этой серии статей будет рассказано о создании отлаженного, работающего приложения для Windows Phone от начала до конца. Приложение называется Realworld Stocks, и полный исходный код будет доступен на CodePlex по мере развития серии. Я буду использовать Mercurial, чтобы поощрять разветвление и, возможно, даже получать запросы от разработчиков, которые хотят внести свои собственные реальные решения.
Убедитесь, что вы загружаете Windows Phone 7.5 SDK
Посмотреть серию Введение и план
Основы навигации
Несмотря на то, что эта серия предполагает базовое понимание разработки Windows Phone, я хочу кратко коснуться основ навигации по страницам. Приложения для Windows Phone следуют базовой парадигме навигации без сохранения состояния, очень похожей на веб-приложение. Каждая страница в приложении представлена URL-адресом, заканчивающимся на .xaml.
Как перейти на новую страницу (плохой путь)
Простейшим способом навигации будет перетаскивание кнопки, добавление обработчика нажатия для кнопки и использование следующего
NavigationService.Navigate(new Uri("/Views/Home.xaml")); NavigationService.Navigate(new Uri("/Views/ProductDetails.xaml?ProductID=5&ProductName=HTC%20Titan"));
Как вы можете видеть, если вам нужно передать параметры на страницу, на которую вы переходите, вы используете значения строки запроса, опять же, точно так же, как при написании веб-приложения.
На первом изображении выше у нас есть список символов акций. Когда пользователь нажимает на элемент MSFT, мы хотим перейти к StockDetailsView.xaml и сообщить ему, по какому символу щелкнули. Мы стремимся достичь чего-то подобного.
NavigationService.Navigate(new Uri("/Views/StockDetailsView.xaml?Symbol=MSFT"));
Доступ к параметрам QueryString (плохой способ)
Теперь мы находимся на нашей новой странице и должны получить доступ к QueryString. Поэтому, если мы откроем код позади StockDetailsView.xaml.cs, мы можем
переопределить
OnNavigatedTo и получить доступ к
NavigationContext.QueryString.
public partial class StockDetailsView : PhoneApplicationPage { public StockDetailsView() { InitializeComponent(); } protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { var symbol = NavigationContext.QueryString["Symbol"]; } }
Ик. В этом сценарии много строк, как для перехода к новому URI, так и для извлечения параметров строки запроса.
Строки могут оказаться проблематичными по нескольким причинам:
- Они очень подвержены опечаткам
- Они не предлагают никакой поддержки рефакторинга / переименования во время компиляции.
К счастью, есть лучший способ …
Навигация с помощью Caliburn
Как я упоминал в предыдущем посте, одна из причин, по которой я люблю Caliburn, заключается в том, что он предоставляет достаточно инфраструктуры, чтобы позволить мне развиваться быстрее, но не настолько властно, что я загнан в угол. Одним из этих преимуществ инфраструктуры является навигация по обеим сторонам уравнения: переход на новую страницу и извлечение параметров строки запроса на новой странице.
Навигация с помощью HyperlinkButton и ActionMessage
На скриншоте выше мы видим ListBox, показывающий список акций. И с этим ListBox у нас, естественно, есть DataTemplate, описывающий, как визуализировать каждый элемент. Но действительно важным моментом здесь является то, что, когда конкретный элемент называется Tapped, мы хотим загрузить представление StockDetails для этого элемента.
HomeWatchListView.xaml
В HomeWatchListView.xaml мы определили следующий DataTemplate, который имеет HyperlinkButton с чем-то особенным — cal: Message.Attach = ”LoadSymbol ($ dataContext)”
<DataTemplate x:Key="WatchListItemTemplate"> <HyperlinkButton cal:Message.Attach="LoadSymbol($dataContext)"> <Grid> <TextBlock Text="{Binding Symbol}"/> <TextBlock Text="{Binding Company}"/> </Grid> </HyperlinkButton> </DataTemplate>
Сначала это может выглядеть довольно странно. Похоже, что он вызывает метод с именем LoadSymbol () с каким-то токеном, передаваемым как $ dataContext — и это именно то, что есть.
HomeViewModel.cs
Теперь в нашей HomeViewModel мы можем видеть, где именно определен этот метод. И это фактически принимает параметр метода типа StockSnapshot. Поскольку это именно то, к чему был привязан наш ListBox в представлении, мы смогли использовать параметр $ dataContext, который представляет один StockSnapshot в списке, в частности, тот, который был Tapped.
Это дает нам дополнительное преимущество написания логина навигации в нашей ViewModel, а не в нашем View. ViewModels легче тестируются, чем Views, и при необходимости их можно использовать повторно.
public class HomeViewModel : Screen { // ... snip ... public void LoadSymbol(StockSnapshot snapshot) { // TODO: Navigate! } }
Как перейти на новую страницу (хороший способ)
Caliburn имеет INavigationService, который мы используем в нашей ViewModel. Он предлагает 2 основных компонента функциональности (и многое другое — см. Раздел с резюме ниже!):
- Создание URI для нас с помощью _navigationSerice.UriFor <T> () — Смотри, мама, никаких строк!
- Возьмите сгенерированный URI и перейдите на новую страницу
public void LoadSymbol(StockSnapshot snapshot) { var uri = _navigation.UriFor<StockDetailsViewModel>() .WithParam(m => m.Symbol, snapshot.Symbol) .BuildUri(); // BuildUri() returns the following URI // /Views/StockDetails/StockDetailsView.xaml?Symbol=MSFT _navigation.UriFor<StockDetailsViewModel>() .WithParam(m => m.Symbol, snapshot.Symbol) .Navigate(); }
Доступ к параметрам QueryString (хороший способ)
Теперь, когда мы перешли к StockDetailsView.xaml, нам нужно выяснить, какой Symbol загрузить — который был передан через строку запроса. Еще одна замечательная часть инфраструктуры — это то, что когда страница инициализируется, Caliburn проверит QueryString страницы для вас. Он будет искать в ViewModel свойства, соответствующие параметрам QueryString, и автоматически вставлять их, выполняя необходимое приведение типов (например, если у ViewModel есть свойство типа Int.)
Это долгий способ сказать следующее: в вашей ViewModel создайте свойство, имя которого соответствует параметру QueryString, и оно будет автоматически введено для вас.
public class StockDetailsViewModel : Screen { private string _symbol; // This property will be populated automatically because // the incoming querystring has a param named ?Symbol=X public string Symbol { get { return _symbol; } set { _symbol = value; NotifyOfPropertyChange(() => Symbol); } } }
Потрясающие! Нет больше строк! Теперь мы можем построить URI, используя строго типизированную модель с полной поддержкой рефакторинга / переименования, и можем извлечь параметр строки запроса на новой странице без прямого доступа к строке запроса. Лично я считаю, что это удивительно экономит время и позволяет очень быстро выполнять рефакторинг и улучшения по мере развития приложения.
Прохождение сложного состояния между страницами
Теперь есть одна последняя часть нашей головоломки. Передача базовых примитивов строки запроса — это хорошо, но что, если у вас уже есть полностью заполненная сложная объектная модель и вы хотите передать ее на следующую страницу? Здесь вещи могут стать немного грязными.
Для этого сценария я обычно объединяю строки запроса с одноэлементным дампом данных под названием GlobalData . Обычно я создаю базовый словарь внутри GlobalData, где я буду хранить объект на основе некоторого ключа, а затем вытаскивать объект из GlobalData с помощью того же ключа — ключ — это то, что я передаю, используя QueryStrings. 3 класса, работающих вместе, описаны ниже.
GlobalData / SnapshotCache
public class SnapshotCache : Dictionary<string, StockSnapshot> { public StockSnapshot GetFromCache(string key) { if (ContainsKey(key)) return this[key]; return null; } } public class GlobalData : NotifyObject { private GlobalData() { } private static GlobalData _current; public static GlobalData Current { get { if (_current == null) _current = new GlobalData(); return _current; } set { _current = value; } } private SnapshotCache _cachedStops; public SnapshotCache Snapshots { get { if (_cachedStops == null) _cachedStops = new SnapshotCache(); return _cachedStops; } } }
Прежде чем перейти на новую страницу, сохраните объект в GlobalData…
public class HomeViewModel : Screen { public void LoadSymbol(StockSnapshot snapshot) { GlobalData.Current.Snapshots[snapshot.Symbol] = snapshot; _navigation.UriFor<StockDetailsViewModel>() .WithParam(m => m.Symbol, snapshot.Symbol) .Navigate(); } }
…on the new page, pull the object out of GlobalData
public class StockDetailsViewModel : Screen { private string _symbol; public string Symbol { get { return _symbol; } set { _symbol = value; NotifyOfPropertyChange(() => Symbol); } } public StockSnapshot Snapshot { get { return GlobalData.Current.Snapshots[Symbol]; } } }
Summary and further reading
This tutorial covered quite a bit but hopefully didn’t throw too much at you. Page navigation is one of the biggest topics that devs ask me when building their first app, and I highly recommend using Caliburn’s excellent infrastructure to make the task a lot easier.
If you want to dive more into using CM with Windows Phone please check out the following pages:
Caliburn Docs: Working with Windows Phone
Caliburn Docs: All About Actions