Sync SelectedItems in a muliselect listbox with a collection in ViewModel
So, assume you have a ViewModel with the following properties:
public ObservableCollection<string> AllItems { get; private set; }
public ObservableCollection<string> SelectedItems { get; private set; }
You would start by binding your AllItems collection to the ListBox:
<ListBox x:Name="MyListBox" ItemsSource="{Binding AllItems}" SelectionMode="Multiple" />
The problem is that the SelectedItems property on ListBox is not a DependencyProperty. This is pretty bad, since you can't bind it to something in your ViewModel.
The first approach is to just put this logic in the code-behind, to tweak the ViewModel:
public MainPage()
{
InitializeComponent();
MyListBox.SelectionChanged += ListBoxSelectionChanged;
}
private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = sender as ListBox;
if(listBox == null) return;
var viewModel = listBox.DataContext as MainVM;
if(viewModel == null) return;
viewModel.SelectedItems.Clear();
foreach (string item in listBox.SelectedItems)
{
viewModel.SelectedItems.Add(item);
}
}
This approach will work, but it is really ugly. My preferred approach is to extract this behavior into an "Attached Behavior". If you do that, you can completely eliminate your code-behind and set it up in the XAML. The bonus is that this "Attached Behavior" is now re-usable in any ListBox:
<ListBox ItemsSource="{Binding AllItems}" Demo:SelectedItems.Items="{Binding SelectedItems}" SelectionMode="Multiple" />
And here is the code for the Attached Behavior:
public static class SelectedItems
{
private static readonly DependencyProperty SelectedItemsBehaviorProperty =
DependencyProperty.RegisterAttached(
"SelectedItemsBehavior",
typeof(SelectedItemsBehavior),
typeof(ListBox),
null);
public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
"Items",
typeof(IList),
typeof(SelectedItems),
new PropertyMetadata(null, ItemsPropertyChanged));
public static void SetItems(ListBox listBox, IList list) { listBox.SetValue(ItemsProperty, list); }
public static IList GetItems(ListBox listBox) { return listBox.GetValue(ItemsProperty) as IList; }
private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = d as ListBox;
if (target != null)
{
GetOrCreateBehavior(target, e.NewValue as IList);
}
}
private static SelectedItemsBehavior GetOrCreateBehavior(ListBox target, IList list)
{
var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior;
if (behavior == null)
{
behavior = new SelectedItemsBehavior(target, list);
target.SetValue(SelectedItemsBehaviorProperty, behavior);
}
return behavior;
}
}
public class SelectedItemsBehavior
{
private readonly ListBox _listBox;
private readonly IList _boundList;
public SelectedItemsBehavior(ListBox listBox, IList boundList)
{
_boundList = boundList;
_listBox = listBox;
_listBox.SelectionChanged += OnSelectionChanged;
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
_boundList.Clear();
foreach (var item in _listBox.SelectedItems)
{
_boundList.Add(item);
}
}
}
I wanted to have true two-way binding so that the ListBox selection reflects the items contained in the SelectedItems collection of the underlying ViewModel. This allows me to control the selection by logic in the ViewModel layer.
Here are my modifications to the SelectedItemsBehavior class. They synchronize the ListBox.SelectedItems collection with the underlying ViewModel property if the ViewModel property implements INotifyCollectionChanged (e. g. implemented by the ObservableCollection<T> type).
public static class SelectedItems
{
private static readonly DependencyProperty SelectedItemsBehaviorProperty =
DependencyProperty.RegisterAttached(
"SelectedItemsBehavior",
typeof(SelectedItemsBehavior),
typeof(ListBox),
null);
public static readonly DependencyProperty ItemsProperty = DependencyProperty.RegisterAttached(
"Items",
typeof(IList),
typeof(SelectedItems),
new PropertyMetadata(null, ItemsPropertyChanged));
public static void SetItems(ListBox listBox, IList list) { listBox.SetValue(ItemsProperty, list); }
public static IList GetItems(ListBox listBox) { return listBox.GetValue(ItemsProperty) as IList; }
private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = d as ListBox;
if (target != null)
{
AttachBehavior(target, e.NewValue as IList);
}
}
private static void AttachBehavior(ListBox target, IList list)
{
var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior;
if (behavior == null)
{
behavior = new SelectedItemsBehavior(target, list);
target.SetValue(SelectedItemsBehaviorProperty, behavior);
}
}
}
public class SelectedItemsBehavior
{
private readonly ListBox _listBox;
private readonly IList _boundList;
public SelectedItemsBehavior(ListBox listBox, IList boundList)
{
_boundList = boundList;
_listBox = listBox;
_listBox.Loaded += OnLoaded;
_listBox.DataContextChanged += OnDataContextChanged;
_listBox.SelectionChanged += OnSelectionChanged;
// Try to attach to INotifyCollectionChanged.CollectionChanged event.
var notifyCollectionChanged = boundList as INotifyCollectionChanged;
if (notifyCollectionChanged != null)
{
notifyCollectionChanged.CollectionChanged += OnCollectionChanged;
}
}
void UpdateListBoxSelection()
{
// Temporarily detach from ListBox.SelectionChanged event
_listBox.SelectionChanged -= OnSelectionChanged;
// Synchronize selected ListBox items with bound list
_listBox.SelectedItems.Clear();
foreach (var item in _boundList)
{
// References in _boundList might not be the same as in _listBox.Items
var i = _listBox.Items.IndexOf(item);
if (i >= 0)
{
_listBox.SelectedItems.Add(_listBox.Items[i]);
}
}
// Re-attach to ListBox.SelectionChanged event
_listBox.SelectionChanged += OnSelectionChanged;
}
void OnLoaded(object sender, RoutedEventArgs e)
{
// Init ListBox selection
UpdateListBoxSelection();
}
void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// Update ListBox selection
UpdateListBoxSelection();
}
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Update ListBox selection
UpdateListBoxSelection();
}
void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Temporarily deattach from INotifyCollectionChanged.CollectionChanged event.
var notifyCollectionChanged = _boundList as INotifyCollectionChanged;
if (notifyCollectionChanged != null)
{
notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
}
// Synchronize bound list with selected ListBox items
_boundList.Clear();
foreach (var item in _listBox.SelectedItems)
{
_boundList.Add(item);
}
// Re-attach to INotifyCollectionChanged.CollectionChanged event.
if (notifyCollectionChanged != null)
{
notifyCollectionChanged.CollectionChanged += OnCollectionChanged;
}
}
}
Thanks for this! I added a small update to support initial loading and DataContext changing.
Cheers,
Alessandro Pilotti [MVP / IIS]
public class SelectedItemsBehavior
{
private readonly ListBox _listBox;
private readonly IList _boundList;
public ListBoxSelectedItemsBehavior(ListBox listBox, IList boundList)
{
_boundList = boundList;
_listBox = listBox;
SetSelectedItems();
_listBox.SelectionChanged += OnSelectionChanged;
_listBox.DataContextChanged += ODataContextChanged;
}
private void SetSelectedItems()
{
_listBox.SelectedItems.Clear();
foreach (object item in _boundList)
{
// References in _boundList might not be the same as in _listBox.Items
int i = _listBox.Items.IndexOf(item);
if (i >= 0)
_listBox.SelectedItems.Add(_listBox.Items[i]);
}
}
private void ODataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetSelectedItems();
}
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
_boundList.Clear();
foreach (var item in _listBox.SelectedItems)
{
_boundList.Add(item);
}
}
}
Updated existing behavior with Selecting the items on Collection Changed & Rebinded
http://rnragu.blogspot.com/2011/04/multiselect-listbox-in-silverlight-use.html
The original solution above works if you remember to create an instance of the observable collection first! Also, you need to ensure that the Observable collection content type matches the content type for your ListBox ItemSource (if you are deviating from the exact example mentioned above).