Click here to Skip to main content
Click here to Skip to main content

A WPF Problem Solved Two Very Different Ways - Using XAML Only - Using a Custom Control

, 28 Oct 2007 CPOL
Rate this:
Please Sign up or sign in to vote.
Article on solving a problem using a XAML only approach and then solving that same problem using WPF custom controls.

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

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 Styles 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}">
        
        <!--This is the secret to having the area that the CheckBoxes are in 
            blend with the color of the parent border control
            making this area look like a separate control.-->
        <Border CornerRadius="0" Background="{TemplateBinding Background}">
                
          <!--Disabling the HorizontalScrollBarVisibility 
                 allows the ContentPresenter TextBlocks to wrap if the 
                 ListBox is not wide enough for the text-->
          <ScrollViewer HorizontalScrollBarVisibility="Disabled">
                
            <!--This border is Mr. Cool. It places a line down the 
                   middle of the control between the CheckBoxes and the 
                   ListBoxes items.-->
            <Border Background="{StaticResource scrollViewerBackgroundBrush}" 
                     Margin="20,0,0,0" 
                     BorderBrush="{StaticResource scrollViewerBorderBrush}" 
                     BorderThickness="1,0,0,0" x:Name="border">
                        
                <!--Our ListBoxItems are here-->
               <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 CheckBoxes 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 Borders. 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 Borders? 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 Borders 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>

            <!--The grid left margin of -20, gets the 
                first column over to the left to give 
                us that look that the CheckBoxes are 
                outside our ListBox control-->
            <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>

            <!--This positions our content in the perfect position-->
            <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 TextBlocks 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 CheckBoxes. 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 SelectionModes. 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 ListBoxes. 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 ListBoxItems 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 StaticResources: 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 ListBoxes or ComboBoxes 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 ListBoxItems 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 ListBoxItems, 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 CheckBoxes. 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}}}">

    <!-- A lightweight drop shadow under the selection indicator. -->
    <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>

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

'Special thanks to Josh Smith for his great teachings.
'Without his below article, this control would have never been authored.
'
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()
    'This OverrideMetadata call tells the system that this 
    '  element wants to provide a style that is different than 
    '    its base class.
    'This style is defined in themes\generic.xaml
    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)

    'this is our insurance policy that the developer does not
    '  add content that is not a ListBox

    If newContent Is Nothing OrElse TypeOf newContent Is ListBox Then
        'this ensures that our reference to the child ListBox 
        '  is always correct or nothing.
        
        'if the child ListBox is removed, our reference is 
        '  set to Nothing
        
        'if the child ListBox is swapped out, our reference 
        '  is set to the newContent
        
        _objListBox = TryCast(newContent, ListBox)
        
        'this removes our references to the ListBox items
        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()

    'when the template is applied, this give the developer 
    '  the opportunity to get  references to name objects 
    '  in the control template.
    
    'in our case, we need a reference to the ItemsControl 
    '  that holds the indicator arrows.
    '
    'what your control does in the absence of an 
    '  expected object in the control template is up 
    '  to the control developer.
    
    'in my case here, without the items control, 
    '  we are dead in the water.
    '
    'remember that custom controls are supposed 
    '  to be Lookless. Meaning the visual 
    '   and code are highly decoupled.
    
    'Any designer using Blend fully expects to be 
    '  able edit the control template anyway they want.
    
    'My using the "PART_" naming convention, you indicate 
    ' that this object is probably necessary for the 
    '  control to work, but this is not true in all cases.
    '
    _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
        'remember how much "fun" tabs were be in VB and Access?  Well...
        '
        'this is here because when you place a custom control 
        '  in a tab, it loads the control once before it runs OnApplyTemplate
        
        'when the TabItem its in gets clicked (focus), OnApplyTemplate runs 
        '  then Loaded runs again.
        '
        'since OnApplyTemplate has not run yet, we are out of here
        Exit Sub
    End If

    _objIndicatorOffsets = _
      New System.Collections.ObjectModel.ObservableCollection(Of Double)
      
    _objIndicatorList.ItemsSource = _objIndicatorOffsets

    'How cool are routed events!  
    'We can listen into the child ListBoxes activities and act accordingly.
    
    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()

    'This is the awesome procedure that Josh Smith 
    '  authored with a few modifications

    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 the user is scrolling horizontality, no reason to run any of our 
    '  attached behavior code
    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 SelectedItems 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.

