Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Binding a collection to SelectedItems in a ListBox without violating MVVM

Tags:

c#

wpf

I have an ObservableCollection called SelectedVNodes' and it contains items from the ObservableCollection VNodes.

The SelectedVNodes should only contain nodes whose property IsSelected = True, otherwise if 'false' it shouldn't be in the list.

ObservableCollection<VNode> SelectedVNodes {...}
ObservableCollection<VNode> VNodes {...}

I've bound my property to maintain being updated on selection change by using this setter

<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />

However that is about as far as I got. I do not know how to append/remove this item from the SelectedVNodes list based on this property changing.

Here is the VNode class

public class VNode : NotifyBase
{
    public string Name { get; set; }
    public int Age { get; set; }
    public int Kids { get; set; }

    private bool isSelected;
    public bool IsSelected
    {
        get { return isSelected; }
        set
        {
            Set(ref isSelected, value);
            Console.WriteLine("selected/deselected");
        }
    }
}

NotifyBase derives from INotifyPropertyChanged.

like image 656
JokerMartini Avatar asked Oct 19 '25 01:10

JokerMartini


1 Answers

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:

  1. 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
    
  2. 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)
                );
        }
    };
    
  3. 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); }
    }
    
  4. 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.

like image 158
15ee8f99-57ff-4f92-890c-b56153 Avatar answered Oct 21 '25 14:10

15ee8f99-57ff-4f92-890c-b56153



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!