WPF: binding to ListBoxItem.IsSelected does not work for items off screen

In my program, I have a set of view model objects for representing items in a ListBox (multi-selection enabled). The view model has an IsSelected property, which I would like to bind to the ListBox to control the state of the selection in the viewmodel, and not in the list itself.

However, the ListBox does not seem to support bindings for most off-screen items, so the IsSelected property is not synchronized correctly. Here is some code that demonstrates the problem. First 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 # Select the entire handler:

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

Two separate problems can be observed.

  • If you press the first element and then press Shift + End, all 200 elements must be selected; however, the title indicates that only 21 items are selected.
  • If you click "Select All," then all items will be selected. If you then click an item in a ListBox, you expect the remaining 199 items to be undone, but this will not happen. Instead, only those elements that are on the screen (and several others) are excluded. All 199 items will not be undone unless you first browse the list from beginning to end (and even then, oddly enough, it does not work if you scroll using a small scroll window).

My questions:

  • Can someone explain why this is happening?
  • Is it possible to avoid or solve the problem?
+8
wpf mvvm binding listbox
Aug 17 '11 at 18:58
source share
3 answers

ListBox , by default, is virtualized. This means that at any given moment, only visible elements (along with a small subset of "almost visible" elements) will actually be displayed in the ItemsSource . This explains why updating the source works as expected (since these elements always exist), but there is simply no navigation on the user interface (since visual representations of these elements are created and destroyed on the fly and never exist together at the same time).

If you want to disable this behavior, one option is to set ScrollViewer.CanContentScroll=False on the ListBox . This will allow you to β€œsmooth out” scrolling and implicitly disable virtualization. To disable virtualization explicitly, you can set VirtualizingStackPanel.IsVirtualizing=False .

+11
Aug 17 '11 at 19:05
source share

Disabling virtualization is often not possible. As people have noticed, performance is terrible with lots of items.

A hack that seems to work for me is to add a StatusChanged listener to the ItemContainerGenerator list box. As new elements scroll into the view, a listener will be called, and you can set the binding if it does not exist.

In the Example.xaml.cs file:

 // 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 }); } } } } } 
+2
Jun 12 '14 at 21:21
source share

There is a way that does not require disabling virtualization (which degrades performance). The problem (as mentioned in the previous answer) is that you cannot rely on ItemContainerStyle to reliably update IsSelected on all your view models, since element containers exist only for visible elements. However, you can get the full set of selected items from the ListBox SelectedItems property.

This requires a connection to the Viewmodel with the view, which is usually a no-no for violating the principles of MVVM. But there is a template so that it all works and that your ViewModel can be tested. Create a view interface for the virtual machine to talk to:

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

In your view model, add the View property:

 public IMainView View { get; set; } 

In your opinion, subscribe to OnDataContextChanged, then run this:

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

And also implement the SelectedItems property:

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

Then, in your view model, you can get all the selected this.View.SelectedItems elements.

When you write unit tests, you can configure IMainView to what you want.

+1
Feb 03 '18 at 17:47
source share



All Articles