简体   繁体   中英

Get all selected items across multiple nested ListBoxes with SelectionMode Multiple/Extended

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.

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

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

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.

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)
                    {
                        // ....
                    }
                }
        };
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM