20

I have a multi-select listbox in a SL3 app using prism and I need a collection in my viewmodel that contains the currently selected items in the listbox.

The viewmodel doesn't know anything about the view so it does not have access to the listbox control. Also I need to be able to clear the selected items in the listbox from the viewmodel.

Not sure how to approach this problem

thanks Michael

MIantosca
  • 833
  • 1
  • 10
  • 33

10 Answers10

40

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);
        }
    }
}
Brian Genisio
  • 47,787
  • 16
  • 124
  • 167
  • 1
    this does not work at all, not only because the class should be static the selected Items are always null. – msfanboy Apr 25 '10 at 15:43
  • This works in SL3, SL4 and WPF. I use this method all the time. Yes, the class that holds the attached behavior should be static. It is part of the "Attached Behavior" pattern in SL and WPF. – Brian Genisio Apr 26 '10 at 17:06
  • This absolutely works, I've been banging my head against this for half an hour. I always forget that attached properties are the "fix anything" solution in Silverlight/WPF – Darko Jun 08 '10 at 06:52
  • This solution almost work for me except, if SelectedItems property is not empty, it won't update UI at all. How can I archieve this? – Anonymous Nov 21 '10 at 12:17
  • What about changes in the other direction? Suppose I want to clear the bound list from my ViewModel? UPDATE: Another answer addresses this. – ultravelocity Nov 02 '11 at 16:46
  • If this is still relevant, please change ListBox to MultiSelector so it allows a wider range of controls (e.g. DataGrid) – Bas Mar 18 '12 at 13:38
  • you gotta be kidding me, those lines of code just to get the selected items of a listbox-like control? Are we talking about the prime UI solution Microsoft is offering? – Matt Feb 26 '15 at 09:59
  • @MattWolf Nope. This is 6 years old. Silverlight is dead. We've all moved past it. – Brian Genisio Feb 27 '15 at 06:05
  • @BrianGenisio, same issue with WPF, how is this solved in any way? – Matt Feb 27 '15 at 08:07
7

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;
      }
    }
  }
candritzky
  • 123
  • 1
  • 5
  • I can't seem to get this working: I select one item in the listbox and then execute a command to move that item one position up in the list. This is done by swapping it with the next item in the collection bound to ItemsSource. I then select the next item using the code you posted. End result is that both the first and second item are selected in the listbox, even though when putting a breakpoint in UpdateListBoxSelection() the _listBox.SelectedItems contains a single element only. – stijn Dec 18 '11 at 17:55
  • ok got it: I first had to clear the SelectedItems, then modify the ItemsSource, then reselect. For some reason first modifying and then changing the selection does not wokr properly.. – stijn Dec 18 '11 at 18:14
  • For those who still could not make this to work, make sure you didn't modify your Windows theme colors like me. It turns out my ListBox background color was matching the selection color when the ListBox was out of focus, thus making it appear like nothing was selected. Change your ListBox background brush to red to check if this is what's happening to you. It made me spend 2 hours until I realized... – CGodo Mar 11 '13 at 17:01
3

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

Updated existing behavior with Selecting the items on Collection Changed & Rebinded

http://rnragu.blogspot.com/2011/04/multiselect-listbox-in-silverlight-use.html

Ragunathan
  • 2,007
  • 1
  • 14
  • 10
1

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).

JonnyB
  • 11
  • 1
0

The solution by Brian Genisio and Samuel Jack here are great. I have implemented it successfully. But I also had a case where this did not work and since, I am no expert with WPF or .Net, I failed to debug it. I am still not certain as to what the issue was, but in due time, I found a workaround for multiselect binding. And in this solution, I did not have to get to the DataContext.

This solution is for people who could not make the above 2 solutions to work. I guess this solution would not be considered as MVVM. It goes like this. Suppose you have 2 collections in ViewModel:

public ObservableCollection<string> AllItems { get; private set; }
public ObservableCollection<string> SelectedItems { get; private set; }

You need a List box:

<ListBox x:Name="MyListBox" ItemsSource="{Binding AllItems}" SelectionMode="Multiple" />

Now add another ListBox and bind it to SelectedItems and set Visibility:

<ListBox x:Name="MySelectedItemsListBox" ItemsSource="{Binding SelectedItems, Mode=OneWayToSource}" SelectionMode="Multiple" Visibility="Collapsed" />

Now, in the code behind of the WPF page, add to the constructor after InitializeComponent() method:

MyListBox.SelectionChanged += MyListBox_SelectionChanged;

And add a method:

private void MyListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    MySelectedItemsListBox.ItemsSource = MyListBox.SelectedItems;
}

And you are done. This will work for sure. I guess this can be used in Silverlight also if the above solution do not work.

Shakti Prakash Singh
  • 2,414
  • 5
  • 35
  • 59
0

For those who still could not make candritzky answer to work, make sure you didn't modify your Windows theme colors like me. It turns out my ListBox background color was matching the selection color when the ListBox was out of focus, thus making it appear like nothing was selected.

Change your ListBox background brush to red to check if this is what's happening to you. It made me spend 2 hours until I realized...

CGodo
  • 1,478
  • 14
  • 15
0

Here's a blog with a solution for this problem, including a sample application so that you can see exactly how to make it work: http://alexshed.spaces.live.com/blog/cns!71C72270309CE838!149.entry

I've just implemented this in my application and it solves the problem nicely

RyanHennig
  • 1,072
  • 9
  • 12
0

The solution for me was to combine Alessandro Pilotti update with Brian Genisio attached behavior. But remove the code for the DataContext changing Silverlight 4 doesn’t support this.

If you are binding the listbox to an ObservableCollection<string> the above works fine but if you are binding to complex objects like ObservableCollection<Person> SelectedItems { get; private set; } via a DataTemplate it doesn’t seem to work. This due to the default implementation of the Equals method the collection is using. You can solve this by telling your Person object which fields to compare when determining if the objects are equal, this is done by implementating the interface IEquatable<T> on your object.

After that the IndexOf(item) code will work and be able to compare if the objects are Equal and select the item in the list

// 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]);

See Link: http://msdn.microsoft.com/en-us/library/ms131190(VS.95).aspx

Edward
  • 1
  • 1
0

Im using EventToCommand object on selection changed event in XAML and passing there ListBox as a parameter. Than command in MMVM is managing ObservableCollection of selected items. Its easy and fast ;)

vmachacek
  • 530
  • 1
  • 11
  • 27