Shadows in Windows (UWP) XAML Applications – Part 1b – ThemeShadow in Lists

Originally, I had planned that the next post was going to talk about making use of the Shadow property and creating a custom shadow. However, following part 1 there was a bit of dialog online talking about the use of shadows in lists. I figured that it shouldn’t be too hard to extend my example … Continue reading “Shadows in Windows (UWP) XAML Applications – Part 1b – ThemeShadow in Lists”

Originally, I had planned that the next post was going to talk about making use of the Shadow property and creating a custom shadow. However, following part 1 there was a bit of dialog online talking about the use of shadows in lists. I figured that it shouldn’t be too hard to extend my example to use the ThemeShadow for items in a list.

Let’s start with a simple list of 1000 items which we’ll be displaying using a ListView – I’m sure there’s a better way to generate random list items but I was just hacking around, so didn’t feel like engineering anything more fancy.

public IList<string> StaticItems { get; } = BuildStaticItems();

private static List<string> BuildStaticItems()
{
    var list = new List<string>();
    const int max = 1000;
    for (int i = 0; i < max; i++)
    {
        list.Add($"{max} items - {i}");
    }
    return list;
}

Next up we need the XAML layout for the ListView:

<Grid x:Name="RootGrid">
    <Grid x:Name="ParentBackgroundGrid" />

    <ListView ItemContainerStyle="{StaticResource ThousandItemContainerStyle}"
                ItemsSource="{x:Bind StaticItems}">
        <ListView.ItemTemplate>
            <DataTemplate x:DataType="x:String">
                <TextBlock Text="{x:Bind }"
                            Margin="6" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Grid>

There are a couple of things to note here:

  • The ItemsSource is bound to the StaticItems property from the previous code block
  • The ItemTemplate is just a simple TextBlock with a Margin set to indent the text from the edge of the item template.
  • There is a ParentBackgroundGrid priors to the ListView which will render beneath the ListView and will be the surface where the shadows are rendered onto.
  • Lastly this snippet defines an ItemContainerStyle called ThousanItemContainerStyle which we’ll go into more detail in a sec.

Note that in this XAML we haven’t defined either an instance of the ThemeShadow and we haven’t set a background on the items in the list. These will both be defined as part of the ItemContainerStyle, so that we can take advantage of the visual states to adjust the elevation (i.e. the Z axis translate), and thus the shadow cast.

Ok, so here’s the ItemContainerStyle.

<Style x:Key="ThousandItemContainerStyle"
        TargetType="ListViewItem">
    <Setter Property="Padding"
            Value="6" />
    <Setter Property="HorizontalContentAlignment"
            Value="Stretch" />
    <Setter Property="VerticalContentAlignment"
            Value="Stretch" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListViewItem">
                <Grid x:Name="Root">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Pressed">
                                <VisualState.Setters>
                                    <Setter Target="ContentBackground.Elevation"
                                            Value="0" />
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="PressedSelected">
                                <VisualState.Setters>
                                    <Setter Target="ContentBackground.Elevation"
                                            Value="0" />
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <local:ItemBackground Elevation="32"
                                            x:Name="ContentBackground"
                                            ContentPadding="{TemplateBinding Padding}" />
                    <ContentPresenter Margin="{TemplateBinding Padding}" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Things to note:

  • I’ve removed a significant portion of the default ItemsContainerStyle you would normally get when you clone the built-in style. What I’m left with is a basic ContentPresenter element and a new UserControl of type ItemBackground.
  • ItemBackground has an initial Elevation of 32 but this property is adjusted when the user presses the button to control the elevation (see the Pressed and PressedSelected visual states), and thus the amount of shadow.

Of course, now we need to look at the ItemsBackground class so we can understand how it’s generating the shadow for each item. The XAML for the ItemBackground UserControl is similar to what we had in my previous post – a root Grid which defines the ThemeShadow resource and includes a Rectangle which is the background of the item. It’s the Rectangle that has the Shadow property set and will be generating the shadow that is to be cast.

<UserControl
    x:Class="ShadowTest.ItemBackground"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
    <Grid Loaded="RootLoaded">
        <Grid.Resources>
            <ThemeShadow x:Name="SharedShadow" />
        </Grid.Resources>
        <Rectangle x:Name="Rectangle2"
                   Fill="Turquoise"
                   Margin="{x:Bind ContentPadding, Mode=OneWay}"
                   Shadow="{StaticResource SharedShadow}" />
    </Grid>
</UserControl>

We’re intercepting the Loaded event on the Grid in order to connect the ThemeShadow with the corresponding Grid to be the receiver for the shadow (in this case the Grid is the ParentBackgroundGrid defined on the page). Note that this logic could definitely be improved. It steps up the visual tree from the current item (i.e. the Grid in the ItemTemplate) all the way up to the Grid defined on the page called RootGrid.

