Monthly Archives: November 2019

Introducing WinUI ItemsRepeater and Friends

In this article we’ll build a fluent NetFlix-inspired single-page UWP app on top of iTunes Movie Trailers data. The app uses some of the newer WinUI controls such as ItemsRepeater, TeachingTip, CommandBarFlyout, as well as classic XAML elements like MediaPlayerElement, acrylic brushes and animation. 

The app allows you to scroll horizontally and vertically through a list of movies per genre, select a movie, and play its trailer. The app is fully functional in touch, mouse, and keyboard mode. Here’s how it looks like – we decided to call it XamlFlix (the runners-up names were .NET Flix and WinUI tunes):

WinUISample

In a previous blog post we described WinUI as the future of XAML development.  For more details, take a look at this great session on “Windows App Development Roadmap: Making Sense of WinUI, UWP, Win32, .NET” from the Ignite event. For a summary of this session, check Paul Thurrot’s article from which we borrowed the following illustration:

win10-app-platform

Getting the data

Our sample app starts with getting the most recent iTunes Movie Trailers content. It is exposed as a public XML document that looks like this:

iTunesXml

We defined a class to represent a movie, with its title and the whereabouts of its poster image and QuickTime trailer:

public class Movie
{
    public string Title { get; set; }

    public string PosterUrl { get; set; }

    public string TrailerUrl { get; set; }
}

For convenience, movies are grouped by genre. Here’s the corresponding class:

public class Genre
{
    public string Name { get; set; }

    public List<Movie> Movies { get; set; }
}

Using HttpClient.GetStringAsyc() we fetch the XML from the internet. Then we Parse() it into an XDocument. First we get the genre names through a fancy XPATH expression using XPathSelectElements():

using (var client = new HttpClient())
{
    xml = await client.GetStringAsync("http://trailers.apple.com/trailers/home/xml/current.xml");
}

var movies = XDocument.Parse(xml);

var genreNames = movies.XPathSelectElements("//genre/name")
                .Select(m => m.Value)
                .OrderBy(m => m)
                .Distinct()
                .ToList();

Then we use some more of this XPATH and LINQ magic (Eat my shorts, JSON!) to query the movies per genre –a movie may appear in more than one genre- and immediately populate all the element collections:

foreach (var genreName in genreNames)
{
    _genres.Add(new Genre()
    {
        Name = genreName,
        Movies = movies.XPathSelectElements("//genre[name='" + genreName + "']")
            .Ancestors("movieinfo")
            .Select(m => new Movie()
            {
                Title = m.XPathSelectElement("info/title").Value,
                PosterUrl = m.XPathSelectElement("poster/xlarge").Value,
                TrailerUrl = m.XPathSelectElement("preview/large").Value
            })
            //.OrderBy(m => m.Title)
            .ToList()
    });
}

Here’s the DataTemplate that represents a Movie in the app. It’s just an image that we made clickable and focusable by placing it inside a button:

        <DataTemplate x:Key="MovieTemplate"
                      x:DataType="local:Movie">
            <Button Click="Movie_Click"
                    DataContext="{x:Bind}"
                    BorderThickness="0"
                    Padding="0"
                    CornerRadius="0">
                <Image Source="{x:Bind PosterUrl}"
                       PointerEntered="Element_PointerEntered"
                       PointerExited="Element_PointerExited"
                       Height="360"
                       Stretch="UniformToFill"
                       HorizontalAlignment="Stretch"
                       VerticalAlignment="Stretch"
                       ToolTipService.ToolTip="{x:Bind Title}">
                </Image>
            </Button>
        </DataTemplate>

Adding some WinUI components

Let’s bring in some WinUI components to create the visual foundation of the app. These components live in the Microsoft.UI.Xaml NuGet package:

WinUINuGet

After adding the NuGet package, don’t forget to import its styles and other WinUI resources. App.xaml is the best place to do this:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
            <!-- Other merged dictionaries here -->
        </ResourceDictionary.MergedDictionaries>
        <!-- Other app resources here -->
        <x:Double x:Key="ContentDialogMaxWidth">800</x:Double>
    </ResourceDictionary>
