Подтвердить что ты не робот

Как отображать номера строк в ListView?

Очевидным решением было бы иметь свойство номера строки в элементе ModelView, но недостатком является то, что вы должны повторно генерировать их при добавлении записей или изменении порядка сортировки.

Есть ли элегантное решение?

4b9b3361

Ответ 1

Я думаю, что у вас есть элегантное решение, но это работает.

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

Ответ 2

Если у вас есть динамический список, в котором элементы добавляются, удаляются или перемещаются, вы все равно можете использовать это очень приятное решение и просто позволить текущему представлению вашего списка обновляться после внесения изменений в исходный список. Этот образец кода удаляет текущий элемент непосредственно в списке источников данных "mySourceList" (который в моем случае является ObservableCollection) и, наконец, обновляет номера строк для исправления значений.

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

Ответ 3

Сначала вам нужно установить AlternationCount на items count +1, например:

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

Затем AlternationIndex покажет реальный индекс даже во время прокрутки:

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

Ответ 4

Это будет работать как шарм, Я не знаю о производительности, Тем не менее мы можем попробовать попробовать

Создайте конвертер с несколькими значениями

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

и ваш Xaml нравится это

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

Идея заключается в том, чтобы отправить объект и перечислить оба в конвертер и позволить конвертеру выбрать номер. Вы можете изменить конвертер для отображения упорядоченного списка.

Ответ 5

Вот еще один способ, включая комментарии к коду, которые помогут вам понять, как это работает.

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

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 используется в особых случаях привязки, когда мы пытаемся связать свойство данного объекта с другим свойством самого объекта [1].

Используя Режим = FindAncestor, мы можем пересечь уровни иерархии и получить указанный элемент, например ListViewItem (мы могли бы даже захватить GridViewColumn). Если у вас есть два элемента ListViewItem, вы можете указать, что вы хотите, с помощью "AncestorLevel = x".

Путь. Здесь я просто беру содержимое ListViewItem (это мой объект "Person" ).

Конвертер. Так как я хочу отображать номера строк в столбце Number, а не для объекта Person, мне нужно создать класс конвертера, который каким-то образом может преобразовать объект Person в соответствующую числовую строку. Но это не возможно, я просто хотел показать, что Path переходит к конвертеру. Удаление пути отправит ListViewItem в конвертер.

ConverterParameter Укажите параметр, который вы хотите передать классу IValueConverter. Здесь вы можете отправить состояние, если вы хотите, чтобы номер строки начинался с 0,1 100 или что-то еще.

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

Ответ 6

Я нашел решение, которое будет работать даже в том случае, если вам нужно переместить элементы внутри коллекции. Так что на самом деле мы должны уведомлять фиктивное свойство "ListNumbersNotify" каждый раз, когда наша коллекция изменяется, и связывать все с помощью этого хитрого конвертера MultiBinding.

XAML:

                <Window ...
                   x:Name="This">
                   ...
                 <ListView Name="ListViewCurrentModules">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <StackPanel Orientation="Horizontal">
                                <Label>
                                    <MultiBinding Converter="{helpers:NumberingConvertor}">
                                        <Binding Path="" />
                                        <Binding ElementName="ListViewCurrentModules" />
                                        <Binding Path="ListNumbersNotify" ElementName="This" />
                                    </MultiBinding>
                                </Label>
                                <Border>
                                 ...
                                </Border>
                            </StackPanel>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>

Преобразователь:

    public abstract class MultiConvertorBase<T> : MarkupExtension, IMultiValueConverter
where T : class, new()
{
    public abstract object Convert(object[] values, Type targetType, object parameter, CultureInfo culture);

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

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (_converter == null)
            _converter = new T();
        return _converter;
    }

    private static T _converter = null;
}

public class NumberingConvertor : MultiConvertorBase<NumberingConvertor>
{
    public override object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return ((ListView)values[1]).Items.IndexOf(values[0]) + 1;
    }
}

