如何在ListBox中绑定和显示ListBoxItem索引?

How to bind and display ListBoxItem index in ListBox?

我有一个包含以下 Nuget 包的 .NET 5 项目:

我有一个模型的 XAML with a ListBoxand a ViewModel with aObservableCollection`。

ObservableCollection 绑定为 ItemSourceListBox

我想达到的目标:

当我将项目拖放到不同位置(或 Add/Delete)时,我希望刷新索引。

示例:

之前Drag/Drop

Drag/Drop

之后

其实我绑定了gong-wpf-dragdrop的drophandler 在删除结束时,我手动刷新列表中的每个索引。

有什么方法可以轻松做到吗?因为实际上我必须手动刷新索引。

总结: 当我 reorder/delete/add 项目时,我希望每个项目的 Model.Index 更新为 ListBox 中的正确索引位置。

我的任务是:

我尝试寻找类似的问题,但没有找到太多可以帮助我的问题。 提前致谢:)


型号:

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();
        }
    }
}

下面是一个带有简单绑定列表的 xaml 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>

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;
        }
    }
}

我认为在 WPF 中没有现成的绑定到容器索引的方法。您的解决方案其实很容易理解。

如果您发现自己经常绑定到索引,您可以创建自己的附加 property/value 转换器,使用 these helpers until it finds the parent ItemsControland makes use of the IndexFromContainer 方法在内部爬升可视化树。


下面是一些让您开始使用此方法的代码:

首先是一个小的辅助函数,用于爬上可视化树以查找通用类型的项目:

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);
    }
}

然后一个值转换器将控件作为输入值,returns它的索引在第一个遇到的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();
    }
}

这就是您在 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>