1

I have a List of Lists and display it with nested ListBoxes:

MainWindow.xaml.cs

using System.Collections.Generic;

namespace WPF_Sandbox
{
    public partial class MainWindow
    {

        public IEnumerable<IEnumerable<string>> ListOfStringLists { get; set; } = new[] { new[] { "a", "b" }, new[] { "c", "d" } };

        public MainWindow()
        {
            InitializeComponent();

            DoSomethingButton.Click += (sender, e) =>
            {
                // do something with all selected items
            };
        }

    }
}

MainWindow.xaml

<Window x:Class="WPF_Sandbox.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        x:Name="ThisControl">
    <StackPanel>
        <ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
            <ListBox.ItemTemplate>
                <ItemContainerTemplate>
                    <ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
                        <ListBox.ItemTemplate>
                            <ItemContainerTemplate>
                                <TextBlock Text="{Binding}" />
                            </ItemContainerTemplate>
                        </ListBox.ItemTemplate>
                    </ListBox>
                </ItemContainerTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Name="DoSomethingButton" Content="DoSomething" />
    </StackPanel>
</Window>

How can I get all selected items across all ListBoxes?

I found a few solutions getting one selected item but could not figure out how to do applie those in my scenario.
I have an idea on how to do this by wrapping the string arrays but I would prefer not doing this.

Community
  • 1
  • 1
Tim Pohlmann
  • 4,140
  • 3
  • 32
  • 61
  • Does "wrapping the string arrays" mean writing viewmodels? You should write viewmodels. – 15ee8f99-57ff-4f92-890c-b56153 Feb 20 '17 at 15:54
  • @EdPlunkett Even if I would use the MVVM for this application (which I don't for various reasons), it would be annoying to "break up" the list of lists. I would like my viewmodel to hold the list of lists of strings and a list of selected strings. This seems to be a little tricky, though. – Tim Pohlmann Feb 21 '17 at 07:47

3 Answers3

1

The easiest way would be to iterate through the items in the ListBoxes:

private void DoSomethingButton_Click(object sender, RoutedEventArgs e)
{
    List<string> selectedStrings = new List<string>();
    foreach (IEnumerable<string> array in outerListBox.Items.OfType<IEnumerable<string>>())
    {
        ListBoxItem lbi = outerListBox.ItemContainerGenerator.ContainerFromItem(array) as ListBoxItem;
        if (lbi != null)
        {
            ListBox innerListBox = GetChildOfType<ListBox>(lbi);
            if (innerListBox != null)
            {
                foreach (string selectedString in innerListBox.SelectedItems.OfType<string>())
                    selectedStrings.Add(selectedString);
            }
        }
    }
}

private static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
    if (depObj == null)
        return null;

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
    {
        var child = VisualTreeHelper.GetChild(depObj, i);
        var result = (child as T) ?? GetChildOfType<T>(child);
        if (result != null)
            return result;
    }
    return null;
}

Note that the ListBoxItem may be virtualized away if you have a lot of inner IEnumerable<string>. You will then have to force the generation of the containers or disable UI virtualization:

WPF ListView virtualization. How to disable ListView virtualization?

This may affect the performance negatively so if this is an issue you should probably consider binding to an IEnumerable<YourType> and bind the SelectedItems property of the inner ListBox to a property of a YourType using a behaviour.

Since the SelectedItems property of a ListBox is read-only you can't bind to it directly: https://blog.magnusmontin.net/2014/01/30/wpf-using-behaviours-to-bind-to-readonly-properties-in-mvvm/.

Community
  • 1
  • 1
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Good solution. Not great that we have to fiddle with the visual tree but ok. Isn't the problem with `SelectedItems` that it is not a `DependencyProperty`? It just being readonly should not prevent me from using a one-way binding, right? – Tim Pohlmann Feb 21 '17 at 08:01
  • 1
    Right. The main issue is that is not a dependency property so you can't bind to it. – mm8 Feb 21 '17 at 10:33
1

I would just add an event handler to the inner ListBox like so if not doing things the MVVM way:

<ListBox ItemsSource="{Binding}" SelectionMode="Multiple" SelectionChanged="ListBox_SelectionChanged">

Then in your code behind implement the ListBox_SelectionChanged like so:

public List<string> FlatStringList = new List<string>();
private void ListBox_SelectionChanged(object sender,System.Windows.Controls.SelectionChangedEventArgs e)
{
    FlatStringList.AddRange(e.AddedItems.Cast<string>());
    foreach(string s in e.RemovedItems)
    {
        FlatStringList.Remove(s);
    }            
}

This is assuming you don't mind storing the selected strings in a flat list. Then you could implement your DoSomething button click event handler to do something with the FlatStringList. Hope that helps.

user1286901
  • 219
  • 3
  • 10
  • This is a very simple solution and works perfectly for the simple example I provided. Unfortunately in my real application, my code behind is in a different class. So when I would decide to use this solution, I would have to find a way of making the FlatStringList available to the other class. This might still be the most simple approach. – Tim Pohlmann Feb 21 '17 at 07:53
1

Why don't you create a wrapper (as you said):

public class MyString : INotifyPropertyChanged
{
    public MyString(string value) { Value = value; }

    string _value;
    public string Value { get { return _value; } set { _value = value; RaisePropertyChanged("Value"); } }

    bool _isSelected;
    public bool IsSelected { get { return _isSelected; } set { _isSelected = value; RaisePropertyChanged("IsSelected"); } }

    public event PropertyChangedEventHandler PropertyChanged;
    void RaisePropertyChanged(string propname)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
    }
} 

Bind the IsSelected property of the ListBoxItems:

<StackPanel>
    <ListBox ItemsSource="{Binding ListOfStringLists, ElementName=ThisControl}">
        <ListBox.ItemTemplate>
            <ItemContainerTemplate>
                <ListBox ItemsSource="{Binding}" SelectionMode="Multiple">
                    <ListBox.ItemTemplate>
                        <ItemContainerTemplate>
                            <TextBlock Text="{Binding Value}" />
                        </ItemContainerTemplate>
                    </ListBox.ItemTemplate>
                    <ListBox.ItemContainerStyle>
                        <Style TargetType="{x:Type ListBoxItem}">
                            <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
                        </Style>
                    </ListBox.ItemContainerStyle>
                </ListBox>
            </ItemContainerTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <Button Name="DoSomethingButton" Content="DoSomething"  />
</StackPanel>

and you are already done:

    public IEnumerable<IEnumerable<MyString>> ListOfStringLists { get; set; } = new[] { new[] { new MyString("a"), new MyString("b") { IsSelected = true } }, new[] { new MyString("c"), new MyString("d") } };

    public MainWindow()
    {
        this.InitializeComponent(); 

        DoSomethingButton.Click += (sender, e) =>
        {
            foreach (var i in ListOfStringLists)
                foreach (var j in i)
                {
                    if (j.IsSelected)
                    {
                        // ....
                    }
                }
        };
    }
rmojab63
  • 3,513
  • 1
  • 15
  • 28
  • This is simpler than what I had in mind. I'm still not a big fan of having to wrap every single string, however. It would be nice to just have the list of lists of strings and a list of selected strings. But it seems that's not as easy as I hoped it would be. – Tim Pohlmann Feb 21 '17 at 07:49