17

Is it possible to know the current item's Index in a ItemsControl?

EDIT This works!

<Window.Resources>

    <x:Array Type="{x:Type sys:String}" x:Key="MyArray">
        <sys:String>One</sys:String>
        <sys:String>Two</sys:String>
        <sys:String>Three</sys:String>
    </x:Array>

</Window.Resources>

<ItemsControl ItemsSource="{StaticResource MyArray}" AlternationCount="100">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Margin="10">

               <!-- one -->
               <TextBlock Text="{Binding Path=., 
                    StringFormat={}Value is {0}}" />

               <!-- two -->
                <TextBlock Text="{Binding Path=(ItemsControl.AlternationIndex), 
                    RelativeSource={RelativeSource TemplatedParent}, 
                    FallbackValue=FAIL, 
                    StringFormat={}Index is {0}}" />

               <!-- three -->
                <TextBlock Text="{Binding Path=Items.Count, 
                    RelativeSource={RelativeSource FindAncestor, 
                        AncestorType={x:Type ItemsControl}}, 
                    StringFormat={}Total is {0}}" />

            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

It looks like this:

enter image description here

Jerry Nixon
  • 29,446
  • 13
  • 110
  • 219
  • One way would be to create a custom IValueConverter and pass the items property as a parameter, then get the index by looking at the collection, though this is not very efficient. – eulerfx Jun 28 '11 at 18:49
  • That is not reliable either. What if it is filtered or sorted by a CollectionViewSource? Clever, I admit, but there are better ways. – Jerry Nixon Jun 28 '11 at 22:01

5 Answers5

23

I asked the same thing a while ago here

There isn't a built in Index property, but you can set the AlternationCount of your ItemsControl to something higher than your item count, and bind to the AlternationIndex

<TextBlock Text="{Binding 
    Path=(ItemsControl.AlternationIndex), 
    RelativeSource={RelativeSource Mode=TemplatedParent}, 
    FallbackValue=FAIL, 
    StringFormat={}Index is {0}}" />

It should be noted that this solution may not work if your ListBox uses Virtualization as bradgonesurfing pointed out here.

Community
  • 1
  • 1
Rachel
  • 122,023
  • 59
  • 287
  • 465
  • 1
    Cheers. Just remember to set your AlternationCount higher than the expected count of items in the ItemsControl! – Jerry Nixon Jun 28 '11 at 21:53
  • 1
    @Rachel: Would you know the solution if someone wanted 1 based index, instead of a zero based index? I guess we could add a converter to increment the count, but just curious if you are aware of a better way. – VoodooChild Oct 15 '11 at 16:55
  • 4
    I wouldn't do this. See my experimentation results for the errors that occur. http://stackoverflow.com/questions/6511180/wpf-itemscontrol-the-current-listitem-index-in-the-itemssource/17962582#17962582 – bradgonesurfing Jul 31 '13 at 06:24
  • Hi Rachel, FYI BradDotNet worked out a different solution that doesn't use AlternationIndex. [It's here, if you're curious](http://stackoverflow.com/a/31436840/656243). – Lynn Crumbling Jul 15 '15 at 21:51
  • Thanks @LynnCrumbling, good to know of alternate solutions :) – Rachel Jul 16 '15 at 17:55
10

This is not quite an answer but a suggestion. Do not use the AlternationIndex technique as suggested. It seems to work first off but there are wierd side effects. It seems that you cannot guarantee that the AlternationIndex starts at 0.

On first rendering it works correctly

enter image description here

but re-sizing the Grid and then expanding results in the index not starting at zero any more. You can see the effect in the below image

enter image description here

This was generated from the following XAML. There are some custom components in there but you will get the idea.

