WEBSITE

Silverlight Recipes : Controls - Customizing the Default ListBoxItem UI

2/19/2013 6:35:57 PM

1. Problem

You want to customize the default look and feel of an item inside a data-bound ListBox beyond what can be done using data templates.

2. Solution

Define and apply a custom control template to the ListBoxItem using the ItemContainerStyle on the ListBox.

3. How It Works

In data-bound ListBox scenarios, you typically do not explicitly add each individual item that the ListBox displays. When you bind the ItemsSource property on the ListBox to a collection (or set it in code), an entry is added to the ListBox automatically for each data item in the collection, optionally formatted based on a data template bound to the ItemTemplate property.

Perhaps you want to change the look and feel of the items beyond what the data template feature affords you. Say you want to change the selection behavior from the default display of a light blue selection bar to some other mode of selection display. Or you may notice that no matter what your data template specifies, each item is displayed within a rectangular boundary, and you don't like that look.

Each such generated item is of type ListBoxItem, in turn derived from ContentControl. The default template applied to this ListBoxItem specifies some of the UI behavior of these items, including the ones just mentioned as examples.

To customize that behavior, you need to design a custom template for the ListBoxItem control and apply it to each ListBoxItem in the ListBox. The ListBox control exposes a property named ItemContainerStyle, which can be bound to a style that gets applied to each ListBoxItem as it is generated. You can use this style to associate your custom template to the ListBoxItems in the ListBox.

4. The Code

This code sample demonstrates a custom template for a ListBoxItem.Listing 1 shows the XAML for the page with the control template defined in the resources section.

Listing 1. XAML for the MainPage showing ListBoxItem control template
<UserControl x:Class="Recipe5_3.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="700" Height="800"
    xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
  <UserControl.Resources>
    <DataTemplate x:Key="dtProductInfo">
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto" />
          <RowDefinition Height="Auto" />
          <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" VerticalAlignment="Center"
                   HorizontalAlignment="Left" Text="{Binding Name}"
                   Margin="3,3,3,3"/>
        <StackPanel HorizontalAlignment="Left" Grid.Row="1"
                    Orientation="Horizontal" Margin="3,3,3,3">
          <TextBlock Text="$" Margin="0,0,2,0" />
          <TextBlock Grid.Row="1" Text="{Binding ListPrice}"/>
        </StackPanel>

        <StackPanel HorizontalAlignment="Left" Grid.Row="2"
                    Orientation="Horizontal" Margin="3,3,3,3">
          <Ellipse Height="15" Width="15"
                   Fill="{Binding InventoryLevelBrush}" Margin="0,0,2,0" />
          <TextBlock Text="{Binding InventoryLevelMessage}"  />
        </StackPanel>
      </Grid>
    </DataTemplate>

    <!-- custom ListBoxItem control template -->
    <ControlTemplate x:Key="ctCustomListBoxItem" TargetType="ListBoxItem">
      <Grid Background="{TemplateBinding Background}"
            Margin="{TemplateBinding Margin}">
        <Grid.RowDefinitions>
          <RowDefinition Height="0.225*" MinHeight="20"/>
          <RowDefinition Height="0.775*"/>
        </Grid.RowDefinitions>

					  

<vsm:VisualStateManager.VisualStateGroups>
          <vsm:VisualStateGroup x:Name="CommonStates">
            <vsm:VisualStateGroup.Transitions>
              <vsm:VisualTransition
                GeneratedDuration="00:00:00.0500000" To="MouseOver"/>
              <vsm:VisualTransition
                GeneratedDuration="00:00:00.0500000" From="MouseOver"/>
            </vsm:VisualStateGroup.Transitions>
            <vsm:VisualState x:Name="Normal">
              <Storyboard/>
            </vsm:VisualState>
            <vsm:VisualState x:Name="MouseOver">
              <Storyboard>
                <ColorAnimationUsingKeyFrames
                  BeginTime="00:00:00"
                  Duration="00:00:00.0010000"
                  Storyboard.TargetName="BottomBorder"
                  Storyboard.TargetProperty=
                  "(Border.Background).(SolidColorBrush.Color)">
                  <SplineColorKeyFrame KeyTime="00:00:00" Value="#FF68A3DE"/>
                </ColorAnimationUsingKeyFrames>
              </Storyboard>
            </vsm:VisualState>
          </vsm:VisualStateGroup>
          <vsm:VisualStateGroup x:Name="SelectionStates">
            <vsm:VisualState x:Name="Unselected">
              <Storyboard/>
            </vsm:VisualState>
            <vsm:VisualState x:Name="Selected">
              <Storyboard>
                <ColorAnimationUsingKeyFrames
                BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="TopBorder"
                Storyboard.TargetProperty=
                  "(Border.Background).(SolidColorBrush.Color)">
                  <SplineColorKeyFrame KeyTime="00:00:00" Value="#FFFF2D00"/>
                </ColorAnimationUsingKeyFrames>
                <ObjectAnimationUsingKeyFrames
                BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="SelectionIndicator"
                Storyboard.TargetProperty="(UIElement.Visibility)">
                  <DiscreteObjectKeyFrame KeyTime="00:00:00">
                    <DiscreteObjectKeyFrame.Value>
                      <vsm:Visibility>Visible</vsm:Visibility>

					  

