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