WPF ListView Styling Tutorial Part II

In the previous article I demonstrated how to fully style a GridView column header in a ListView. In this next article, I’ll be covering row styling and final wrap ups, focusing on maintaining every behavior that’s expected from a typical grid row.

This time around, I’ll create and customize a template for each column, thus overriding all the visual aspects of a single row. The first step is to change the GridView column declarations and set the CellTemplate attribute to a local resource instead of the previous simpler approach, that used DisplayMembershipBinding and simple TextBlock based data templates. So for starters, I’ll change the ListView xaml declaration to reference new DataTemplates in each column:

<ListView Name="ListView" ItemsSource="{Binding}" ItemContainerStyle="{DynamicResource ListViewItemContainerStyle}">
    <ListView.View>
        <GridView>
            <GridViewColumn CellTemplate="{StaticResource PersonItemTemplate}" Header="Name"/>
            <GridViewColumn CellTemplate="{StaticResource AgeItemTemplate}" Header="Age" />
            <GridViewColumn CellTemplate="{StaticResource MovieItemTemplate}" Header="Favorite Movie" />
        </GridView>
    </ListView.View>
</ListView>

Notice that I’ve also referenced an ItemContainerStyle resource. This will be used to define generic row behavior upon selection and mouse over.

Now, let’s define each column style separately:

<DataTemplate x:Key="AgeItemTemplate">
    <Border BorderThickness="0,0,0,0" BorderBrush="#6FBDE8">
        <TextBlock Margin="2" Text="{Binding Age}" VerticalAlignment="Center" Grid.Column="1" />
    </Border>
</DataTemplate>

<DataTemplate x:Key="PersonItemTemplate">
    <Border BorderThickness="0,0,0,0" BorderBrush="#6FBDE8">
        <Grid Margin="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="32" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Image Source="Images/person.png" Width="24" Height="24" Grid.Column="0" HorizontalAlignment="Center" />
            <TextBlock Text="{Binding Name}" VerticalAlignment="Center" Grid.Column="1" />
        </Grid>
    </Border>
</DataTemplate>

<DataTemplate x:Key="MovieItemTemplate">
    <Border BorderThickness="0,0,0,0" BorderBrush="#6FBDE8">
        <Grid Margin="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="32" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Image Source="Images/movie.png" Width="24" Height="24" Grid.Column="0" HorizontalAlignment="Center" />
            <TextBlock Text="{Binding FavoriteMovie}" VerticalAlignment="Center" Grid.Column="1" />
        </Grid>
    </Border>
</DataTemplate>

Each one of these visual structures will be nested within an item container, whose style we should define in order to provide visual responses to events like row selection, mouse over, like I mentioned earlier. I’ll then define the ItemContainerStyle resource named ListViewItemCongainerStyle like so:

<Style x:Key="ListViewItemContainerStyle" TargetType="{x:Type ListViewItem}">
    <Setter Property="Background" Value="#ffffff" />
    <Setter Property="HorizontalContentAlignment" Value="Left" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Foreground" Value="Black" />
    <Setter Property="Margin" Value="0,0,0,0" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListViewItem}">
                <Border x:Name="Bd" Background="{TemplateBinding Background}" SnapsToDevicePixels="true" BorderThickness="0,0,0,1" BorderBrush="#6FBDE8">
                    <GridViewRowPresenter VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected"  Value="true">
                        <Setter TargetName="Bd" Property="BorderBrush" Value="#FF143c65" />
                        <Setter Property="Background" TargetName="Bd">
                            <Setter.Value>
                                <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStopCollection>
                                            <GradientStop Color="#FF75aac7" Offset="0"/>
                                            <GradientStop Color="#FF143c65" Offset="1"/>
                                        </GradientStopCollection>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Setter.Value>
                        </Setter>
                    </Trigger>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="Bd" Property="Background" Value="#e0eff8" />
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true" />
                            <Condition Property="Selector.IsSelectionActive" Value="True" />
                        </MultiTrigger.Conditions>
                        <Setter Property="Background" TargetName="Bd">
                            <Setter.Value>
                                <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStopCollection>
                                            <GradientStop Color="#FF75aac7" Offset="0"/>
                                            <GradientStop Color="#FF143c65" Offset="1"/>
                                        </GradientStopCollection>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Setter.Value>
                        </Setter>
                        <Setter Property="BorderBrush" TargetName="Bd" Value="#FF143c65"/>
                        <Setter Property="Foreground" Value="White"/>
                    </MultiTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Notice that the GridViewRowPresenter container object marks the place where actual row data will appear and defines its layout. Header style template can be defined through the ColumnHeaderContainerStyle property, although in the current example, we’ve already defined a generic GridViewColumnHeader template in part I of this tutorial.