</DiscreteObjectKeyFrame.Value>
                  </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
              </Storyboard>
            </vsm:VisualState>
            <vsm:VisualState x:Name="SelectedUnfocused">
              <Storyboard>
                <ColorAnimationUsingKeyFrames
                BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="TopBorder"
                Storyboard.TargetProperty=
                  "(Border.Background).(SolidColorBrush.Color)">
                  <SplineColorKeyFrame KeyTime="00:00:00" Value="#FFFF2D00"/>
                </ColorAnimationUsingKeyFrames>
                <ObjectAnimationUsingKeyFrames
                BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="SelectionIndicator"
                Storyboard.TargetProperty="(UIElement.Visibility)">
                  <DiscreteObjectKeyFrame KeyTime="00:00:00">
                    <DiscreteObjectKeyFrame.Value>
                      <vsm:Visibility>Visible</vsm:Visibility>
                    </DiscreteObjectKeyFrame.Value>
                  </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
              </Storyboard>
            </vsm:VisualState>
          </vsm:VisualStateGroup>
          <vsm:VisualStateGroup x:Name="FocusStates">
            <vsm:VisualState x:Name="Unfocused">
              <Storyboard/>
            </vsm:VisualState>
            <vsm:VisualState x:Name="Focused">
              <Storyboard>
                <ObjectAnimationUsingKeyFrames
                BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="FocusRect"
                Storyboard.TargetProperty="(UIElement.Visibility)">
                  <DiscreteObjectKeyFrame KeyTime="00:00:00">
                    <DiscreteObjectKeyFrame.Value>
                      <vsm:Visibility>Visible</vsm:Visibility>
                    </DiscreteObjectKeyFrame.Value>
                  </DiscreteObjectKeyFrame>

					  

</ObjectAnimationUsingKeyFrames>
              </Storyboard>
            </vsm:VisualState>
          </vsm:VisualStateGroup>
        </vsm:VisualStateManager.VisualStateGroups>
        <Border HorizontalAlignment="Stretch"
                Margin="0,0,0,0"
                VerticalAlignment="Stretch"
                CornerRadius="5,5,0,0"
                BorderBrush="#FF000000"
                BorderThickness="2,2,2,0"
                Background="#00000000"
                x:Name="TopBorder">
          <Grid x:Name="SelectionIndicator" Visibility="Collapsed"
                Width="18" Height="18"
                HorizontalAlignment="Left"
                VerticalAlignment="Center" Margin="2,2,2,2">
            <Path x:Name="Path" Stretch="Fill"
                  StrokeThickness="1.99975" StrokeLineJoin="Round"
                  Stroke="#FF000000" Fill="#FF27BC0F"
                  Data="F1 M 0.999876,18.0503C 2.60366,
                  16.4731 4.23013,14.9006 5.86216,13.3491L 12.6694,
                  18.7519C 14.239,10.2011 20.9487,3.27808 29.8744,
                  0.999878L 31.4453,2.68387C 23.1443,
                  9.95105 17.8681,19.7496 16.5592,
                  30.3293L 16.5592,30.2592L 0.999876,18.0503 Z "/>
          </Grid>
        </Border>
        <Border Margin="0,0,0,0" CornerRadius="0,0,5,5"
                BorderBrush="#FF000000" BorderThickness="2,2,2,2"
                Grid.Row="1" Padding="3,3,3,3" x:Name="BottomBorder"
                Background="#00000000">
          <Grid>
            <ContentPresenter  HorizontalAlignment="Left"
              Margin="3,3,3,3"
              Content="{TemplateBinding Content}"
              ContentTemplate="{TemplateBinding ContentTemplate}"/>
            <Rectangle HorizontalAlignment="Stretch" Margin="0,0,0,0" Width="Auto"
                       Stroke="#FF000000"
                       StrokeDashArray="0.75 0.15 0.25 0.5 0.25"
                       x:Name="FocusRect" Visibility="Collapsed"/>
          </Grid>
        </Border>
      </Grid>
    </ControlTemplate>

					  

