WPF: привязка к ListBoxItem.IsSelected не работает для элементов вне экрана - PullRequest
8 голосов
/ 17 августа 2011

В моей программе у меня есть набор объектов модели представления для представления элементов в ListBox (допускается множественный выбор). У модели представления есть свойство IsSelected, которое я хотел бы привязать к ListBox, чтобы состояние выбора управлялось в модели представления, а не в самом списке.

Однако очевидно, что ListBox не поддерживает привязки для большинства элементов вне экрана, поэтому в общем случае свойство IsSelected не синхронизируется правильно. Вот некоторый код, который демонстрирует проблему. Первый XAML:

<StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock>Number of selected items: </TextBlock>
        <TextBlock Text="{Binding NumItemsSelected}"/>
    </StackPanel>
    <ListBox ItemsSource="{Binding Items}" Height="200" SelectionMode="Extended">
        <ListBox.ItemContainerStyle>
            <Style TargetType="{x:Type ListBoxItem}">
                <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
            </Style>
        </ListBox.ItemContainerStyle>
    </ListBox>
    <Button Name="TestSelectAll" Click="TestSelectAll_Click">Select all</Button>
</StackPanel>

C # Выбрать все обработчик:

private void TestSelectAll_Click(object sender, RoutedEventArgs e)
{
    foreach (var item in _dataContext.Items)
        item.IsSelected = true;
}

C # viewmodel:

public class TestItem : NPCHelper
{
    TestDataContext _c;
    string _text;
    public TestItem(TestDataContext c, string text) { _c = c; _text = text; }

    public override string ToString() { return _text; }

    bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set {
            _isSelected = value; 
            FirePropertyChanged("IsSelected");
            _c.FirePropertyChanged("NumItemsSelected");
        }
    }
}
public class TestDataContext : NPCHelper
{
    public TestDataContext()
    {
        for (int i = 0; i < 200; i++)
            _items.Add(new TestItem(this, i.ToString()));
    }
    ObservableCollection<TestItem> _items = new ObservableCollection<TestItem>();
    public ObservableCollection<TestItem> Items { get { return _items; } }

    public int NumItemsSelected { get { return _items.Where(it => it.IsSelected).Count(); } }
}
public class NPCHelper : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void FirePropertyChanged(string prop)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(prop));
    }
}

Можно наблюдать две отдельные проблемы.

  1. Если щелкнуть первый элемент, а затем нажать Shift + End, следует выбрать все 200 элементов; однако в заголовке указывается, что выбран только 21 элемент.
  2. Если вы нажмете «Выбрать все», тогда все элементы действительно будут выбраны. Если вы затем щелкнете по элементу в ListBox, вы можете ожидать отмены выбора других 199 элементов, но этого не произойдет. Вместо этого отменяются выбор только тех элементов, которые находятся на экране (и некоторых других). Все 199 элементов не будут отменены, если вы сначала не прокрутите список от начала до конца (и даже тогда, как ни странно, он не будет работать, если вы выполните прокрутку с помощью небольшого блока прокрутки).

Мои вопросы:

  1. Может кто-нибудь объяснить точно, почему это происходит?
  2. Могу ли я избежать или обойти проблему?

Ответы [ 3 ]

11 голосов
/ 17 августа 2011

ListBox по умолчанию виртуализированный пользовательский интерфейс. Это означает, что в любой данный момент будут отображаться только видимые элементы (вместе с небольшим подмножеством «почти видимых» элементов) в ItemsSource. Это объясняет, почему обновление source работает должным образом (так как эти элементы всегда существуют), а простое перемещение по пользовательскому интерфейсу - нет (поскольку визуальные представления этих элементов создаются и уничтожаются на лету и никогда не существуют). вместе сразу.)

Если вы хотите отключить это поведение, один из вариантов - установить ScrollViewer.CanContentScroll=False на ListBox. Это включит «плавную» прокрутку и неявно отключит виртуализацию. Чтобы явно отключить виртуализацию, вы можете установить VirtualizingStackPanel.IsVirtualizing=False.

2 голосов
/ 13 июня 2014

Отключение виртуализации часто невозможно.Как заметили люди, с большим количеством элементов производительность ужасна.

Мне кажется, что хак, работающий на меня, - это присоединение слушателя StatusChanged к ItemContainerGenerator в списке.Когда новые элементы прокручиваются в поле зрения, будет вызван слушатель, и вы можете установить привязку, если ее там нет.

В файле Example.xaml.cs:

// Attach the listener in the constructor
MyListBox.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged_FixBindingsHack;


private void ItemContainerGenerator_StatusChanged_FixBindingsHack(object sender, EventArgs e)
{
    ItemContainerGenerator generator = sender as ItemContainerGenerator;
    if (generator.Status == GeneratorStatus.ContainersGenerated)
    {
        foreach (ValueViewModel value in ViewModel.Values)
        {
            var listBoxItem = mValuesListBox.ItemContainerGenerator.ContainerFromItem(value) as ListBoxItem;
            if (listBoxItem != null)
            {
                var binding = listBoxItem.GetBindingExpression(ListBoxItem.IsSelectedProperty);
                if (binding == null)
                {
                    // This is a list item that was just scrolled into view.
                    // Hook up the IsSelected binding.
                    listBoxItem.SetBinding(ListBoxItem.IsSelectedProperty, 
                        new Binding() { Path = new PropertyPath("IsSelected"), Mode = BindingMode.TwoWay });
                }
            }
        }
    }
}
1 голос
/ 03 февраля 2018

Существует способ обойти это, не требуя отключения виртуализации (что снижает производительность). Проблема (как упоминалось в предыдущем ответе) заключается в том, что вы не можете полагаться на ItemContainerStyle для надежного обновления IsSelected на всех ваших моделях просмотра, поскольку контейнеры элементов существуют только для видимых элементов. Однако вы можете получить полный набор выбранных элементов из свойства ListBox SelectedItems.

Для этого требуется обмен данными с Viewmodel с представлением, что обычно является нарушением принципов MVVM. Но есть шаблон, который заставит все это работать и обеспечит возможность тестирования вашего модуля ViewModel. Создайте интерфейс представления для виртуальной машины, с которой можно общаться:

public interface IMainView
{
    IList<MyItemViewModel> SelectedItems { get; }
}

В вашей модели представления добавьте свойство View:

public IMainView View { get; set; }

По вашему мнению, подпишитесь на OnDataContextChanged, затем запустите это:

this.viewModel = (MainViewModel)this.DataContext;
this.viewModel.View = this;

А также реализовать свойство SelectedItems:

public IList<MyItemViewModel> SelectedItems => this.myList.SelectedItems
    .Cast<MyItemViewModel>()
    .ToList();

Затем в вашей модели просмотра вы можете получить все выбранные элементы, набрав this.View.SelectedItems.

Когда вы пишете модульные тесты, вы можете настроить IMainView на то, что вам нужно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...