License

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

Share

About the Author

Karl Shifflett
Architect Gayle Manufacturing Company
United States United States
Karl loves .NET, WPF, WCF, ASP.NET, VB.NET and C#.
 
Awards:
 
  • December 2008 VB.NET Code Project Article Award
  • 2009 Code Project MVP
  • 2008 Code Project MVP
  • 2008 Microsoft MVP - Client App Dev
  • December 2007 VB.NET Code Project Article Award
  • Gold Medal Winner at IBM's 1998 PROIV Programming Contest in Las Vegas
Click here to check out my Blog
 
Click here to learn about Mole 2010 debugging tool for Visual Studio 2010
 
Click here to read about XAML Power Toys
 

Just a grain of sand on the worlds beaches.

Follow on   Twitter

Comments and Discussions

 
QuestionResource Dictionary PinmemberWilliam Gaskill3-Jun-14 6:53 
GeneralMy vote of 5 Pinmemberprachi_sathep28-Jun-11 22:12 
GeneralMy vote of 1 Pinmembersivakmr17-Oct-10 23:41 
QuestionWhy does Listbox always select first item? PinmemberSevententh10-Mar-10 23:46 
AnswerRe: Why does Listbox always select first item? PinmemberKarl Shifflett11-Mar-10 3:48 
GeneralRe: Why does Listbox always select first item? PinmemberSevententh11-Mar-10 23:27 
GeneralRe: Why does Listbox always select first item? PinmemberSevententh11-Mar-10 23:42 
GeneralRe: Why does Listbox always select first item? PinmemberKarl Shifflett13-Mar-10 6:32 
General[Message Deleted] Pinmembermerill14-Oct-09 16:42 
General[Message Deleted] PinmvpKarl Shifflett15-Oct-09 9:29 
GeneralRe: Simpler WPF CheckedListBox Implementation (poor comment) Pinmembermerill15-Oct-09 11:22 
GeneralRe: Simpler WPF CheckedListBox Implementation (poor comment) PinmvpKarl Shifflett15-Oct-09 21:42 
GeneralRe: Simpler WPF CheckedListBox Implementation (poor comment) Pinmembermerill18-Nov-09 10:19 
GeneralRe: Simpler WPF CheckedListBox Implementation (poor comment) PinmvpKarl Shifflett19-Nov-09 3:58 
GeneralI downloaded the .SLN file - it won't open Pinmemberb-k-w12-Sep-08 11:59 
GeneralRe: I downloaded the .SLN file - it won't open Pinmemberb-k-w15-Sep-08 5:00 
GeneralRe: I downloaded the .SLN file - it won't open PinmvpKarl Shifflett22-Sep-08 4:37 
QuestionHow to pre-load the selection with multi select? PinmemberJohn Mairs29-Aug-08 12:34 
AnswerRe: How to pre-load the selection with multi select? PinmvpKarl Shifflett30-Aug-08 7:01 
John,
 
I'm off line for a few days. I'll post an answer next week.
 
Cheers, Karl
 
» CodeProject 2008 MVP
 
My Blog | Mole's Home Page

Just a grain of sand on the worlds beaches.


GeneralRe: How to pre-load the selection with multi select? PinmemberJohn Mairs30-Aug-08 9:36 
GeneralRe: How to pre-load the selection with multi select? PinmemberJohn Mairs3-Sep-08 12:32 
GeneralRe: How to pre-load the selection with multi select? PinmemberJohn Mairs15-Sep-08 8:09 
GeneralRe: How to pre-load the selection with multi select? PinmvpKarl Shifflett22-Sep-08 4:36 
GeneralRe: How to pre-load the selection with multi select? PinmemberJohn Mairs1-Oct-08 9:34 
GeneralRe: How to pre-load the selection with multi select? PinmemberJohn Mairs1-Oct-08 10:57 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 28 Oct 2007
Article Copyright 2007 by Karl Shifflett
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid