Apr 01, 2007

The power of Styles and Templates in WPF

37PlanetsListBox

In WPF, there is a very clean separation between the behavior of a Control and the look of a Control. For example, a Button’s behavior consists only of listening to Click events, but its look can be anything – you can make a Button look like an arrow, a fish, or whatever else suits your application. Redefining the look of a Control is very easy to do in VS with Styles and Templates, and even easier if you have Blend. In this sample, I will show you how I redefined the look of a ListBox representing a list of planets.

I started out by implementing a data source with planets and the sun. I defined a class called “SolarSystemObject” with several properties (Name, Orbit, Diameter, Image and Details). I overrode the ToString(…) method in this class to return the name of the solar system object. Then I added a class called “SolarSystem” with a property called “SolarSystemObjects” of type ObservableCollection<SolarSystemObject>. In the constructor for the “SolarSystem” class, I added the sun and the nine planets to the “SolarSystemObjects” collection.

Once I had my source defined, I was ready to add a ListBox to the Window, bound to this collection:

    <Window.Resources>
        <local:SolarSystem x:Key="solarSystem" />
        (…)
    </Window.Resources>

    <ListBox ItemsSource="{Binding Source={StaticResource solarSystem}, Path=SolarSystemObjects}" />

The ListBox displays the planets, but visually this is still a little plain:

At this point, I started brainstorming ways to display the planets in a more realistic way – my goal was to achieve a look similar to the solar system diagrams in school books. The first step was to change the layout of the ListBoxItems. The default layout for a ListBox is a StackPanel, which causes the ListBoxItems to be displayed one above another (to be more precise, it’s a VirtualizingStackPanel, which adds virtualization to the traditional StackPanel). In order to display the planets the way I wanted, I needed a Canvas, which allows me to position the items within it by specifying the number of pixels to the Top and Left of that Canvas. There is an ItemsPanel property on ListBox of type ItemsPanelTemplate that can be used to change the layout of the ListBox, which is what I used in my sample. Here is how I did that:

    <Style TargetType="ListBox">
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <Canvas Width="590" Height="590" Background="Black" />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
    </Style>

My next step was to define the look of each planet, which I did by using a DataTemplate. I decided to represent each planet by its image, with a white ellipse simulating its orbit around the sun. I also added a tooltip with more information about the planet, which appears when you hover over the image.

    <DataTemplate DataType="{x:Type local:SolarSystemObject}">
        <Canvas Width="20" Height="20" >
            <Ellipse
                Canvas.Left="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-1.707}"
                Canvas.Top="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=-0.293}"
                Width="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
                Height="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=2}"
                Stroke="White"
                StrokeThickness="1"/>
            <Image Source="{Binding Path=Image}" Width="20" Height="20">
                <Image.ToolTip>
                    <StackPanel Width="250" TextBlock.FontSize="12">
                        <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" />
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="Orbit: " />
                            <TextBlock Text="{Binding Path=Orbit}" />
                            <TextBlock Text=" AU" />
                        </StackPanel>
                        <TextBlock Text="{Binding Path=Details}" TextWrapping="Wrap"/>
                    </StackPanel>
                </Image.ToolTip>
            </Image>
        </Canvas>
    </DataTemplate>

    <Style TargetType="ListBoxItem">
        <Setter Property="Canvas.Left" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
        <Setter Property="Canvas.Bottom" Value="{Binding Path=Orbit, Converter={StaticResource convertOrbit}, ConverterParameter=0.707}"/>
        (…)
    </Style>

As you can see in the template and style above, the properties that specify the position of the ListBoxItem and the position and size of the Ellipse depend on the orbit of the planet, and all use the same converter with different parameters. The converter’s job is to transform distances between solar system objects to distances in device independent pixels within the canvas. My original implementation of this converter simply multiplied the orbit value by a constant, but I found that the inner planets were too crowded together, so I changed the math a little to make it non-linear. I also decided to have the converter take a parameter that scales the result by a factor, so I could reuse this logic.

    public class ConvertOrbit : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            double orbit = (double)value;
            double factor = System.Convert.ToDouble(parameter);
            return Math.Pow(orbit / 40, 0.4) * 770 * factor;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException("This method should never be called");
        }
    }

If you run the application now, you will see that the planets are all correctly positioned in relation to the sun. If you hover over them, you will get more detailed information about the planet. If you click on a planet, the default ListBoxItem template assigns a blue background to the selected item, which shows a little bit around the image. This is not the effect I was looking for, so I decided to change the look of the selected item.

In order to change that style, I figured it would be easier to use Expression Blend to look at the default template, and then tweak it to the look I had in mind. I started by selecting the ListBox in Blend, then I went to the “Object” menu, selected “Edit Other Styles”, “Edit ItemContainerStyle”, and “Edit a Copy”. Then I gave the style a name, and clicked “OK”. If you go to the XAML tab at this point, you will see the full default Style for the ListBoxItems, which includes the following template:

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Border SnapsToDevicePixels="true" x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
                    <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="true">
                        <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                            <Condition Property="Selector.IsSelectionActive" Value="false"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                    </MultiTrigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>

Using this as a base, I came up with a simpler template that adds a yellow ellipse around a planet when selected:

    <Style TargetType="ListBoxItem">
        (…)
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                    <Grid>
                        <Ellipse x:Name="selectedPlanet" Margin="-10" StrokeThickness="2"/>
                        <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsSelected" Value="true">
                            <Setter Property="Stroke" TargetName="selectedPlanet" Value="Yellow"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Below is a screenshot of the final application. If you hover over the images of the planets, you will get more information about them. If you click on a planet, a yellow ellipse will encircle it.

