将多选 ListBox 与 MVVM 同步 [英] Synchronizing multi-select ListBox with MVVM

查看:30
本文介绍了将多选 ListBox 与 MVVM 同步的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have two views of some data: a list view (a ListBox now, but I've been meaning to switch to ListView) and a fancy graphical representation on a map. In either view the user can click an object and it will be selected in both views. Multiselect is also possible, so each ViewModel instance has its own IsSelected property.

目前我正在将 ListBoxItem.IsSelected 绑定到 ViewModel.IsSelected,但这仅在 ListBox 未虚拟化(见这里).不幸的是,禁用虚拟化会影响性能,而且我的应用程序变得太慢.

Currently I'm binding ListBoxItem.IsSelected to ViewModel.IsSelected, but this only works properly if the ListBox is NOT virtualizing (see here). Unfortunately, disabling virtualization hurts performance and my app has become too slow.

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

So I have to enable virtualization again. In order to maintain the ViewModel.IsSelected property of off-screen items, I noticed that ListBox and ListView have a SelectionChanged event that I can (presumably) use to propagate the selection state from the ListBox/ListView to the ViewModel.

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

My question is, how do I propagate selection state in the reverse direction? The SelectedItems property of ListBox/ListView is read-only! Suppose the user clicks an item in the graphical representation, but it is off-screen w.r.t. the list. If I just set ViewModel.IsSelected then the ListBox/ListView will be unaware of the new selection, and as a consequence it will fail to deselect that item if the user clicks a different item in the list. I could call ListBox.ScrollIntoView from the ViewModel, but there are a couple of problems:

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

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

So, my dear WPF experts, any thoughts?

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

I ended up switching to an Infragistics control and using an ugly and rather slow solution. The point is, I no longer need an answer.

推荐答案

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

You can create a Behavior that synchronizes ListBox.SelectedItems with a collection in your 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;
        }
    }

}

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

This behavior can be used as shown below:

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

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

(note that the SelectedItems collection in your ViewModel has to be initialized; the behavior won't set it, it will only change its content)

这篇关于将多选 ListBox 与 MVVM 同步的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