Вот и прошли новогодние каникулы и пришло время порадовать хабрачитателей продолжением истории про написания WinRT приложения на XAML/C# на примере простой RSS читалки.
Начало истории Ч.1 (создание приложения из шаблона, привязка и получение данных RSS, плитки разных размеров на начальной странице) и продолжение Ч.2 (разные плитки в разных группах, разные шаблоны для плиток, живые тайлы, контракт Share).
Отображение частично форматированного HTML
Если посмотреть на результат, который мы получили после второй части, видно, что мы всё сделали красиво, кроме отображения HTML. HTML из RSS мы просто чистим от тегов и отображаем в виде текста. Как базовое решение — это вполне приемлемый результат, однако, хотелось бы «сделать красиво» и здесь.
Для того, чтобы решить эту задачу — можно воспользоваться примером решения от atreides07, о котором можно кратко прочитать здесь, и которое доступно через NuGet.
Приступим. Для этого надо открыть проект с приложением и добавить в него, используя Library Package Manager, библиотеку с NuGet. Я воспользовался для этого консолью менеджера пакетов (Package Manager Console),
где выполнил команду:
Install-Package WinRTExtensions
и дождался установки библиотеки:
Теперь, надо добавить его в XAML-файл страницу, где должен отображаться HTML, убрать оттуда встроенную разбивку простого текста на колонки, а также убрать очистку HTML от тегов из метода: AddGroupForFeedAsync.
Добавляем на страницу ItemDetailPage.xaml:
<common:LayoutAwarePage
x:Name="pageRoot"
x:Class="MyReader.ItemDetailPage"
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyReader"
xmlns:data="using:MyReader.Data"
xmlns:common="using:MyReader.Common"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ext="using:WinRTExtensions"
mc:Ignorable="d">
Убираем разбивку текста на колонки и используем возможности расширения::
<RichTextBlock x:Name="richTextBlock" Width="560"
Style="{StaticResource ItemRichTextStyle}"
IsTextSelectionEnabled="False"
ext:RichTextBlockExtensions.HtmlContent="{Binding Content}">
<Paragraph>
<Run FontSize="26.667" FontWeight="Light" Text="{Binding Title}"/>
<LineBreak/>
<LineBreak/>
<Run FontWeight="Normal" Text="{Binding Subtitle}"/>
</Paragraph>
</RichTextBlock>
Убираем очистку содержимого RSS от HTML тегов в функции AddGroupForFeedAsync:
if (i.Summary != null)
clearedContent = i.Summary.Text;
else
if (i.Content != null)
clearedContent = i.Content.Text;
Теперь можно собрать и посмотреть, как теперь выглядит отображение постов:
Есть ещё огрехи, но в целом выглядит гораздо лучше.
Адаптированное отображение плиток
Когда мои коллеги отревьювили пример, который я написал, они мне попеняли, что на стандартных для планшета разрешениях, неэффективно используется начальный экран приложения — там слишком мало плиток с новостями. Давайте попытаемся начать движение в этом направлении, которое вы сможете закончить самостоятельно при желании.
Итак, самое простое, с чего можно начат — это уменьшить размер базовой плитки, соответствующим образом изменив шаблон.
Уменьшаем размер базовой плитки в 4 раза в файле GroupedItemsPage.xaml:
<GroupStyle.Panel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid Orientation="Vertical"
Margin="0,0,80,0"
ItemWidth="100"
ItemHeight="62"
MaximumRowsOrColumns="18" />
</ItemsPanelTemplate>
</GroupStyle.Panel>
Изменяем базовые шаблоны:
<DataTemplate x:Key="CustomItemTemplate">
<Grid HorizontalAlignment="Left">
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
<Image Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
</Border>
<StackPanel Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}" VerticalAlignment="Bottom">
<TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource ExtendedTitleTextStyle}" Height="45" Margin="15,0,15,0" FontSize="15" />
</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="CustomItemTemplate2">
<Grid HorizontalAlignment="Right">
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}">
<Image Source="{Binding Image}" Stretch="UniformToFill" AutomationProperties.Name="{Binding Title}"/>
</Border>
<StackPanel Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}" VerticalAlignment="Top">
<TextBlock Text="{Binding Title}" Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource ExtendedTitleTextStyle}" Height="45" Margin="15,0,15,0" FontSize="15" />
</StackPanel>
</Grid>
</DataTemplate>
Теперь на начальном экране при небольших разрешениях, например, при разрешении 1366х768, помещается больше плиток:
Это только первый шаг в создании адаптивного дизайна, но весь возможный инструментарий в виде шаблонов, таблицы с ячейками разных размеров, определения логики размеров ячеек у нас уже есть.
Контракт поиска
Пришло время добавить в приложение контракт поиска. Сделать это достаточно просто. Надо добавить в проект новый Item типа Search Contract:
При этом добавится страница отображения поиска, которую я назвал MySearchResultsPage.xaml.
Если после добавления контракта собрать приложение и попытаться по нему искать, отобразится именно эта страница. Причём, поскольку мы не добавили никакой логики отображения — она будет пустая.
Давайте теперь добавим логику, по которой будет происходить поиск. Для простоты мы будем отрабатывать сценарий поиска в уже работающем приложении, предполагая, что данные из RSS находятся в памяти приложения.
Сначала удалим лишнее определения названия приложения со страницы MySearchResultsPage.xaml, у нас эта переменная уже определена в App.xaml:
<Page.Resources>
<CollectionViewSource x:Name="resultsViewSource" Source="{Binding Results}"/>
<CollectionViewSource x:Name="filtersViewSource" Source="{Binding Filters}"/>
<common:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<!-- TODO: Update the following string to be the name of your app -->
</Page.Resources>
Далее, модифицируем класс Filer, который определён в MySearchResultsPage.xaml.cs, так, чтобы он отдавал список Item нашего типа:
private sealed class Filter<T> : MyReader.Common.BindableBase
{
private String _name;
private bool _active;
private List<T> _results;
public Filter(String name, IEnumerable<T> results, bool active = false)
{
this.Name = name;
this.Active = active;
this.Results = results.ToList();
}
public List<T> Results
{
get { return _results; }
set { if (this.SetProperty(ref _results, value)) this.OnPropertyChanged("Description"); }
}
public override String ToString()
{
return Description;
}
public String Name
{
get { return _name; }
set { if (this.SetProperty(ref _name, value)) this.OnPropertyChanged("Description"); }
}
public int Count
{
get { return _results.Count; }
}
public bool Active
{
get { return _active; }
set { this.SetProperty(ref _active, value); }
}
public String Description
{
get { return String.Format("{0} ({1})", _name, this.Count); }
}
}
Не забудьте добавить в блок using директиву:
using MyReader.Data;
Теперь собственно необходимо определить логику поиска в методе LoadState файла MySearchResultsPage.xaml.cs:
var filterList = new List<Filter<RSSDataItem>>(
from feed in RSSDataSource.AllGroups
select new Filter<RSSDataItem>(feed.Title,
feed.Items.Where(item => (item.Title != null && item.Title.Contains(queryText) ||
(item.Content != null && item.Content.Contains(queryText)))),
false));
filterList.Insert(0, new Filter<RSSDataItem>("All", filterList.SelectMany(f => f.Results), true));
А в обработчике Filter_SelectionChanged изменить обращение к фильтру в соответсвии с нашими изменениями выше, а также присвоить полученный результат this.DefaultViewModel[«Results»] :
var selectedFilter = e.AddedItems.FirstOrDefault() as Filter<RSSDataItem>;
this.DefaultViewModel["Results"] = selectedFilter.Results;
Если теперь запустить приложение, и воспользоваться чудо-кнопкой поиска, мы сможем увидеть следующее:
При выборе результатов поиска, у нас не происходит перехода на соответствующую страницу записи, поскольку мы не обрабатываем это событие. Давайте добавим этот функционал. Для этого определим обработчик события ItemClick для resultsGridView: :
<GridView
x:Name="resultsGridView"
AutomationProperties.AutomationId="ResultsGridView"
AutomationProperties.Name="Search Results"
TabIndex="1"
Grid.Row="1"
Margin="0,-238,0,0"
Padding="110,240,110,46"
SelectionMode="None"
IsSwipeEnabled="false"
IsItemClickEnabled="True"
ItemsSource="{Binding Source={StaticResource resultsViewSource}}"
ItemTemplate="{StaticResource StandardSmallIcon300x70ItemTemplate}"
ItemClick="resultsGridView_ItemClick">
И в коде:
private void resultsGridView_ItemClick(object sender, ItemClickEventArgs e)
{
var itemId = ((RSSDataItem)e.ClickedItem).UniqueId;
this.Frame.Navigate(typeof(ItemDetailPage), itemId);
}
Теперь можно запустить приложение и проверить, что всё работает, как ожидается.
Контракт настроек
Приложение мы пишем не просто так — мы хоти опубликовать его в Windows Store. Если прибавить к этому, что наше приложение ходит в интернет, и прочитать требования к сертификации приложений, окажется, что нам необходимо реализовать контакт настроек, который бы тем или иным образом указывал на Privacy Settings (Политику конфиденциальности).
В рамках примера, я буду использовать политику конфиденциальности блогов MSDN. При создании реального приложения, вам потребуется использовать соответствующие политики конфиденциальности.
Добавим контракт настроек (Settings) только для начальной страницы. Для этого переопределим методы OnNavigateTo и OnNavigateFrom, регистрирую и де-регистрирую обработчик для настроек:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
SettingsPane.GetForCurrentView().CommandsRequested -= Settings_CommandsRequested;
base.OnNavigatedFrom(e);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
SettingsPane.GetForCurrentView().CommandsRequested += Settings_CommandsRequested;
base.OnNavigatedTo(e);
}
Теперь определим обработчик Settings_CommandsRequested:
private void Settings_CommandsRequested(SettingsPane sender, SettingsPaneCommandsRequestedEventArgs args)
{
var viewPrivacyPage = new SettingsCommand("", "Privacy Statement", cmd =>
{
Launcher.LaunchUriAsync(new Uri("http://go.microsoft.com/fwlink/?LinkId=248681", UriKind.Absolute));
});
args.Request.ApplicationCommands.Add(viewPrivacyPage);
}
Он очень простой. Мы отображаем пункт настроек с названием Privacy Statement и, по нажатию на него, отправляем на Privacy Policy блогов MSDN.
Если теперь запустить приложение и выбрать на главной странице в чудо-панели настройки, мы увидим следующее:
Теперь мы фактически имеем пример приложения, который по практически готов к публикации в Windows Store. Не хватает обработки загрузки данных приложения и проверки состояния сети при старте приложения. В следующей части мы разберёмся, как добавить этот функционал, а также попробуем сделать наше приложение более общим.
Автор: stasus