Here you can find the VS project with this sample code. This works with RTM WPF bits.

This sample is part of a talk I presented last Tuesday, in an event here at Microsoft in Redmond where several customers came to learn more about WPF. It was a lot of fun to talk directly to customers and reply to their questions. Here you can find my slides for this talk (all done in WPF).

20 Comments
  1. Kent boogaart

    Very cool Beatriz – thanks.

    A little off topic, but do you know of a canvas-like panel that autosizes to its content? That way you wouldn’t need to hard code the width and height of the canvas.

    PS. Hey, what’s Pluto doing on there? Wasn’t he demoted? :)

    • Bea

      Hi Kent,

      Grid autosizes to its content. However, for this example, I wanted only a quarter of the ellipses to be showing, so autosizing to content would not work for me. For the presentation, I added this ListBox to a Viewbox, so that it scales to fit the screen size, independently of the resolution of the screen.

      About Pluto… I thought Wikipedia was never wrong!! :) http://en.wikipedia.org/wiki/Pluto

      Bea

  2. Olivier Dewit

    CQFD, excellente présentation !

    Just a little problem in ConvertOrbit.Convert() with the decimal separator for countries outside US (or perhaps other planets :) , which can be fixed by using the NumberFormatInfo from the culture argument :

    double factor = System.Convert.ToDouble(parameter, culture.NumberFormat);

    • Bea

      Merci Olivier. You would think that being from a non-english speaking country I would catch that :)

      I made the correction and updated the zip.

      Bea

  3. Anonymous

    Thanks for including those slides from a “this talk”. Does this mean there was a presentation at some point, if so, is it available for download somewhere? (or do you mean that these slides are further samples of the blog post talk)

    • Bea

      Hi,

      The slides are from an internal presentation I did at Microsoft for a customer event, a couple of weeks ago. Fortunately yes, the presentation was video taped and I will be able to share it on my blog as soon as I get the OK from our technical evangelists. So stay tuned…

      Bea

  4. Ondrej

    Hello,
    I have a question, maybe it’s a bit off topic… I have a template for ListViewHeader because I want the header to have a Label and TextBox(for sorting a content of the column). It’s not a problem to make this, but I’d like to have only 1 template for more columns, it’s a problem because I need to have diferent Name property of TextBox, for each column… I really don’t know how to do that, I’m lost.
    If you can help pleas help, thank you Ondrej

    • Bea

      Hi Ondrej,

      You can use a DataTemplateSelector to have a similar DataTemplate for each column that only varies by a property (in this case Name).

      I made a sample for you that shows how to use HeaderTemplateSelector. You can find it here.

      Thanks!
      Bea

  5. RN

    A question on using templates in ResourceDictionary. How can I access elements in my DataTemplate which is part of a ResourceDictionary from a Page’s code-behind (The DataTemplate is used by the Page’s Content Control)

    From a little bit of research I did, I found that I can use the following
    this.ContentControl.Template.LoadContent()
    which will give me the root element and then use FindName to get the element that I’m interested in. The problem with this approach is that I’m able to access the DataTemplate but not the instance of that DataTemplate because of which all my Elements’ properties return null or empty.

    The reason why I need to do is because myTextBox element in the DataTemplate has Binding with UpdateSourceTrigger = Explicit and I need to call BindingExpression.UpdateSource(0 from my Page’s code-behind. Any help would be appreciated.

    - RN

    • Bea

      Hi RN,

      Did you post this in the forum? My answer to this questions sort of replies to your question.

      The instance you need to pass as a parameter to the FindName method is the ContentPresenter. This is an element within the ControlTemplate that serves as the link between the ControlTemplate and the DataTemplate. Unfortunately there is no simple method you can call on the ControlTemplate to get to the ContentPresenter. In the sample I showed in the forum, I simply walk the visual tree manually because I know it’s really small for that particular scenario. If you have a more complicated ControlTemplate, I recommend writing a simple utility method that walks the tree until it finds an element of type ContentPresenter.

      We know that this is quite hard to do, it should be much simpler. I have an item on my “wishlist” about simplifying this scenario, but unfortunately we weren’t able to get to it for the next version.

      Thanks,
      Bea

    • Bea

      Hi,
      I replied to your forum question directly in the forum.
      Bea

  6. Anonymous

    Please post Beatriz, there is so much wealth of information in this blog that it’s a shame to only see 1 post every 1.5/2 months…

    • Bea

      Thanks for your comment.

      Yeah, I know I should be posting more often. Lately my job has been taking over my life and I find myself with little free time to do anything else. But you’re right, I should bump the priority of the blog. This blog is the coolest, most fun work related activity I have going on and I should keep myself and my readers happy :)

      Thanks!
      Bea

  7. Anonymous

    Is there a way to freeze WPF ListView columns (make a specified column and it’s header stay in place when the user scrolls horizontally)?

    • Bea

      Hello,

      Unfortunately we don’t support that feature. I can’t think of a relatively simple workaround either. That would be a good feature to support when we release a DataGrid Control.

      Thanks for your comment!
      Bea

  8. Marcel Bradea

    This example unfortunately doesn’t seem to work in Silverlight (SL3 Beta). Setting a binding expression on a style setter results in “Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED)” error. Is this a known bug?

    I tried setting the style both in the root resources, the ListBox’s resources, as well as its ItemContainerStyle property, all of which result in the same effect.

  9. NT

    This is all very well until you realize the performance impact of these WPF toys.

    • Bea

      Hi NT,

      It’s true that certain scenarios can be tricky to implement in a performant way. I always recommend profiling your app before you’re ready to release it. Once you understand where the bottlenecks are, there are ways to improve them. I find this MSDN article a good read on this topic.

      Bea

Comments are closed.