将多选 ListBox 与 MVVM 同步

Posted

技术标签:

【中文标题】将多选 ListBox 与 MVVM 同步【英文标题】:Synchronizing multi-select ListBox with MVVM 【发布时间】:2011-12-26 16:17:22 【问题描述】:

我有一些数据的两种视图:一个列表视图(现在是ListBox,但我一直想切换到ListView)和地图上的精美图形表示。在任一视图中,用户都可以单击一个对象,它将在两个视图中被选中。多选也是可能的,因此每个ViewModel 实例都有自己的IsSelected 属性。

目前我将ListBoxItem.IsSelected 绑定到ViewModel.IsSelected,但这只有在ListBox 没有虚拟化(see here)时才能正常工作。不幸的是,禁用虚拟化会影响性能,而且我的应用变得太慢了。

所以我必须再次启用虚拟化。为了维护屏幕外项目的ViewModel.IsSelected 属性,我注意到ListBoxListView 有一个SelectionChanged 事件,我可以(大概)使用它来将选择状态从ListBox/ListView 传播到ViewModel.

我的问题是,如何反向传播选择状态? ListBox/ListViewSelectedItems 属性是只读的!假设用户单击图形表示中的一个项目,但它在屏幕外 w.r.t.名单。如果我只是设置ViewModel.IsSelected,那么ListBox/ListView 将不知道新的选择,因此如果用户单击列表中的其他项目,它将无法取消选择该项目。我可以从ViewModel 拨打ListBox.ScrollIntoView,但有几个问题:

在我的 UI 中,如果它们以图形方式位于同一位置,实际上可以一键选择两个项目,尽管它们可能位于 ListBox/ListView 中完全不同的位置。 它破坏了 ViewModel 的隔离(我的 ViewModel 完全不知道 WPF,我想保持这种状态。)

那么,亲爱的 WPF 专家,有什么想法吗?

编辑:我最终切换到 Infragistics 控件并使用了一个丑陋且相当缓慢的解决方案。重点是,我不再需要答案了。

【问题讨论】:

这能回答你的问题吗? Bind to SelectedItems from DataGrid or ListBox in MVVM 【参考方案1】:

您可以创建一个Behavior,将ListBox.SelectedItems 与您的 ViewModel 中的集合同步:

public class MultiSelectionBehavior : Behavior<ListBox>

    protected override void OnAttached()
    
        base.OnAttached();
        if (SelectedItems != null)
        
            AssociatedObject.SelectedItems.Clear();
            foreach (var item in SelectedItems)
            
                AssociatedObject.SelectedItems.Add(item);
            
        
    

    public IList SelectedItems
    
        get  return (IList)GetValue(SelectedItemsProperty); 
        set  SetValue(SelectedItemsProperty, value); 
    

    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MultiSelectionBehavior), new UIPropertyMetadata(null, SelectedItemsChanged));

    private static void SelectedItemsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    
        var behavior = o as MultiSelectionBehavior;
        if (behavior == null)
            return;

        var oldValue = e.OldValue as INotifyCollectionChanged;
        var newValue = e.NewValue as INotifyCollectionChanged;

        if (oldValue != null)
        
            oldValue.CollectionChanged -= behavior.SourceCollectionChanged;
            behavior.AssociatedObject.SelectionChanged -= behavior.ListBoxSelectionChanged;
        
        if (newValue != null)
        
            behavior.AssociatedObject.SelectedItems.Clear();
            foreach (var item in (IEnumerable)newValue)
            
                behavior.AssociatedObject.SelectedItems.Add(item);
            

            behavior.AssociatedObject.SelectionChanged += behavior.ListBoxSelectionChanged;
            newValue.CollectionChanged += behavior.SourceCollectionChanged;
        
    

    private bool _isUpdatingTarget;
    private bool _isUpdatingSource;

    void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    
        if (_isUpdatingSource)
            return;

        try
        
            _isUpdatingTarget = true;

            if (e.OldItems != null)
            
                foreach (var item in e.OldItems)
                
                    AssociatedObject.SelectedItems.Remove(item);
                
            

            if (e.NewItems != null)
            
                foreach (var item in e.NewItems)
                
                    AssociatedObject.SelectedItems.Add(item);
                
            

            if (e.Action == NotifyCollectionChangedAction.Reset)
            
                AssociatedObject.SelectedItems.Clear();
            
        
        finally
        
            _isUpdatingTarget = false;
        
    

    private void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
    
        if (_isUpdatingTarget)
            return;

        var selectedItems = this.SelectedItems;
        if (selectedItems == null)
            return;

        try
        
            _isUpdatingSource = true;

            foreach (var item in e.RemovedItems)
            
                selectedItems.Remove(item);
            

            foreach (var item in e.AddedItems)
            
                selectedItems.Add(item);
            
        
        finally
        
            _isUpdatingSource = false;
        
    


这种行为可以如下所示使用:

        <ListBox ItemsSource="Binding Items"
                 DisplayMemberPath="Name"
                 SelectionMode="Extended">
            <i:Interaction.Behaviors>
                <local:MultiSelectionBehavior SelectedItems="Binding SelectedItems" />
            </i:Interaction.Behaviors>
        </ListBox>

(请注意,您的 ViewModel 中的 SelectedItems 集合必须被初始化;行为不会设置它,它只会更改其内容)

【讨论】:

要完成这项工作,应该注意您必须找到 System.Windows.Interactivity.dll 的副本(它位于 Expression Blend SDK 中)并且您必须定义“i: " XAML 根元素中的前缀,如 xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"... 您还必须定义 xmlns:local 以指向包含多选行为。 奇怪的是,虽然这个类是从 Behavior 派生的,但我可以不加修改地将它附加到 ListView!原来 ListView 是从 ListBox 派生的。 PRISM MVVM Reference Implementation 也使用这种方法。它有一个称为 SynchronizeSelectedItems 的行为,在 Prism4\MVVM RI\MVVM.Client\Views\MultipleSelectionView.xaml 中使用,它将选中的项目与名为 Selections 的 ViewModel 属性同步 @Qwertie 请将这个很棒的答案标记为答案。谢谢托马斯 让它在我的应用程序中运行。诀窍:我必须在我的视图模型中为SelectedItems 使用ObservableCollection,否则它的计数一直为0。

以上是关于将多选 ListBox 与 MVVM 同步的主要内容,如果未能解决你的问题,请参考以下文章

将多选列表变成一个漂亮且易于使用的带有复选框的列表。

wpf 下拉多选控件!

c# winform wmplayer简单播放器,实现自动下一首播放,随机播放,显示歌词,多选listbox

php将多选标签保存到文本文件

JSON将多选行编码为数组[重复]

如何将多选框的值发送到 django 后端