将多选ListBox与MVVM同步

Qwe*_*tie 18 .net wpf mvvm multi-select virtualizingstackpanel

我有一些数据的两个视图:一个列表视图(一个ListBox现在,但我一直意味着切换到ListView)和一个奇特的图形表示在地图上.在任一视图中,用户都可以单击对象,并且将在两个视图中选择该对象.Multiselect也是可能的,因此每个ViewModel实例都有自己的IsSelected属性.

目前我绑定ListBoxItem.IsSelectedViewModel.IsSelected,但是这只是工作,如果正常的ListBox不是虚拟化(见这里).不幸的是,禁用虚拟化会损害性能,而我的应用程序变得太慢了.

所以我必须再次启用虚拟化.为了保持ViewModel.IsSelected屏幕外项目的属性,我注意到ListBox并且ListView有一个SelectionChanged事件我可以(可能)用来将选择状态从传播ListBox/ListViewViewModel.

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

  • 在我的UI中,如果它们以图形方式位于相同的位置,实际上可以通过一次单击选择两个项目,尽管它们可能位于完全不同的位置ListBox/ListView.
  • 它打破了ViewModel的隔离(我的ViewModel完全没有意识到WPF,我想保持这种方式.)

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

编辑:我最终切换到Infragistics控件并使用一个丑陋而缓慢的解决方案.关键是,我不再需要答案了.

Tho*_*que 25

您可以创建与ViewModel中的集合同步的行为ListBox.SelectedItems:

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

}
Run Code Online (Sandbox Code Playgroud)

可以使用此行为,如下所示:

        <ListBox ItemsSource="{Binding Items}"
                 DisplayMemberPath="Name"
                 SelectionMode="Extended">
            <i:Interaction.Behaviors>
                <local:MultiSelectionBehavior SelectedItems="{Binding SelectedItems}" />
            </i:Interaction.Behaviors>
        </ListBox>
Run Code Online (Sandbox Code Playgroud)

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

  • 要做到这一点,应该注意你必须追踪System.Windows.Interactivity.dll的副本(它在Expression Blend SDK中),你必须在XAML根元素中定义"i:"前缀, as xmlns:i ="clr-namespace:System.Windows.Interactivity; assembly = System.Windows.Interactivity"...您还必须定义xmlns:local以指向包含MultiSelectionBehavior的命名空间. (4认同)
  • [PRISM MVVM参考实现](http://msdn.microsoft.com/en-us/library/gg405492%28v=pandp.40%29.aspx)也使用这种方法.它有一个名为SynchronizeSelectedItems的行为,在Prism4\MVVM RI\MVVM.Client\Views\MultipleSelectionView.xaml中使用,它将已检查项与名为Selections的ViewModel属性同步 (3认同)
  • 奇怪的是,虽然这个类是从 Behavior&lt;ListBox&gt; 派生的,但我可以不加修改地将它附加到 ListView!事实证明,ListView 是从 ListBox 派生的。 (2认同)
  • 在我的应用程序中工作了.诀窍:我必须在我的视图模型中为`SelectedItems`使用`ObservableCollection`,否则它的计数为0. (2认同)