private void RootLoaded(object sender, RoutedEventArgs e)
{
    var parent = VisualTreeHelper.GetParent(this);
    while (parent != null)
    {
        if(parent is Grid grid)
        {
            if(grid.Name == "RootGrid")
            {
                var bg = grid.Children.FirstOrDefault(x => x is Grid g && g.Name == "ParentBackgroundGrid");
                SharedShadow.Receivers.Add(bg);
                return;
            }
        }
        parent = VisualTreeHelper.GetParent(parent);

    }
}

Again, remembering that we can’t specify a ancestor as a Receiver for the shadow. Instead we’re going to find the first child element with the name ParentBackgroundGrid. This Grid will then be set as a Receiver for the ThemeShadow.

As we want to be able to adjust the Z axis translate when the user presses on the item in the list, we need to expose a mechanism whereby the ItemContainerStyle can simply set a property in the visual state definition. We need to define two dependency properties: ContentPadding, which determines the inset of content, and Elevation, which will determine the z translate value.

public Thickness ContentPadding
{
    get { return (Thickness)GetValue(ContentPaddingProperty); }
    set { SetValue(ContentPaddingProperty, value); }
}

public static readonly DependencyProperty ContentPaddingProperty =
    DependencyProperty.Register("ContentPadding", typeof(Thickness), typeof(ItemBackground), new PropertyMetadata(new Thickness(0)));

public int Elevation
{
    get { return (int)GetValue(ElevationProperty); }
    set { SetValue(ElevationProperty, value); }
}

public static readonly DependencyProperty ElevationProperty =
    DependencyProperty.Register("Elevation", typeof(int), typeof(ItemBackground), new PropertyMetadata(0, ElevationChanged));

private static void ElevationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    (d as ItemBackground).ChangeElevation((int)e.NewValue);
}

private void ChangeElevation(int newValue)
{
    Rectangle2.Translation = new Vector3(0, 0, newValue);
}

Let’s give this a run – the following image shows the list of items with the shadow appearing for each item. The only difference between the left and right sides is that in the list on the left side, the item “1000 items – 3” has been pressed.

What’s interesting about applying the shadow to the list of items is that the items almost appear to have rounded corners, in addition to the apparent elevation from the background.

The code in this blog post is very raw and I’ve no doubt there is a way to abstract the various steps to make it easier to define and connect the ThemeShadow with the shadow receiver.

Shadows in Windows (UWP) XAML Applications – Part 1 – ThemeShadow

I’ve been watching James Montemagno bring his AnimalCrossing app to life. The progress continues: #AnimalCrossing Turnip Tracking App powered by #Xamarin and amazing library creators. 99.9% shared code 🙂 pic.twitter.com/7CSlmk6nCJ — James Montemagno – ? Live on Twitch (@JamesMontemagno) April 11, 2020 What’s super impressive is that this doesn’t look anything like a regular out … Continue reading “Shadows in Windows (UWP) XAML Applications – Part 1 – ThemeShadow”

I’ve been watching James Montemagno bring his AnimalCrossing app to life.

What’s super impressive is that this doesn’t look anything like a regular out of the box Xamarin.Forms application. The use of custom fonts, curves, rounded corners and shadows gives this app its own unique look. Unfortunately, it would appear that some of the libraries used aren’t supported on Windows (UWP).

This got me thinking about the use of shadows in UWP, which lead to me sinking time into investigating what is and isn’t support. In this series of posts I’ll walk through some of the options.

Built-in Shadows

Before we get into adding our own shadows to elements in our application, it’s worth noting that UWP already comes with a lot of built-in shadow support. The guidance with regards to using shadows in UWP is that “shadows should be used in a purposeful rather than aesthetic manner”. As such, the built-in shadows appear on elements such as popups and tooltips but don’t appear gratuitously on every list item. For example, the following tooltip has a shadow that makes it appear elevated in relation to the rest of the content.

Dark Mode

I also wanted to see what support there was for dark theme. After switching my Windows theme to dark mode it was evident that there isn’t really any support for shadows. For example, here’s the same tooltip in dark mode.

Whilst there is still some illusion of elevation, this predominately comes from the border that’s been added to the tooltip, rather than an actual shadow. There is definitely no visible shadow around the sides and top of the tooltip, like there is in the light theme.

Now you might be saying “well duh, there is a shadow, you just can’t see it on the black background”. To which, my response is that a shadow, when it comes to applications, is much less about a shadow cast by a light hitting and object, and much more about the illusion of elevation and vertical separation of elements (as described by the Microsoft documentation on Z-depth). As such, even in dark mode you’d expect to see some shadow that illustrates a vertical separation of elements.

WinUI 3

Before we move on, I thought I’d take a quick look at whether there are any difference with regards to shadows in WinUI 3 (grab the WinUI 3 templates to get started). The following image shows the exact same button and tooltip combination, this time using the controls from WinUI 3.