<!-- style using the custom ListBoxItem control template -->
    <Style x:Key="styleCustomListBoxItem" TargetType="ListBoxItem">
      <Setter Property="Template"
            Value="{StaticResource ctCustomListBoxItem}"/>
      <Setter Property="Margin" Value="3,5,3,5" />
    </Style>
  </UserControl.Resources>
  <Grid x:Name="LayoutRoot" Background="White" Height="Auto" Margin="20,20">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Stretch"
                HorizontalAlignment="Stretch">
      <ListBox x:Name="lbxStandard" HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch" Margin="0,0,25,0"
               ItemTemplate="{StaticResource dtProductInfo}" />

      <!-- applying a custom ListBoxItemTemplate using the ItemContainerStyle -->
      <ListBox x:Name="lbxCustom"
               HorizontalAlignment="Stretch"
               VerticalAlignment="Stretch"
               ItemTemplate="{StaticResource dtProductInfo}"
               ItemContainerStyle="{StaticResource styleCustomListBoxItem}"/>
    </StackPanel>

  </Grid>
</UserControl>

					  

Your control template named ctCustomListBoxItem is defined as two Border elements placed in two Rows of a top-level Grid. The Border element named TopBorder contains a Grid SelectionIndicator, encapsulating a Path that represents a check mark. The BottomBorder element contains a ContentPresenter with appropriate TemplateBindings defined for several properties, including the Content and the ContentTemplate properties so that, once data bound, the data for each ListBoxItem gets displayed through this ContentPresenter inside BottomBorder. You also include a Rectangle named FocusRect with a dotted border; this Rectangle is overlaid on the ContentPresenter but is initially kept hidden because you set the Visibility property to Visibility.Collapsed.

Figure 1 compares the Normal state of a ListBoxItem using this template to that of the default look and feel (with both ListBoxes bound to the same data source and using the same data template, defined as dtProductInfo in Listing 1). 

Figure 1. Normal ListBoxItem state with (left) and without (right) the custom template

If you refer to the storyboard for the MouseOver visual state in Listing 5-3, you will see that the background color of BottomBorder changes to indicate the state change. Figure 5-15 shows the result.

Figure 5-15. MouseOver state with (left) and without (right) the custom template

On a transition to the Selected state, you change the background color of TopBorder and make visible the SelectionIndicator Grid that contains the check mark. This gives the selected item a colored top bar with a check mark in it. For the Focused state, you make visible the focus indicator Rectangle FocusRect. Note that you also define a storyboard for the SelectedUnfocused state when an item is selected but the current focus is elsewhere; in that case, the colored top border and check mark are visible but the focus rectangle is hidden. Figure 2 shows the results in comparison.

Figure 2. Selected state with focus with (left) and without (right) the custom template

You also define a style resource, styleCustomListBoxItem, in Listing 1 that associates the control template to a ListBoxItem. To show the control template in action, you have added two ListBoxes, named lbxStandard and lbxCustom, to your page, each using the same data template (dtProductInfo) as the ItemTemplate. However, lbxCustom has its style set to styleCustomListBoxItem.

Listing 2 shows the codebehind for the page.

Listing 2. Codebehind for the MainPage containing the ListBox
using System;
using System.Windows.Controls;
using System.Windows.Media;
using Recipe5_3.AdvWorks;

namespace Recipe5_3
{
  public partial class MainPage : UserControl
  {
    //WCF service client
    AdvWorksDataServiceClient client =
      new AdvWorksDataServiceClient();
    public MainPage()
    {

InitializeComponent();
      GetData();
    }

    private void GetData()
    {
      client.GetInventoryCompleted +=
        new EventHandler<GetInventoryCompletedEventArgs>(
          delegate(object sender, GetInventoryCompletedEventArgs e)
          {
            Product product = e.UserState as Product;
            product.ProductInventories = e.Result;
            product.InventoryLevelBrush = null;
            product.InventoryLevelMessage = null;

          });
      client.GetProductsCompleted +=
        new EventHandler<GetProductsCompletedEventArgs>(
          delegate(object sender, GetProductsCompletedEventArgs e)
          {

            lbxStandard.ItemsSource = e.Result;
            lbxCustom.ItemsSource = e.Result;

            foreach (Product p in e.Result)
            {
              client.GetInventoryAsync(p, p);
            }
          });

      client.GetProductsAsync();
    }
  }
}

