If you've worked with XAML for a while and customized controls like Buttons, ListView, etc., you are familiar with what happens when you use Visual Studio's Edit Template functionality. You get a copy of the entire underlying Template or Style of the control. This can be many lines of code added to your XAML which drowns out the important stuff. Then you want to change one or two properties in that pile of XAML. Hardly worth it. In this post, I offer you a way to help with that and make your code more readable and maintainable.
Background
Visual Studio (I'm using 2019) gives users help in customizing built in controls and it's great functionality giving you everything you need to customize any of their build in controls. In this tip, we will be creating a <ListView/>
with some styling. It will be a list of items, each with a FontIcon
in front of a gray circle.
- Here, we have the
ListView
in its simplest form:
<ListView/>
A wonder to behold, but by itself resides renders nothing. We style the items in the list by modifying the ItemTemplate
...
- Right click the
ListView
in the Document Outline - Select Edit Additional Templates->Edit Generated Items (ItemTemplate)->Create Empty...
<DataTemplate x:Key="ListViewItemTemplate1">
<Grid/>
</DataTemplate>
...
<ListView ItemTemplate="{StaticResource ListViewItemTemplate1}"/>
Still, pretty nice. The ListView
's ItemTemplate
is pretty concise and well suited for modification. Let's add some styling...
- Add the
Ellipse
and FontIcon
. Also, add some data so each FontIcon
has a character to bind to.
<DataTemplate x:Key="ListViewItemTemplate1">
<Grid>
<Ellipse Width="80" Height="80" Fill="#16808080" />
<FontIcon Glyph="{Binding}" FontSize="36"
VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>
</DataTemplate>
...
<ListView ItemTemplate="{StaticResource ListViewItemTemplate1}>
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
</ListView>
Now it's a vertical list of circles with Glyphs in front of them. Nice, but we want them horizontal and wrapping in case the user has the gall to resize the window.
- Right click the
ListView
in the Document Outline. - Select Edit Additional Templates->Edit Layout Items (ItemsPanel)->Edit a Copy...
...
<ItemsPanelTemplate x:Key="ListViewItemsPanel1">
<ItemsStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
...
<ListView ItemTemplate="{StaticResource ListViewItemTemplate1}
ItemsPanel="{StaticResource ListViewItemsPanel1}">
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
</ListView>
Again, pretty concise and well suited for modification. Now let's make it wrap.
- Replace the
ItemsStackPanel
with ItemsWrapGrid
.
...
<ItemsPanelTemplate x:Key="ListViewItemsPanel1">
<ItemsWrapGrid Orientation="Horizontal" />
</ItemsPanelTemplate>
...
If you made it this far, you're probably wondering what's your point? Hang in there. I'm almost there. Here's a little nugget of value add to tide you over. I like things compartmentalized. The ListView
does its thing and a Style
does the styling of it. You know, separation of responsibility.
<Style x:Key="CircleGlyphItem" TargetType="ListView">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<Ellipse Width="80" Height="80" Fill="#16808080" />
<FontIcon Glyph="{Binding}" FontSize="36"
VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style>
...
<ListView Style="{StaticResource CircleGlyphItem}">
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
</ListView>
Looks great if I do say so myself, but it could be better. I'd like a bit more space between the items. In order to do this, you need to modify the ItemContainerStyle
. This step produces something that is not at all concise and well suited for modification. And, all I want to do is add a little margin.
Interesting Part
- Right click the
ListView
in the Document Outline. - Select Edit Additional Templates->Edit Generated Items Container (ItemContainerStyle)->Edit a Copy...
<Style x:Key="ListViewItemContainerStyle1" TargetType="ListViewItem">
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
<Setter Property="Background" Value="{ThemeResource ListViewItemBackground}"/>
<Setter Property="Foreground" Value="{ThemeResource ListViewItemForeground}"/>
<Setter Property="TabNavigation" Value="Local"/>
<Setter Property="IsHoldingEnabled" Value="True"/>
<Setter Property="Padding" Value="12,0,12,0"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="MinWidth" Value="{ThemeResource ListViewItemMinWidth}"/>
<Setter Property="MinHeight" Value="{ThemeResource ListViewItemMinHeight}"/>
<Setter Property="AllowDrop" Value="False"/>
<Setter Property="UseSystemFocusVisuals"
Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="FocusVisualMargin" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<ListViewItemPresenter x:Name="Root"
CheckBrush="{ThemeResource ListViewItemCheckBrush}"
ContentMargin="{TemplateBinding Padding}"
CheckBoxBrush="{ThemeResource ListViewItemCheckBoxBrush}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CheckMode="{ThemeResource ListViewItemCheckMode}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource ListViewItemDragBackground}"
DragForeground="{ThemeResource ListViewItemDragForeground}"
FocusBorderBrush="{ThemeResource ListViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusSecondaryBorderBrush="{ThemeResource
ListViewItemFocusSecondaryBorderBrush}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Control.IsTemplateFocusTarget="True"
PressedBackground="{ThemeResource ListViewItemBackgroundPressed}"
PlaceholderBackground="{ThemeResource ListViewItemPlaceholderBackground}"
PointerOverForeground="{ThemeResource ListViewItemForegroundPointerOver}"
PointerOverBackground="{ThemeResource ListViewItemBackgroundPointerOver}"
RevealBorderThickness="{ThemeResource
ListViewItemRevealBorderThemeThickness}"
ReorderHintOffset="{ThemeResource ListViewItemReorderHintThemeOffset}"
RevealBorderBrush="{ThemeResource ListViewItemRevealBorderBrush}"
RevealBackground="{ThemeResource ListViewItemRevealBackground}"
SelectedForeground="{ThemeResource ListViewItemForegroundSelected}"
SelectionCheckMarkVisualEnabled="{ThemeResource
ListViewItemSelectionCheckMarkVisualEnabled}"
SelectedBackground="{ThemeResource ListViewItemBackgroundSelected}"
SelectedPressedBackground="{ThemeResource
ListViewItemBackgroundSelectedPressed}"
SelectedPointerOverBackground="{ThemeResource
ListViewItemBackgroundSelectedPointerOver}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Selected"/>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="Root.(RevealBrush.State)"
Value="PointerOver"/>
<Setter Target="Root.RevealBorderBrush"
Value="{ThemeResource
ListViewItemRevealBorderBrushPointerOver}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOverSelected">
<VisualState.Setters>
<Setter Target="Root.(RevealBrush.State)"
Value="PointerOver"/>
<Setter Target="Root.RevealBorderBrush"
Value="{ThemeResource
ListViewItemRevealBorderBrushPointerOver}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOverPressed">
<VisualState.Setters>
<Setter Target="Root.(RevealBrush.State)"
Value="Pressed"/>
<Setter Target="Root.RevealBorderBrush"
Value="{ThemeResource
ListViewItemRevealBorderBrushPressed}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="Root.(RevealBrush.State)"
Value="Pressed"/>
<Setter Target="Root.RevealBorderBrush"
Value="{ThemeResource
ListViewItemRevealBorderBrushPressed}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PressedSelected">
<VisualState.Setters>
<Setter Target="Root.(RevealBrush.State)"
Value="Pressed"/>
<Setter Target="Root.RevealBorderBrush"
Value="{ThemeResource
ListViewItemRevealBorderBrushPressed}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DisabledStates">
<VisualState x:Name="Enabled"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="Root.RevealBorderThickness"
Value="0"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ListViewItemPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
...
<ListView Style="{StaticResource CircleGlyphItem}"
ItemContainerStyle="{StaticResource ListViewItemContainerStyle1}" >
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
</ListView>
Holy code bloat. That's a big bunch of XAML that I have to manage now. I'm going to add a Margin setter, and that's it. All that code for one simple change. But wait, there's something we can do about that. Before modifying it,
- Copy the
Style
as is to Global.xaml. If you don't have one, see the Global.xaml section below. - Back in our original XAML file in the original copy of the style, remove all the
Setters
in the style
. - Rename it to something more appropriate. It doesn't matter what as that name will be gone in the final rendition.
- Add a
BasedOn
reference:
<Style x:Key="ListViewItemContainerWithMargin"
BasedOn="{StaticResource ListViewItemContainerStyle1}" TargetType="ListViewItem">
</Style>
...
- Add a
Setter
to set the margin.
<Style x:Key="ListViewItemContainerWithMargin" TargetType="ListViewItem"
BasedOn="{StaticResource ListViewItemContainerStyle1}">
<Setter Property="Margin" Value="0 0 20 0" />
</Style>
...
UI looks just how I want it to look. All that XAML is out of site. The next developer that comes along can see exactly what is changing in the ListViewItemContainerStyle
. I just wish the SDK made global resources out of the styles and templates. That way, we can reference them directly with the BasedOn
attribute and not have to make our own copy. Lastly, we want to do that separation of responsibility thing like we did at the end of the previous section...
- Put the
ItemContainerStyle
inside our CircleGlyphStyle
:
<Style x:Key="CircleGlyphItem" TargetType="ListView">
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<Grid>
<Ellipse Width="80" Height="80" Fill="#16808080" />
<FontIcon Glyph="{Binding}" FontSize="36"
VerticalAlignment="Center" HorizontalAlignment="Center" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<ItemsWrapGrid Orientation="Horizontal"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListViewItem"
BasedOn="{StaticResource ListViewItemContainerStyle}">
<Setter Property="Margin" Value="0 0 20 0" />
</Style>
</Setter.Value>
</Setter>
</Style>
...
<ListView Style="{StaticResource CircleGlyphItem}">
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
<x:String></x:String>
</ListView>
IMHO, elegant code. Now you have a clean ListView
element with its data and a clean styling of that ListView
. Future developers that come across this will clearly understand that this is a ListView
with an Ellipse
under a Glyph
that it's layed out it a WrapGrid
and that it has extra margin between items. The list contains four items. Of course, the functionality of what happens when the list items are selected is a whole other topic...
Change Happens
As new WinRt versions are released, the expanded templates for a given control can change. It's a good idea to go through a process after upgrading your SDK to expand the templates again and replace the contents you put in your Global.xaml with the freshly expanded contents.
Conclusion
In this, I took you through expanding a large Style
(the ListView
's ListContainerStyle
) which is huge and modify it a bit in an elegant and maintainable way. We also saw how to separate responsibility in your XAML separating your UI component from its styling. In other parts of your app, you can have new Styles that are BasedOn
ListViewContainerStyle
that modify other properties. Maybe you don't like that the items are vertically centered. Make a new Style
where you change the VerticalContentAlignment
to something else. You can further increase code reuse by moving the CircleGlyphItem Style
definition to the Global.xaml file. It can then be reused in other parts of your app where you may have similar lists.
Global.xaml
Often, you want to style controls and then share that styling with other XAML files in your app. One way to do it is to add it to your App
resources in your App.xaml file. Another, more compartmentalized way is to add it to a new file (Global.xaml) resource dictionary.
Note: I actually do create another resource dictionary called WinRt.xaml and put all these expanded templates in there. I use Global.xaml for Styles
, converters and the like. I put a big note in the WinRt.xaml that the items in the dictionary are the expansions from a specific WinRt SDK version and that they are not to be modified.
History
- 19th June, 2020: Initial version