Introduction
This article is about developing a XAML only WPF CheckListBox
and ListBox
with Selection Indicator control styles, and taking the same requirements and building a custom control that takes a very different approach to the problem. This article is about taking a concept and running with it. It's about this developer's desire to recreate a complex control using only XAML markup. We will explore the XAML only approach, then toss everything into a blender, and pour out two WPF Custom Controls using VB.NET.
This article and its controls were inspired by Josh Smith's awesome article, The WPF Thought Process. It's with Josh's permission and encouragement I write this article and post it here.
I read Josh's article several times, attempting to glean and learn as I do from all the WPF articles here on The Code Project.
To get the most from this article, please read Josh's article before reading this one. I won't be repeating his teaching here. We both solved the same problem in three different ways.
Some would ask, why would you want to spend time developing a solution that doesn't look like a 'standard' ListBox
? Well...
I've only been programming WPF since April 2007. So I take every opportunity to learn the how and why of a solution to a problem. I can't tell you how many times I've read a posting or article and two days later could use those techniques to solve a problem I was working on.
I dove into this project because you never know where a coding technique will lead. In this case, after finishing the ListBoxWithSelectedIndicator
custom control, I was thinking about it and the creative juices started to flow. Why not replace the indicator arrow with a CheckBox
and build a WPF CheckListBox
? WPF does not come with a CheckListBox
control. I used the ASP.NET CheckListBox
control when doing web sites, and figured I'll need one soon in my WPF work. Honestly, coding a WPF CheckListBox
was a snap to code after fully understanding how to place an indicator arrow next to an item in a ListBox
control. *** Note *** you can place CheckBoxe
s inside a ListBox
control in XAML markup. Some of my WPF books show how to swap out the DataTemplate
and data bind to the ListBox
which will create a CheckBox
for each data item. However, there is a catch. When the SelectionMode
property is set to Extended
and you start selecting and deselecting items, it does not work correctly. For example, if you select the first item and scroll to the last and Shift-click, you would expect all items are selected - this does not happen.
In mid October, I gave two sessions at the Charlotte Developers GUILD Fall Code Camp, and wanted to use something new for one of my presentations.
Lastly, I did this because I love writing custom controls for ASP.NET, WinForms, and WPF. Some of you can remember the days of VB 1.0 and Access 1.0 and wanting to write our own controls but didn't know how. During those days, I spent most of my time programming VAX/VMS and SCO Unix, so I didn't have time to learn MFC. Today, Microsoft and the .NET Framework have made this task easy and fun for their development environments and languages.
My Expedition
I love challenges. So I set out to recreate the cool ListBoxWithIndicator
solution Josh authored, except I wanted to only use XAML. No Code, Just XAML! This desire also led to authoring a custom control version. The custom control version functions very differently from the XAML version, and can do one thing that the XAML version can't - scroll the ListBoxItem
horizontally while at the same time not scrolling the CheckBox
bullet or indicator arrow. The XAML version uses text wrapping for narrowing a ListBox
with long text items.
Let's have a look at the requirements to see where we are heading. In addition, to staying in lock step with Josh's ListBox
specifications, I added the following requirements:
XAML Only Requirements
- Allow the
ListBoxItem
s to be added in XAML. - Allow the
ListBox
to be data bound in XAML. - Allow the
ListBox
to be data bound in code. - Allow the indicator size to be set in XAML.
- Allow the indicator brush to be set in XAML.
- Provide a UI where the indicator arrow or
CheckBox
is visually separated from the corresponding ListBoxItem
. ListBox
must support the Single
, Multiple
, and Extended
SelectionMode
s.
Custom Control Additional Requirements
- Support the control being declared and instantiated in code without any XAML markup.
- Support the changing out of the child
ListBox
control at run time.
For the XAML version, I would need to develop a Style
for the ListBox
control that would accomplish the mission. Many years of web programming actually made this task easier, and I'll point out down below where this came into play.
I completed the original XAML version, but it had one minor limitation (the version here does not). I could not make the text scroll horizontally without moving the CheckBox
bullet or indicator arrow. So I wrote it to not scroll and called it a limitation, hey just make the ListBox
wide enough for the text. No, that is not a good solution, so for a few days, I put the XAML only version on the shelf. As you can tell, at this point, I had not thought about text wrapping the text.
Not wanting to be denied my solution, I wrote a custom control that would meet all the above requirements and used that control for my presentation. This solution is very different from the XAML version. My control actually wraps a ListBox
control, listens in to what that control is doing, and takes the proper action to deliver the required UI. Technically speaking, I set up Routed Event handlers.
Once the custom control was completed, I didn't want to give up on my XAML only version. So I went to work and found the solution that had escaped me. While the XAML only version still can't scroll the text horizontally without also scrolling the CheckBox
or arrow indicator, I got around this by making the ListBoxItem
text wrap if it's longer than the width of the ListBox
. Problem solved, and No Code, Just XAML is born.
No Code, Just XAML! Version
The project download includes an application with six XAML only demos, along with six custom control demos. I'll show a few images from the application.
Sample One
Image shows our Style
sporting the indicator arrow inside its own region. The ListBoxItem
item selection colors have been overridden and set to Transparent
. The indicator arrow region has its own color while the ListBox
Border
surrounds the arrow region. The samples below do not have the surrounding border.