namespace Recipe5_3.AdvWorks
{

  public partial class Product
  {
    private SolidColorBrush _InventoryLevelBrush;

    public SolidColorBrush InventoryLevelBrush
    {
      get

					  

{
        return (this.ProductInventories == null
          || this.ProductInventories.Count == 0) ?
          new SolidColorBrush(Colors.Gray) :
          (this.ProductInventories[0].Quantity > this.SafetyStockLevel ?
          new SolidColorBrush(Colors.Green) :
          (this.ProductInventories[0].Quantity > this.ReorderPoint ?
          new SolidColorBrush(Colors.Yellow) : new SolidColorBrush(Colors.Red)));
      }
      set
      {
        //no actual value set here - just property change raised
        //can be set to null in code to cause rebinding, when
        //ProductInventories changes
        RaisePropertyChanged("InventoryLevelBrush");
      }

    }
    private string _InventoryLevelMessage;

    public string InventoryLevelMessage
    {
      get
      {
        return (this.ProductInventories == null
          || this.ProductInventories.Count == 0) ? "Stock Level Unknown"
          : (this.ProductInventories[0].Quantity > this.SafetyStockLevel
          ? "In Stock" :
          (this.ProductInventories[0].Quantity > this.ReorderPoint ?
          "Low Stock" : "Reorder Now"));

      }
      set
      {
        //no actual value set here - just property change raised
        //can be set to null in code to cause rebinding,
        //when ProductInventories changes
        RaisePropertyChanged("InventoryLevelMessage");
      }
    }
  }
}

					  

You set the ItemsSource properties for both lbxStandard and lbxCustom to a list of Product data items obtained from the AdventureWorks WCF service, as shown in the GetData() method in Listing 2. You also populate inventory information for each Product instance from the same service as a collection of ProductInventory instances.

In the declaration of dtProductInfo in Listing 1, note the Ellipse with its Fill property bound to InventoryLevelBrush and the TextBlock with its Text property bound to InventoryLevelMessage. These are both calculated values, exposed as properties on the Product class extended using the partial class facility, as shown in Listing 2. The InventoryLevelBrush property returns a SolidColorBrush of different colors based on whether the total inventory is above or below certain levels, indicated by the SafetyStockLevel and ReorderPoint properties of the Product data class. The InventoryLevelMessage property applies the same logic to return differently formatted text messages instead.
Other  
 
Top 10
Review : Sigma 24mm f/1.4 DG HSM Art
Review : Canon EF11-24mm f/4L USM
Review : Creative Sound Blaster Roar 2
Review : Philips Fidelio M2L
Review : Alienware 17 - Dell's Alienware laptops
Review Smartwatch : Wellograph
Review : Xiaomi Redmi 2
Extending LINQ to Objects : Writing a Single Element Operator (part 2) - Building the RandomElement Operator
Extending LINQ to Objects : Writing a Single Element Operator (part 1) - Building Our Own Last Operator
3 Tips for Maintaining Your Cell Phone Battery (part 2) - Discharge Smart, Use Smart
REVIEW
- First look: Apple Watch

- 3 Tips for Maintaining Your Cell Phone Battery (part 1)

- 3 Tips for Maintaining Your Cell Phone Battery (part 2)
VIDEO TUTORIAL
- How to create your first Swimlane Diagram or Cross-Functional Flowchart Diagram by using Microsoft Visio 2010 (Part 1)

- How to create your first Swimlane Diagram or Cross-Functional Flowchart Diagram by using Microsoft Visio 2010 (Part 2)

- How to create your first Swimlane Diagram or Cross-Functional Flowchart Diagram by using Microsoft Visio 2010 (Part 3)
Popular Tags
Video Tutorail Microsoft Access Microsoft Excel Microsoft OneNote Microsoft PowerPoint Microsoft Project Microsoft Visio Microsoft Word Active Directory Exchange Server Sharepoint Sql Server Windows Server 2008 Windows Server 2012 Windows 7 Windows 8 Adobe Flash Professional Dreamweaver Adobe Illustrator Adobe Photoshop CorelDRAW X5 CorelDraw 10 windows Phone 7 windows Phone 8 Iphone