Numbered listbox
I have a sorted listbox and need to display each item's row number. In this demo I have a Person class with a Name string property. The listbox displays a a list of Persons sorted by Name. How can I add to the datatemplate of the listbox the row number???
XAML:
<Window x:Class="NumberedListBox.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<ListBox
ItemsSource="{Binding Path=PersonsListCollectionView}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
Code behind:
using System;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Windows;
using System.ComponentModel;
namespace NumberedListBox
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Persons = new ObservableCollection<Person>();
Persons.Add(new Person() { Name = "Sally"});
Persons.Add(new Person() { Name = "Bob" });
Persons.Add(new Person() { Name = "Joe" });
Persons.Add(new Person() { Name = "Mary" });
PersonsListCollectionView = new ListCollectionView(Persons);
PersonsListCollectionView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
DataContext = this;
}
public ObservableCollection<Person> Persons { get; private set; }
public ListCollectionView PersonsListCollectionView { get; private set; }
}
public class Person
{
public string Name { get; set; }
}
}
Finally! If found a way much more elegant and probably with better performance either. (see also Accessing an ItemsControl item as it is added)
We "misuse" the property ItemsControl.AlternateIndex
for this. Originally it is intended to handle every other row within a ListBox
differently. (see http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol.alternationcount.aspx)
1. Set AlternatingCount to the amount of items contained in the ListBox
<ListBox ItemsSource="{Binding Path=MyListItems}"
AlternationCount="{Binding Path=MyListItems.Count}"
ItemTemplate="{StaticResource MyItemTemplate}"
...
/>
2. Bind to AlternatingIndex your DataTemplate
<DataTemplate x:Key="MyItemTemplate" ... >
<StackPanel>
<Label Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplatedParent.(ItemsControl.AlternationIndex)}" />
...
</StackPanel>
</DataTemplate>
So this works without a converter, an extra CollectionViewSource
and most importantly without brute-force-searching the source collection.
This should get you started:
http://weblogs.asp.net/hpreishuber/archive/2008/11/18/rownumber-in-silverlight-datagrid-or-listbox.aspx
It says it's for Silverlight, but I don't see why it wouldn't work for WPF. Basically, you bind a TextBlock to your data and use a custom value converter to output the current item's number.
The idea in David Brown's link was to use a value converter which worked. Below is a full working sample. The list box has row numbers and can be sorted on both name and age.
XAML:
<Window x:Class="NumberedListBox.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NumberedListBox"
Height="300" Width="300">
<Window.Resources>
<local:RowNumberConverter x:Key="RowNumberConverter" />
<CollectionViewSource x:Key="sortedPersonList" Source="{Binding Path=Persons}" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListBox
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding Source={StaticResource sortedPersonList}}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Text="{Binding Converter={StaticResource RowNumberConverter}, ConverterParameter={StaticResource sortedPersonList}}"
Margin="5" />
<TextBlock Text="{Binding Path=Name}" Margin="5" />
<TextBlock Text="{Binding Path=Age}" Margin="5" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1" Grid.Column="0" Content="Name" Tag="Name" Click="SortButton_Click" />
<Button Grid.Row="1" Grid.Column="1" Content="Age" Tag="Age" Click="SortButton_Click" />
</Grid>
</Window>
Code behind:
using System;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Windows;
using System.ComponentModel;
using System.Windows.Controls;
namespace NumberedListBox
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Persons = new ObservableCollection<Person>();
Persons.Add(new Person() { Name = "Sally", Age = 34 });
Persons.Add(new Person() { Name = "Bob", Age = 18 });
Persons.Add(new Person() { Name = "Joe", Age = 72 });
Persons.Add(new Person() { Name = "Mary", Age = 12 });
CollectionViewSource view = FindResource("sortedPersonList") as CollectionViewSource;
view.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
DataContext = this;
}
public ObservableCollection<Person> Persons { get; private set; }
private void SortButton_Click(object sender, RoutedEventArgs e)
{
Button button = sender as Button;
string sortProperty = button.Tag as string;
CollectionViewSource view = FindResource("sortedPersonList") as CollectionViewSource;
view.SortDescriptions.Clear();
view.SortDescriptions.Add(new SortDescription(sortProperty, ListSortDirection.Ascending));
view.View.Refresh();
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
Value converter:
using System;
using System.Windows.Data;
namespace NumberedListBox
{
public class RowNumberConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
CollectionViewSource collectionViewSource = parameter as CollectionViewSource;
int counter = 1;
foreach (object item in collectionViewSource.View)
{
if (item == value)
{
return counter.ToString();
}
counter++;
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Yet another answer. I tried the above, which works in WPF (AlternationCount
solution), but I needed code for Silverlight, so I did the following. This is more elegant than the other brute force method.
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:RowNumber" x:Name="userControl"
x:Class="RowNumber.MainPage" Width="640" Height="480">
<Grid x:Name="LayoutRoot" Background="White">
<ListBox ItemsSource="{Binding Test, ElementName=userControl}">
<ListBox.Resources>
<local:ListItemIndexConverter x:Key="IndexConverter" />
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width="30"
Text="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Converter={StaticResource IndexConverter}}" />
<TextBlock Text="{Binding}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
And behind
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
namespace RowNumber
{
public class ListItemIndexConverter : IValueConverter
{
// Value should be ListBoxItem that contains the current record. RelativeSource={RelativeSource AncestorType=ListBoxItem}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var lbi = (ListBoxItem)value;
var listBox = lbi.GetVisualAncestors().OfType<ListBox>().First();
var index = listBox.ItemContainerGenerator.IndexFromContainer(lbi);
// One based. Remove +1 for Zero based array.
return index + 1;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); }
}
public partial class MainPage : UserControl
{
public MainPage()
{
// Required to initialize variables
InitializeComponent();
}
public List<string> Test { get { return new[] { "Foo", "Bar", "Baz" }.ToList(); } }
}
}
This is newly available in Silverlight 5 with the introduction of RelativeSource
binding.