1

I adapted some code from this answer (https://stackoverflow.com/a/51254960/3797778) in the hopes of adding SelectedValuePath functionality since the collection i want to store is actually a collection of property values contained in a collection of objects that the listbox is actually bound to. So basically i want to bind the selected items to a list of ID's rather than the complete objects.

My adapted code for the ListBox is as follows:

public class MultipleSelectionListBox : ListBox, INotifyPropertyChanged
{
    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(IEnumerable<dynamic>), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(IEnumerable<dynamic>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public event PropertyChangedEventHandler PropertyChanged;

    public IEnumerable<dynamic> BindableSelectedItems
    {
        get => (IEnumerable<dynamic>)GetValue(BindableSelectedItemsProperty);
        set {
            SetValue(BindableSelectedItemsProperty, value);
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("BindableSelectedItems"));
        }
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        BindableSelectedItems = SelectedItems.Cast<dynamic>();
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
        {
            List<dynamic> newSelection = new List<dynamic>();
            if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
                foreach (var item in listBox.BindableSelectedItems)
                {
                    var collectionValue = item.GetType().GetProperty(listBox.SelectedValuePath).GetValue(item, null);
                    foreach (var lbItem in listBox.Items)
                    {
                        if (lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null) == collectionValue)
                            newSelection.Add(lbItem);
                    }
                }
            else
                newSelection = listBox.BindableSelectedItems as List<dynamic>;

            listBox.SetSelectedItems(listBox.BindableSelectedItems);
        }
    }
}

the rest you can probibly assume but i'll block out the basics anyway.

My objects in the LB:

public class DeviceChannelInfo
{
    public DeviceStateInfo parentDeviceState { get; set; }
    public string name { get; set; }
    public string displayName { get; set; }
    public int id { get; set; }
}

My LB code:

<uc:MultipleSelectionListBox ItemsSource="{Binding Source={x:Static local:SharedProperties.deviceChannelInfos}, Mode=OneWay}" SelectionMode="Extended" SelectedValuePath="name" IsSynchronizedWithCurrentItem="True" BindableSelectedItems="{Binding MyCollectionOfSelectedIDs, Mode=TwoWay}">

The binding never seems to communicate with with my 'MyCollectionOfSelectedIDs' prop.

Wobbles
  • 3,033
  • 1
  • 25
  • 51
  • The setter of a CLR wrapper of a dependency property should not raise the PropertyChanged event...it should only call SetValue. What is MyCollectionOfSelectedIDs anyway? What's the DataContext of the ListBox? And exaclty how you expect the properties to "communicate"? – mm8 Aug 13 '18 at 12:13
  • @mm8 assume `MyCollectionOfSelectedIDs` is the obvious, `ObservableCollection`, data context should not make any difference when raising updating bindings for `BindableSelectedItems` just as it does not matter when updating binding for `ItemsSource` which obviously already works just fine. And I have no idea what "how you expect the properties to "communicate"" means, the selected items prop changes, the bound prop gets updated, pretty much wat everybody expects the control to do already but MS fails to implement. – Wobbles Aug 13 '18 at 14:13
  • The ItemsSource is bound to a static property. Are you sure that the binding to the MyCollectionOfSelectedIDs property actually works? There is nothing in your incomplete sample that tells. Please provide a MCVE: https://stackoverflow.com/help/mcve – mm8 Aug 13 '18 at 14:17
  • @mm8 ItemsSource works just fine, prop changed events work just fine with statics when implemented right. The issue is not Items Source, but instead SelectedItems. MCVE also tells you to limit irrelevant information which bindings related to ItemsSource certainly is irrelevant. – Wobbles Aug 13 '18 at 14:22
  • See my answer for a working example. Note that I have removed all unnecessary stuff like for example implementing the INotifyPropertyChanged interface in the control class and setting the Mode of the bindings to TwoWay. – mm8 Aug 13 '18 at 14:40

2 Answers2

1

Please refer to the following sample code.

