#1,186 – Default ItemContainerStyle for a ListBox

The default style for the ItemContainerStyle of a ListBox (version 4.5.1 of the .NET Framework) is shown below.  The template is included within a style that includes some other default property values.

This style is applied to each ListBoxItem rendered in the ListBox.  The structure of each ListBoxItem is a Border surrounding a ContentPresenter.  More importantly, the style shows the default triggers that control how items are rendered when you move the mouse over an item or select an item.

        <Style x:Key="lbDefaultItemContainerStyle" TargetType="{x:Type ListBoxItem}">
            <Setter Property="SnapsToDevicePixels" Value="True"/>
            <Setter Property="Padding" Value="4,1"/>
            <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ListBoxItem}">
                        <Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="IsMouseOver" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.MouseOver.Border}"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="False"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Border}"/>
                            </MultiTrigger>
                            <MultiTrigger>
                                <MultiTrigger.Conditions>
                                    <Condition Property="Selector.IsSelectionActive" Value="True"/>
                                    <Condition Property="IsSelected" Value="True"/>
                                </MultiTrigger.Conditions>
                                <Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
                                <Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Border}"/>
                            </MultiTrigger>
                            <Trigger Property="IsEnabled" Value="False">
                                <Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

The style references some other predefined resources, shown below:

        <Style x:Key="FocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="Item.MouseOver.Background" Color="#1F26A0DA"/>
        <SolidColorBrush x:Key="Item.MouseOver.Border" Color="#a826A0Da"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Background" Color="#3DDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedInactive.Border" Color="#FFDADADA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Background" Color="#3D26A0DA"/>
        <SolidColorBrush x:Key="Item.SelectedActive.Border" Color="#FF26A0DA"/>

You would use the main style as follows:

        <ListBox Margin="5" ItemsSource="{Binding MovieList}"
                 ItemContainerStyle="{DynamicResource lbDefaultItemContainerStyle}" />

#1,043 – Using a DockPanel as the Items Panel for a ListBox

You can replace the default StackPanel used as the items panel for a ListBox with any other panel element.  Below is an example of displaying some news stories in a DockPanel.

Assume that we have a NewsStory class as follow:

    public class NewsStory
    {
        public string Story { get; set; }
        public Brush Color { get; set; }
        public Dock Dock { get; set; }
        public double Rotate { get; set; }

        public NewsStory(string story, Color color, Dock dock, double rotate)
        {
            Story = story;
            Color = new SolidColorBrush(color);
            Dock = dock;
            Rotate = rotate;
        }

        public override string ToString()
        {
            return Story;
        }
    }

We can then create a collection of NewsStory items that we’ll bind to.  Notice that we create a spiral pattern by setting consecutive Dock properties to Bottom/Left/Top/Right.  We also use the Angle property to rotate every other element.

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;

            Stories = new ObservableCollection<NewsStory>
            {
                new NewsStory("Diaper Market Bottoms Out", Colors.AliceBlue, Dock.Bottom, 0.0),
                new NewsStory("Antique Stripper to Display Wares", Colors.AntiqueWhite, Dock.Left, -90.0),
                new NewsStory("Cancer Society Honors Marlboro Man", Colors.Aqua, Dock.Top, 0.0),
                new NewsStory("War Dims Hope for Peace", Colors.Aquamarine, Dock.Right, -90.0)
                // more entries go here..
            };
            RaisePropertyChanged("Stories");

        }

        public ObservableCollection<NewsStory> Stories { get; protected set; }

        // INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        private void RaisePropertyChanged(string propName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

In XAML, we set the ItemContainerStyle to do the docking and specify a DockPanel as the ItemsPanel.

        <ListBox ItemsSource="{Binding Stories}">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="DockPanel.Dock" Value="{Binding Dock}"/>
                    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                    <Setter Property="VerticalContentAlignment" Value="Stretch"/>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <DockPanel IsItemsHost="True"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding Story}"
                           Background="{Binding Color}">
                        <Label.LayoutTransform>
                            <RotateTransform Angle="{Binding Rotate}"/>
                        </Label.LayoutTransform>
                    </Label>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

Here’s what the end result looks like:

1044-001

#1,007 – Changing Scrollbar Settings for a ListBox

By default, the visibility of both horizontal and vertical scrollbars for a ListBox is set to Auto, indicating that the scrollbars should show up automatically, as needed.

