Changing Panels and DataTemplates with ItemsControls in WPF/Avalon

I have posted before about the philosophy behind the ItemsControl, and now I will pull together some concepts from previous posts. I will use the RadialPanel and some ValueConverter magic to show how we can take a ListBox (the most famous of the ItemsControl family) and have it:

  • Respond to changes to a data collection.
  • Swap between using a radial and stacking panel.
  • Swap between showing the text of the items and showing the hash of the items.

One of the first things we need to do in order to build this sample is make a subclass of the RadialPanel that does not require the items to have the angle set on them. We do this because we want the items to arrange in a neat circle by themselves.

The simplest way to do this is to derive from RadialPanel, and override the MeasureOverride method to set the angles on the children to be evenly spaced before doing the base measure pass. This is very straightforward:

using System;

using System.Windows;

namespace ItemsControlExample

{

    public class ItemsRadialPanel : RadialPanel

    {

        protected override Size MeasureOverride(Size availableSize)

        {

            double angle = 0.0;

            double spacing = (360.0 / this.InternalChildren.Count);

            // set the angle of the children based on where they are in the list

            foreach (UIElement element in this.InternalChildren)

            {

                SetAngle(element, angle);

   angle += spacing;

            }

            return base.MeasureOverride(availableSize);

        }

    }

}

Now we have a panel that we can use in an ItemsControl. Another piece of the puzzle that we need is a data converter that takes any object and returns the hash of it:

using System;

using System.Windows;

using System.Windows.Data;

using System.Globalization;

namespace ItemsControlExample

{

    public class TextToHashConverter : IValueConverter

    {

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)

        {

            return value.GetHashCode();

        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

        {

            throw new NotSupportedException();

        }

    }

}

As far as value converters go, this is a simple one. But it is enough to demonstrate the idea. Now we have most of the code for building our control. Let's build a window in XAML that has the pieces we need to show it off. First we need a window with a resource section:

<Window x:Class="ItemsControlExample.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:ItemsControlExample"

    Title="ItemsControlExample" Height="300" Width="300"

    >

  <Window.Resources>

  </Window.Resources>

</Window>

What do we put in the resources? Well, first we need to have an instance of the converter that we will be using later. So we declare that in the resources:

    <local:TextToHashConverter x:Key="TextToHashConverter" />

Now we can define some control templates for the ListBox. One of them will use the radial panel:

    <ControlTemplate x:Key="radialTemplate">

      <local:ItemsRadialPanel IsItemsHost="True" />

    </ControlTemplate>

And the other will use a stacking panel:

    <ControlTemplate x:Key="scrollTemplate">

      <ScrollViewer ScrollViewer.HorizontalScrollBarVisibility="Disabled">

        <VirtualizingStackPanel IsItemsHost="True" />

      </ScrollViewer>

    </ControlTemplate>

Now we can define some data templates for the ListBox. The DataTemplate is what is used to create UI for each piece of data in the ListBox. The first template is simple - we center the text of the item inside a Border element. The binding statement has nothing in it because that will return the DataContext of the element, which is the item:

    <DataTemplate x:Key="textTemplate">

      <Border Background="Red">

        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding}" />

      </Border>

    </DataTemplate>

And now we can also define the other template, which will just have a different converter and no background:

    <DataTemplate x:Key="hashTemplate">

      <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Converter={StaticResource TextToHashConverter}}" />

    </DataTemplate>

Now we all we have to do is to declare our ListBox in the window. We will make a DockPanel with a bunch of buttons along the top so that we can change stuff:

  <DockPanel>

    <UniformGrid DockPanel.Dock="Top" Rows="1" Columns="5">

      <Button Content="Add item" Name="addButton" />

      <Button Content="Radial" Name="radialButton" />

      <Button Content="Scroll" Name="scrollButton" />

      <Button Content="Text" Name="textButton" />

      <Button Content="Hash" Name="hashButton" />

    </UniformGrid>

    <ListBox Name="listBox" Template="{StaticResource scrollTemplate}" ItemTemplate="{StaticResource textTemplate}" />

  </DockPanel>

As you can see, we started off with the normal text template and the normal scrolling template. But if we run our code now we will have nothing in the ListBox and the buttons will also do nothing. We need to add some code to fix that. First of all we can add some initial content by adding a member to the window class which will be our collection:

        /// <summary>

        /// We use ObservableCollection because it will fire events to let the ListBox keep things in sync.

        /// </summary>

        ObservableCollection<string> _strings = new ObservableCollection<string>();

And adding some code after InitializeComponent to put some initial content in there and wire up to the ListBox:

            // add some strings to start with

            _strings.Add("string 1");

            _strings.Add("string 2");

           

            // wire up the string collection to the ListBox

            listBox.ItemsSource = _strings;

All that is left now is handling events on the buttons. First we add some handlers in the window constructor:

            // wire up handlers for the buttons

            addButton.Click += new RoutedEventHandler(OnClickAdd);

            radialButton.Click += new RoutedEventHandler(OnClickRadial);

            scrollButton.Click += new RoutedEventHandler(OnClickScroll);

            textButton.Click += new RoutedEventHandler(OnClickText);

            hashButton.Click += new RoutedEventHandler(OnClickHash);

And now we can write the handlers. The handler for add is very simple - we just add a new item to the collection. Notice that we don't have to tell the ListBox about it explicitly - ObservableCollection will fire an event to tell the ListBox that it has changed and the ListBox will automatically create the UI for it and display it. This is why ItemsControl is so cool:

        /// <summary>

        /// When we click add, we want to add an item to the end of the list.

        /// </summary>

  private void OnClickAdd(object sender, RoutedEventArgs e)

        {

            int stringNumber = _strings.Count + 1;

            _strings.Add("string " + stringNumber.ToString());

        }

Now we can add handlers for changing the data template. They simply set the appropriate property to the appropriate resource:

        /// <summary>

        /// When we click on the hash button, we show the hash data template

        /// </summary>

        private void OnClickHash(object sender, RoutedEventArgs e)

        {

            listBox.ItemTemplate = (DataTemplate)FindResource("hashTemplate");

        }

        /// <summary>

        /// When we click on the text button, we show the text data template

        /// </summary>

        private void OnClickText(object sender, RoutedEventArgs e)

        {

            listBox.ItemTemplate = (DataTemplate)FindResource("textTemplate");

        }

And the control template changing handlers work in much the same way:

        /// <summary>

        /// When we click on the scroll button, we want to see a scrolling ListBox.

        /// </summary>

        private void OnClickScroll(object sender, RoutedEventArgs e)

        {

            listBox.Template = (ControlTemplate)FindResource("scrollTemplate");

        }

        /// <summary>

        /// When we click on the radial button, we want to see a radial ListBox.

        /// </summary>

        private void OnClickRadial(object sender, RoutedEventArgs e)

        {

            listBox.Template = (ControlTemplate)FindResource("radialTemplate");

        }

Now if you build and run you can swap control templates for the ListBox (and hence the Panels) as well as swap the data templates for the ListBox. You can also add items to the listbox and see everything update.

Unfortunately the radial listbox has troubles using the area efficiently. Maybe if I get time one day I will write a panel that does the right hit testing and rendering to draw things in a nice pie shape.

 

 

 

 

ItemsControlExample.zip

Comments