次の方法で共有


Windows Phone Silverlight to Windows Runtime 8 case study: Bookstore3

[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 is the third in a series showing how to port Windows Phone Silverlight apps to Universal Windows apps (using the Windows Runtime, or WinRT). It is intended to complement the porting techniques we learned in Bookstore1 and Bookstore2, and the app we'll look at here adds important new features to its view model and to its user interface.

  • The app uses a view model with a flatter class hierarchy. Specifically, the view model contains no class that represents a group of BookSku (in Bookstore2, that class was Author). This is typical of an app that accesses data from a cloud service. Instead, a flat collection of BookSku is maintained, and a LINQ expression is used to dynamically create a group from like property values of the BookSku objects.
  • This gives us the flexibility to group books by author and by genre (etc.) without needing special group classes such as Author and Genre (etc.). The app shows books grouped by author and by genre in a Pivot control.
  • The grouped-by-author page illustrates a new grouping technique. Books are grouped not by the author's full name (as they were in Bookstore2) but by the first letter of the author's surname. This, for example, groups books by both Steinbeck and Stevenson under "s". Similarly, the jump list shows only single letters, arranged in a grid, and styled to indicate which letters have items and which are empty.
  • The view contains an app bar, and the app demonstrates binding the view to properties and commands of the view model. Not only does the relevant code in the view model of the Windows Phone Silverlight app port over very easily to the Windows Runtime, but the binding story in the UI also becomes simpler and more toolable in the new platform.

Downloads

Download the Bookstore3WPSL8 Windows Phone Silverlight app.

Download the Bookstore3Universal Universal Windows app.

The Windows Phone Silverlight app

The illustration below shows what Bookstore3WPSL8—the app that we're going to port—looks like. If you download, build, and run the app then you can see it in action.

A Pivot control contains two LongListSelectors showing books grouped by author and by genre. On the app bar, appearing in the form of both buttons and menu items, are two commands. Move to the genre view and note that the Adventure group contains only one book title. Tap the whale's tail icon to invoke the command that adds a new book title to the list; this new book (Moby-Dick, by Herman Melville) immediately appears under the Adventure genre. The new book can also be found in the author view (under "m"). Next, on the author view, use the jump list to navigate to the "l" authors. Tap the "A" button and watch the name of one of the books in the list ("Atticus") change from its pre-publication title to a title that you'll recognize as more familiar. Again, this change is immediately updated in the view because the property being changed (BookSku.Title) is observable, and the view is bound to it. After a command has been executed, its CanExecute condition evaluates to false (where it was previously true). Once that happens, the app bar controls bound to that command immediately become disabled in response. This is the standard behavior associated with an implementation of the ICommand interface.

Let's look at how to port all these pieces from Windows Phone Silverlight technology to the Windows Runtime.

Porting to a Universal App project

In Visual Studio, create a Universal App project named Bookstore3Universal. Copy files over from Bookstore3WPSL8 and include them in the Universal App project (right-click and click Include In Project). Copy the following files to the Shared project node, making sure to retain the source folder hierarchy (except where indicated):

  • Book cover image PNG files (Assets\CoverImages\*.*).
  • App bar button icons (Assets\appbar.mobydick.rest.png and Assets\appbar.atticus.rest.png). But rename them by removing the ".rest" segment from both filenames.
  • View model source file (ViewModel\BookstoreViewModel.cs).
  • Property change notifier and relay command helper class source files (SdkHelper\*.cs).
  • MainPage.xaml.
  • Copy Resources\AppResources.resx, but rename the folder and the file, and change the file extension, to give: LocalizedStrings\en-US\Resources.resw. Confirm that Build Action is set to PRIResource and Copy to Output Directory is set to Do not copy.

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 Bookstore3Universal 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, begin by making these porting changes:

  • Change System.ComponentModel.DesignerProperties to DesignMode and then use the Resolve command on it. Delete the IsInDesignTool 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;, using System.Windows.Media.Imaging;, and using Microsoft.Phone.Globalization;.
  • Change the value returned by the Bookstore2Universal.BookstoreViewModel.AppName property from "BOOKSTORE3WPSL8" to "BOOKSTORE3UNIVERSAL".
  • Just as we did previously, update the implementation of the BookSku.CoverImage property (see Binding an Image to a view model).

In SdkHelper\PropertyChangedNotifier.cs:

  • Use the Resolve command on DependencyObject, and delete using System.Windows;.

In MainPage.xaml, these initial porting changes are needed:

  • Change phone:PhoneApplicationPage to Page (including the occurrences in property element syntax).
  • Delete the phone and shell namespace prefix declarations.
  • Change "clr-namespace" to "using" in the remaining namespace prefix declaration.
  • Delete SupportedOrientations="Portrait", and Orientation="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: to Windows_UI_Xaml_Controls_Primitives:.
  • Just as we did previously, replace all references to the PhoneTextExtraLargeStyleTextBlock style with a reference to ListViewItemTextBlockStyle, and replace PhoneTextSubtleStyle with ListViewItemSubheaderTextBlockStyle.
  • In AuthorGroupHeaderTemplate, replace the reference to the PhoneFontFamilySemiLight resource with the value "Segoe WP" and add FontWeight="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.
  • Remove the phone: prefix from phone:Pivot and from phone:PivotItem.

Replacing the SortedLocaleGrouping

Bookstore3WPSL8 uses the SortedLocaleGrouping class, which can generate a sorted list of alphabetic group keys for any locale. Bookstore3WPSL8 uses the class to generate the group keys for the first letters of authors' surnames, so it really only needs keys for the en-us locale.

SortedLocaleGrouping is not available in the Windows Runtime, though, so we'll have to improvise and write a similar class which, for parity, only needs to support the en-us locale. If you've downloaded the Bookstore3Universal project files then you'll find that class—named SortedEnGrouping—in the view model source file. The class is short but we won't list it out here; just copy the source code for the class into the project that you have in-progress in Visual Studio (paste it in the same place, at the top of ViewModel\BookstoreViewModel.cs).

Refactor the name of the GroupByHelper<T>.CreateSortedLocaleGroupingByInitial method to CreateSortedEnGroupingByInitial, refactor to remove the CultureInfo parameter from that method, and change the method's implementation to construct a SortedEnGrouping using its default constructor. Also, in that same implementation, change ci to CultureInfo.CurrentUICulture.

Using pure LINQ instead of GroupByHelper

We ported over the GroupByHelper<T> class from Bookstore3WPSL8, and we're currently using it to implement both BookstoreViewModel.Authors and BookstoreViewModel.Genres. Authors is special in that we need some custom code to generate the set of alphabetic letters for those groups. But Genres is a very simple collection of groups of BookSku, and we can create one of those using pure LINQ—we don't need a group-by helper class nor any other kind of helper for that case. And the majority of data-grouping cases will be like Genres, so let's see how we can take advantage of the more flexible binding options that moving to the Windows Runtime gives us and simplify that implementation to eliminate the dependency on GroupByHelper<T>.

First, change the assignment statement in the implementation of the BookstoreViewModel.Genres property. Instead of calling the helper class, just use this very simple LINQ expression:

    this.genres = from book in this.bookSkus group book by book.genre into grp orderby grp.Key select grp;

Next, change the type of the property from List<GroupByHelper<BookSku>> to IOrderedEnumerable<IGrouping<string, BookSku>>. You'll also similarly need to change the type of the private genres field.

The LINQ expression generates an object that implements IGrouping (in another example of the is-a-group pattern that we described in Bookstore2) and it returns a collection of those group objects. In this case, as you can see from the type parameters of the IGrouping, each group is a collection of BookSku grouped by the same string key (the name of the genre).

You can now delete the GroupByHelper<T>.CreateSortedGroupingByKeysPresent method. And remember that for a simple app that needs only simple groupings and uses LINQ in this way, you would not need the GroupByHelper<T> and SortedEnGrouping classes at all.

Replacing the ApplicationBar

This section highlights an important benefit of moving this app to the Windows Runtime. We can now use the CommandBar and AppBarButton controls, and that means that we can bind our UI directly to commands on our view model (that is, properties of type ICommand). It also means that we can use x:Uid attributes in markup to directly reference localized strings. Consequently, we can dispense with the nearly 100 lines of imperative code that we needed in Bookstore3WPSL8 in MainPage.xaml.cs. That code performed services that are now part of the Windows Runtime platform itself, so there's no need to do that work in our app anymore. And that's great: code that you don't need to write is code that you don't need to test nor maintain. Here are the steps to replace the ApplicationBar with a CommandBar.

  • Change Page.ApplicationBar to Page.BottomAppBar.
  • Change shell:ApplicationBar to CommandBar.
  • Replace the first shell:ApplicationBarIconButton with this markup:
    <AppBarButton x:Uid="MobyStringShort" Command="{Binding MobyDickCommand}">
        <AppBarButton.Icon>
            <BitmapIcon UriSource="Assets/appbar.mobydick.png"/>
        </AppBarButton.Icon>
    </AppBarButton>
  • Replace the second shell:ApplicationBarIconButton with similar markup, except set x:Uid to AtticusStringShort, bind to AtticusCommand, and use Assets/appbar.atticus.png.
  • The x:Uid attributes refer to the names of string resources in LocalizedStrings\en-US\Resources.resw. But here we want to interpret the values of those resources as our AppBarButton.Label property values. To achieve that, open Resources.resw and edit the names of the four label resources. Change AtticusStringLong to AtticusStringLong.Label, and so on. See Localization and globalization
  • Replace the shell:ApplicationBar.MenuItems element and its inner xml with this markup:
    <CommandBar.SecondaryCommands>
        <AppBarButton x:Uid="MobyStringLong" Command="{Binding MobyDickCommand}">
            <AppBarButton.Icon>
                <BitmapIcon UriSource="Assets/appbar.mobydick.png"/>
            </AppBarButton.Icon>
        </AppBarButton>
    </CommandBar.SecondaryCommands>
  • Notice that the AppBarButton here is virtually the same as the first two (this one's x:Uid is referencing a longer version of the label string) except it is set as the SecondaryCommands property of the CommandBar. And that causes it to render as a menu item in a Windows Phone Store app. The default content property of CommandBar is PrimaryCommands, and an AppBarButton placed inside that property (like the earlier ones) renders as a button.
  • Add a second AppBarButton to SecondaryCommands, except set x:Uid to AtticusStringShort, bind to AtticusCommand, and use Assets/appbar.atticus.png.

Replacing the LongListSelectors

The steps to replace the LongListSelectors with SemanticZoom controls are similar to those in Bookstore2, so we should be able to move through them quickly. Add two CollectionViewSources to MainPage.xaml inside <Page.Resources>.

    <CollectionViewSource
        x:Name="AuthorIsACollectionOfBookSku"
        Source="{Binding Authors}"
        IsSourceGrouped="true"/>

    <CollectionViewSource
        x:Name="GenreIsACollectionOfBookSku"
        Source="{Binding Genres}"
        IsSourceGrouped="true"/>

Replace the two phone:LongListSelectors inside the PivotItems with this markup:

    <SemanticZoom>
        <SemanticZoom.ZoomedInView>
            <ListView
                ItemsSource="{Binding Source={StaticResource AuthorIsACollectionOfBookSku}}"
                ItemTemplate="{StaticResource BookTemplate}">
                <ListView.GroupStyle>
                    <GroupStyle
                        HeaderTemplate="{StaticResource AuthorGroupHeaderTemplate}"
                        HidesIfEmpty="True"/>
                </ListView.GroupStyle>
            </ListView>
        </SemanticZoom.ZoomedInView>
        <SemanticZoom.ZoomedOutView>
            <ListView
                ItemsSource="{Binding CollectionGroups, Source={StaticResource AuthorIsACollectionOfBookSku}}"
                ItemTemplate="{StaticResource ZoomedOutAuthorTemplate}"/>
        </SemanticZoom.ZoomedOutView>
    </SemanticZoom>

    ...

    <SemanticZoom>
        <SemanticZoom.ZoomedInView>
            <ListView
                ItemsSource="{Binding Source={StaticResource GenreIsACollectionOfBookSku}}"
                ItemTemplate="{StaticResource BookTemplate}">
                <ListView.GroupStyle>
                    <GroupStyle
                        HeaderTemplate="{StaticResource GenreGroupHeaderTemplate}"
                        HidesIfEmpty="True"/>
                </ListView.GroupStyle>
            </ListView>
        </SemanticZoom.ZoomedInView>
        <SemanticZoom.ZoomedOutView>
            <ListView
                ItemsSource="{Binding CollectionGroups, Source={StaticResource GenreIsACollectionOfBookSku}}"
                ItemTemplate="{StaticResource ZoomedOutGenreTemplate}"/>
        </SemanticZoom.ZoomedOutView>
    </SemanticZoom>

Delete the AuthorNameJumpListStyle and GenreNameJumpListStyle styles and replace them with these data templates:

    <DataTemplate x:Key="ZoomedOutAuthorTemplate">
        <Border Margin="5" Background="{Binding Converter={StaticResource JumpListItemBackgroundConverter}}" Width="86" Height="86">
            <TextBlock Margin="9.6,0,9.6,4.8" Text="{Binding Group.Key}" Style="{StaticResource ListViewItemTextBlockStyle}"
                Foreground="{Binding Converter={StaticResource JumpListItemForegroundConverter}}" VerticalAlignment="Bottom"
                FontWeight="SemiBold"/>
        </Border>
    </DataTemplate>

    <DataTemplate x:Key="ZoomedOutGenreTemplate">
        <Border Margin="9.6,0.8" Background="{Binding Converter={StaticResource JumpListItemBackgroundConverter}}">
            <TextBlock Margin="9.6,0,9.6,4.8" Text="{Binding Group.Key}" Style="{StaticResource ListViewItemTextBlockStyle}"
                Foreground="{Binding Converter={StaticResource JumpListItemForegroundConverter}}" VerticalAlignment="Bottom"
                FontWeight="SemiBold"/>
        </Border>
    </DataTemplate>

The Windows Phone project will now build and run, and it is already functionally equivalent to the Windows Phone Silverlight app including commands that work the same way. But, 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 zoomed-out views need some work. When we removed the author jump list style from Bookstore3WPSL8, we lost the LayoutMode property that it provided, which was set to Grid. That setting was what caused the alphabetic letters to be wrapped around into a grid. To get the same effect, we need to edit the zoomed-out view of the authors SemanticZoom and add an items panel template containing the right layout panel. Like this:

    <ListView
        ...
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <ItemsWrapGrid Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
    </ListView>

For the genres SemanticZoom, we need the same HeaderContainerStyle and ItemContainerStyle fixes that we needed for authors in Bookstore2. This time we'll use a more condensed form of markup, and put the styles inline in the controls. Here's how that looks:

    <SemanticZoom.ZoomedInView>
        <ListView
            ...
            <ListView.GroupStyle>
                <GroupStyle
                    ...
                    <GroupStyle.HeaderContainerStyle>
                        <Style TargetType="ListViewHeaderItem">
                            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        </Style>
                    </GroupStyle.HeaderContainerStyle>
                    ...

    <SemanticZoom.ZoomedOutView>
        <ListView
            ...
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                </Style>
            </ListView.ItemContainerStyle>
            ...

Edit AuthorGroupHeaderTemplate and, on the Border, set the Margin to "0,0,0,9.6" and remove the HorizontalAlignment, because that's redundant now. Change the attributes on the TextBlock so that it looks like this:

    <TextBlock Margin="9.6,0,9.6,4.8" Text="{Binding Key}" Style="{StaticResource ListViewItemTextBlockStyle}"
        VerticalAlignment="Bottom" FontWeight="SemiBold"/>

Make the same changes to GenreGroupHeaderTemplate that you just made to AuthorGroupHeaderTemplate.

Edit BookTemplate and set the Margin to "9.6,0" on both TextBlocks.

Lastly, set the appropriate Background on the authors and on the genre zoomed-out views, like this:.

    <SemanticZoom.ZoomedOutView>
        <ListView
            Background="#CC000000"
            ...

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

Most of the refactoring steps that we followed in Bookstore2 also apply to taking the Windows Phone Store app version of Bookstore3Universal and making it into a Universal Windows app by adding a Windows Store app version. Read on for extra steps and details that are particular to Bookstore3Universal, and then bear them in mind while referring back to Bookstore2/Making the app universal and Bookstore2/Styling the SemanticZoom for Windows and applying the steps there to this app, too. That way you'll be able to take Bookstore3Universal across the finishing line. At any time you can download the the Bookstore3Universal project files and use them for reference.

Note  

Be aware that the Windows Store app version of Bookstore3Universal won't build and run yet after the Bookstore2/Making the app universal stage (as version 2 did). Also, you'll need to interpret the steps in terms of there being two SemanticZoom controls this time. So for example there will be two group header templates to move, rather than one. There will be other slight differences, so just make the changes that make sense, being sure to understand why you're making each one.

 

Instead of naming the new UserControl project items SeZoUC.xaml, name them PivotUC.xaml. And this time, because there are now two CollectionViewSource resources, move those out into a new ResourceDictionary project item in the Shared project node (name it SharedResources.xaml), and merge that new ResourceDictionary into both PivotUC.xaml files.

In the Windows version of PivotUC.xaml, we have some changes to make to replace the Pivot control (Pivot isn't available in a Windows Store app) with a simple alternative that still functions as a pivot between two views.

  • Delete the Pivot and PivotItem markup to leave the two SemanticZoom controls side-by-side with their Visibility set to Visible and Collapsed, and their x:Name set to "AuthorSemanticZoom" and "GenreSemanticZoom", respectively.
  • Wrap the two SemanticZoom controls inside a Grid like this:
    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="116,137,40,46">
        ...
    </Grid>
  • Next, open the PivotUC.xaml file in the Windows project node of the the Bookstore3Universal project files so that you can copy some markup from that file and paste it into the PivotUC.xaml that you're working in. Copy and paste TitlePanel immediately above ContentPanel. This replaces the Pivot control's title, and also gives us radio buttons that we can use to pivot between the group-by-author and group-by-genre views.
  • Copy the opening and closing tags of LayoutRoot to enclose both TitlePanel and ContentPanel. Also copy and paste the RowDefinitions and VisualStateGroups markup. These pivot visual states control the visibility of AuthorSemanticZoom and GenreSemanticZoom.
  • Copy and paste PivotItemRadioButtonStyle to give the radio buttons a simple, typographic style.
  • Copy and paste the two event handlers (and the assignment in the constructor) from the code-behind file PivotUC.xaml.cs. These event handlers respond to interaction with the radio buttons, and they cause the pivot visual state to switch in response.
  • The zoomed-out view of AuthorSemanticZoom has an items panel template, which contains an ItemsWrapGrid. Add the attribute MaximumRowsOrColumns="7" to the ItemsWrapGrid so that the alphabetic letters in the zoomed-out view are arranged in 4 rows of 7 columns.

Windows: hiding the secondary commands

As we mentioned earlier, an AppBarButton placed in the SecondaryCommands property of CommandBar is rendered as a menu item in a Windows Phone Store app. On Windows, things work slightly differently: SecondaryCommands is used to align buttons on the left-hand side of the device, while the default PrimaryCommands property causes its button contents to be aligned on the right. So, for this app, we want to hide the buttons in SecondaryCommands when we're running on Windows. One solution is to make and maintain two copies of the command bar. But here's an alternative:

First, add a new property to ViewModel\BookstoreViewModel.cs:

    using Windows.UI.Xaml;
    ...
    public class BookstoreViewModel : PropertyChangedNotifier
    {
        ...
        public Visibility CollapsedIfWindowsApp
        {
            get
            {
#if WINDOWS_APP
                return Visibility.Collapsed;
#else
                return Visibility.Visible;
#endif // WINDOWS_APP
            }
        }
    }

Then bind the Visibility of the two SecondaryCommands buttons to that property by adding Visibility="{Binding CollapsedIfWindowsApp}". I chose the name CollapsedIfWindowsApp for the property to indicate that it's general enough to re-use for any case where you want to collapse an element on Windows.

Windows: a jump list item background

As you can see in the screen-shots above, our Windows Phone Store app colors each group in the zoomed-out views according to whether the group contains items or not. It uses jump list item converters to do that, but those types are not available in a Windows Store app. Instead, let's add a new property to ViewModel\BookstoreViewModel.cs:

    public class GroupByHelper<T> : List<T>
    {
        ...
        public Visibility GroupHasItemsVisibility
        {
            get
            {
                return (this.Count == 0) ? Visibility.Collapsed : Visibility.Visible;
            }
        }
    }

Now we can drop a theme-colored background element into our zoomed-out author template and bind its Visibility to our new property:

    <DataTemplate x:Key="ZoomedOutAuthorTemplate">
        <Grid HorizontalAlignment="Left" Width="150" Height="86" >
            <Border Background="{StaticResource ListViewItemPlaceholderBackgroundThemeBrush}"/>
            <Grid Background="{StaticResource ListViewItemSelectedBackgroundThemeBrush}" Visibility="{Binding Group.GroupHasItemsVisibility}"/>
            <TextBlock Foreground="{StaticResource ListViewItemOverlayForegroundThemeBrush}" Style="{StaticResource SubheaderTextBlockStyle}"
                Margin="15,0" Text="{Binding Group.Key}"/>
        </Grid>
    </DataTemplate>

These last changes leave the Windows Phone Store app looking unchanged, and the Windows Store app looking like this:

Conclusion

This third case study involved a view model and data classes more typical of an app that accesses data from a cloud service. It used a LINQ expression to dynamically and flexibly generate an object at run-time to represent a group of items. The case study demonstrated an alphabetically-sorted jump list, and an app bar (and other UI) bound to properties and commands of the view model. We saw how easily the view model code could be ported, and how it was even possible to eliminate large sections of code thanks to new Windows Runtime facilities. Whenever a facility was absent from the Windows Runtime, or wasn't available in a Windows Store app, we were able quickly to build an alternative. We again 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.