<DataGrid
    VirtualizingPanel.VirtualizationMode="Recycling"
    ItemsSource="{Binding MoineauPumpFlanks.Stator.Flank.Boundary, Mode=OneWay}"
    AlternationCount="{Binding MoineauPumpFlanks.Stator.Flank.Boundary.Count, Mode=OneWay}"
    AutoGenerateColumns="False"
    HorizontalScrollBarVisibility="Hidden" 
    >
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Id">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock 
                            Margin="0,0,5,0"
                            TextAlignment="Right"
                            Text="{Binding RelativeSource={ RelativeSource 
                                                            Mode=FindAncestor, 
                                                            AncestorType=DataGridRow}, 
                                           Path=AlternationIndex}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
         <DataGridTemplateColumn  >
            <DataGridTemplateColumn.Header>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Point ["/>
                    <Controls:DisplayUnits DisplayUnitsAsAbbreviation="True" DisplayUnitsMode="Length"/>
                    <TextBlock Text="]"/>
                </StackPanel>
            </DataGridTemplateColumn.Header>
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Controls:LabelForPoint ShowUnits="False" Point="{Binding}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

I am searching for an alternate solution :(

bradgonesurfing
  • 28,325
  • 12
  • 101
  • 188
  • 5
    I think it's the virtualization that does that. I wouldn't recommend using `AlternationIndex` in cases where you have enough items to need virtualization. – Rachel Jul 31 '13 at 11:50
  • Will have to try to see if Virtualization does this. Still it's a nasty cross effect I want nothing to do with. My other answer above details the simple safe solution I have. – bradgonesurfing Jul 31 '13 at 14:24
  • FWIW, someone came up with [this other approach](https://stackoverflow.com/a/31436840/656243) in response to a similar question. – Lynn Crumbling Oct 14 '17 at 17:48
6

When you use Alternation Count remember that you can also Bind the AlternationCount property to the current count of Items of the collection you are binding to since AlternationCount is a DependencyProperty.

AlternationCount="{Binding Path=OpeningTimes.Count,FallbackValue='100'}"

Hope it helps.

Johannes Wanzek
  • 2,717
  • 2
  • 27
  • 42
1

Yes it is! ItemsControl exposes an ItemContainerGenerator property. The ItemContainerGenerator has methods such as IndexFromContainer which can be used to find the index of a given item. Note that if you bind your ItemsControl to a collection of objects, a container is automatically generated for each. You can find the container for each bound item using the ContainerFromItem method.

ColinE
  • 64,631
  • 12
  • 149
  • 215
  • 2
    Can you access those in XAML? – Jerry Nixon Jun 28 '11 at 18:51
  • no, you cannot. if you need to do it without code, why not use the MVVM pattern? create a list of items that contain their index. – ColinE Jun 28 '11 at 18:52
  • 3
    Because my CollectionViewSource may reorder or filter that list. – Jerry Nixon May 31 '12 at 21:12
  • @ColinE: Unfortunately, there doesn't seem to be a good (built-in) way to actually do that, as there is no [list item-projection wrapper](http://stackoverflow.com/questions/13920703/observablecollection-element-wise-transform-projection-wrapper) available in the framework. It does seem to me like it is the best way (thus requiring building one's own such wrapper), though the lack of such a class makes me wonder whether creating "a list of items that contain their index" was meant to be the way to go by the WPF/MVVM designers. – O. R. Mapper Dec 07 '13 at 17:43
  • This shows this solution in action: http://stackoverflow.com/questions/7290147/is-possible-to-get-a-index-from-a-item-in-a-list – Skychan May 19 '16 at 15:58
0

A more reliable way is to use a value converter to generate a new collection with an index. With a couple of helpers this is pretty painless. I use ReactiveUI's IEnumerable<T>.CreateDerivedCollection() and a helper class I wrote for other purposes called Indexed.

public struct Indexed<T>
{
    public int Index { get; private set; }
    public T Value { get; private set; }
    public Indexed(int index, T value) : this()
    {
        Index = index;
        Value = value;
    }

    public override string ToString()
    {
        return "(Indexed: " + Index + ", " + Value.ToString () + " )";
    }
}

public class Indexed
{
    public static Indexed<T> Create<T>(int indexed, T value)
    {
        return new Indexed<T>(indexed, value);
    }
}

and the converter

public class IndexedConverter : IValueConverter
{
    public object Convert
        ( object value
        , Type targetType
        , object parameter
        , CultureInfo culture
        )
    {
        IEnumerable t = value as IEnumerable;
        if ( t == null )
        {
            return null;
        }

        IEnumerable<object> e = t.Cast<object>();

        int i = 0;
        return e.CreateDerivedCollection<object, Indexed<object>>
           (o => Indexed.Create(i++, o));

    }

    public object ConvertBack(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        return null;
    }
}

and in the XAML I can do

 <DataGrid
     VirtualizingPanel.VirtualizationMode="Recycling"
     ItemsSource="{Binding 
         MoineauPumpFlanks.Stator.Flank.Boundary, 
         Mode=OneWay, 
         Converter={StaticResource indexedConverter}}"
     AutoGenerateColumns="False"
     HorizontalScrollBarVisibility="Hidden" 
     >
     <DataGrid.Columns>

         <DataGridTemplateColumn Header="Id">

             <DataGridTemplateColumn.CellTemplate>
                 <DataTemplate>
                     <!-- Get the index of Indexed<T> -->
                     <TextBlock 
                             Margin="0,0,5,0"
                             TextAlignment="Right"
                             Text="{Binding Path=Index}"/>
                 </DataTemplate>
             </DataGridTemplateColumn.CellTemplate>
         </DataGridTemplateColumn>

          <DataGridTemplateColumn Header="Point" >
             <DataGridTemplateColumn.CellTemplate>
                 <DataTemplate>
                     <!-- Get the value of Indexed<T> -->
                     <TextBlock Content="{Binding Value}" />
                 </DataTemplate>
             </DataGridTemplateColumn.CellTemplate>
         </DataGridTemplateColumn>
     </DataGrid.Columns>
 </DataGrid>
bradgonesurfing
  • 28,325
  • 12
  • 101
  • 188
  • Isn't that very bad on the performance-side? I mean, observable collections provide the exact info in their `CollectionChanged` events about what changes were done to which items, so it is not required to refresh the complete list because the handler knows exactly what has changed. And here we introduce a converter that will, upon any change to the list, provide a whole new list to the GUI that binds to the list property. Or is there any additional magic going on I'm not thinking of now? – O. R. Mapper Dec 07 '13 at 17:20
  • No it doesn't generate a complete new list on every change. CreateDerivedCollection creates a new readonly collection. It only updates the parts of the downstream collection that need to be changed based on changes to the upstream collection. – bradgonesurfing Mar 03 '14 at 06:03
  • but doesnt this render the indexes invalid if the upstream collection exchanges two instances? – Firo Jul 15 '14 at 14:02