WPF Binding: The power of styles and templates in WPF.

Original author: Beatriz Costa
  • Transfer
In WPF, there is a very clear distinction between the behavior of Control and what it looks like. For example, the behavior of an object of the Button class is to respond to various events by clicking, but its appearance can be anything - you can make a button in the form of an arrow, a fish, or anything else that is suitable for your application. Overriding a Control’s display is very simple when using VS with styles and templates, and even easier if you have Microsoft Expression Blend. In this example, I will show you how to override the display of the ListBox, which is used to display the list of planets.

I decided to start by creating a data source with planets and the sun. I defined a SolarSystemObject class with the following properties: Name, Orbit, Diameter, Image and Details. I overloaded the ToString () method in this class so that it returns the name of the solar system object. Then I added the SolarSystem class with the SolarSystemObjects property of type ObservableCollection. In the constructor for the SolarSystem class, I added the sun and nine planets to the SolarSystemObjects collection.

As soon as I determined the data source, I was ready to add to the main window the ListBox that was associated with this collection:
  1. <Window.Resources>
  2.   <local:SolarSystem x:Key=”solarSystem” />
  3.   (…)
  4. </Window.Resources>
  5.   <ListBox ItemsSource=”{Binding Source={StaticResource solarSystem}, Path=SolarSystemObjects}” />
* This source code was highlighted with Source Code Highlighter.

And so, the ListBox displays the planets, but visually it still looks a bit rustic:

At this stage, I began to think about how to display the planets in the most realistic way - my goal was to achieve a display similar to the diagrams of the solar system in school books. The first step was to change the layout of ListBoxItem's. The standard layout for the ListBox is the StackPanel, which causes the ListBoxItems to display one after the other (to be more precise, this is VirtualizingStackPanel, which adds virtualization to the traditional StackPanel). In order to display the planets the way I want, I need a Canvas, which allows me to position the elements in it according to a certain number of pixels to the left and top of the borders of this Canvas. The ListBox has an ItemsPanel property with the ItemsPanelTemplate type, which can be used to change the ListBox's layout. as is done in my example. Here's how I did it:
  1. <Style TargetType=”ListBox”>
  2.   <Setter Property=”ItemsPanel”>
  3.     <Setter.Value>
  4.       <ItemsPanelTemplate>
  5.         <Canvas Width=”590? Height=”590? Background=”Black” />
  6.       </ItemsPanelTemplate>
  7.     </Setter.Value>
  8.   </Setter>
  9. </Style>
* This source code was highlighted with Source Code Highlighter.

My next step was to define the mapping of each planet. I did this using a DataTemplate. I decided to represent each planet with its image and white ellipse imitating its orbit around the sun. I also added a tooltip with detailed information about the planet that appears when the cursor is over the planet.
  1. <DataTemplate DataType="{x:Type local:SolarSystemObject}">
  2.   <Canvas Width="20" Height="20" >
  3.     <Ellipse
  4.       Canvas.Left="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-1.707}"
  5.       Canvas.Top="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-0.293}"
  6.       Width="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
  7.       Height="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
  8.       Stroke="White"
  9.       StrokeThickness="1"/>
  10.     <Image Source="{Binding Path=Image}" Width="20" Height="20">
  11.       <Image.ToolTip>
  12.         <StackPanel Width="250" TextBlock.FontSize="12">
  13.           <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" />
  14.           <StackPanel Orientation="Horizontal">
  15.             <TextBlock Text="Orbit: " />
  16.             <TextBlock Text="{Binding Path=Orbit}" />
  17.             <TextBlock Text=" AU" />
  18.           </StackPanel>
  19.           <TextBlock Text="{Binding Path=Details}" TextWrapping="Wrap"/>
  20.         </StackPanel>
  21.       </Image.ToolTip>
  22.     </Image>
  23.   </Canvas>
  24. </DataTemplate>
  26. <Style TargetType="ListBoxItem">
  27.   <Setter Property="Canvas.Left" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
  28.   <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
  29.   (…)
  30. </Style>
* This source code was highlighted with Source Code Highlighter.

As you can see in the template and style above, the properties that determine the position of the ListBoxItem and the position and size of Ellips are based on the orbit of the planet and all use the same converter, but with different parameters. The converter's task is to convert the distances between the objects of the solar system into the distances inside the Canvas in pixels. My first implementation of this converter simply multiplied the value of the orbit by a constant, but I found that the inner planets were very close to each other. Therefore, I decided to slightly modify the calculation to make it non-linear. I also decided that the converter accept a certain parameter that would scale the final result by a certain value so that I could use this logic many times.
  1. public class ConvertOrbit : IValueConverter
  2. {
  3.   public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  4.   {
  5.     double orbit = (double)value;
  6.     double factor = System.Convert.ToDouble(parameter);
  7.     return Math.Pow(orbit / 40, 0.4) * 770 * factor;
  8.   }
  10.   public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  11.   {
  12.     throw new NotSupportedException(”This method should never be called”);
  13.   }
  14. }