</Application.Resources>

Here comes ItemsRepeater

XamlFlix is built around collections: it displays a collection of genres that each display a collection of movies. ItemsRepeater is an ideal host for this: it’s a WinUI element that is designed to be used inside custom controls that display collections. It does not come with a default UI and it provides no policy around focus, selection, or user interaction. It supports virtualization so it allows you to deal with very large collections out of the box.

The main beef of the app’s UI are ItemsRepeater controls: there’s one that repeats the genres vertically, and genre comes a horizontal repeater on the movies.

We started with defining two  reusable StackLayout resources: 

<controls:StackLayout x:Key="HorizontalStackLayout"
                        Orientation="Horizontal" />
<controls:StackLayout x:Key="VerticalStackLayout"
                        Orientation="Vertical"
                        Spacing="0" />

An ItemsRepeater is a data-driven panel that does not come with its own scrolling infrastructure, so you may need to wrap it in a ScrollViewer. Here’s the declaration of the repeater for the Genre instances:

<ScrollViewer VerticalScrollBarVisibility="Auto"
                VerticalScrollMode="Auto"
                HorizontalScrollMode="Disabled"
                Grid.Row="2">
    <controls:ItemsRepeater x:Name="GenreRepeater"
                            ItemTemplate="{StaticResource GenreTemplate}"
                            Layout="{StaticResource VerticalStackLayout}"
                            HorizontalAlignment="Stretch"
                            VerticalAlignment="Stretch" />
</ScrollViewer>

Each genre has its name displayed on top of a horizontal list of Movie instances – again implemented as an ItemsRepeater inside a ScrollViewer:

<DataTemplate x:Key="GenreTemplate"
                x:DataType="local:Genre">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Image VerticalAlignment="Stretch"
                HorizontalAlignment="Left"
                Margin="6 0 0 0"
                Width="52"
                Source="/Assets/FilmStrip.png"
                Stretch="Fill"
                Grid.RowSpan="2" />
        <TextBlock Foreground="Silver"
                    FontSize="36"
                    Margin="66 0 0 0"
                    Text="{x:Bind Name}" />
        <ScrollViewer HorizontalScrollBarVisibility="Visible"
                        HorizontalScrollMode="Enabled"
                        VerticalScrollMode="Disabled"
                        Margin="66 0 0 0"
                        Grid.Row="1">
            <controls:ItemsRepeater ItemsSource="{x:Bind Movies}"
                                    ItemTemplate="{StaticResource MovieTemplate}"
                                    Layout="{StaticResource HorizontalStackLayout}" />
        </ScrollViewer>
    </Grid>
</DataTemplate>

After the content was loaded, we set its items source programmatically:

GenreRepeater.ItemsSource = Genres

The page then looks like this:

ItemsRepeater

Let’s move our focus to the behavior now.

On a touch screen the page reacts appropriately to horizontal and vertical panning. Mouse scrolling is a bit problematic: the movie repeaters take almost all the screen area and hence they get the mouse input and only trigger horizontal scrolling. So we decided to place a film strip on the left. It creates an area that facilitates vertical scrolling through the genres.

Hello TeachingTip

The mouse behavior is not very intuitive for first-time users, so we added a TeachingTip –a WinUI Flyout with an arrow- to draw the user’s attention to the film strip on the left.

Here’s its XAML declaration:

<controls:TeachingTip x:Name="ScrollTeachingTip"
                        Target="{x:Bind FakeTarget}"
                        Title="I looks like you are trying to scroll."
                        Subtitle="With the mouse on this filmstrip you'll scroll vertically."
                        PreferredPlacement="Right"
                        IsOpen="False">
    <controls:TeachingTip.IconSource>
        <controls:SymbolIconSource Symbol="Sort" />
    </controls:TeachingTip.IconSource>
</controls:TeachingTip>