You can change this behavior by setting the value of the ScrollViewer.HorizontalScrollBarVisibility or ScrollViewer.VerticalScrollBarVisibility properties.  They can take one of the following values:

  • Auto – Scrollbar visible if required
  • Visible – Scrollbar always visible
  • Hidden – Scrollbar not shown, but content allowed to scroll (e.g. using arrow keys)
  • Disabled – Content does not scroll
        <ListBox ItemsSource="{Binding ActorList}" Margin="10"
                 ScrollViewer.VerticalScrollBarVisibility="Auto"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Source="{Binding Image}" Height="100"/>
                        <StackPanel Margin="10,0">
                            <TextBlock Text="{Binding FullName}" FontWeight="Bold" />
                            <TextBlock Text="{Binding Dates}"/>
                            <TextBlock Text="{Binding KnownFor}" FontStyle="Italic"/>
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

1007-001

#1,006 – Scrollbars in a ListBox Appear as Needed

Scrolling in a ListBox is managed by a ScrollViewer, which contains the items panel of the ListBox.  The ScrollViewer provides scrolling logic and makes available both a horizontal and a vertical scrollbar.

The visibility of the scrollbars in a ScrollViewer is controlled by the HorizontalScrollBarVisibility and VerticalScrollBarVisibility properties.  By default, both of these properties are set to Auto for the ScrollViewer used in a ListBox.  This means that the scrollbar only appears if the content of the ListBox does not fit in the associated dimension.

1006-0011006-002

 

1006-003

#1,004 – Setting CanContentScroll Disables Virtualization

When you set ScrollViewer.CanContentScroll to false for a ListBox, you switch from scrolling on an item basis to scrolling on a pixel basis.  However, when you do this, virtualization on the VirtualizingStackPanel being used as an items panel is disabled.  This happens because the virtualization logic works on an item-by-item basis and so is turned off when you are scrolling on a pixel-by-pixel basis.

This means that when you have a large number of items in an ItemsControl, you’ll typically want to leave content scrolling enabled.

 

#1,003 – Set CanContentScroll to False for Smooth Scrolling in a ListBox

By default, the ListBox control scrolls intelligently, one item at a time.  Scrolling behavior in a ListBox is provided by a ScrollViewer.  By default, the CanContentScroll property of the containing ScrollViewer is set to true, indicating that the items panel (e.g. a StackPanel) is responsible for the scrolling.  The StackPanel scrolls one item at a time as you drag the scrollbar thumb.

1003-001 1003-002

If you want to allow scrolling by pixels, rather than by items, you can set the ScrollViewer.CanContentScroll property on the ListBox to false.  This delegates scrolling responsibility back to the ScrollViewer.

        <ListBox ItemsSource="{Binding ActorList}" Width="300"
                 ScrollViewer.CanContentScroll="False">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Source="{Binding Image}" Height="100"/>
                        <StackPanel Margin="10,0">
                            <TextBlock Text="{Binding FullName}" FontWeight="Bold" />
                            <TextBlock Text="{Binding Dates}"/>
                            <TextBlock Text="{Binding KnownFor}" FontStyle="Italic"/>
                        </StackPanel>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

1003-003

#1,001 – Setting an Alternating Background Color in an ItemsControl

The AlternationCount and AlternationIndex properties of controls inheriting from ItemsControl allow setting property values that alternate across the collection of items.

The AlternationCount property dictates how many unique styles you alternate between.  The most common value is 2, indicating that every other item has a particular style.  In the example below, we use these properties to set up an alternating background color.

    <Window.Resources>
        <AlternationConverter x:Key="altconvBackground">
            <SolidColorBrush Color="AliceBlue"/>
            <SolidColorBrush Color="White"/>
        </AlternationConverter>
    </Window.Resources>

    <Grid Margin="10" >
        <ListBox ItemsSource="{Binding ActorList}" Width="200"
                 AlternationCount="2">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Background"
                            Value="{Binding RelativeSource={RelativeSource Self}, Path=(ItemsControl.AlternationIndex), Converter={StaticResource altconvBackground}}"/>
                </Style>
            </ListBox.ItemContainerStyle>
        </ListBox>
    </Grid>

We set an ItemContainerStyle because we want to set a property for a given ListBoxItem.  We then set the Background of each item to derive its value from the AlternationIndex property of the item.  The AlternationConverter is used to convert this index, 0 or 1, into a SolidColorBrush.

#1,000 – Displaying the Contents of a ListBox in a Circle

We can arrange the child elements of a ListBox into a circle by defining a new class that inherits from Panel, as follows:

    public class CircularPanel : Panel
    {
        protected override System.Windows.Size MeasureOverride(System.Windows.Size availableSize)
        {
            foreach (UIElement child in Children)
                child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));

            return base.MeasureOverride(availableSize);
        }

        // Arrange stuff in a circle
        protected override System.Windows.Size ArrangeOverride(System.Windows.Size finalSize)
        {
            if (Children.Count > 0)
            {
                // Center & radius of panel
                Point center = new Point(finalSize.Width / 2, finalSize.Height / 2);
                double radius = Math.Min(finalSize.Width, finalSize.Height) / 2.0;
                radius *= 0.8;   // To avoid hitting edges

                // # radians between children
                double angleIncrRadians = 2.0 * Math.PI / Children.Count;

                double angleInRadians = 0.0;

                foreach (UIElement child in Children)
                {
                    Point childPosition = new Point(
                        radius * Math.Cos(angleInRadians) + center.X,
                        radius * Math.Sin(angleInRadians) + center.Y);

                    child.Arrange(new Rect(childPosition, child.DesiredSize));

                    angleInRadians += angleIncrRadians;
                }
            }

            return finalSize;
        }
    }

We can now use this panel as the ItemsPanel for a ListBox.

        <ListBox ItemsSource="{Binding ActorList}">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <local:CircularPanel />
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>

1000-001
1000-002
(Thanks to Jobi Joy for an example of this).

#999 – Using a Canvas as the Items Panel for a ListBox

You can replace the default StackPanel used as the items panel for a ListBox with any other panel element.  If you have items that you want to display at arbitrary locations, you can use a Canvas for your items panel.

The example below presents a list of cities, where each city is placed at its proper latitude and longitude.

Assuming that we have a City class that accepts a name and latitude/longitude values passed to its constructor, we can create a list of cities:

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = this;

            CityList = new ObservableCollection<City>
            {
                new City("Duluth", 46.83, 92.18),
                new City("Redmond", 44.27, 121.15),
                new City("Tucson", 32.12, 110.93),
                new City("Denver", 39.75, 104.87),
                new City("Boston", 42.37, 71.03),
                new City("Tampa", 27.97, 82.53)
            };
        }

        private ObservableCollection<City> cityList;
        public ObservableCollection<City> CityList
        {
            get { return cityList; }
            set
            {
                cityList = value;
                RaisePropertyChanged("CityList");
            }
        }

        // INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        private void RaisePropertyChanged(string propName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

We then bind a ListBox to this list of cities.  We also:

  • Use its ItemContainerStyle to map latitude and longitude values to the attached Top and Left properties of the Canvas element
  • Use value converters to convert latitude and longitude values to canvas positions
  • Specify the Canvas as the ItemsPanel
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow"
        Width="470" Height="310">

    <Window.Resources>
        <ResourceDictionary>
            <local:LatValueConverter x:Key="latValueConverter" />
            <local:LongValueConverter x:Key="longValueConverter" />
            <sys:Double x:Key="mapWidth">440</sys:Double>
            <sys:Double x:Key="mapHeight">240</sys:Double>
        </ResourceDictionary>
    </Window.Resources>

    <StackPanel Orientation="Horizontal" Margin="5" >
        <ListBox ItemsSource="{Binding CityList}"
                 DisplayMemberPath="Name">
            <ListBox.ItemContainerStyle>
                <Style TargetType="{x:Type ListBoxItem}">
                    <Setter Property="Canvas.Left"
                            Value="{Binding Longitude, Converter={StaticResource longValueConverter},
                                            ConverterParameter={StaticResource mapWidth}}"/>
                    <Setter Property="Canvas.Top"
                            Value="{Binding Latitude, Converter={StaticResource latValueConverter},
                                            ConverterParameter={StaticResource mapHeight}}"/>
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas IsItemsHost="True"
                            Width="{StaticResource mapWidth}"
                            Height="{StaticResource mapHeight}"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>
    </StackPanel>
</Window>

Here is the implementation of the value converters:

    public static class Constants
    {
        public const double LatTop = 50.0;
        public const double LatBottom = 24.0;

        public const double LongLeft = 125.0;
        public const double LongRight = 66.0;
    }

    public class LatValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double latitude = (double)value;
            double height = (double)parameter;

            int top = (int)((Constants.LatTop - latitude) / (Constants.LatTop - Constants.LatBottom) * height);
            return top;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

    public class LongValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            double longitude = (double)value;
            double width = (double)parameter;

            int left = (int)((Constants.LongLeft - longitude) / (Constants.LongLeft - Constants.LongRight) * width);
            return left;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

The end result is that the city names are displayed at their proper locations on the canvas.  Note that because the cities are displayed in a ListBox, we can still select one of them.

999-001

#998 – Orient a ListBox Horizontally

You can make a ListBox render its items horizontally, rather than vertically, by setting its ItemsPanel.

In the example below, we set the ItemsPanel to a template containing a horizontally-oriented StackPanel.  (The default is a vertically-oriented StackPanel).

        <ListBox Width="230" Height="70"
                 ItemsSource="{Binding ActorList}"
                 DisplayMemberPath="LastName">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <VirtualizingStackPanel IsItemsHost="True" Orientation="Horizontal"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
        </ListBox>

998-001