Sample Two
Image shows our Style
sporting the customized CheckListBox
bullet, and the CheckBox
cleanly separated from the ListBoxItem
. Notice that this separation is different from the above image. There is no visible border around the CheckBox
bullets. The ListBoxItem
item selection colors have been overridden and set to Transparent
. With Halloween coming, I couldn't resist with the pumpkins.

Sample Three
Image shows our Style
sporting the CheckListBox
with the CheckBox
cleanly separated from the ListBoxItem
. Notice the ListBoxItem
item selection colors are normal.

Sample Four
Image shows our Style
sporting the CheckListBox
with the CheckBox
cleanly separated from the ListBoxItem
. The ListBoxItem
item selection colors have been overridden and set to Transparent
.

XAML Only Markup
There are essentially three pieces to the solution: the ListBox
and ListBoxItem
control templates and the XAML markup for the actual ListBox
. I'll go over the three pieces from the Demo3.xaml file. When running the application, its tab item header text reads, CheckListBox. This is also the demo that I figured most developers may want to use, a XAML only WPF CheckListBox
. Keep in mind, each XAML only demo control template varies slightly because we don't have any code that can make changes at run time.
To use the sample XAML Style
s in your applications, copy the two control templates from the resources in the desired file and author your ListBox
markup like the demo does. Remember, each demo has slightly different control templates and ListBox
markup. Better yet, go ahead and place the Style
in a ResourceDictionary
in your application.
ListBox Control Template for Sample Four Above
<Style x:Key="aeroCheckListBoxStyle" TargetType="{x:Type ListBox}">
<Setter Property="ItemContainerStyle"
Value="{StaticResource aeroCheckListBoxItemStyle}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<Border CornerRadius="0" Background="{TemplateBinding Background}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
<Border Background="{StaticResource scrollViewerBackgroundBrush}"
Margin="20,0,0,0"
BorderBrush="{StaticResource scrollViewerBorderBrush}"
BorderThickness="1,0,0,0" x:Name="border">
<ItemsPresenter/>
</Border>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The second line is where the ListBoxItem
has its Style
assigned by setting the ItemContainerStyle
property. Placing this assignment here saves the consuming designer or developer from having to do this in the ListBox
XAML markup.
I mentioned that web programming helped out on this project; well, here is the first place.
Take a look at the Sample Four image. The CheckBox
es are contained inside the ListBox
. Our spec calls for the CheckBox
to not have a visible border around it and for the CheckBox
to be visually separated from the ListBoxItem
.
This is accomplished by nesting Border
controls. The outer Border
provides the Background
color but is void of any Border
s. The inner Border
'Mr. Cool' has the responsibilities of providing the single line Border
between the CheckBox
and the ListBoxItem
. Setting the left margin to 20 effectively moves the Border
in between them, right where we need it.
Someone has to be thinking now, where are the top, right, and bottom Border
s? Well, we can't allow the ListBox
control to provide them in this case because we want the CheckBox
to not have a Border
around it. So, when we get to the third piece of the solution, we'll pull another web trick to get the Border
. You may want to minimize this article now, open an empty solution, and give this exercise a go and see if you can get your UI to look like the example.
Another reader may wonder why Mr. Cool just can't provide all the borders? Well, Mr. Cool is a child of the ScrollViewer
, and the top and bottom Border
s would move out of view when the ScrollViewer
scrolls vertically.
Speaking of the ScrollViewer
, setting the HorizontalScrollBarVisibility
to Disabled
actually allows the ListBoxItem
text to wrap. Without this setting, the text will not wrap.
The Border
control that wraps the ItemsPresenter
provides the Background
color for the ScrollViewer
scrolling region. This is accomplished by the {StaticResource scrollViewerBackgroundBrush}
binding. This solution will not work without this color because as you'll read below, there is a black Rectangle
sitting below this ScrollViewer
.
ListBoxItem Control Template for Sample Four Above
I have removed some of the XAML from the control template below to highlight more important sections of code. Some of the removed code are the standard triggers that the ListBoxItem
control template has. The provided source has all the code.
<Style x:Key="aeroCheckListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="Padding" Value="2,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Background="Transparent"
Width="16" Height="16"
HorizontalAlignment="Left"
Margin="-20,0,0,0">
<BulletDecorator Margin="2,0,0,0"
SnapsToDevicePixels="true"
Background="Transparent"
VerticalAlignment="Center">
<BulletDecorator.Bullet>
<Microsoft_Windows_Themes:BulletChrome
BorderBrush="{StaticResource checkBoxBorderBrush}"
IsChecked="{Binding Path=IsSelected,
RelativeSource={RelativeSource AncestorLevel=1,
AncestorType={x:Type ListBoxItem},
Mode=FindAncestor}}"
RenderMouseOver="{TemplateBinding IsMouseOver}" />
</BulletDecorator.Bullet>
</BulletDecorator>
</Grid>
<Border Margin="-10,0,10,0"
Grid.Column="1"
SnapsToDevicePixels="true"
x:Name="Bd"
VerticalAlignment="Center"
MinHeight="16"
Background="Transparent">
<ContentPresenter Content="{TemplateBinding Content}">
<ContentPresenter.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Again, web programming comes into play here. Please keep in mind, this control template represents a ListBoxItem
. A task we have to accomplish is the CheckBox
needs to be placed in the colored section of the ListBox
control. This is accomplished by setting a negative left margin (-20) on the CheckBox
parent container.
The BulletChrome
is data bound to the ListBoxItem
IsSelected
property.
One small detail that could be over looked. The Border
control that wraps the ContentPresenter
has its MinHeight
property set to 16. This is here and in all other templates in case the ListBoxItem
text is smaller in height than an indicator arrow or custom sized CheckBox
. Since WPF will only allocate the required height of the text, we set the minimum height of this Grid
cell to match the height of our indicator or CheckBox
. Need to give credit again to Josh for seeing this while he was testing the code, thanks Josh.
We finally get to the ContentPresenter
that displays the actual ListBoxItem
Content
. Since this XAML only approach can't allow the ListBoxItem
to scroll horizontally, I had to force the ListBoxItem
text to wrap. By wrapping the ContentPresenter
with a Style
that targets a TextBlock
, we can set the required TextWrapping
for any TextBlock
s that the ContentPresenter
creates.
ListBox XAML Markup for Sample Four Above
<Border CornerRadius="10"
Background="{StaticResource outerBorderBackgroundBrush}"
Padding="10"
Grid.Row="1"
BorderBrush="{StaticResource outerBorderBorderBrush}"
BorderThickness="1"
Margin="10,10,10,10" >
<Grid>
<Rectangle
Margin="20,-1,-1,-1"
Fill="{StaticResource scrollViewerBorderBrush}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<ListBox Background="{StaticResource outerBorderBackgroundBrush}"
ItemsSource="{Binding Path=Items, Mode=OneWay,
Source={StaticResource someTestData}}"
SelectionMode="{Binding Path=Text,
ElementName=selectionModeCombo, Mode=Default}"
Style="{StaticResource aeroCheckListBoxStyle}"
IsSynchronizedWithCurrentItem="True" FontSize="12"
Height="Auto" VerticalAlignment="Stretch" SelectedIndex="0"/>
</Grid>
</Border>
The outer Border
here provides a container for color and spacing for the ListBox
.
The Grid
houses a Rectangle
and our ListBox
. Remember, we still don't have a top, right, or bottom Border
for the ListBox
. The Rectangle
provides this magic by settings its margins. The left margin of 20 pushes our Rectangle
right so that it does not display behind the CheckBox
es. To get the appearance of a Border
surrounding the ListBox
, we stretch the Rectangle
with the negative margins (-1) up, to the right and down. Since the ListBox
is smaller than the Rectangle
, a small amount of the Rectangle
Fill
color is visible. This, fellow developers, is the last of that web programmer stuff.
The ListBox
is just standard WPF stuff. Allowing the control template to do all the work, just like it should be.
XAML Only Solution Remarks
Go ahead and fire up the demo application and view all the XAML only demos. Try out all the SelectionMode
s. Notice that when in Extended
mode, you can Shift-Click groups of items, by clicking over the ListBoxItem
text or CheckBox
.
Each XAML only demo has two ListBox
es. The top one is data bound to an array resource and the bottom one is filled in the XAML markup with text and shapes.
Don't forget to look for the included Easter Egg. When you think you've found it, keep looking for the real one. By the way: The Easter Egg is No Code, Just XAML too.
Custom Control Solution
When I sat down to author this control, I figured, creating a control that included a ContentPresenter
that would have a standard WPF ListBox
as its Content
was the way to go. The reason for this approach was that it allowed the consuming developer or designer to use the familiar ListBox
and just attach behavior to it. The first beta release was derived from Control
and included a Content
property. Josh set me straight, and suggested I derive from ContentControl
because it has the Content
property built in.
By using a ListBox
as the control Content
, the first three requirements are met. Since our control in effect just attaches a behavior to a ListBox
, the developer is free to load ListBoxItem
s in the XAML markup, using declarative data binding, or add items in code without taking our control into consideration.
This concept of writing a decoupled wrapper control appealed to me. The consumer is free to modify their ListBox
as they see fit, without bothering our control. The same limitation Josh mentioned in his article, about the ListBox
rendering its child items horizontally, also applies here. This would not work with the current implementation.
Before diving into a bunch of control code, let's look at how simple the control XAML markup is. In each of the examples below, a standard ListBox
is the child or Content
of our control.
Example XAML Markup
Example One
The image below shows the selection indicator height and width set to 32 and a larger ListBoxItem
text font. ListBoxItem
item selection colors have been left using the default color, transparent. Notice the horizontal scroll bar. The XAML only solution didn't have this feature. This solution is actually wrapping its child ListBox
so we can now allow the ListBox
to scroll.

<CustomControls:ListBoxWithSelectedItemIndicator
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="Auto" Height="Auto" Margin="10,10,10,10"
FontSize="12" ClipToBounds="True" Background="#FFDEFDFF"
BorderBrush="#FF00A7B9" BorderThickness="1,1,1,1"
Padding="5,5,5,5" Grid.Row="3" Grid.Column="1"
IndicatorHeightWidth="32" >
<ListBox ScrollViewer.CanContentScroll="False"
x:Name="lbDemoTwo" Width="Auto" Height="Auto"
IsSynchronizedWithCurrentItem="True"
DataContext="{DynamicResource SystemDrawingPrintingValues2}"
ItemsSource="{Binding}"
FontSize="24" SelectionMode="{Binding Path=Text,
ElementName=SelectionModeCombo, Mode=OneWay}"/>
</CustomControls:ListBoxWithSelectedItemIndicator>
That is really not much in the way of XAML markup! In the above code, the last property of the control, IndicatorHeightWidth
, has been set to 32, which makes the indicator twice the size of the default indicator.
The child ListBox
control is data binding to a DynamicResource
from within the XAML markup. I used the System.Drawing.Printing.PaperKind
enum as the data source because that enum has a lot of members.
The child ListBox
control also has smooth scrolling enabled. When the attached property ScrollViewer.CanContentScroll
is set to False
, it actually turns on smooth scrolling by telling the ScollViewer
that the ListBox
does not know how to scroll its items, so the ScollViewer
takes over the job of scrolling the content. Normally, when a ListBox
scrolls up or down, it scrolls one ListBoxItem
at a time; setting this attached property just overrides this default scrolling.
Example Two
The image below shows the standard indicator with the modified ListBoxItem
item selection colors.