The TeachingTip needs to point to the film strip as its a Target. While visually the strip looks like a whole, it’s actually composed of multiple images: it’s repeated per genre. That makes it hard to use as target for the teaching tip – at least declaratively.

We provided an alternative Target by placing a UI-less control center left of the page:

<ContentControl x:Name="FakeTarget"
                VerticalAlignment="Center"
                HorizontalAlignment="Left"
                Width="40"
                Grid.Row="2" />

Here’s the result, the teaching tip points right to the middle of the film strip:

TeachingTip

We couldn’t resist creating a version with an image of Clippy as HeroContent:

TeachingTipClippy

It doesn’t make sense to pop-up the tip while the movie images are still loading, so we delayed its appearance:

await Task.Delay(2000);
ScrollTeachingTip.IsOpen = true;

We covered both touch and mouse input for navigation. Let’s now take a look at keyboard input.

To our great surprise we observed that the page already behaved properly on keyboard input. Here’s how navigation with the arrow keys looks like – not bad for a lightweight control that does not support selection. The visual border around the movie is the focus rectangle of the button in the Movie data template:

ArrowNavigation

To highlight the movie under the mouse cursor, our first attempt was a ToolTip with the title. It’s hardly visible and it feels like HTML. Here’s an example (hint: it’s on Spice in disguise):

Tooltip

We went for a more fluent experience. When digging through the awesome XAML Controls Gallery we found this nice sample that animates the size of a button on hover (don’t try it out here, it’s just a screenshot):

XamlControlsGalleryAnimation

We decided to subtly grow and shrink the image under the mouse cursor on hovering. It only required a straight copy/paste from the gallery sample code. The code uses the Compositor to hook a SpringVector3NaturalMotionAnimation to the scale of the image. We increase it by 2% on PointerEntered and then shrink back on PointerExited:

private Compositor _compositor = Window.Current.Compositor;
private SpringVector3NaturalMotionAnimation _springAnimation;

private void CreateOrUpdateSpringAnimation(float finalValue)
{
    if (_springAnimation == null)
    {
        _springAnimation = _compositor.CreateSpringVector3Animation();
        _springAnimation.Target = "Scale";
    }

    _springAnimation.FinalValue = new Vector3(finalValue);
}

private void Element_PointerEntered(object sender, PointerRoutedEventArgs e)
{
    // Scale up a little.
    CreateOrUpdateSpringAnimation(1.02f);

    (sender as UIElement).StartAnimation(_springAnimation);
}

private void Element_PointerExited(object sender, PointerRoutedEventArgs e)
{
    // Scale back down.
    CreateOrUpdateSpringAnimation(1.0f);

    (sender as UIElement).StartAnimation(_springAnimation);
}

Here’s how the result looks like – the real animation is a lot smoother that the animated gif suggests:

ScaleAnimation

That looks decent, no? Let’s add some more functionality now.

Introducing CommandBarFlyout

The CommandBarFlyout is another WinUI control that lives up to its name: it is literally a CommandBar in a Flyout. It groups AppBarButton instances in primary and secondary commands that are applicable to a specific UI element.

XamlFlix uses a CommandBarFlyout to display the possible actions for the selected movie: play, buy, rate, …. For the sake of simplicity we hooked them all to the same event handler: whatever menu you select, you always get to play the movie trailer.

Here’s the declaration of the movie menu:

<controls:CommandBarFlyout x:Name="MovieCommands"
                            Placement="Right">
    <AppBarButton Label="Play"
                    Icon="Play"
                    ToolTipService.ToolTip="Play"
                    Click="Element_Click" />
    <AppBarButton Label="Info"
                    Icon="List"
                    ToolTipService.ToolTip="Info"
                    Click="Element_Click" />
    <AppBarButton Label="Download"
                    Icon="Download"
                    ToolTipService.ToolTip="Download"
                    Click="Element_Click" />
    <controls:CommandBarFlyout.SecondaryCommands>
        <AppBarButton Label="Buy"
                        Click="Element_Click" />
        <AppBarButton Label="Rate"
                        Click="Element_Click" />
    </controls:CommandBarFlyout.SecondaryCommands>
