Davide Pavani
Davide Pavani

Reputation: 97

How to bind and display ListBoxItem index in ListBox?

I have a .NET 5 project with following Nuget Packages:

I have a XAML with a ListBoxand a ViewModel with aObservableCollection` of Model.

The ObservableCollection is binded as ItemSource of ListBox

What I want to achieve:

When i Drag and Drop an item in a different position (or Add/Delete), I want the indexes to be refreshed.

Example:

Before Drag/Drop

enter image description here

After Drag/Drop

enter image description here

Actually, i binded the drophandler of gong-wpf-dragdrop and at the end of the drop, i manually refresh every single Index in my list.

there is a way to do it easily? because actually i have to refresh indexes manually.

Summarizing: When i reorder/delete/add items i want Model.Index of every item updated with the correct index position in ListBox.

My Mandate is:

I tried looking for similar questions but didn't find much that could help me. Thanks in advance :)


Model:

public class Model : BindablePropertyBase
{
    private int index;
    private string name;

    public int Index
    {
        get { return index; }
        set
        {
            index = value;
            RaisePropertyChanged();
        }
    }
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            RaisePropertyChanged();
        }
    }
}

And below a xaml with a simple binded list MainWindow.xaml

<hc:Window x:Class="ListBoxIndexTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:hc="https://handyorg.github.io/handycontrol"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
       xmlns:dd="clr-namespace:GongSolutions.Wpf.DragDrop;assembly=GongSolutions.Wpf.DragDrop"
    xmlns:local="clr-namespace:ListBoxIndexTest"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="600">
<Window.DataContext>
    <local:MainWindowViewModel />
</Window.DataContext>
<Grid>
    <ListBox ItemsSource="{Binding TestList}" dd:DragDrop.DropHandler="{Binding}"
             dd:DragDrop.IsDropTarget="True" dd:DragDrop.IsDragSource="True">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid Margin="10"
                      Background="Aqua">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>

                    <TextBlock Text="{Binding Index}" Margin="10,0"/>

                    <TextBlock Text="{Binding Name}" 
                               Grid.Column="1"/>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

</hc:Window>

MainWindowViewModel.cs

public class MainWindowViewModel : BindablePropertyBase, IDropTarget
{
    private ObservableCollection<Model> test_list;
    public ObservableCollection<Model> TestList
    {
        get 
        {
            return test_list;
        }
        set
        {
            test_list = value;
            RaisePropertyChanged();
        }
    }

    // Constructor
    public MainWindowViewModel()
    {
        TestList = new ObservableCollection<Model>()
        {
            new Model()
            {
                Index = 1,
                Name = "FirstModel"
            },
            new Model()
            {
                Index = 2,
                Name = "SecondModel"
            },
            new Model()
            {
                Index = 3,
                Name = "ThirdModel"
            }
        };
    }

    public void DragOver(IDropInfo dropInfo)
    {
        Model sourceItem = dropInfo.Data as Model;
        Model targetItem = dropInfo.TargetItem as Model;

        if (sourceItem != null && targetItem != null)
        {
            dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
            dropInfo.Effects = DragDropEffects.Move;
        }
    }

    public void Drop(IDropInfo dropInfo)
    {
        Model sourceItem = dropInfo.Data as Model;
        Model targetItem = dropInfo.TargetItem as Model;

        if(sourceItem != null && targetItem != null)
        {
            int s_index = sourceItem.Index - 1;
            int t_index = targetItem.Index - 1;

            TestList.RemoveAt(s_index);
            TestList.Insert(t_index, sourceItem);

            RefreshAllIndexes();
        }
    }

    private void RefreshAllIndexes()
    {
        for (int i = 0; i < TestList.Count; i++)
        {
            TestList[i].Index = i + 1;
        }
    }
}

Upvotes: 0

Views: 575

Answers (1)

Corentin Pane
Corentin Pane

Reputation: 4943

I don't believe there is an out-of-the box way to bind to a container index in WPF. Your solution is actually easy to understand.

If you find yourself binding often to index, you could create your own attached property/value converter that internally climbs up the visual tree using these helpers until it finds the parent ItemsControland makes use of the IndexFromContainer method.


Here is some code to get you started with this method:

First a small helper function to climb up the visual tree looking for an item of generic type:

public static DependencyObject FindParentOfType<T>(DependencyObject child) where T : DependencyObject {
    //We get the immediate parent item
    DependencyObject parentObject = VisualTreeHelper.GetParent(child);

    //we've reached the end of the tree
    if (parentObject == null) {
        return null;
    }

    //check if the parent matches the type we're looking for
    if (parentObject is T parent) {
        return parent;
    } else {
        return FindParentOfType<T>(parentObject);
    }
}

Then a value converter that takes a control as input value and returns its index in the first encountered ItemsControl:

public class ContainerToIndexConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        //Cast the passed value as an ItemsControl container
        DependencyObject container = value as ContentPresenter;
        if (container == null) {
            container = value as ContentControl;
        }

        //Finds the parent ItemsControl by looking up the visual tree
        var itemControls = (ItemsControl)FindParentOfType<ItemsControl>(container);
        //Gets the index of the container from the parent ItemsControl
        return itemControls.ItemContainerGenerator.IndexFromContainer(container);
    }

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

And this is how you would use it in XAML:

<ListBox.ItemTemplate>
    <DataTemplate>
        <!-- This will display the index of the list item. -->
        <TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentPresenter}, Converter={StaticResource ContainerToIndexConverter}}" />
    </DataTemplate>
</ListBox.ItemTemplate>

Upvotes: 2

Related Questions