Note that other than a change to the shape and border on the tooltip, there’s really no difference to the shadow cast by the tooltip. The behaviour in dark mode it basically the same too. Although I must admit, do like the new look tooltip with the slightly rounded corners and more gradient border, more than the standard UWP look.

Shadows using ThemeShadow

Whilst support for shadows has been around for quite a while for UWP applications, it was only recent that the ThemeShadow class was added (Windows 10, version 1903 – v10.0.18362.0), making it easier for developers to add shadow support to their applications. To demonstrate this in action, we’ll use a rather contrived example of two overlapping Rectangles, as defined in the following XAML.

<Grid>
    <Rectangle x:Name="Rectangle1"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Top"
                HorizontalAlignment="Right" />
    <Rectangle x:Name="Rectangle2"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Bottom"
                HorizontalAlignment="Left" />
</Grid>

Running this example we can see that both Rectangle shapes appear as one connected shape, flat against the white background. We’ll use a ThemeShadow to elevate the Rectangle2 (bottom left) off the background, and provide some separation from Rectangle1 (top right).

We’ll start by creating a ThemeShadow instance as a Resource with the name SharedShadow. This will be set as the Shadow property on Rectangle2.

<Grid>
    <Grid.Resources>
        <ThemeShadow x:Name="SharedShadow" />
    </Grid.Resources>
    <Grid x:Name="BackgroundGrid" />
    <Rectangle x:Name="Rectangle1"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Top"
                HorizontalAlignment="Right" />
    <Rectangle x:Name="Rectangle2"
                Height="200"
                Width="200"
                Fill="Turquoise"
                VerticalAlignment="Bottom"
                HorizontalAlignment="Left" 
                Shadow="{StaticResource SharedShadow}" />
</Grid>

Note that creating this resource and setting the Shadow property, creates the shadow but it won’t display until you tell the application what elements should show the shadow. This is done by adding elements to the Receivers collection on the ThemeShadow resource.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    SharedShadow.Receivers.Add(BackgroundGrid);
}

It’s important to note here that you can’t add an ancestor of the element casting the shadow to the Receivers collection. This seems rather counter-intuitive since you’d quite often want the shadow to appear on the background behind the element casting the shadow (which would typically be the parent grid or panel). However, it’s also easy to solve – in the above XAML a Grid called BackgroundGrid was added on which the shadow can be cast.

Even after doing all this, if you run this example you still won’t see a shadow. This is because all the elements are all technically at the same elevation (i.e. they have the same Z axis value of 0) so there is no shadow to be cast. To fix this, we simply need to translate Rectangle2 in the Z dimension.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    SharedShadow.Receivers.Add(BackgroundGrid);	
    Rectangle2.Translation += new Vector3(0, 0, 32);
}

Now we’re getting somewhere. The following image shows a shadow around Rectangle2. However, it still shows the two rectangles as being a single shape.

The reason there is no shadow being shown on Rectangle1 is that we haven’t told the application that it should show the shadow there. As before, the fix for this is to add Rectangle1 to the Receivers list of the ThemeShadow resource.

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    SharedShadow.Receivers.Add(BackgroundGrid);	
    SharedShadow.Receivers.Add(Rectangle1);
    Rectangle2.Translation += new Vector3(0, 0, 32);
}

And there we have it – a shadow appearing on all sides of Rectangle2.

Of course, we couldn’t just leave it there as I’m sure you’re all wondering what happens in dark mode. Well, there’s good and bad news, as you can see in the following image.

The good news is that the shadow is still being created (see between the rectangles). The bad news is that the shadow hasn’t been inverted with the theme, meaning that you can’t see the shadow against the black background. This is seriously frustrating as it makes the ThemeShadow kind of pointless – it’s main premise was that it was suppose to handle everything for the developer.

Rounded Corners

Updated: I’d forgotten to add this last section on rounded corners in the initial post.

Going back to the designs that James had in the app he’s working on, the items all have rounded corners, giving it a softer, more casual look and feel. To do this in my simple example is relatively easy as I can just set the RadiusX and RadiusY properties.

<Rectangle x:Name="Rectangle2"
            Margin="50"
            Height="200"
            Width="200"
            Fill="Turquoise"
            VerticalAlignment="Bottom"
            HorizontalAlignment="Left"
            RadiusX="40"
            RadiusY="40"
            Shadow="{StaticResource SharedShadow}" />

But what happens to my shadow? Well, the answer is nothing good, as you can see from the following image

In addition to not being able to handle dark mode, it also appears that the ThemeShadow can’t handle non-square corners. According to the documentation, there’s also no support color. Overall the ThemeShadow is severely limited in the scenarios that it would be useful for.

Next up we’ll take a more in depth look at the Shadow property in UWP and how you can implement a shadow yourself.