</controls:CommandBarFlyout>

The control comes with several FlyoutShowOptions to configure position, placement, and behavior. Here we define it to appear on top of the image, in an expanded state and grabbing the focus. The ShowAt() method is called when a button in a movie data template is clicked. It opens the menu for the targeted UI element:

private void Movie_Click(object sender, RoutedEventArgs e)
{
    FlyoutShowOptions options = new FlyoutShowOptions();
    options.ShowMode = FlyoutShowMode.Standard;
    options.Placement = FlyoutPlacementMode.Top;

    MovieCommands.ShowAt(sender as FrameworkElement, options);
}

This is how the menu looks like in XamlFlix (on top of Top Gun):

CommandBarFlyout

Here’s MediaPlayerElement

For playing the movie trailer there are not too much options. We went for the classic UWP MediaPlayerElement and placed it in a ContentDialog:

<ContentDialog x:Name="MediaPlayerDialog"
                Closing="MediaPlayerDialog_Closing"
                CloseButtonText="Close">
    <StackPanel>
        <TextBlock x:Name="TitleText"
                    Margin="0 0 0 20" />
        <MediaPlayerElement x:Name="Player"
                            AreTransportControlsEnabled="True"
                            MinWidth="600"
                            AutoPlay="True" />
    </StackPanel>
</ContentDialog>

The dialog needs more space than the maximum of 548 pixels that a popup normally gets in UWP, so you have to override ContentDialogMaxWidth in app.xaml:

<x:Double x:Key="ContentDialogMaxWidth">800</x:Double>

When an element of the command bar flyout is clicked, we open the dialog and create a MediaSource from the trailer’s URL. We need to Hide() the command bar flyout, since it lives in the same layer as the dialog:

private async void Element_Click(object sender, RoutedEventArgs e)
{
    // It stays on top of the dialog.
    MovieCommands.Hide();

    var movie = (sender as FrameworkElement)?.DataContext as Movie;
    var source = MediaSource.CreateFromUri(new Uri(movie.TrailerUrl));

    TitleText.Text = movie.Title;
    Player.Source = source;
    await MediaPlayerDialog.ShowAsync();
}

private void MediaPlayerDialog_Closing(ContentDialog sender, ContentDialogClosingEventArgs args)
{
    // Prevent the player to continue playing.
    Player.Source = null;
}

Here’s the resulting UI:

MediaPlayer

The styles of the current WinUI v2.2 do not apply to ContentDialog yet, that’s why the dialog itself and its buttons have no rounded corners. Don’t worry: this will change in WinUI v2.3.

Let’s sprinkle some Acrylic

These days, fluent apps need a touch of acrylic material. The XamlFlix UI area is almost entirely covered with movie poster images, so we decided to use an AcrylicBrush for the whole background:

<Page.Background>
    <AcrylicBrush BackgroundSource="HostBackdrop"
                    TintColor="{ThemeResource SystemColorBackgroundColor}"
                    TintOpacity="0.9"
                    FallbackColor="{ThemeResource ApplicationPageBackgroundThemeBrush}" />
</Page.Background>

We blended that background into the title bar:

private void ExtendAcrylicIntoTitleBar()
{
    CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true;
    ApplicationViewTitleBar titleBar = ApplicationView.GetForCurrentView().TitleBar;
    titleBar.ButtonBackgroundColor = Colors.Transparent;
    titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
}

In this setting Windows only draws the system buttons, and makes you responsible for displaying the app title. Here’s an appropriate (and reusable) window title declaration:

<TextBlock xmlns:appmodel="using:Windows.ApplicationModel"
            Text="{x:Bind appmodel:Package.Current.DisplayName}"
            Style="{StaticResource CaptionTextBlockStyle}"
            IsHitTestVisible="False"
            Margin="12 8 0 0" />

The Code

The XamlFlix sample app lives here on GitHub. For more WinUI samples also check the XAML Controls Gallery and the Windows Community Toolkit Sample App in the Store (sources are also on GitHub).