<CustomControls:ListBoxWithSelectedItemIndicator
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="Auto" Height="Auto" Margin="10,10,10,10"
FontSize="12" ClipToBounds="True" Background="#FFFFFFFF"
BorderBrush="#FF000000" BorderThickness="1,1,1,1"
Padding="5,5,5,5" Grid.Row="3" IndicatorHeightWidth="16">
<CustomControls:ListBoxWithSelectedItemIndicator.Resources>
<LinearGradientBrush
x:Key="{x:Static SystemColors.HighlightBrushKey}"
EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="#FF3FBA00" Offset="0"/>
<GradientStop Color="#FFFFFFFF" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush
x:Key="{x:Static SystemColors.ControlBrushKey}" Color="#FFCDFFC9"/>
</CustomControls:ListBoxWithSelectedItemIndicator.Resources>
<ListBox x:Name="lbDemoOne" Width="Auto" Height="Auto"
IsSynchronizedWithCurrentItem="True"
DataContext="{DynamicResource SystemDrawingPrintingValues}"
ItemsSource="{Binding}"
FontSize="16" SelectionMode="{Binding Path=Text,
ElementName=SelectionModeCombo, Mode=OneWay}"/>
</CustomControls:ListBoxWithSelectedItemIndicator>
This control is authored to override the standard ListBoxItem
item selector colors and make them transparent. This is performed in the control template. The markup shows one way to override the transparent color using markup by assigning a Brush
to two StaticResource
s: SystemColors.HighlightBrushKey
and SystemColors.ControlBrushKey
.
The SystemColors.HighlightBrushKey
resource is used by the ListBoxItem
control as the color for selected items when the control has focus.
The SystemColors.ControlBrushKey
resource is used by the ListBoxItem
control as the color for selected items when the control does not have focus.
Since these transparent colors were assigned in the control template, by placing resources between the child ListBox
and the parent control, the ListBoxItem
will use the resources declared here. A developer could also edit the control template and change the colors there, but this is much simpler. Hint: you can use this same technique with ListBoxe
s or ComboBox
es not associated with this control. I understand that a green gradient is probably not the proper ListBoxItem
item selected color, but how to change the color is an important thing to understand.
Example Three
The image below shows a custom indicator Brush
. The ListBoxItem
s have been added in the XAML markup. The control was also wrapped in a border. ListBoxItem
item selection colors have been left using the default color, transparent.

