Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

The WPF Thought Process

Rate me:
Please Sign up or sign in to vote.
4.79/5 (57 votes)
13 Oct 2007CPOL11 min read 204.9K   867   129   52
An introspective journey of solving problems using WPF

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 ListBoxItems. 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.

  1. 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.
  2. 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.
  3. 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:

  1. The selection indicators must be outside of the ListBox, according to my aesthetic preferences.
  2. The selection indicators need to have their own space to exist, so that they do not overlap with neighboring controls.
  3. 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 ListBoxItems 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:

XML
<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 Polygons 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 Doubles 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:

XML
<DataTemplate>
  <Grid Width="16" Height="16">
    <!-- 
    A lightweight drop shadow 
    under the selection indicator. 
    -->
    <Polygon Fill="LightGray">
      <Polygon.Points>
        <Point X="4" Y="4" />
        <Point X="16" Y="10" />
        <Point X="4" Y="16" />
      </Polygon.Points>
    </Polygon>

    <!-- 
    The selection indicator itself.
    -->
    <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:

XML
<!-- 
Host all of the selection indicators
within a Canvas panel.
-->
<ItemsControl.ItemsPanel>
  <ItemsPanelTemplate>
    <Canvas />
  </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

<!-- 
Position a selection indicator based on the 
offset value to which it is bound. 
-->
<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:

C#
public ListBoxWithIndicator()
{
    InitializeComponent();

    // Set up the list of selection indicator offsets
    // as the data source for the ItemsControl.
    _indicatorOffsets = new ObservableCollection<double>();
    _indicatorList.ItemsSource = _indicatorOffsets;

    // Move the indicators when the set of 
    // selected items is modified.
    _listBox.SelectionChanged += delegate
    {
        this.UpdateIndicators();
    };

    // Move the indicators when the ListBox's
    // ScrollViewer is scrolled.
    _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:

XML
<!-- This is invalid XAML. -->
<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:

XML
<!-- In ListBoxWithIndicator.xaml -->
<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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Josh creates software, for iOS and Windows.

He works at Black Pixel as a Senior Developer.

Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.

Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.

Check out his Advanced MVVM[^] book.

Visit his WPF blog[^] or stop by his iOS blog[^].

See his website Josh Smith Digital[^].

Comments and Discussions

 
QuestionOffset Calc? Pin
jastewart28-Jan-10 2:12
jastewart28-Jan-10 2:12 
AnswerRe: Offset Calc? Pin
Josh Smith28-Jan-10 2:52
Josh Smith28-Jan-10 2:52 
GeneralRe: Offset Calc? Pin
jastewart28-Jan-10 3:01
jastewart28-Jan-10 3:01 
GeneralNice job--thanks Pin
David Veeneman19-Sep-09 6:20
David Veeneman19-Sep-09 6:20 
GeneralRe: Nice job--thanks Pin
Josh Smith19-Sep-09 8:04
Josh Smith19-Sep-09 8:04 
Generalm-v-vm help with xceed data grid Pin
dlbirch5-Dec-08 3:51
dlbirch5-Dec-08 3:51 
GeneralRe: m-v-vm help with xceed data grid Pin
Josh Smith5-Dec-08 3:57
Josh Smith5-Dec-08 3:57 
GeneralComplexity level of example Pin
Todd Beaulieu27-Feb-08 9:06
Todd Beaulieu27-Feb-08 9:06 
GeneralRe: Complexity level of example Pin
Josh Smith27-Feb-08 9:17
Josh Smith27-Feb-08 9:17 
GeneralExcellent Article Pin
CarpeThis21-Oct-07 16:23
CarpeThis21-Oct-07 16:23 
GeneralRe: Excellent Article Pin
Josh Smith22-Oct-07 1:56
Josh Smith22-Oct-07 1:56 
GeneralYou are my WPF HERO Pin
Jac16-Oct-07 14:08
Jac16-Oct-07 14:08 
GeneralRe: You are my WPF HERO Pin
Josh Smith16-Oct-07 14:20
Josh Smith16-Oct-07 14:20 
GeneralExcellent Pin
Douglas Troy16-Oct-07 9:38
Douglas Troy16-Oct-07 9:38 
GeneralRe: Excellent Pin
Josh Smith16-Oct-07 9:42
Josh Smith16-Oct-07 9:42 
GeneralScreenshots Pin
Serge Baltic16-Oct-07 8:36
Serge Baltic16-Oct-07 8:36 
GeneralRe: Screenshots Pin
Josh Smith16-Oct-07 8:42
Josh Smith16-Oct-07 8:42 
I don't know, but you might want to post that question to the WPF Forum[^]. Someone there might be able to point you in the right direction.

:josh:
My WPF Blog[^]
Without a strive for perfection I would be terribly bored.

GeneralRe: Screenshots Pin
Serge Baltic10-Jul-08 13:09
Serge Baltic10-Jul-08 13:09 
GeneralWPF controls and New York Times reader Pin
Lily Bristol16-Oct-07 8:26
Lily Bristol16-Oct-07 8:26 
GeneralRe: WPF controls and New York Times reader Pin
Josh Smith16-Oct-07 8:39
Josh Smith16-Oct-07 8:39 
GeneralRe: WPF controls and New York Times reader Pin
Lily Bristol16-Oct-07 8:54
Lily Bristol16-Oct-07 8:54 
QuestionUsability ? Pin
to_be_defined16-Oct-07 0:49
to_be_defined16-Oct-07 0:49 
AnswerRe: Usability ? Pin
Josh Smith16-Oct-07 1:37
Josh Smith16-Oct-07 1:37 
GeneralExcellent article Pin
Greg Russell15-Oct-07 21:18
professionalGreg Russell15-Oct-07 21:18 
GeneralRe: Excellent article Pin
Rei Miyasaka15-Oct-07 23:28
Rei Miyasaka15-Oct-07 23:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.