将多选列表框与MVVM同步 [英] Synchronizing multi-select ListBox with 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
属性,我注意到ListBox
和ListView
都有一个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/ListView
的SelectedItems
属性是只读的!假设用户单击图形表示形式的项目,但该项目不在屏幕上.列表.如果我只是设置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:
- 在我的用户界面中,如果它们位于图形上的同一位置,则实际上可以一键选择两个项目,尽管它们可能位于
ListBox/ListView
中的完全不同的位置. - 它破坏了ViewModel的隔离(我的ViewModel完全不了解WPF,我想保持这种状态.)
- In my UI it's actually possible to select two items with one click if they are in the same location graphically, although they may be located in completely different locations in the
ListBox/ListView
. - It breaks ViewModel isolation (my ViewModel is totally unaware of WPF and I'd like to keep it that way.)
那么,我亲爱的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.
推荐答案
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)
这篇关于将多选列表框与MVVM同步的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!