.net WPF:绑定到ListBoxItem.IsSelected不适用于屏幕外项目

.net WPF:绑定到ListBoxItem.IsSelected不适用于屏幕外项目,.net,wpf,mvvm,binding,listbox,.net,Wpf,Mvvm,Binding,Listbox,在我的程序中,我有一组视图模型对象来表示列表框中的项目,允许多选。viewmodel有一个IsSelected属性,我想将其绑定到ListBox,以便在viewmodel中而不是在ListBox本身中管理选择状态 但是,显然ListBox不维护大多数屏幕外项目的绑定,因此通常IsSelected属性没有正确同步。下面是一些演示该问题的代码。第一个XAML: <StackPanel> <StackPanel Orientation="Horizontal">

在我的程序中,我有一组视图模型对象来表示列表框中的项目,允许多选。viewmodel有一个IsSelected属性,我想将其绑定到ListBox,以便在viewmodel中而不是在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视图模型:

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));
    }
}
可以观察到两个不同的问题

如果单击第一个项目,然后按Shift+End,则应选择所有200个项目;但是,标题报告仅选择了21项。 如果单击“全选”,则所有项目都将被选中。如果然后单击列表框中的某个项目,您可能希望取消选择其他199个项目,但这不会发生。相反,只有屏幕上的项目和少数其他项目被取消选择。所有199个项目都不会被取消选择,除非您首先从开始到结束滚动列表,而且奇怪的是,如果您使用小滚动框进行滚动,那么即使如此,它也不起作用。 我的问题是:

有人能准确地解释为什么会发生这种情况吗? 我可以避免或解决这个问题吗? 默认情况下,ListBox是UI虚拟化的。这意味着在任何给定时刻,只有可见项以及ItemsSource中几乎可见项的一小部分将实际渲染。这就解释了为什么更新源代码会像预期的那样有效,因为这些项总是存在的,但是仅仅导航UI是不行的,因为这些项的可视表示是动态创建和销毁的,并且永远不会同时存在


如果要关闭此行为,一个选项是在列表框上设置ScrollViewer.CanContentScroll=False。这将启用平滑滚动,并隐式关闭虚拟化。要显式禁用虚拟化,可以设置VirtualizationStackPanel.isVirtualization=False。

关闭虚拟化通常是不可行的。正如人们所注意到的,很多物品的表现都很糟糕

对我来说,似乎可行的方法是在列表框的ItemContainerGenerator上附加一个StatusChanged侦听器。当新项目滚动到视图中时,将调用侦听器,如果不存在绑定,则可以设置绑定

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

有一种方法可以解决这一问题,它不需要禁用会影响性能的虚拟化。前面的回答中提到的问题是,您不能依赖ItemContainerStyle可靠地更新所有ViewModel上的IsSelected,因为item容器只存在于可见元素中。但是,您可以从列表框的SelectedItems属性中获取完整的选定项集

这需要从Viewmodel到视图的通信,这通常是违反MVVM原则的禁忌。但有一种模式可以让所有这些都正常工作,并保持ViewModel单元的可测试性。为虚拟机创建一个要与之对话的视图界面:

public interface IMainView
{
    IList<MyItemViewModel> SelectedItems { get; }
}
在您的视图中,订阅OnDataContextChanged,然后运行以下操作:

this.viewModel = (MainViewModel)this.DataContext;
this.viewModel.View = this;
并实现SelectedItems属性:

public IMainView View { get; set; }
public IList<MyItemViewModel> SelectedItems => this.myList.SelectedItems
    .Cast<MyItemViewModel>()
    .ToList();
然后在viewmodel中,您可以通过this.View.SelectedItems获取所有选定项目


当您编写单元测试时,您可以将该IMainView设置为执行任何您想要的操作。

或者您可以使用显式禁用它:-@Code裸体是的,当然也可以:将更新以包括该测试。此测试的XAML:VirtualizangStackPanel.IsVirtualization=False将其添加为ListBoxWow的属性,但这对性能很糟糕。如果我在测试列表中放入10000个项目,显示该列表似乎需要花费很长时间,选择All takes forever,根据Task Manager,内存使用量增加了近80MB,这意味着WPF需要为每个项目增加8KB,记住,这是针对没有数据模板的琐碎项目!微软,你有时让我恶心!。我的应用程序只需要大约1000个项目,但是,这对于MVVM来说是一个巨大的代价。哦,另外,当你按下10000个项目列表中的箭头键时,WPF需要4秒钟来响应。哇,奇怪的行为真的。。。禁用虚拟化是毫无疑问的,为了改进这个答案,我们不需要ViewModel;我们只能使用生成器来处理生成器。例如,Items是绑定集合
public IList<MyItemViewModel> SelectedItems => this.myList.SelectedItems
    .Cast<MyItemViewModel>()
    .ToList();