<CustomControls:ListBoxWithSelectedItemIndicator
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="Auto" Height="Auto" FontSize="12" ClipToBounds="True"
Background="#FFFFFFFF" BorderBrush="{x:Null}"
BorderThickness="0,0,0,0" Padding="5,5,5,5" Grid.Row="2"
Grid.Column="2" IndicatorHeightWidth="16" >
<CustomControls:ListBoxWithSelectedItemIndicator.IndicatorBrush>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF8FF72" Offset="0"/>
<GradientStop Color="#FFFF0000" Offset="0.992"/>
</LinearGradientBrush>
</CustomControls:ListBoxWithSelectedItemIndicator.IndicatorBrush>
<ListBox x:Name="lbDemoThree" Width="Auto" Height="Auto"
IsSynchronizedWithCurrentItem="True"
FontSize="16" SelectionMode="{Binding Path=Text,
ElementName=SelectionModeCombo, Mode=OneWay}">
<ListBoxItem Content="How"/>
<ListBoxItem Content="Cool"/>
<ListBoxItem Content="Is"/>
<ListBoxItem Content="WPF!"/>
<Ellipse Fill="#FFF32929" Stroke="#FF000000"
Margin="0,3,3,0" Width="30" Height="30"/>
<Ellipse Fill="#FF0C5FB7" Stroke="#FF000000"
Margin="0,3,3,0" Width="30" Height="30"/>
<Ellipse Fill="#FF13D141" Stroke="#FF000000"
Margin="0,3,3,0" Width="30" Height="30"/>
<Rectangle Fill="#FFE46614" Stroke="#FFFF0000"
RadiusX="5" RadiusY="5" Margin="0,3,3,0"
Width="50" Height="30"/>
<Rectangle Fill="#FFD5D724" Stroke="#FFFF0000"
RadiusX="5" RadiusY="5" Margin="0,3,3,0"
Width="50" Height="30"/>
<Rectangle Fill="#FF2133CF" Stroke="#FFFF0000"
RadiusX="5" RadiusY="5" Margin="0,3,3,0"
Width="50" Height="30"/>
<Rectangle Fill="#FF67DF1F" Stroke="#FFFF0000"
RadiusX="5" RadiusY="5" Margin="0,3,3,0"
Width="50" Height="30"/>
<Rectangle Fill="#FF09A18C"
Stroke="#FFFF0000" RadiusX="5"
RadiusY="5" Margin="0,3,3,0"
Width="50" Height="30"/>
</ListBox>
</CustomControls:ListBoxWithSelectedItemIndicator>
Here, the ListBox
is loaded with the items in the XAML markup. Our solution does not choke on vastly different ListBoxItem
s, text and shapes.
The example shows how to set a custom indicator Brush
using XAML markup.
WPF CheckListBox Examples
The screenshot from the demo application shows the WPF CheckListBox
version of this custom control. The CheckBox
Stroke
brush and StrokeThickness
are set by custom dependency properties of the control. Also, the size of the CheckBox
is controlled by a dependency property. Notice the middle CheckListBox
has larger CheckBox
es. Again, this control delivers a nice visual separation between the CheckBox
bullet and the ListBoxItem
, and shows off the horizontal scrollbar that only scrolls the ListBoxItem
Content
and not the CheckBox
bullet.
This control is included in the download. It is very similar to the control that renders an indicator arrow, so I won't be writing about it.

