Windows Phone Silverlight to Windows Runtime 8 case study: Bookstore2
[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]
Note For info about porting to a Universal Windows Platform (UWP) app for Windows 10, see Move from Windows Phone Silverlight to UWP.
This case study, which builds on the info given in Bookstore1, begins with a Windows Phone Silverlight app that displays grouped data in a LongListSelector. In the view model, each instance of the class Author represents the group of the books written by that author, and in the LongListSelector we can either view the list of books grouped by author or we can zoom out to see a jump list of authors. The jump list affords much quicker navigation than scrolling through the list of books. We walk through the steps of porting the app to a Universal Windows app (using the Windows Runtime, or WinRT).
Downloads
Download the Bookstore2WPSL8 Windows Phone Silverlight app.
Download the Bookstore2Universal Universal Windows app.
The Windows Phone Silverlight app
The illustration below shows what Bookstore2WPSL8—the app that we're going to port—looks like. It's a vertically-scrolling LongListSelector of books grouped by author. You can zoom out to the jump list and from there you can navigate back into any group. There are two main pieces to this app: the view model that provides the grouped data source, and the user interface that binds to that view model. As we'll see, both of these pieces port easily from Windows Phone Silverlight technology to the Windows Runtime.
Porting to a Universal App project
We begin in Visual Studio by creating a Universal App project named Bookstore2Universal. Next we need to copy files over from Bookstore2WPSL8 and include them in the Universal App project. Copy the following files to the Shared project node, making sure to retain the source folder hierarchy: the book cover image PNG files (Assets\CoverImages\*.*), the view model source file (ViewModel\BookstoreViewModel.cs), and MainPage.xaml. Keep the App.xaml, and App.xaml.cs that Visual Studio generated with the Universal App project. Move the MainPage.xaml.cs from the Windows project node to the Shared project node, and make sure all the source code and markup files are defining their types in the Bookstore2Universal
namespace. Delete any remaining copies of MainPage.xaml and MainPage.xaml.cs from the Windows and Windows Phone project nodes.
In the imperative code in the view model source file, these are the only porting changes needed:
- Change
System.ComponentModel.DesignerProperties
toDesignMode
and then use the Resolve command on it. Delete theIsInDesignTool
property and use IntelliSense to add the correct property name:DesignModeEnabled
. - Use the Resolve command on
ImageSource
. - Use the Resolve command on
BitmapImage
. - Delete
using System.Windows.Media;
andusing System.Windows.Media.Imaging;
. - Change the value returned by the Bookstore2Universal.BookstoreViewModel.AppName property from "BOOKSTORE2WPSL8" to "BOOKSTORE2UNIVERSAL".
- Just as we did for Bookstore1, update the implementation of the BookSku.CoverImage property (see Binding an Image to a view model).
In MainPage.xaml, these initial porting changes are needed:
- Change
phone:PhoneApplicationPage
toPage
(including the occurrences in property element syntax). - Delete the
phone
andshell
namespace prefix declarations. - Change "clr-namespace" to "using" in the remaining namespace prefix declaration.
- Delete
SupportedOrientations="Portrait"
, andOrientation="Portrait"
, and configure Portrait in the app package manifest in the Windows Phone project. - Delete
shell:SystemTray.IsVisible="True"
. - The types of the jump list item converters (which are present in the markup as resources) are now in the Windows.UI.Xaml.Controls.Primitives namespace. So, add the namespace prefix declaration Windows_UI_Xaml_Controls_Primitives and map it to Windows.UI.Xaml.Controls.Primitives. On the jump list item converter resources, change the prefix from
phone:
toWindows_UI_Xaml_Controls_Primitives:
. - Just as we did for Bookstore1, replace all references to the
PhoneTextExtraLargeStyle
TextBlock style with a reference toListViewItemTextBlockStyle
, replacePhoneTextSubtleStyle
withListViewItemSubheaderTextBlockStyle
, replacePhoneTextNormalStyle
withTitleTextBlockStyle
, and replacePhoneTextTitle1Style
withHeaderTextBlockStyle
. - In
AuthorGroupHeaderTemplate
, replace the reference to thePhoneFontFamilySemiBold
resource with the value"Segoe WP"
and addFontWeight="SemiBold"
. - Because of changes related to view pixels, go through the markup and multiply any fixed size dimension (margins, width, height, etc) by 0.8.
Replacing the LongListSelector
Replacing the LongListSelector with a SemanticZoom control will take several steps, so let's make a start on that. A LongListSelector binds directly to the grouped data source, but a SemanticZoom contains ListView or GridView controls, which bind indirectly to the data via a CollectionViewSource adapter. The CollectionViewSource needs to be present in the markup as a resource, so let's begin by adding that to the markup in MainPage.xaml inside <Page.Resources>
.
<CollectionViewSource
x:Name="AuthorHasACollectionOfBookSku"
Source="{Binding Authors}"
IsSourceGrouped="true"/>
Note that the binding on LongListSelector.ItemsSource becomes the value of CollectionViewSource.Source, and LongListSelector.IsGroupingEnabled becomes CollectionViewSource.IsSourceGrouped. The CollectionViewSource has a name (note: not a key, as you might expect) so that we can bind to it.
Next, replace the phone:LongListSelector
with this markup which will give us a preliminary SemanticZoom to work with:
<SemanticZoom>
<SemanticZoom.ZoomedInView>
<ListView
ItemsSource="{Binding Source={StaticResource AuthorHasACollectionOfBookSku}}"
ItemTemplate="{StaticResource BookTemplate}">
<ListView.GroupStyle>
<GroupStyle
HeaderTemplate="{StaticResource AuthorGroupHeaderTemplate}"
HidesIfEmpty="True"/>
</ListView.GroupStyle>
</ListView>
</SemanticZoom.ZoomedInView>
<SemanticZoom.ZoomedOutView>
<ListView
ItemsSource="{Binding CollectionGroups, Source={StaticResource AuthorHasACollectionOfBookSku}}"
ItemTemplate="{StaticResource ZoomedOutAuthorTemplate}"/>
</SemanticZoom.ZoomedOutView>
</SemanticZoom>
The LongListSelector notion of flat list and jump list modes is answered in the SemanticZoom notion of a zoomed-in and a zoomed-out view, respectively. As the values of each of those view properties we place a ListView bound to our CollectionViewSource. The zoomed-in view uses the same item template, group header template, and HideEmptyGroups setting (now named HidesIfEmpty) as the LongListSelector's flat list does. And the zoomed-out view uses an item template very much like the one inside the LongListSelector's jump list style (AuthorNameJumpListStyle
). Also note that the zoomed-out view binds to a special property of the CollectionViewSource named CollectionGroups, which is a collection containing the groups rather than the items.
We no longer need AuthorNameJumpListStyle
, at least not all of it. We only need the data template for the groups (which are authors in this app) in the zoomed-out view. So we delete the AuthorNameJumpListStyle
style and replace it with this data template:
<DataTemplate x:Key="ZoomedOutAuthorTemplate">
<Border Margin="9.6,0.8" Background="{Binding Converter={StaticResource JumpListItemBackgroundConverter}}">
<TextBlock Margin="9.6,0,9.6,4.8" Text="{Binding Group.Name}" Style="{StaticResource ListViewItemTextBlockStyle}"
Foreground="{Binding Converter={StaticResource JumpListItemForegroundConverter}}" VerticalAlignment="Bottom"
FontFamily="Segoe WP" FontWeight="SemiBold"/>
</Border>
</DataTemplate>
Note that, since the data context of this data template is a group rather than an item, we bind to a special property named Group.
The Windows Phone project will now build and run, although as you can see we need to do a little more styling and templating work. Here’s how the Windows Phone Store app looks:
Styling and templating the Windows Phone Store app
The group headers and the authors in the zoomed-out view are left-aligned instead of stretched, and they could use a bit more breathing space around them, so let's work on that. Create a HeaderContainerStyle for the zoomed-in view with HorizontalContentAlignment set to Stretch
. And create an ItemContainerStyle for the zoomed-out view containing that same Setter. Here's the result:
<Style x:Key="AuthorGroupHeaderContainerStyle" TargetType="ListViewHeaderItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
<Style x:Key="ZoomedOutAuthorItemContainerStyle" TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
...
<SemanticZoom.ZoomedInView>
<ListView
...
<ListView.GroupStyle>
<GroupStyle
...
HeaderContainerStyle="{StaticResource AuthorGroupHeaderContainerStyle}"
...
<SemanticZoom.ZoomedOutView>
<ListView
...
ItemContainerStyle="{StaticResource ZoomedOutAuthorItemContainerStyle}"
...
Also, edit AuthorGroupHeaderTemplate
and set a Margin of "0,0,0,9.6"
on the Border.
One thing that the LongListSelector does when showing the jump list is that it fades out the flat list. As you can see from the image above, the SemanticZoom isn't doing that, yet. But that's easy to remedy: we just need to set an appropriate Background on the zoomed-out view.
<SemanticZoom.ZoomedOutView>
<ListView
Background="#CC000000"
...
Edit BookTemplate
and set the Margin to "9.6,0"
on both TextBlocks.
Lastly, inside TitlePanel
, remove the top Margin on the second TextBlock by setting the value to "7.2,0,0,0"
.
Here’s the original Windows Phone Silverlight app again, on the left, and our ported Windows Phone Store app on the right:
Making the app universal
We now have a Windows Phone Store app version of Bookstore2Universal. To make this app into a Universal Windows app, we need a Windows Store app version of it, with the same functionality but with styling and layout tailored to make best use of the larger form factor of PCs and tablets. And that means doing a little refactoring. Let's begin by doing just enough refactoring to get the Windows project to build and run, and we'll take stock of how it looks after that.
Just like we did in Bookstore1, add new ResourceDictionary project items to the Windows and the Windows Phone project nodes, naming them both BookstoreStyles.xaml. Merge that new BookstoreStyles.xaml ResourceDictionary into MainPage.xaml, again using the same syntax we did last time. Now, move the jump list item converters, and the namespace prefix declaration that they need, out of MainPage.xaml and into the Windows Phone version of BookstoreStyles.xaml. Also move AuthorGroupHeaderTemplate
, ZoomedOutAuthorTemplate
, and BookTemplate
out of MainPage.xaml, and put a copy in both versions of BookstoreStyles.xaml.
In the Windows version of those data templates, we'll now need to remove some resource references so that we can run (we can get the styling right after that). So, delete the properties that reference PhoneAccentBrush
, ListViewItemTextBlockStyle
, JumpListItemBackgroundConverter
, JumpListItemForegroundConverter
, and ListViewItemSubheaderTextBlockStyle
. Also remove the FontFamily property values.
Lastly, in MainPage.xaml, refactor HeaderTextBlockStyle
into the same two PageTitleTextBlockStyle
s that we arrived at in Bookstore1. That's all we need to do to get the Windows Store apps to build and run. Here's a first look at it:
Styling the SemanticZoom for Windows
If you create a new Grid App (Windows) project in Visual Studio you'll notice that it uses a GridView (rather than the ListView that we've used so far Bookstore2) to make good use of the horizontal space available to a Windows app. We can very easily make a copy of the SemanticZoom markup that we already have, and change the ListViews to GridViews. To contain the forked markup, add new UserControl project items to the Windows and the Windows Phone project nodes, naming them both SeZoUC.xaml. Here are the steps to follow to do the last refactoring and styling tasks:
- Move the SemanticZoom and the CollectionViewSource out of MainPage.xaml, and put a copy in both versions of SeZoUC.xaml in place of the Grid. In the Windows version of SeZoUC.xaml, change the ListViews to GridViews (including the occurrence in property element syntax), and set a Margin on the SemanticZoom to
"116,137,40,46"
. - Move
AuthorGroupHeaderContainerStyle
andZoomedOutAuthorItemContainerStyle
out of MainPage.xaml and into the Windows Phone version of SeZoUC.xaml. In the Windows version of SeZoUC.xaml, delete theHeaderContainerStyle
andItemContainerStyle
properties. - Merge the BookstoreStyles.xaml ResourceDictionary into both versions of SeZoUC.xaml.
- In MainPage.xaml, where the SemanticZoom was, add
<Bookstore2Universal:SeZoUC/>
.
Running the Windows app now will show some nice improvement, but let's continue with the last styling tweaks.
- The groups in the zoomed-in view could use more breathing space around them; and the groups in the zoomed-out view would look better oriented horizontally. Creating and referencing a couple of items panel templates will give us the results we want. Here's how the markup looks:
<ItemsPanelTemplate x:Key="ZoomedInItemsPanelTemplate">
<ItemsWrapGrid Orientation="Vertical" GroupPadding="0,0,80,0"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="ZoomedOutItemsPanelTemplate">
<ItemsStackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
...
<SemanticZoom.ZoomedInView>
<GridView
...
ItemsPanel="{StaticResource ZoomedInItemsPanelTemplate}">
...
<SemanticZoom.ZoomedOutView>
<GridView
...
ItemsPanel="{StaticResource ZoomedOutItemsPanelTemplate}"
...
- In each version of BookstoreStyles.xaml, copy over the
BookTemplateTitleTextBlockStyle
andBookTemplateAuthorTextBlockStyle
styles that we created for Bookstore1. In the Windows version, change the based-on styles toSubtitleTextBlockStyle
andCaptionTextBlockStyle
, respectively. In the Windows Phone version, change references toListViewItemTextBlockStyle
toBookTemplateTitleTextBlockStyle
, and references toListViewItemSubheaderTextBlockStyle
toBookTemplateAuthorTextBlockStyle
. - In the Windows version of BookstoreStyles.xaml, replace the contents of
AuthorGroupHeaderTemplate
with<TextBlock Margin="8,-7,10,10" Style="{StaticResource SubheaderTextBlockStyle}" Text="{Binding Name}"/>
. And replace the contents ofZoomedOutAuthorTemplate
with:
<Grid HorizontalAlignment="Left" Width="250" Height="250" >
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"/>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}"
Style="{StaticResource SubheaderTextBlockStyle}"
Height="80" Margin="15,0" Text="{Binding Group.Name}"/>
</StackPanel>
</Grid>
- Also in the Windows version of BookstoreStyles.xaml, replace the contents of
BookTemplate
with:
<Grid HorizontalAlignment="Left" Width="250" Height="250">
<Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"/>
<Image Source="{Binding CoverImage}" Stretch="UniformToFill"/>
<StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundThemeBrush}">
<TextBlock Style="{StaticResource BookTemplateTitleTextBlockStyle}" TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" Margin="12,0,24,0" Text="{Binding Title}"/>
<TextBlock Style="{StaticResource BookTemplateAuthorTextBlockStyle}" Text="{Binding Author.Name}"
Foreground="{StaticResource ListViewItemOverlaySecondaryForegroundThemeBrush}" TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" Margin="12,0,12,12"/>
</StackPanel>
</Grid>
- In the Windows version of BookstoreStyles.xaml, add a new resource:
<Thickness x:Key="TitlePanelMargin">140,50,0,0</Thickness>
. In the Windows Phone version, add the same resource but with a value of9.6,13.6,0,22.4
. In MainPage.xaml, onTitlePanel
, replace the value of Margin with"{StaticResource TitlePanelMargin}"
.
This last sequence of changes leaves the Windows Phone Store app looking unchanged, and the Windows Store app looking like this:
Making the view model more flexible
This section contains an example of facilities that open up to us by virtue of having moved our app to use the Windows Runtime. Here, we explain optional steps that you can follow to make your view model more flexible when accessed via a CollectionViewSource. The view model (the source file is in ViewModel\BookstoreViewModel.cs) that we ported from the Windows Phone Silverlight app Bookstore2WPSL8 contains a class named Author, which derives from List<T>, where T is BookSku. That means that the Author class is a group of BookSku.
When we bind CollectionViewSource.Source to Authors, the only thing we're communicating is that each Author in Authors is a group of something. We leave it to the CollectionViewSource to determine that Author is, in this case, a group of BookSku. That works: but it's not flexible. What if we want Author to be both a group of BookSku and a group of the addresses where the author has lived? Author can't be both of those groups. But Author can have any number of groups. And that's the solution: use the has-a-group pattern instead of, or in addition to, the is-a-group pattern that we're using currently. Here's how:
- Change Author so that it no longer derives from List<T>.
- Add this field to Author:
private ObservableCollection<BookSku> bookSkus = new ObservableCollection<BookSku>();
. - Add this property to Author:
public ObservableCollection<BookSku> BookSkus { get { return this.bookSkus; } }
. - And of course we can repeat the above two steps to add as many groups to Author as we need.
- Change the implementation of the AddBookSku method to
this.BookSkus.Add(bookSku);
. - Now that Author has at least one group, we need to communicate to the CollectionViewSource which of those groups it should use. To do that, add this property to each of the two CollectionViewSources:
ItemsPath="BookSkus"
Those changes leave this app functionally unchanged, but you now know how you could extend Author, and the CollectionViewSource, should you need to. Let's make one last change to Author so that, if we use it without specifying CollectionViewSource.ItemsPath, a default group of our choosing will be used:
public class Author : IEnumerable<BookSku>
{
...
public IEnumerator<BookSku> GetEnumerator()
{
return this.BookSkus.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.BookSkus.GetEnumerator();
}
}
And now we can choose to remove ItemsPath="BookSkus"
if we like and the app will still behave the same way.
Conclusion
This case study involved a more ambitious user interface than the previous one. All of the facilities and concepts of the Windows Phone Silverlight LongListSelector—and more—were found to be available to a Windows Runtime app in the form of SemanticZoom, ListView, GridView, and CollectionViewSource. We showed how to re-use, or copy-and-edit, both imperative code and markup in a Universal Windows app to achieve functionality, UI, and interactions tailored for the PC, tablet, and phone form factors.
The next case study is Bookstore3, in which we look at even more flexible ways of generating and displaying grouped data, as well as binding to observable properties and commands.