How to display row numbers in a ListView?


How to display row numbers in a ListView?



The obvious solution would be to have a row number property on a ModelView element, but the drawback is that you have to re-generate those when you add records or change sort order.



Is there an elegant solution?




8 Answers
8



I think you have the elegant solution, but this works.



XAML:


<ListView Name="listviewNames">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn
Header="Number"
DisplayMemberBinding="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ListViewItem}},
Converter={StaticResource IndexConverter}}" />
<GridViewColumn
Header="Name"
DisplayMemberBinding="{Binding Path=Name}" />
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>



ValueConverter:


public class IndexConverter : IValueConverter
{
public object Convert(object value, Type TargetType, object parameter, CultureInfo culture)
{
ListViewItem item = (ListViewItem) value;
ListView listView = ItemsControl.ItemsControlFromItemContainer(item) as ListView;
int index = listView.ItemContainerGenerator.IndexFromContainer(item);
return index.ToString();
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}





Nice solution! My only problem is that when changing the list (e.g. adding or removing items) the row numbers don't update, so I end up with duplicate and missing numbers. Any ideas?
– Allon Guralnek
Sep 19 '12 at 11:22





A little addition: if you want the index start to be easily changed (0 based, 1 based, whatever...): int paramValue; if (Int32.TryParse(parameter as string, out paramValue)) index += paramValue;
– jv42
Oct 18 '12 at 15:16



int paramValue; if (Int32.TryParse(parameter as string, out paramValue)) index += paramValue;





Then in XAML, you can do: (...) Converter={StaticResource IndexConverter}, ConverterParamter=1} for a numbering starting at 1.
– jv42
Oct 18 '12 at 15:17


(...) Converter={StaticResource IndexConverter}, ConverterParamter=1}





It doesn't work when you start scrolling. scrolling changes the index.
– VahidN
Aug 2 '14 at 9:56






It doesn't update the number when we delete row.
– Supitchaya
Sep 1 '15 at 4:50



If you have a dynamic list where items are added, deleted or moved, you can still use this very nice solution and simply let the currentview of your listview refresh itself after the changements in your source list are done.
This code sample removes the current item directly in the data source list "mySourceList" (which is in my case an ObservableCollection) and finally updates the line numbers to correct values .


ICollectionView cv = CollectionViewSource.GetDefaultView(listviewNames.ItemsSource);
if (listviewNames.Items.CurrentItem != null)
{
mySourceList.RemoveAt(cv.CurrentPosition);
cv.Refresh();
}



First you need to set the AlternationCount to items count+1, for instance:


AlternationCount


items count


<ListView AlternationCount="1000" .... />



Then AlternationIndex will show the real index, even during the scrolling:


AlternationIndex


<GridViewColumn
Header="#" Width="30"
DisplayMemberBinding="{Binding (ItemsControl.AlternationIndex),
RelativeSource={RelativeSource AncestorType=ListViewItem}}" />





Why a magic number ? If we have a collection we could also write <ListView AlternationCount="{Binding ProjColl.Count}" ItemsSource="{Binding ProjColl}" >
– NGI
Feb 17 '17 at 17:13





I couldn't get this to work, and it would interfere with alternating row coloring anyway.
– Chris
Aug 2 '17 at 16:52





I get 0 based index number, any way to get index number starting from 1?
– Raven
Nov 15 '17 at 21:52



Here is another way, including code comments that will help you understand how it works.


public class Person
{
private string name;
private int age;
//Public Properties ....
}

public partial class MainWindow : Window
{

List<Person> personList;
public MainWindow()
{
InitializeComponent();

personList= new List<Person>();
personList.Add(new Person() { Name= "Adam", Agen= 25});
personList.Add(new Person() { Name= "Peter", Agen= 20});

lstvwPerson.ItemsSource = personList;
//After updates to the list use lstvwPerson.Items.Refresh();
}
}



The XML


<GridViewColumn Header="Number" Width="50"
DisplayMemberBinding="{
Binding RelativeSource= {RelativeSource Mode=FindAncestor, AncestorType={x:Type ListViewItem}},
DELETE Path=Content, DELETE
Converter={StaticResource IndexConverter},
ConverterParameter=1
}"/>



RelativeSource is used in particular binding cases when we try to bind a property of a given object to another property of the object itself [1].



Using Mode=FindAncestor we can traverse the hierarchy layers and get a specified element, for example the ListViewItem (we could even grab the GridViewColumn). If you have two ListViewItem elements you can specify which you want with "AncestorLevel = x".



Path: Here I simply take the content of the ListViewItem (which is my object "Person").



Converter Since I want to display row numbers in my Number column and not the object Person I need to create a Converter class which can somehow transform my Person object to a corresponding number row. But its not possible, I just wanted to show that the Path goes to the converter. Deleting the Path will send the ListViewItem to the Converter.



ConverterParameter Specify a parameter you want to pass to the IValueConverter class. Here you can send the state if you want the row number to start at 0,1,100 or whatever.


public class IndexConverter : IValueConverter
{
public object Convert(object value, Type TargetType, object parameter, System.Globalization.CultureInfo culture)
{
//Get the ListViewItem from Value remember we deleted Path, so the value is an object of ListViewItem and not Person
ListViewItem lvi = (ListViewItem)value;
//Get lvi's container (listview)
var listView = ItemsControl.ItemsControlFromItemContainer(lvi) as ListView;

//Find out the position for the Person obj in the ListView
//we can get the Person object from lvi.Content
// Of course you can do as in the accepted answer instead!
// I just think this is easier to understand for a beginner.
int index = listView.Items.IndexOf(lvi.Content);

//Convert your XML parameter value of 1 to an int.
int startingIndex = System.Convert.ToInt32(parameter);

return index + startingIndex;
}

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}