This is what we have so far:

Now if you notice closely, you can see that the ListView column header row has a slight left and right offset of about 2px. After inspecting this with Snoop, it becomes clear that the GridViewHeaderRowPresenter object, which is the object used to define the layout of our row of column headers, has a margin set to 2,0,2,0.

To counter this, we must also override the ScrollViewer style for the ListView, exposing all the layout attributes and customize them at our will. In this style I’ll set margin to zero and fine tune column header offset:

<Style x:Key="{x:Static GridView.GridViewScrollViewerStyleKey}" TargetType="ScrollViewer">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ScrollViewer">
                <Grid Background="{TemplateBinding Background}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <DockPanel Margin="{TemplateBinding Padding}">
                        <ScrollViewer DockPanel.Dock="Top" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" Focusable="false">
                            <GridViewHeaderRowPresenter Margin="0,0,0,0" Columns="{Binding Path=TemplatedParent.View.Columns,
                                RelativeSource={RelativeSource TemplatedParent}}" ColumnHeaderContainerStyle="{Binding Path=TemplatedParent.View.ColumnHeaderContainerStyle, RelativeSource={RelativeSource TemplatedParent}}"
                                ColumnHeaderTemplate="{Binding Path=TemplatedParent.View.ColumnHeaderTemplate,RelativeSource={RelativeSource TemplatedParent}}"
                                ColumnHeaderTemplateSelector="{Binding Path=TemplatedParent.View.ColumnHeaderTemplateSelector, RelativeSource={RelativeSource TemplatedParent}}"
                                AllowsColumnReorder="{Binding Path=TemplatedParent.View.AllowsColumnReorder, RelativeSource={RelativeSource TemplatedParent}}"
                                ColumnHeaderContextMenu="{Binding Path=TemplatedParent.View.ColumnHeaderContextMenu, RelativeSource={RelativeSource TemplatedParent}}"
                                ColumnHeaderToolTip="{Binding Path=TemplatedParent.View.ColumnHeaderToolTip, RelativeSource={RelativeSource TemplatedParent}}"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                        </ScrollViewer>
                        <ScrollContentPresenter Name="PART_ScrollContentPresenter"
                                KeyboardNavigation.DirectionalNavigation="Local"
                                CanContentScroll="True" CanHorizontallyScroll="False"
                                CanVerticallyScroll="False"/>
                    </DockPanel>
                    <ScrollBar Name="PART_HorizontalScrollBar" Orientation="Horizontal" Grid.Row="1" Maximum="{TemplateBinding ScrollableWidth}" ViewportSize="{TemplateBinding ViewportWidth}" Value="{TemplateBinding HorizontalOffset}" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
                    <ScrollBar Name="PART_VerticalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableHeight}" ViewportSize="{TemplateBinding ViewportHeight}" Value="{TemplateBinding VerticalOffset}" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now to complete styling I will customize the style for the ListView itself, adding a custom background and border. You can then extend this as you wish:

<Style x:Key="{x:Type ListView}" TargetType="ListView">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
    <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListView">
                <Border Name="Border" BorderThickness="1" BorderBrush="#999999" Background="#DFDFDF">
                    <ScrollViewer Style="{DynamicResource {x:Static GridView.GridViewScrollViewerStyleKey}}">
                        <ItemsPresenter />
                    </ScrollViewer>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter TargetName="Border" Property="Background" Value="#BBBBBB"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And this is the final result:

You now have full control over the ListView visual template. Extend it, play with it but most of all, have fun.

You can download the sample solution here straight from our web host. And here’s the summary of all tutorial articles:

WPF ListView Styling Tutorial Part I

WPF ListView Styling Tutorial Part II

Technorati Tags: , ,

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)

3 comments so far

  1. 001 Kamala

    Excellent article!!

    July 21st, 2011
  2. 002 kannan

    the article was very helpful, I need to enable scrollbar how can i do it

    January 13th, 2012
  3. Have you experienced with the scrollviewer?

    January 13th, 2012

Add a comment