Introduction
I can assure you that this article was very difficult for me to write. This is my thirtieth article on CodeProject, so I thought it was time for a new challenge. The challenge is not only for me, but for you, the reader, as well. Hopefully I have overcome the challenge of writing this article well enough so that you might now face the far more daunting challenge of learning how to think in WPF. If you are already a seasoned WPF developer, feel free to read on and see how someone else solves problems using WPF.
This article attempts to explain the thought processes I went through while designing and implementing a solution to a problem using WPF. If you are fairly new to WPF and are just starting to ascend its infamous learning cliff, perhaps this article will help to shed some light on why and how one might put many of the WPF concepts to use. I am not claiming that my way of approaching WPF is the "right" or "best" way, but simply the way I think about it.
Background
This article assumes that you are already somewhat familiar with WPF. We will not be covering the basics here. If you need to learn about the fundamentals of WPF, you might want to check out my five-part Guided Tour of WPF right here on CodeProject.
The Problem to be Solved
I wanted to create sleek-looking selection indicators for the items in a ListBox
. Instead of having my ListBox
look like this…
…I wanted it to look something like this instead…
In both of those screenshots above, the same three items are selected in the ListBox
. The top image shows the standard look of a ListBox
on Windows XP with the Olive theme. The bottom image shows the standard ListBox
with triangular selection indicators instead of highlighted ListBoxItem
s. If you were to scroll through the ListBox
, those selection indicators need to stay directly next to their associated ListBoxItem
at all times (otherwise they're rather meaningless).
The selection indicators do not need to be interactive. If the user clicks on an indicator, nothing should happen. It is only a visual feature, and does not affect the state of the ListBox
.
I also want the "selection indicator" functionality to be reusable, so that I can easily apply selection indicators to any ListBox
in any application. I need this functionality to be encapsulated, but things like colors and font sizes should be customizable.
Where to Start?
At this point we have a pretty clear understanding of the problem which needs to be solved. Now it is time to compare and contrast various possible solutions to the problem. A few approaches come to mind, so let's review them.
- We could give the
ListBox
an ItemTemplate
containing the selection indicator. The template could have a trigger which hides the indicator when the ListBoxItem
is not selected. The problem with this solution is that a selection indicator will appear to be "part of" a ListBoxItem
. I'd prefer to have the indicators be external to the entire ListBox
, as seen in the screenshot above. This aesthetic decision makes the ItemTemplate
approach inappropriate.
- We could render a selection indicator in the adorner layer of a
ListBoxItem
. This approach frees us from having to render the indicator within the ListBoxItem
's bounds, but it presents a new problem. If the ListBox
is placed directly next to another control, then the selection indicators might be rendered on top of that neighboring control. This would result in some strange visual problems, so it seems that we need to allocate some screen real estate specifically for the selection indicators.
- We could create a
ControlTemplate
for ListBox
and allocate some space in the template for selection indicators. This would allow us to make it appear that the selection indicators are external to the ListBox
, and also give the indicators some space of their own. However, why should we insist to other developers that having selection indicators and using their own custom ControlTemplate
are mutually exclusive options? What if they need both? Since there is no way to customize or "subclass" an existing ControlTemplate
, this approach won't work either.
We just reviewed three possible approaches to how and where the selection indicators will be rendered. None of them worked out, but we learned three important points along the way. Those points are:
- The selection indicators must be outside of the
ListBox
, according to my aesthetic preferences.
- The selection indicators need to have their own space to exist, so that they do not overlap with neighboring controls.
- Using selection indicators should not limit what else you can do with the
ListBox
, such as prohibiting you from applying a custom ControlTemplate
.
The third point needs some clarification. We can only provide support for so much customization to the ListBox
. If the user swaps out the ListBox
's ItemsPanel
with some other layout panel, we cannot guarantee that our selection indicators will always line up correctly with the selected items. We need to assume that the ListBoxItem
s will be stacked vertically, as seen by default.
Based on all of the points introduced above, we must now decide how to move forward and start implementing this feature. We can satisfy all of our constraints by creating a UserControl
subclass containing a ListBox
and a Grid
panel, which hosts the selection indicators, directly next to it. The basic structure of that UserControl
, which is called ListBoxWithIndicator
, can be seen below:
<UserControl>
<DockPanel>
<Grid DockPanel.Dock="Left" />
<ListBox />
</DockPanel>
</UserControl>
How to Draw the Selection Indicators?
The selection indicators are not part of the ListBox
. They exist in a neighboring panel, and must be created/positioned/removed when items are selected/scrolled/deselected in the ListBox
. What is a good way to accomplish that in WPF? Before reading any further, think about that question for a while.
Welcome back. If you took some time to contemplate how the selection indicators should be managed, you probably realized that there are many ways to skin that cat. If your first instinct was to owner-draw little triangles next to the selected items, you should take a look around you and realize that you aren't in Kansas anymore. WPF certainly allows you to do low-level rendering, somewhat similar to working with an HDC or Graphics
object, but that would be taking the high road for absolutely no good reason.
One seemingly viable approach would be to hook the ListBox
's SelectionChanged
event and, when it is raised, create some Polygon
elements (i.e. triangular selection indicators) in the selection indicator area. You could position those Polygon
s so that they are each next to a selected ListBoxItem
by setting their Margin
's Top
to some calculated offset. That would effectively "push" each Polygon
down to the correct location next to a ListBoxItem
.
That technique would certainly work, but it just doesn't feel "right" to me. In my opinion we should not be manually creating and positioning the selection indicators. They should create and position themselves, based purely on some XAML markup. This reduces the number of moving parts in our code, which means that there will be fewer bugs to fix. So how can we implement this logic without writing too much code?
The solution to this problem makes use of several powerful features of WPF: an items panel, data binding an attached property, and a DataTemplate
. Let's review the solution I came up with to see how it works.
A selection indicator has a fixed width and height, and also has a fixed horizontal offset from the left edge of the container in which it lives. The only variable it does not know by itself is its vertical offset from the top of the container in which it lives. That vertical offset effectively determines which selected ListBoxItem
it "points at."
Suppose we were to calculate the vertical offsets needed to display selection indicators next to each selected ListBoxItem
, and store those offsets in a collection. If we supplied those values to an ItemsControl
as its ItemsSource
, the ItemsControl
would look like this (the offset values are circled in red):
Obviously that is not the visual effect we're after, but it is a start. At this point we have an ItemsControl
next to the ListBox
, and it contains a list of Double
s which represent how far away from the top of the ItemsControl
each selection indicator needs to be. Next we need to give the ItemsControl
's ItemTemplate
property a DataTemplate
which renders a selection indicator, as seen below:
<DataTemplate>
<Grid Width="16" Height="16">
-->
<Polygon Fill="LightGray">
<Polygon.Points>
<Point X="4" Y="4" />
<Point X="16" Y="10" />
<Point X="4" Y="16" />
</Polygon.Points>
</Polygon>
-->
<Polygon Fill="{Binding ElementName=mainControl, Path=IndicatorBrush}">
<Polygon.Points>
<Point X="2" Y="2" />
<Point X="14" Y="8" />
<Point X="2" Y="14" />
</Polygon.Points>
</Polygon>
</Grid>
</DataTemplate>
Once we do that, the UI looks like this:
That certainly doesn't look right! What's the problem here? Why aren't the selection indicators next to the selected items? Take a moment, think about it before continuing. It's OK, I'll wait…
The problem here is that our selection indicators have no idea that the Double
value they represent in the ItemsControl
should be used as their vertical offset. Just because we tell the ItemsControl
to render each item as a little triangle doesn't mean that it will position them at the correct location for us. We need to explain how those offset values should be put to use. To do that we can make use of some powerful WPF capabilities: a custom items panel and binding an attached property.
By default ItemsControl
lays out its items in a vertical stack. We don't want it to do that in this situation. Instead we need it to lay out the items in a Canvas
, so that we can tell the Canvas
where to position the selection indicators. We inform the Canvas
of each selection indicator's vertical offset by binding the attached Canvas.Top
property on each indicator. That XAML is seen below, and is part of the ItemsControl
declaration:
<!---->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!---->
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Path=.}" />
</Style>
</ItemsControl.ItemContainerStyle>
Since ItemsControl
internally creates a ContentPresenter
to host each item, we need to set the Canvas.Top
property on that element so that it will be positioned correctly by the Canvas
. When this is in place, and a few visual tricks are applied to remove the highlight color of a selected ListBoxItem
, the UI looks like this:
When are the Selection Indicator Offsets Calculated?
The exact details of how the offsets are calculated are not relevant for this discussion, but it is interesting to note when they are calculated. In two situations it is important to update the offsets, when the selected items change and when the items are scrolled. Here is the ListBoxWithIndicator
constructor, which sets up handlers for those two events:
public ListBoxWithIndicator()
{
InitializeComponent();
_indicatorOffsets = new ObservableCollection<double>();
_indicatorList.ItemsSource = _indicatorOffsets;
_listBox.SelectionChanged += delegate
{
this.UpdateIndicators();
};
_listBox.AddHandler(
ScrollViewer.ScrollChangedEvent,
new ScrollChangedEventHandler(delegate
{
this.UpdateIndicators();
}));
}
The way that the ScrollViewer
's ScrollChanged
event is handled is pretty interesting in that we never actually have to find the actual ScrollViewer
and directly hook its event. Instead we rely on the bubbling nature of the routed event and let the event come to us, so to speak. Initially I planned on writing some code which walked down the visual tree looking for the ListBox
's ScrollViewer
, but decided that it's both easier and safer to just listen for the bubbling event. It is safer to use this technique because the more code you write, the more possibilities there are for bugs!
How to Make the ListBox and Selection Indicators Customizable?
So far we have figured out a way to render the selection indicators and keep them up-to-date as the user interacts with the ListBox
. One thing that we have not yet figured out is how to make it easy for a developer to use the ListBoxWithIndicator
control. In my mind there are two major concerns: you need to be able to configure the ListBox
from XAML, and you need to be able to easily specify what color(s) the selection indicators should be. Unfortunately XAML like this won't work:
<!---->
<local:ListBoxWithIndicator>
<local:ListBoxWithIndicator.ListBox>
<ListBox.ItemsSource>
<SomeData />
</ListBox.ItemsSource>
</local:ListBoxWithIndicator.ListBox>
</local:ListBoxWithIndicator>
The problem is that there's no way to easily access the inner ListBox
from within XAML. You cannot set properties on a sub-object of an object in XAML, unless you are creating that sub-object. So, how can we let a developer set properties on the ListBox
within our ListBoxWithIndicator
control? Once again, take a moment to think about this one…
The solution I decided to use is to simply expose a dependency property on ListBoxWithIndicator
, called ListBoxStyle
, and then bind our ListBox
's Style
property to it. Here's how that works:
<!---->
<ListBox
x:Name="_listBox"
Style="{Binding ElementName=mainControl, Path=ListBoxStyle}"
/>
When you create an instance of the control, you can set its ListBoxStyle
property to a Style which sets any number of properties on the inner ListBox
.
I also created a public dependency property called IndicatorBrush
which the selection indicators bind their Fill property against. That enables a developer to have control over the colors of the indicators too.
Conclusion
If you are new to WPF, but have experience with older UI platforms, it is no small feat to unlearn your old way of doing things and learn the WPF way. There are many ways that WPF offers the developer new powers, but you have to be willing to go through the humbling experience of being a newbie all over again. Hopefully this article will help to accelerate that painful process for you, assuming you need any help in the first place.
Revision History
- October 13, 2007 – Created the article