Одной из самых мощных функций в XAML является поддержка данных времени разработки. Допустим, у вас есть приложение, которое отображает некоторые новости, полученные из RSS-канала с использованием элемента управления ListView или GridView. Если вы попытаетесь отредактировать визуальный макет вашего приложения с помощью Blend или дизайнера Visual Studio, вы не сможете получить предварительный просмотр окончательного результата: поскольку данные извлекаются из удаленной службы, ListView будет выглядеть пустым, поскольку дизайнер не может загрузить реальные данные. Этот сценарий затрудняет для дизайнера реальный предварительный просмотр конечного результата или редактирование некоторых элементов (например, ItemTemplate объекта ListView).
Проектные данные — это решение этой проблемы: в основном это способ создания поддельных данных, который загружается только тогда, когда представление отображается в Blend или в конструкторе Visual Studio. Давайте посмотрим, как управлять этим сценарием с помощью Caliburn Micro.
Пример приложения: простой читатель новостей
В качестве примера мы разработаем простую программу чтения новостей, которая будет читать и анализировать RSS-канал этого блога. Реальное приложение будет отображать реальные сообщения из блога, в то время как дизайнер будет отображать несколько поддельных сообщений, просто чтобы дать дизайнеру представление о контенте, который будет отображаться на странице. Во-первых, давайте начнем настраивать настоящее приложение, используя соглашения, которые мы изучили в предыдущих постах. Приложение будет иметь только один вид, который будет отображать список сообщений с помощью элемента управления ListView.
Сначала давайте определим простой класс, который будет отображать некоторую информацию о публикации, которую мы хотим отобразить на странице:
public class FeedItem { public string Title { get; set; } public DateTimeOffset PublishDate { get; set; } public string Description { get; set; } }
Затем нам нужно загрузить RSS-канал из Интернета и преобразовать XML в список объектов FeedItem . Для этой цели мы собираемся создать отдельный класс, который позаботится об обработке RSS-канала: мы определим метод, который будет принимать в качестве входных данных URL-адрес RSS-канала и будет возвращать как output — коллекция объектов FeedItem . Вот как выглядит интерфейс класса:
public interface IFeedService { Task<IEnumerable<FeedItem>> GetNews(string feedUrl); }
Для его реализации мы будем использовать один из новых API-интерфейсов 8.1, который называется SyndicationClient: это специальная реализация HttpClient, которая может загружать RSS-канал и автоматически его анализировать, чтобы вы могли работать с классами и объектами вместо сырой XML. Вот реализация:
public class FeedService: IFeedService { public async Task<IEnumerable<FeedItem>> GetNews(string feedUrl) { SyndicationClient client = new SyndicationClient(); SyndicationFeed feed = await client.RetrieveFeedAsync(new Uri(feedUrl, UriKind.Absolute)); List<FeedItem> feedItems = feed.Items.Select(x => new FeedItem { Title = x.Title.Text, Description = x.Summary.Text, PublishDate = x.PublishedDate }).ToList(); return feedItems; } }
Мы используем метод RetrieveFeedAsync () , который принимает на вход URL-адрес RSS-канала (который передается в качестве параметра метода GetNews () ), и он возвращает вам объект SyndicationFeed , в котором хранится вся информация о корм. Что нас интересует, так это коллекция Items , которая содержит все новости, включенные в ленту новостей (в нашем случае это список всех публикаций, опубликованных в этом блоге). Используя LINQ, мы преобразуем его в коллекцию объектов FeedItem , захватывая заголовок, описание и дату публикации поста.
Теперь нам нужно использовать этот класс в ViewModel, который связан с нашей страницей. Самый простой способ — просто создать новый экземпляр класса FeedService и вызвать метод GetNews () , как в следующем примере:
public class MainPageViewModel : Screen { public MainPageViewModel() { } private List<FeedItem> _news; public List<FeedItem> News { get { return _news; } set { _news = value; NotifyOfPropertyChange(() => News); } } protected override async void OnActivate() { IFeedService feedService = new FeedService(); IEnumerable<FeedItem> items = await feedService.GetNews("http://feeds.feedburner.com/qmatteoq_eng"); News = items.ToList(); } }
Однако у этого подхода есть некоторые недостатки. Предположим, что из-за некоторых изменений в требованиях вам нужно поменять класс FeedService () на другую реализацию, сохранив интерфейс IFeedService в качестве основы. При таком подходе вам придется заходить в каждый ViewModel, в котором вы используете класс FeedService, и заменять реализацию. Но подождите, есть лучший способ сделать это: использовать контейнер для инъекций зависимостей, предоставленный Caliburn Micro!
Давайте перейдем к файлу App.xaml.cs и в методе Configure () зарегистрируем FeedService , как мы это делали для разных ViewModels:
protected override void Configure() { container = new WinRTContainer(); container.RegisterWinRTServices(); container.PerRequest<MainPageViewModel>(); container.PerRequest<IFeedService, FeedService>(); }
Единственное отличие состоит в том, что на этот раз, поскольку у нас есть интерфейс, который описывает наш класс, мы используем другой подход: вместо регистрации только класса (путем вызова метода PerRequest <T> () ) мы связываем интерфейс с конкретным реализация, используя метод PerRequest <T, Y> () . Теперь, чтобы получить ссылку на класс FeedService в нашей ViewModel, нам просто нужно добавить новый параметр в конструктор, тип которого IFeedService , как в следующем примере:
public class MainPageViewModel : Screen { private readonly IFeedService _feedService; public MainPageViewModel(IFeedService feedService) { _feedService = feedService; } private List<FeedItem> _news; public List<FeedItem> News { get { return _news; } set { _news = value; NotifyOfPropertyChange(() => News); } } protected override async void OnActivate() { IEnumerable<FeedItem> items = await _feedService.GetNews("http://feeds.feedburner.com/qmatteoq_eng"); News = items.ToList(); } }
Мы просто добавили параметр в конструктор, типом которого является IFeedService : контейнер внедрения зависимостей позаботится о разрешении параметра во время выполнения путем внедрения реальной реализации, которую мы определили в классе App (в нашем случае, FeedService ). Таким образом, если нам нужно использовать другую реализацию класса IFeedService , мы просто должны вернуться к классу App и изменить метод Configure (), который мы ранее определили, вместо того, чтобы менять код в каждом ViewModel, который должен использовать этот класс. Теперь мы можем просто определить наш View, используя элемент управления ListView, чтобы отобразить сообщения, удаленные FeedService :
<ListView ItemsSource="{Binding Path=News}"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=Title}" Style="{StaticResource TitleTextBlockStyle}" /> <TextBlock Text="{Binding Path=PublishDate}" Style="{StaticResource BodyTextBlockStyle}"></TextBlock> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView>
Теперь, если вы запустите приложение (не имеет значения, версия Windows 8.1 или Windows Phone 8.1), вы должны увидеть списки сообщений, недавно опубликованных в этом блоге, загруженных из следующего канала RSS: http: //feeds.feedburner .com / qmatteoq_eng
Использование проектных данных
Если вы попытаетесь открыть свой проект в Blend или включите дизайнер Visual Studio, вы заметите, что страница будет пустой: реальный FeedService не выполняется во время разработки, поэтому у вас нет возможности увидеть список постов. Целью нашей работы будет отображение списка фальшивых постов, если представление отображается внутри дизайнера.
Одна вещь, которую вы должны были заметить при работе с Caliburn, это то, что вы не получаете Intellisense, когда имеете дело с привязкой, в отличие от того, когда вы работаете с другими инструментами, такими как MVVM Lights. Это происходит потому, что вы явно не устанавливаете DataContext на странице: он автоматически разрешается во время выполнения с использованием специального соглашения по именованию Caliburn. В результате, когда вы пишете XAML, View не знает, какая модель ViewModel связана с ним. Это является следствием требования к проектным данным: нам нужно сообщить представлению, которое является подключенной моделью ViewModel, чтобы оно могло включить Intellisense и чтобы оно могло предоставлять нам данные проектирования.
Вот как это сделать:
<Page x:Class="DesignData.Views.MainPageView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:DesignData" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:micro="using:Caliburn.Micro" xmlns:viewModels="using:DesignData.ViewModels" mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=viewModels:MainPageViewModel, IsDesignTimeCreatable=True}" micro:Bind.AtDesignTime="True" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Grid> <ListView ItemsSource="{Binding Path=News}"> <ListView.ItemTemplate> <DataTemplate> <StackPanel> <TextBlock Text="{Binding Path=Title}" Style="{StaticResource TitleTextBlockStyle}" /> <TextBlock Text="{Binding Path=PublishDate}" Style="{StaticResource BodyTextBlockStyle}"></TextBlock> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </Page>
Сначала мы определили, что такое DataContext страницы, но сделали это по-другому, добавив префикс d : таким образом, мы сообщаем компилятору XAML, что это значение следует использовать только во время разработки, в то время как во время выполнения приложение будет продолжать использовать стандартный DataContext (в нашем случае ViewModel, назначенный Caliburn). С этим определением, в основном, мы вручную определяем, что Caliburn автоматически делает во время выполнения: мы сообщаем текущему представлению, что используемый класс DataContext называется MainPageViewModel и поддерживает данные времени разработки.
Затем нам нужно использовать специальное присоединенное свойство Caliburn, которое способно выполнить привязку для нас во время проектирования: оно называется Bind.AtDesignTime, и его значение должно быть установлено в True . Обратите внимание, что для того, чтобы этот код работал, нам нужно определить два пространства имен XAML: первое (называемое viewModels в образце), которое указывает на папку проекта, которая содержит все наши ViewModels (в нашем случае, используя: DesignData.ViewModels , где DesignData — название проекта); второй (называемый в образце микро ) это Caliburn по умолчанию, который использует: Caliburn.Micro .
После этой настройки вы сразу заметите, что Intellisense будет запущен и запущен: если вы попытаетесь настроить новую привязку с помощью элемента управления, Visual Studio предложит вам одно из свойств, которые были объявлены в ViewModel. ,
Теперь пришло время определить данные времени проектирования: важно отметить, что для правильной работы этой функции в Caliburn требуется конструктор без параметров. В нашем случае он будет отличаться от стандартного (который вместо этого содержит параметр, тип которого IFeedService ): в этом пустом конструкторе мы собираемся установить поддельные данные, заполнив коллекцию News (отображаемую). в элементе управления ListView ) со списком поддельных сообщений, как в следующем примере.
public class MainPageViewModel : Screen { private readonly IFeedService _feedService; public MainPageViewModel(IFeedService feedService) { _feedService = feedService; } public MainPageViewModel() { if (Execute.InDesignMode) { News = new List<FeedItem> { new FeedItem { Title = "First news", Description = "First news", PublishDate = new DateTimeOffset(DateTime.Now) }, new FeedItem { Title = "Second news", Description = "Second news", PublishDate = new DateTimeOffset(DateTime.Now) }, new FeedItem { Title = "Third news", Description = "Third news", PublishDate = new DateTimeOffset(DateTime.Now) } }; } } private List<FeedItem> _news; public List<FeedItem> News { get { return _news; } set { _news = value; NotifyOfPropertyChange(() => News); } } protected override async void OnActivate() { IEnumerable<FeedItem> items = await _feedService.GetNews("http://feeds.feedburner.com/qmatteoq_eng"); News = items.ToList(); } }
Как видите, основной код ViewModel такой же: у нас есть один конструктор, который заботится об инициализации объекта IFeedService ; у нас есть свойство News , которое содержит список объектов FeedItem ; в методе OnActivate () (который вызывается при отображении представления) мы используем класс FeedService для получения списка сообщений и его отображения в представлении.
Разница в том, что теперь у нас есть конструктор без параметров, в котором мы инициализируем список поддельных постов: мы вручную создаем некоторые объекты FeedItem и добавляем их в коллекцию News . Конечно, мы хотим сделать это, только если в конструкторе отображается представление: мы не хотим отображать поддельные данные, когда приложение работает на телефоне. Для этой цели мы можем использовать класс Caliburn с именем Execute , который предоставляет некоторые полезные свойства и методы: для нашего сценария мы можем использовать свойство с именем InDesignMode , которое является простым логическим значением, для которого установлено значение true в случае, если представление отображается внутри дизайнер. Мы собираемся добавить поддельные данные в новостиcollection, только если значение этого свойства установлено в true .
Теперь создайте свой проект и попробуйте открыть дизайнер: вы заметите, что представление больше не будет пустым, но оно будет отображать поддельные данные, которые мы предоставили.
Завершение
Как обычно, я отправил все примеры, описанные в этом посте, в моем проекте GitHub ( https://github.com/qmatteoq/CaliburnMicro-UniversalApp-Demo ) внутри папки с именем DesignData . Повеселись!