XAML Tip: Graphics with ItemsControl

February 23, 2013

Sometimes in a WPF or Windows Store or Windows Phone application we need to draw some things based on some collection of data items. Suppose we have the following simple data item:

class CarData {

    public double Distance { get; set; }

    public string Image { get; set; }

}

Suppose we have a collection of CarData objects, and the requirement was to show a set of images along a line with a particular distance, like in the following screenshot:

image

The distance from the left is determined by the Distance property, and the image is determined by the Image property. How would we achieve that?

One obvious option is to use a loop that iterates through the CarData collection, and adding (e.g.) Image elements to a Canvas (by calling the Children.Add method), while setting the Canvas.Left attached property to a value based on the Distance property.

Although this technically works, it’s inelegant to say the least. If a new CarData object is added or removed, we’d have to rerun the loop to regenerate the images or manually add/remove the appropriate image. Dealing directly with the UI instead of just with the data object is just looking for trouble.

The obvious solution to the problem is data binding; it’s classic – bind the data objects to some UI using appropriate binding expressions, and just deal with the data objects; forget about the UI; let the data bindings managed everything automatically.

But how can we bind the collection of CarData objects to a Canvas’ Children? We can’t – the Children property is not bindable.

The solution is to use an ItemsControl control (or one of its derivatives) and bind its ItemsSource property to the collection. The problem with ItemsControl (and its derivatives) is that they display items in a vertical StackPanel (or VirtualizingStackPanel, but that’s unimportant for this discussion). How can we change that? Easy – use the ItemsPanel property:

<ListBox ItemsSource="{Binding}">

    <ListBox.ItemsPanel>

        <ItemsPanelTemplate>

            <Canvas />

        </ItemsPanelTemplate>

    </ListBox.ItemsPanel>

    <ListBox.ItemTemplate>

        <DataTemplate>

            <Image Source="{Binding Image}" Stretch="None" 

                Canvas.Left="{Binding Distance}" Canvas.Top="100" />

        </DataTemplate>

    </ListBox.ItemTemplate>

</ListBox>

We change the ItemsPanel to a Canvas and provide a DataTemplate (in the ItemTemplate property) that consists of an Image moved to the correct distance with the Canvas.Left attached property bound to the CarData.Distance property. Running this produces the following:

image

It’s certainly disappointing; it seems the Canvas.Left property had no effect. Can you spot the problem?

The problem is a subtle one: Every item in a ListBox is hosted in a ListBoxItem control. This means changing the Image’s Canvas.Left property has no effect, because the Image is not in any Canvas; the ListBoxItem is.

Fixing this is just a matter of applying an ItemContainerStyle that has the required properties; the Image element has no use for them:

<ListBox.ItemContainerStyle>

    <Style TargetType="ListBoxItem">

        <Setter Property="Canvas.Left" Value="{Binding Distance}" />

        <Setter Property="Canvas.Top" Value="100" />

    </Style>

</ListBox.ItemContainerStyle>

<ListBox.ItemTemplate>

    <DataTemplate>

        <Image Source="{Binding Image}" Stretch="None" />

    </DataTemplate>

</ListBox.ItemTemplate>

The Canvas.Left property is applied on the container item – a ListBoxItem in this case.

This little trick can help to produce interesting results in an ItemsControl-derived control without the expected “list” look.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*