Authoring the Control
From the above examples, it's clear that our control wraps a child ListBox
. Because of the awesome WPF architecture and our new best friend Routed Events, our control can easily listen in to what the child ListBox
and the ListBox
's child ScrollViewer
are doing and then take action.
I suggest putting your custom controls in their own projects. When you first add a Custom Control (WPF) to your project, you will get a code file, and a shell control template will be added to the generic.xaml file in the \themes subdirectory of your project. If you don't have this subdirectory, it will be added for you by Visual Studio.
Normally, I'll add the required dependency properties and Routed Events to my code before beginning the control template editing. This way, as I'm editing the control template, I can make use of the dependency properties and possibly data bind to them if my control requires this.
Dependency Property Declaration
Let us have a look at how a dependency property is declared and exposed in the VB.NET code file.
Public Shared ReadOnly IndicatorHeightWidthProperty As DependencyProperty = _
DependencyProperty.Register("IndicatorHeightWidth", GetType(Double), _
GetType(ListBoxWithSelectedItemIndicator), New PropertyMetadata(16.0))
<Description("Size of indictor. Indicator is ..."), Category("Custom")> _
Public Property IndicatorHeightWidth() As Double
Get
Return CType(GetValue(IndicatorHeightWidthProperty), Double)
End Get
Set(ByVal value As Double)
SetValue(IndicatorHeightWidthProperty, value)
End Set
End Property
- When naming your dependency properties, it is necessary to name the property and end the name with the word Property. The
IndicatorHeightWidth
dependency property below is named IndicatorHeightWidthProperty
. - Watch out: This property also has a default value of 16. The property is of type
Double
. When setting a default value for a Double
, you must use 16.0 and not 16; otherwise, .NET will throw an exception. By the way, 16D does not work either (yes, I wasted 10 minutes trying to figure this out). - The
IndicatorHeightWidth
property is a wrapper for the IndicatorHeightWidthProperty
dependency property. This allows .NET code to access the property with normal get and set operations. Without this wrapper, developers would have to use the GetValue
and SetValue
statements to access the property. - When authoring controls, it helps if you add the
System.ComponentModel.Descrption
and System.ComponentModel.Category
attributes to your dependency property wrappers. If you do this, developers or designers using Microsoft Expression Blend can view the Description
attribute information in the ToolTip that displays when the user mouses over the property name. Also, adding the Category
attribute adds another section to the bottom of Blend's property inspector. If you put all your dependency properties in the same Category
, it can make it easier for developers and designers to find them. Otherwise, your properties will be placed in the Misc section.
Consuming the IndicatorHeightWidth Dependency Property in the Control Template
<DataTemplate>
<Grid
Width="{Binding Path=IndicatorHeightWidth,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:ListBoxWithSelectedItemIndicator}}}"
Height="{Binding Path=IndicatorHeightWidth,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:ListBoxWithSelectedItemIndicator}}}">
<Path Fill="LightGray" Stretch="Uniform" Data="M4,4 L16,10 L4,16 z"
RenderTransformOrigin="0.5,0.5" SnapsToDevicePixels="True">
<Path.RenderTransform>
<TransformGroup>
<TranslateTransform X="2" Y="2"/>
</TransformGroup>
</Path.RenderTransform>
</Path>
<Path
Fill="{Binding Path=IndicatorBrush,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type local:ListBoxWithSelectedItemIndicator}}}"
Stretch="Uniform" Data="M2,2 L14,8 L2,14 z"
SnapsToDevicePixels="True"/>
</Grid>
</DataTemplate>
The IndicatorHeightWidth
property is used within the control template to set the height and width of the selection indicator arrow by controlling the size of the Grid
control that has the above two Path
controls as its children. Since the two Path
controls have their Stretch
property set to Uniform
, the arrow indicator simply sizes itself to its container. Nothing like WPF's vector based rendering!
What's interesting is how the IndicatorHeightWidth
property is accessed in the XAML. When working with control templates, and often with User Controls, the object that has the property you want to bind to is not accessible by using its Name
property. Instead, you must walk up the element tree and find the object you are looking for that has the property you want to bind to. This is easily accomplished with the binding statement, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:ListBoxWithSelectedItemIndicator}}
. WPF does the heavy lifting for you, walks up the element tree until it finds the first object of the type you requested in the AncestorType
property, and then binds to the object property indicated by the binding Path
.
The IndicatorHeightWidth
property fulfills the requirement to Allow the indicator size to be set in XAML.
Likewise, the control's other dependency property IndicatorBrush
fulfills the requirement to Allow the indicator brush to be set in XAML.
Custom Control Code
In WPF, custom controls do not have a XAML file directly associated with them the way the Window.xaml or UserControl.xaml files have a corresponding .vb or .cs code file. Instead, a control template is associated with the custom control. Custom controls have a default control template that is defined in their Shared
(static
for C#) constructor, but like we all know, WPF control templates can be replaced at will by the consumer. Additionally, a custom control can have multiple control templates defined by the developer, and these can be applied by a theme or can be swapped out in code. Such flexibility! Don't you just love WPF!
Imports System.ComponentModel
<TemplatePart(Name:="PART_IndicatorList", Type:=GetType(ItemsControl))> _
Public Class ListBoxWithSelectedItemIndicator
Inherits System.Windows.Controls.ContentControl
#Region " Private Declarations "
Private _objIndicatorList As ItemsControl
Private _objIndicatorOffsets As _
System.Collections.ObjectModel.ObservableCollection(Of Double)
Private _objListBox As ListBox
#End Region
#Region " Public Declarations "
Public Shared ReadOnly IndicatorBrushProperty As DependencyProperty = _
DependencyProperty.Register("IndicatorBrush", GetType(Brush), _
GetType(ListBoxWithSelectedItemIndicator), _
New PropertyMetadata(New _
LinearGradientBrush(Colors.LightBlue, Colors.Blue, _
New Point(0.5, 0), New Point(0.5, 1))))
Public Shared ReadOnly IndicatorHeightWidthProperty As _
DependencyProperty = _
DependencyProperty.Register("IndicatorHeightWidth", _
GetType(Double), GetType(ListBoxWithSelectedItemIndicator), _
New PropertyMetadata(16.0))
#End Region
#Region " Properties "
<Description("Brush used to paint the indicator."), Category("Custom")> _
Public Property IndicatorBrush() As Brush
Get
Return CType(GetValue(IndicatorBrushProperty), Brush)
End Get
Set(ByVal value As Brush)
SetValue(IndicatorBrushProperty, value)
End Set
End Property
<Description("Size of indictor. Default value is 16."), _
Category("Custom")> _
Public Property IndicatorHeightWidth() As Double
Get
Return CType(GetValue(IndicatorHeightWidthProperty), Double)
End Get
Set(ByVal value As Double)
SetValue(IndicatorHeightWidthProperty, value)
End Set
End Property
#End Region
#Region " Methods "
Shared Sub New()
DefaultStyleKeyProperty.OverrideMetadata( _
GetType(ListBoxWithSelectedItemIndicator), _
New FrameworkPropertyMetadata( _
GetType(ListBoxWithSelectedItemIndicator)))
End Sub
Public Sub New()
End Sub
Protected Overrides Sub OnContentChanged(ByVal oldContent As Object, _
ByVal newContent As Object)
MyBase.OnContentChanged(oldContent, newContent)
If newContent Is Nothing OrElse TypeOf newContent Is ListBox Then
_objListBox = TryCast(newContent, ListBox)
If _objIndicatorOffsets IsNot Nothing AndAlso _
_objIndicatorOffsets.Count > 0 Then
_objIndicatorOffsets.Clear()
End If
Exit Sub
Else
Throw New System.NotSupportedException("Invalid content." & _
" ListBoxWithSelectedItemIndicator only accepts" & _
" a ListBox control as its content.")
End If
End Sub
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
_objIndicatorList = _
TryCast(GetTemplateChild("PART_IndicatorList"), ItemsControl)
If _objIndicatorList Is Nothing Then
Throw New Exception("Hey! The PART_IndicatorList" & _
" is missing from the" _
" template or is not an ItemsControl. " & _
"Sorry but this ItemsControl is required.")
End If
End Sub
Private Sub ListBoxWithSelectedItemIndicator_Loaded(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
If _objIndicatorList Is Nothing Then
Exit Sub
End If
_objIndicatorOffsets = _
New System.Collections.ObjectModel.ObservableCollection(Of Double)
_objIndicatorList.ItemsSource = _objIndicatorOffsets
Me.AddHandler(ListBox.SelectionChangedEvent, _
New SelectionChangedEventHandler(AddressOf ListBox_SelectionChanged))
Me.AddHandler(ScrollViewer.ScrollChangedEvent, _
New ScrollChangedEventHandler(AddressOf ListBox_ScrollViewer_ScrollChanged))
UpdateIndicators()
End Sub
Private Sub UpdateIndicators()
If _objIndicatorOffsets Is Nothing Then
Exit Sub
End If
If _objListBox Is Nothing Then
Exit Sub
End If
If _objIndicatorOffsets IsNot Nothing AndAlso _
_objIndicatorOffsets.Count > 0 Then
_objIndicatorOffsets.Clear()
End If
If _objListBox.SelectedItems.Count = 0 Then
Exit Sub
End If
Dim objGen As ItemContainerGenerator = _objListBox.ItemContainerGenerator
If objGen.Status <> _
Primitives.GeneratorStatus.ContainersGenerated Then
Exit Sub
End If
For Each objSelectedItem As Object In _objListBox.SelectedItems
Dim lbi As ListBoxItem = _
TryCast(objGen.ContainerFromItem(objSelectedItem), ListBoxItem)
If lbi Is Nothing Then
Continue For
End If
Dim objTransform As GeneralTransform = _
lbi.TransformToAncestor(_objListBox)
Dim pt As Point = objTransform.Transform(New Point(0, 0))
Dim dblOffset As Double _
= pt.Y + (lbi.ActualHeight / 2) - (Me.IndicatorHeightWidth / 2)
_objIndicatorOffsets.Add(dblOffset)
Next
End Sub
Private Sub ListBox_ScrollViewer_ScrollChanged(ByVal sender As Object, _
ByVal e As System.Windows.Controls.ScrollChangedEventArgs)
If e.VerticalChange = 0 Then
Exit Sub
End If
UpdateIndicators()
End Sub
Private Sub ListBox_SelectionChanged(ByVal sender As Object, _
ByVal e As System.Windows.Controls.SelectionChangedEventArgs)
UpdateIndicators()
End Sub
Private Sub ListBoxWithSelectedItemIndicator_Unloaded(ByVal sender As Object,_
ByVal e As System.Windows.RoutedEventArgs) Handles Me.Unloaded
Me.RemoveHandler(ListBox.SelectionChangedEvent, _
New SelectionChangedEventHandler(AddressOf _
ListBox_SelectionChanged))
Me.RemoveHandler(ScrollViewer.ScrollChangedEvent, _
New ScrollChangedEventHandler(AddressOf _
ListBox_ScrollViewer_ScrollChanged))
End Sub
#End Region
End Class
Code High Points
Control Content
When authoring a custom control that has a Content
property and your control needs to act on that Content
, you'll want to override OnContentChanged
. This routine gives the developer access to the oldContent
and newContent
properties when the consumer is adding, removing, or swapping out the Content
.
In our control, we want to make sure two things happen, and OnContentChanged
is the place to do them.
- Our control requires that our
Content
property contains either nothing or a ListBox
control. OnContentChanged
allows us to test the type of Content
and reject it if necessary. - Our control wraps the
ListBox
and acts upon it. It's important that our control removes any references to the child ListBox
when the consumer removes or swaps it out. Use the OnContentChanged
routine to clean up any references to the Content
, if required.
Control Template Child Controls
WPF custom controls should be lookless. A new term that means the control visual and code are highly decoupled. Translated, consumers of a control are free to modify the control template, and the control code should continue to function normally. If your default control template contains child controls that the code requires to function, the OnApplyTemplate
routine is the place you can verify their existence and also obtain a reference to them. It is up to the control developer to decide to throw an exception or continue gracefully without the child control it was looking for.
If your control has child controls that the template needs to function, you should attribute your control class to indicate this. The <TemplatePart(Name:="PART_IndicatorList", Type:=GetType(ItemsControl))>
attribute accomplishes this for our control. By convention, these control names are prefixed with PART_
.
Tapping into the Child ListBox Control Activities
When WPF was released, a new kid on the block appeared. Routed Events. Let's look at how Routed Events allow our control to act upon the user's activity with our child ListBox
.
In the Loaded
event of our control, we add two Routed Event handlers:
Me.AddHandler(ListBox.SelectionChangedEvent, New SelectionChangedEventHandler(AddressOf ListBox_SelectionChanged))
Me.AddHandler(ScrollViewer.ScrollChangedEvent, New ScrollChangedEventHandler(AddressOf ListBox_ScrollViewer_ScrollChanged))
These Routed Event handlers are not attached to an instance of any specific control. This is vastly different from standard event handlers we use in ASP.NET or WinForms programming. Instead, we are listening for any child ListBox
or ScrollViewer
controls that raise the event we are targeting. In our case, these are the SelectionChangedEvent
and ScrollChangedEvent
events. It is important to mention, these Routed Event handlers are handling events for any ListBox
or ScrollViewer
that is a child of our control. If our control had multiple ListBox
or ScrollViewer
controls, our Routed Event handler code must be authored to handle this. In our case, there is only one of each of these controls.
When our child ListBox
selection changes or the child ListBox
's ScrollViewer
is scrolled, our control's event handler code gets called.
Both of these event handlers call the UpdateIndicators
code which synchronizes the ListBox
's SelectedItem
s with the indicator arrows. It does this by looping through the child ListBox
's SelectedItems
collection and adding the ListBoxItem
's vertical offset position within the ListBox
to a module level collection in our control. That collection of offsets is data bound to an ItemsControl
in our control template which renders an arrow indicator at the same vertical offset position as its corresponding ListBoxItem
. This is the heart of Josh's article mentioned above.
Control Template
The control template XAML markup for this control was borrowed from Josh's article. He had authored his awesome solution as a UserControl
, I moved it into a custom control, and changed the way the control interacts with the ListBox
by wrapping the child ListBox
and adding behavior to it. His article provides a blow by blow account of how the code in this control template works.
Conclusion
I hope you like this article and code. I was really stretched during this expedition into XAML only land. I hope that you gained some insight into the WPF concept of loosely coupled Routed Event handling that allows us to write applications where a parent control can handle events from child controls. Microsoft really did an awesome job with WPF.
History
- 28 Oct. 2007: Initial release.