WPF ItemsControl the current ListItem Index in the ItemsSource
Asked Answered
M

5

18

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

Moralez answered 28/6, 2011 at 18:14 Comment(2)
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.Wicked
That is not reliable either. What if it is filtered or sorted by a CollectionViewSource? Clever, I admit, but there are better ways.Moralez
S
24

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.

Sarah answered 28/6, 2011 at 19:25 Comment(5)
Cheers. Just remember to set your AlternationCount higher than the expected count of items in the ItemsControl!Moralez
@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.Pokey
I wouldn't do this. See my experimentation results for the errors that occur. #6511680Sparerib
Hi Rachel, FYI BradDotNet worked out a different solution that doesn't use AlternationIndex. It's here, if you're curious.Partida
Thanks @LynnCrumbling, good to know of alternate solutions :)Sarah
S
11

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 :(

Sparerib answered 31/7, 2013 at 5:51 Comment(3)
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.Sarah
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.Sparerib
FWIW, someone came up with this other approach in response to a similar question.Partida
S
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.

Shrike answered 13/12, 2012 at 9:55 Comment(0)
K
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.

Kimberlykimberlyn answered 28/6, 2011 at 18:47 Comment(5)
Can you access those in XAML?Moralez
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.Kimberlykimberlyn
Because my CollectionViewSource may reorder or filter that list.Moralez
@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 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.Morphine
This shows this solution in action: #7290647Bangtail
S
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>
Sparerib answered 31/7, 2013 at 6:31 Comment(4)
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?Morphine
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.Sparerib
but doesnt this render the indexes invalid if the upstream collection exchanges two instances?Jule
Would like to see a dynamic data of this solution (the IndexedConverter) if possible? :)Cuff

© 2022 - 2024 — McMap. All rights reserved.