Код позади:

    public partial class AddModulesWindow: Window, INotifyPropertyChanged
    {
    ...
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string prop)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }

    public object ListNumbersNotify { get; }

    public AddModulesWindow(ICore core)
    {
        InitializeComponent();

        this.core = core;
        CurrentModuleInfos = new ObservableCollection<ModuleInfo>(core.Modules.Select(m => m?.ModuleInfo));
        CurrentModuleInfos.CollectionChanged += CurrentModuleTypes_CollectionChanged;

        ListViewCurrentModules.ItemsSource = CurrentModuleInfos;
    }

    private void CurrentModuleTypes_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        OnPropertyChanged("ListNumbersNotify");
    }

Ответ 7

Это дополнение к answer для amaca для проблем, обнаруженных Аллоном Гуральнеком и Вахидом. Проблема прокрутки решена с помощью параметра ListView.ItemsPanel для StackPanel в XAML:

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

Эта замена по умолчанию VirtualizingStackPanel с помощью простой StackPanel отключает автоматическую регенерацию внутренней коллекции ListViewItem. Таким образом, индексы не будут хаотично меняться при прокрутке. Но эта замена может уменьшить производительность на больших коллекциях. Кроме того, изменения динамической нумерации могут быть достигнуты при вызове CollectionViewSource.GetDefaultView(ListView.ItemsSource).Refresh() при изменении коллекции ItemsSource. Также как с фильтрация ListView. Когда я попытался добавить обработчик этого вызова в событие INotifyCollectionChanged.CollectionChanged, мой вывод ListView дублировал последнюю добавленную строку (но с правильной нумерацией). Исправлено это путем размещения вызова обновления после каждого изменения коллекции в коде. Плохое решение, но оно отлично подходит для меня.

Ответ 8

amaca ответ велик для статических списков. Для динамического:

  • Мы должны использовать MultiBinding, вторая привязка предназначена для изменения коллекции;
  • После удаления ItemsControl не содержит удаленный объект, но содержит ItemContainerGenerator. Конвертер для динамических списков (я использую его для TabControl TabItem):

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

Ответ 9

Вот мой маленький конвертер, который отлично работает с WPF в 2017 году с .NET 4.7.2, в том числе с полностью включенным 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();
};

Добавьте экземпляр этого IValueConverter к Resources GridViewColumn.CellTemplate или в другом месте. Или создать его экземпляр на месте на Binding связанного элемента, как я показываю здесь. В любом случае вам нужно создать экземпляр ItemIndexConverter и не забудьте привязать к нему всю исходную коллекцию. Здесь я извлекаю ссылку на исходную коллекцию из свойства ItemsSource объекта ListView --but, что влечет за собой некоторые не связанные с этим проблемы с доступом к корню XAML, поэтому, если у вас есть более удобный и простой способ обращения к исходной коллекции, вы должен сделать это.

Что касается доступа к свойству в корне XAML, корню ListView в XAML присваивается имя w_root, а расширение разметки XAML 2009 {x:Reference...} используется для доступа к корневому элементу XAML. Я не думаю, что привязка "ElementName" будет работать здесь, так как ссылка происходит в контексте шаблона.

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

Это! Кажется, он работает довольно быстро с большим количеством строк, и снова вы можете видеть, что сообщаемые индексы являются правильными при произвольной прокрутке и что VirtualizingStackPanel.IsVirtualizing действительно имеет значение True.

enter image description here

Не уверен, что на самом деле необходимо следующее, но обратите внимание, что объявление xmlns= для WPF обновлено, чтобы указать XAML 2009, в поддержку использования {x:Reference} упомянутого выше. Обратите внимание, что есть два изменения; "/winfx/" необходимо изменить на "/netfx/" при переключении с "2006" на "2009".

Ответ 10

Следуя наилучшему решению, я обнаружил проблему, когда индексы все еще не обновлялись после удаления/замены элементов в представлении списка. Чтобы решить, что есть один не очень понятный намек (я предлагаю использовать его в небольших коллекциях): после выполнения удаления/замены элемента следует вызвать событие ObservableCollection(INotifyCollectionChanged).CollectionChanged с действием Reset. Это возможно сделать с расширением существующего ObservableCollection, которым является ItemsSource или использовать отражение, когда это невозможно.

Ex.

public class ResetableObservableCollection<T> : ObservableCollection<T>
{
        public void NotifyReset()
        {
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
}


private void ItemsRearranged() 
{
    Items.NotifyReset();
}