public class MultipleSelectionListBox : ListBox
{
    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(IEnumerable<dynamic>), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(IEnumerable<dynamic>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public IEnumerable<dynamic> BindableSelectedItems
    {
        get => (IEnumerable<dynamic>)GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        BindableSelectedItems = SelectedItems.Cast<dynamic>();
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
        {
            List<dynamic> newSelection = new List<dynamic>();
            if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
                foreach (var item in listBox.BindableSelectedItems)
                {
                    var collectionValue = item.GetType().GetProperty(listBox.SelectedValuePath).GetValue(item, null);
                    foreach (var lbItem in listBox.Items)
                    {
                        if (lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null) == collectionValue)
                            newSelection.Add(lbItem);
                    }
                }
            else
                newSelection = listBox.BindableSelectedItems as List<dynamic>;

            listBox.SetSelectedItems(listBox.BindableSelectedItems);
        }
    }
}

View Model:

public class ViewModel : INotifyPropertyChanged
{
    public ViewModel()
    {
        //select 1 and 4 initially:
        MyCollectionOfSelectedIDs = new List<dynamic> { Items[0], Items[3] };
    }

    public IList<DeviceChannelInfo> Items { get; } = new List<DeviceChannelInfo>()
    {
        new DeviceChannelInfo{ name = "1", displayName = "1", id =1 },
        new DeviceChannelInfo{ name = "2", displayName = "2", id =2 },
        new DeviceChannelInfo{ name = "3", displayName = "3", id =3 },
        new DeviceChannelInfo{ name = "4", displayName = "4", id =4 }
    };

    private IEnumerable<dynamic> _mCollectionOfSelectedIDs;
    public IEnumerable<dynamic> MyCollectionOfSelectedIDs
    {
        get { return _mCollectionOfSelectedIDs; }
        set { _mCollectionOfSelectedIDs = value; NotifyPropertyChanged(); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "") =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

View:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp4"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="300">
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <local:MultipleSelectionListBox ItemsSource="{Binding Items}" 
                                        SelectionMode="Extended"
                                        DisplayMemberPath="displayName"
                                        SelectedValuePath="name" 
                                        BindableSelectedItems="{Binding MyCollectionOfSelectedIDs}" />

        <TextBlock Grid.Row="1" Text="{Binding MyCollectionOfSelectedIDs.Count}" />
    </Grid>
</Window>
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Nitpicky point: Shouldn't you use `ICollection` rather than `IEnumerable` for the property since you are binding to `MyCollectionOfSelectedIDs.Count` at one point? (Which is not in the IEnumerable contract.) – Mike Guthrie Aug 13 '18 at 14:56
  • The binding to `MyCollectionOfSelectedIDs.Count` in the view is basically just a test to verify that the control works as expected. The type of source property should be `IEnumerable` since that is what `SelectedItems.Cast()` in the control returns. – mm8 Aug 13 '18 at 15:00
  • It looks like my entire approach is flawed, I am swapping out the property, which would work in most instances, but what I really need to do is be modifying the collection. I almost have the control->property binding working, I am just having some casting issues with trying to keep it non type specific for reusability which I will post in a separate Q. – Wobbles Aug 13 '18 at 17:29
  • Thanks for the try guys, I was in fact doomed from the start using this method of swapping the actual property value holding the collection rather than modifying the collection directly. – Wobbles Aug 13 '18 at 23:01
1

I cracked this egg.

The most important thing to me was to avoid using behaviors, or code behind, and I believe I have accomplished this (with some likely needed testing but working so far)

public class MultipleSelectionListBox : ListBox
{
    internal bool processSelectionChanges = false;

    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(object), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(ICollection<object>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public dynamic BindableSelectedItems
    {
        get => GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }


    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);

        if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls

        if (e.AddedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Add((dynamic)item);
            }

        if (e.RemovedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Remove((dynamic)item);
            }
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
        {
            List<dynamic> newSelection = new List<dynamic>();
            if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
                foreach (var item in listBox.BindableSelectedItems)
                {
                    foreach (var lbItem in listBox.Items)
                    {
                        var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
                        if ((dynamic)lbItemValue == (dynamic)item)
                            newSelection.Add(lbItem);
                    }
                }
            else
                newSelection = listBox.BindableSelectedItems as List<dynamic>;

            listBox.SetSelectedItems(newSelection);
        }
    }
}

I was originally swapping the property value that held the selecteditems and this is what was breaking my higher level bindings, but this control modifies the collection directly keeping all references intact. I have not fully tested two way binding yet, but it loads the correct values on initial load and raises all the proper collection changed events when edited in the listbox.

Wobbles
  • 3,033
  • 1
  • 25
  • 51