If I recall correctly, at the conclusion of our last episode, we were using some whimsical WPF control that doesn't let you bind SelectedItems
properly, so that's out. But if you can do it, it's by far the best way:
<NonWhimsicalListBox
ItemsSource="{Binding VNodes}"
SelectedItems="{Binding SelectedVNodes}"
/>
But if you're using System.Windows.Controls.ListBox
, you have to write it yourself using an attached property, which is actually not so bad. There's a lot of code here, but it's almost entirely boilerplate (most of the C# code in this attached property was created by a VS IDE code snippet). Nice thing here is it's general and any random passerby can use it on any ListBox
that's got anything in it.
public static class AttachedProperties
{
#region AttachedProperties.SelectedItems Attached Property
public static IList GetSelectedItems(ListBox obj)
{
return (IList)obj.GetValue(SelectedItemsProperty);
}
public static void SetSelectedItems(ListBox obj, IList value)
{
obj.SetValue(SelectedItemsProperty, value);
}
public static readonly DependencyProperty
SelectedItemsProperty =
DependencyProperty.RegisterAttached(
"SelectedItems",
typeof(IList),
typeof(AttachedProperties),
new PropertyMetadata(null,
SelectedItems_PropertyChanged));
private static void SelectedItems_PropertyChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var lb = d as ListBox;
IList coll = e.NewValue as IList;
// If you want to go both ways and have changes to
// this collection reflected back into the listbox...
if (coll is INotifyCollectionChanged)
{
(coll as INotifyCollectionChanged)
.CollectionChanged += (s, e3) =>
{
// Haven't tested this branch -- good luck!
if (null != e3.OldItems)
foreach (var item in e3.OldItems)
lb.SelectedItems.Remove(item);
if (null != e3.NewItems)
foreach (var item in e3.NewItems)
lb.SelectedItems.Add(item);
};
}
if (null != coll)
{
if (coll.Count > 0)
{
// Minor problem here: This doesn't work for initializing a
// selection on control creation.
// When I get here, it's because I've initialized the selected
// items collection that I'm binding. But at that point, lb.Items
// isn't populated yet, so adding these items to lb.SelectedItems
// always fails.
// Haven't tested this otherwise -- good luck!
lb.SelectedItems.Clear();
foreach (var item in coll)
lb.SelectedItems.Add(item);
}
lb.SelectionChanged += (s, e2) =>
{
if (null != e2.RemovedItems)
foreach (var item in e2.RemovedItems)
coll.Remove(item);
if (null != e2.AddedItems)
foreach (var item in e2.AddedItems)
coll.Add(item);
};
}
}
#endregion AttachedProperties.SelectedItems Attached Property
}
Assuming AttachedProperties
is defined in whatever the "local:
" namespace is in your XAML...
<ListBox
ItemsSource="{Binding VNodes}"
SelectionMode="Extended"
local:AttachedProperties.SelectedItems="{Binding SelectedVNodes}"
/>
ViewModel:
private ObservableCollection<Node> _selectedVNodes
= new ObservableCollection<Node>();
public ObservableCollection<Node> SelectedVNodes
{
get
{
return _selectedVNodes;
}
}
If you don't want to go there, I can think of threethree and a half straightforward ways of doing this offhand:
When the parent viewmodel creates a VNode
, it adds a handler to the new VNode
's PropertyChanged
event. In the handler, it adds/removes sender
from SelectedVNodes
according to (bool)e.NewValue
var newvnode = new VNode();
newvnode.PropertyChanged += (s,e) => {
if (e.PropertyName == "IsSelected") {
if ((bool)e.NewValue) {
// If not in SelectedVNodes, add it.
} else {
// If in SelectedVNodes, remove it.
}
}
};
// blah blah blah
Do that event, but instead of adding/removing, just recreate SelectedVNodes
:
var newvnode = new VNode();
newvnode.PropertyChanged += (s,e) => {
if (e.PropertyName == "IsSelected") {
// Make sure OnPropertyChanged("SelectedVNodes") is happening!
SelectedVNodes = new ObservableCollection<VNode>(
VNodes.Where(vn => vn.IsSelected)
);
}
};
Do that event, but don't make SelectedVNodes
Observable at all:
var newvnode = new VNode();
newvnode.PropertyChanged += (s,e) => {
if (e.PropertyName == "IsSelected") {
OnPropertyChanged("SelectedVNodes");
}
};
// blah blah blah much else blah blah
public IEnumerable<VNode> SelectedVNodes {
get { return VNodes.Where(vn => vn.IsSelected); }
}
Give VNode
a Parent property. When the parent viewmodel creates a VNode
, it gives each VNode
a Parent reference to the owner of SelectedVNodes
(presumably itself). In VNode.IsSelected.set
, the VNode does the add or remove on Parent.SelectedVNodes
.
// In class VNode
private bool _isSelected = false;
public bool IsSelected {
get { return _isSelected; }
set {
_isSelected = value;
OnPropertyChanged("IsSelected");
// Elided: much boilerplate checking for redundancy, null parent, etc.
if (IsSelected)
Parent.SelectedVNodes.Add(this);
else
Parent.SelectedVNodes.Remove(this);
}
}
None of the above is a work of art. Version 1 is least bad maybe.
Don't use the IEnumerable
one if you've got a very large number of items. On the other hand, it relieves you of the responsibility to make this two-way, i.e. if some consumer messes with SelectedVNodes
directly, you should really be handling its CollectionChanged
event and updating the VNodes
in question. Of course then you have to make sure you don't accidentally recurse: Don't add one to the collection that's already there, and don't set vn.IsSelected = true
if vn.IsSelected
is true already. If your eyes are glazing over like mine right now and you're starting to feel the walls closing in, allow me to recommend option #3.
Maybe SelectedVNodes
should publicly expose ReadOnlyObservableCollection<VNode>
, to get you off that hook. In that case number 1 is your best bet, because the VNodes
won't have access to the VM's private mutable ObservableCollection<VNode>
.
But take your pick.