* This source code was highlighted with Source Code Highlighter.

If you run the application now, you will see that all the planets are correctly positioned relative to the sun. If you mouse over them, you will get more detailed information about the planet. If you click on the planet, then the standard ListBoxItem template will assign a blue background to the selected item, which looks like a small frame around the item. This is not the effect that I would like to see, so I decided to change the presentation of the selected element.
To change this style, I think it would be easier to use Microsoft Expression Blend to look at the standard template, and then remake it the way you want. I started by selecting the ListBox in Blend, then I went to the “Object” menu, selected “Edit Other Styles”, “Edit ItemContainerStyle” and then “Edit a Copy”. Then I gave a name to the template and clicked “OK”. If at this stage you follow the “XAML” tab, then you will see the full standard style for the ListBoxItem, which includes the following template:
  1. <Setter Property=”Template”>
  2.   <Setter.Value>
  3.     <ControlTemplate TargetType=”{x:Type ListBoxItem}”>
  4.       <Border SnapsToDevicePixels=”true” x:Name=”Bd” Background=”{TemplateBinding Background}” BorderBrush=”{TemplateBinding BorderBrush}” BorderThickness=”{TemplateBinding BorderThickness}” Padding=”{TemplateBinding Padding}”>
  5.         <ContentPresenter SnapsToDevicePixels=”{TemplateBinding SnapsToDevicePixels}” HorizontalAlignment=”{TemplateBinding HorizontalContentAlignment}” VerticalAlignment=”{TemplateBinding VerticalContentAlignment}”/>
  6.       </Border>
  7.       <ControlTemplate.Triggers>
  8.         <Trigger Property=”IsSelected” Value=”true”>
  9.           <Setter Property=”Background” TargetName=”Bd” Value=”{DynamicResource {x:Static SystemColors.HighlightBrushKey}}”/>
  10.           <Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}”/>
  11.         </Trigger>
  12.         <MultiTrigger>
  13.           <MultiTrigger.Conditions>
  14.             <Condition Property=”IsSelected” Value=”true”/>
  15.             <Condition Property=”Selector.IsSelectionActive” Value=”false”/>
  16.           </MultiTrigger.Conditions>
  17.           <Setter Property=”Background” TargetName=”Bd” Value=”{DynamicResource {x:Static SystemColors.ControlBrushKey}}”/>
  18.           <Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}”/>
  19.         </MultiTrigger>
  20.         <Trigger Property=”IsEnabled” Value=”false”>
  21.           <Setter Property=”Foreground” Value=”{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}”/>
  22.         </Trigger>
  23.       </ControlTemplate.Triggers>
  24.     </ControlTemplate>
  25.   </Setter.Value>
  26. </Setter>
* This source code was highlighted with Source Code Highlighter.

Using it as a basis, I created a simple template that adds a yellow ellipse around the selected planet:
  1. <Style TargetType=”ListBoxItem”>
  2.   (…)
  3.   <Setter Property=”Template”>
  4.     <Setter.Value>
  5.       <ControlTemplate TargetType=”{x:Type ListBoxItem}”>
  6.         <Grid>
  7.           <Ellipse x:Name=”selectedPlanet” Margin=”-10” StrokeThickness=”2”/>
  8.           <ContentPresenter SnapsToDevicePixels=”{TemplateBinding SnapsToDevicePixels}”
  9.             HorizontalAlignment=”{TemplateBinding HorizontalContentAlignment}”
  10.             VerticalAlignment=”{TemplateBinding VerticalContentAlignment}”/>
  11.         </Grid>
  12.         <ControlTemplate.Triggers>
  13.           <Trigger Property=”IsSelected” Value=”true”>
  14.             <Setter Property=”Stroke” TargetName=”selectedPlanet” Value=”Yellow”/>
  15.           </Trigger>
  16.         </ControlTemplate.Triggers>
  17.       </ControlTemplate>
  18.     </Setter.Value>
  19.   </Setter>
  20. </Style>
* This source code was highlighted with Source Code Highlighter.

The following screenshot shows the final version of the application. If you hover over a picture of a planet, you will get more detailed information about it. If you click on a planet, a yellow ellipse will surround the planet.

Here you can find the project for Visual Studio with the code that was used in the article.

Also popular now: