3

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.

JokerMartini
  • 5,674
  • 9
  • 83
  • 193
  • Hi, you should add a command and a commandParameter inside ur checkbox declaration. Add Command inside ur viewmodel, the command parameter should be the checked element (VNode Type), update the property IsSelected = false and it should update normaly. Take note that you will have to RelativeSource the command since it's nested in the listview – Guillaume Jan 12 '16 at 16:52
  • @Guillaume could you show exactly how i would do this? – JokerMartini Jan 12 '16 at 17:07
  • http://wpftution.blogspot.ca/2012/05/mvvm-sample-using-datagrid-control-in.html Its the same implementation instead of button its Checkbox, in removeCommand you would update ur Vnodes.IsSelected = true and you should change AncestorType={x:Type DataGrid}}}" "DataGrid" To Gridview or listView . Sorry for the quick reply I'm during lunchtime :) – Guillaume Jan 12 '16 at 17:17
  • What control are you using to display/select the `VNode`s? If the control supports multi-select then it already should have a collection like `SelectedItems` or something similar. – Good Night Nerd Pride Jan 12 '16 at 17:36
  • I'm selecting things on a canvas – JokerMartini Jan 12 '16 at 17:56

2 Answers2

3

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.

  • Those all great approaches to this. In my case the control does not have a selected Items property since I'm using a canvas. Is there a way to add an extensions to the canvas to collect the selected items? – JokerMartini Jan 12 '16 at 18:09
  • So Canvas is the ItemsPanel used by an ItemsControl (or whatever)? Like this: http://stackoverflow.com/a/1265419/424129 or something? – 15ee8f99-57ff-4f92-890c-b56153 Jan 12 '16 at 18:15
  • does that help make things easier at all? – JokerMartini Jan 12 '16 at 18:43
  • @JokerMartini OK, I wrote an attachment property that reflects a ListBox's SelectedItems state to a collection (anything that implements `IList` provided by a viewmodel. Makes no difference what the ItemsPanel is. – 15ee8f99-57ff-4f92-890c-b56153 Jan 12 '16 at 18:54
  • @JokerMartini Well, that's done. It works for reflecting changes in selection coming in through the UI. Haven't tested changes going the other way. – 15ee8f99-57ff-4f92-890c-b56153 Jan 12 '16 at 19:07
  • How do i use this with a canvas? – JokerMartini Jan 12 '16 at 19:08
  • I thought you said you were using the Canvas as the ItemsPanel for a collection control. If the collection control supports selection, it's very likely a ListBox or a ListView (which is a subclass of ListBox). If you want to select items in a Canvas, use a ListBox with a Canvas for the ItemsPanel; virtually all forms of suicide are preferable to reimplementing selection by hand. And I'm specifically thinking about a large number of small sharks here. – 15ee8f99-57ff-4f92-890c-b56153 Jan 12 '16 at 19:11
  • Alright sounds great! You are correct, I'm using the listbox. Thanks for your help. I'm going to test this out right now. I'm definitely not recreating the wheel of selection! – JokerMartini Jan 12 '16 at 19:14
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/100507/discussion-between-jokermartini-and-ed-plunkett). – JokerMartini Jan 12 '16 at 19:23
0

One method of doing this without the use of INotifyPropertyChanged, would be to add an if construct to the setter:

private bool isSelected;
    public bool IsSelected
    {
        get { return isSelected; }
        set
        {
            Set(ref isSelected, value);

            if(isSelected)
            {
                if(!SelectedVNodes.Any(v => v.Name == this.Name))
                SelectedVNodes.Add(this);
            }
            else{
                if(SelectedVNodes.Any(v => v.Name == this.Name))
                SelectedVNodes.Remove(this);
            }
            Console.WriteLine("selected/deselected");
        }
    }
KidCode
  • 3,990
  • 3
  • 20
  • 37