#999 – Using a Canvas as the Items Panel for a ListBox
January 31, 2014 1 Comment
You can replace the default StackPanel used as the items panel for a ListBox with any other panel element. If you have items that you want to display at arbitrary locations, you can use a Canvas for your items panel.
The example below presents a list of cities, where each city is placed at its proper latitude and longitude.
Assuming that we have a City class that accepts a name and latitude/longitude values passed to its constructor, we can create a list of cities:
public partial class MainWindow : Window, INotifyPropertyChanged { public MainWindow() { InitializeComponent(); this.DataContext = this; CityList = new ObservableCollection<City> { new City("Duluth", 46.83, 92.18), new City("Redmond", 44.27, 121.15), new City("Tucson", 32.12, 110.93), new City("Denver", 39.75, 104.87), new City("Boston", 42.37, 71.03), new City("Tampa", 27.97, 82.53) }; } private ObservableCollection<City> cityList; public ObservableCollection<City> CityList { get { return cityList; } set { cityList = value; RaisePropertyChanged("CityList"); } } // INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged = delegate { }; private void RaisePropertyChanged(string propName) { PropertyChanged(this, new PropertyChangedEventArgs(propName)); } }
We then bind a ListBox to this list of cities. We also:
- Use its ItemContainerStyle to map latitude and longitude values to the attached Top and Left properties of the Canvas element
- Use value converters to convert latitude and longitude values to canvas positions
- Specify the Canvas as the ItemsPanel
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:local="clr-namespace:WpfApplication1" Title="MainWindow" Width="470" Height="310"> <Window.Resources> <ResourceDictionary> <local:LatValueConverter x:Key="latValueConverter" /> <local:LongValueConverter x:Key="longValueConverter" /> <sys:Double x:Key="mapWidth">440</sys:Double> <sys:Double x:Key="mapHeight">240</sys:Double> </ResourceDictionary> </Window.Resources> <StackPanel Orientation="Horizontal" Margin="5" > <ListBox ItemsSource="{Binding CityList}" DisplayMemberPath="Name"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="Canvas.Left" Value="{Binding Longitude, Converter={StaticResource longValueConverter}, ConverterParameter={StaticResource mapWidth}}"/> <Setter Property="Canvas.Top" Value="{Binding Latitude, Converter={StaticResource latValueConverter}, ConverterParameter={StaticResource mapHeight}}"/> </Style> </ListBox.ItemContainerStyle> <ListBox.ItemsPanel> <ItemsPanelTemplate> <Canvas IsItemsHost="True" Width="{StaticResource mapWidth}" Height="{StaticResource mapHeight}"/> </ItemsPanelTemplate> </ListBox.ItemsPanel> </ListBox> </StackPanel> </Window>
Here is the implementation of the value converters:
public static class Constants { public const double LatTop = 50.0; public const double LatBottom = 24.0; public const double LongLeft = 125.0; public const double LongRight = 66.0; } public class LatValueConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { double latitude = (double)value; double height = (double)parameter; int top = (int)((Constants.LatTop - latitude) / (Constants.LatTop - Constants.LatBottom) * height); return top; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } } public class LongValueConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { double longitude = (double)value; double width = (double)parameter; int left = (int)((Constants.LongLeft - longitude) / (Constants.LongLeft - Constants.LongRight) * width); return left; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
The end result is that the city names are displayed at their proper locations on the canvas. Note that because the cities are displayed in a ListBox, we can still select one of them.