WPF: Binding to ListBoxItem.IsSelected doesn't work for off-screen items

耗尽温柔 提交于 2019-12-03 06:44:20

ListBox is, by default, UI virtualized. That means that at any given moment, only the visible items (along with a small subset of "almost visible" items) in the ItemsSource will actually be rendered. That explains why updating the source works as expected (since those items always exist,) but just navigating the UI doesn't (since the visual representations of those items are created and destroyed on the fly, and never exist together at once.)

If you want to turn off this behaviour, one option is to set ScrollViewer.CanContentScroll=False on your ListBox. This will enable "smooth" scrolling, and implicitly turn off virtualization. To disable virtualization explicitly, you can set VirtualizingStackPanel.IsVirtualizing=False.

Turning off virtualization is often not feasible. As people have noticed, the performance is terrible with lots of items.

The hack that seems to work for me is to attach a StatusChanged listener on the list box's ItemContainerGenerator. As new items are scrolled into view, the listener will be invoked, and you can set the binding if it's not there.

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

There's a way around this that doesn't require disabling virtualization (which hurts performance). The issue (as mentioned in the previous answer) is that you can't rely on an ItemContainerStyle to reliably update IsSelected on all your viewmodels, since the item containers only exist for visible elements. However you can get the full set of selected items from the ListBox's SelectedItems property.

This requires communication from the Viewmodel to the view, which is normally a no-no for violating MVVM principles. But there's a pattern to make it all work and keep your ViewModel unit testable. Create a view interface for the VM to talk to:

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

In your viewmodel, add a View property:

public IMainView View { get; set; }

In your view 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 viewmodel you can get all the selected items by this.View.SelectedItems .

When you write unit tests you can set that IMainView to do whatever you want.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!