This will work like a charm,
I don't know about performance,
Still we can give it a try



Create a Multi Value Converter


public class NumberingConvertor : IMultiValueConverter
{
public object Convert(object values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values != null && values.Any() && values[0] != null && values[1] != null)
{
//return (char)(((List<object>)values[1]).IndexOf(values[0]) + 97);
return ((List<object>)values[1]).IndexOf(values[0]) + 1;
}
return "0";
}

public object ConvertBack(object value, Type targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
}



and
your Xaml like this


<ItemsControl ItemsSource="{Binding ListObjType}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label>
<MultiBinding Converter="{StaticResource NumberingConvertor}">
<Binding Path="" />
<Binding Path="ItemsSource"
RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
</MultiBinding>
</Label>
<TextBlock Text="{Binding }" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>



Idea is to send Object and list both to the converter and let converter decide the number. You can modify converter to display ordered list.



It's the addition to answer of amaca for problems found by Allon Guralnek and VahidN. Scrolling problem is solved with setting ListView.ItemsPanel to StackPanel in XAML:


<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListView.ItemsPanel>



This replacement of default VirtualizingStackPanel with simple StackPanel disables automatic regeneration of internal collection of ListViewItem. So indices would not chaotically change when scrolling. But this replacement can decrease perfomance on large collections. Also, dynamic numeration changes can be achieved with call CollectionViewSource.GetDefaultView(ListView.ItemsSource).Refresh() when ItemsSource collection changed. Just like with ListView filtering. When I tried to add handler with this call on event INotifyCollectionChanged.CollectionChanged my ListView output was duplicating last added row (but with correct numeration). Fixed this by placing refresh call after every collection change in code. Bad solution, but it works perfect for me.


CollectionViewSource.GetDefaultView(ListView.ItemsSource).Refresh()


INotifyCollectionChanged.CollectionChanged



amaca answer is great for static lists. For dynamic:



After deleting ItemsControl not contains deleted object, but ItemContainerGenerator contains.
Converter for dynamic lists (I use it for TabControl TabItem's):


public class TabIndexMultiConverter : MultiConverterBase
{
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
TabItem tabItem = value.First() as TabItem;
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(tabItem);
object context = tabItem?.DataContext;

int idx = ic == null || context == null // if all objects deleted
? -1
: ic.Items.IndexOf(context) + 1;

return idx.ToString(); // ToString necessary
}
}



Here's my little converter which works great as of WPF in 2017 with .NET 4.7.2, including with the VirtualizingStackPanel fully enabled:


VirtualizingStackPanel


[ValueConversion(typeof(IList), typeof(int))]
public sealed class ItemIndexConverter : FrameworkContentElement, IValueConverter
{
public Object Convert(Object data_item, Type t, Object p, CultureInfo _) =>
((IList)DataContext).IndexOf(data_item);

public Object ConvertBack(Object o, Type t, Object p, CultureInfo _) =>
throw new NotImplementedException();
};



Add an instance of this IValueConverter to the Resources of the GridViewColumn.CellTemplate, or elsewhere. Or, instantiate it in-situ on the Binding of the bound element, like I show here. In any case, you need to create an instance of the ItemIndexConverter and don't forget to bind the whole source collection to it. Here I'm pulling a reference to the source collection out of the ItemsSource property of the ListView--but this entails some unrelated hassles over accessing the XAML root, so if you have a better and easier way to refer to the source collection, you should do so.


IValueConverter


Resources


GridViewColumn.CellTemplate


Binding


ItemIndexConverter


ItemsSource


ListView



As for accessing a property on the XAML root, the ListView root in XAML is given the name w_root, and the XAML 2009 markup extension {x:Reference ...} is used to access the XAML root element. I don't think "ElementName" binding will work here since the reference occurs in a template context.


ListView


w_root


{x:Reference ...}


<ListView x:Class="myApp.myListView"
x:Name="w_root"
xmlns="http://schemas.microsoft.com/netfx/2009/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:myApp"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">

<ListView.View>
<GridView>
<GridViewColumn Width="50">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<Binding>
<Binding.Converter>
<local:ItemIndexConverter DataContext="{Binding
Source={x:Reference w_root},
Path=(ItemsControl.ItemsSource)}" />
</Binding.Converter>
</Binding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>



That's it! It seems to work pretty quickly with a large number of rows, and again, you can see that the reported indices are correct when arbitrarily scrolling around, and that VirtualizingStackPanel.IsVirtualizing is indeed set to True.


VirtualizingStackPanel.IsVirtualizing


True



enter image description here



Not sure the following is actually necessary, but notice that the xmlns= declaration for WPF is updated to indicate XAML 2009, in support of the {x:Reference} usage mentioned above. Do notice that there are two changes; "/winfx/" has to be changed to "/netfx/" when switching from "2006" to "2009".


xmlns=


{x:Reference}






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

api-platform.com Unable to generate an IRI for the item of type

How to set up datasource with Spring for HikariCP?

Display dokan vendor name on Woocommerce single product pages