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.