WPF Listbox + Expander events

2019-03-21 00:16发布

问题:

I have an Expander in the ItemTemplate of a ListBox. Renders fine. The issue I have run into is that I would like the ListBox_SelectionChanged event to fire when the expander is expanded and/or selected. The MouseDown event does not seem to bubble up to the ListBox.

What I need is the SelectedIndex of the ListBox. Because the ListBox_SelectionChanged does not get fired, the index is -1 and I cannot determine which item has been selected.

The ListBox_SelectionChanged Event is fired if a user clicks on the Content of the Expander after it has been expanded. If they only click on the expander, the event is not fired. This is confusing to the user because visually, they think they have already clicked on that item when actually clicking on the Expander Header. I need the ListBox Item selected when the user Expands the Expander because as far as the user is concerned, the item is now selected when it really isn't.

Any suggests on how to get this to work or alternate ways of determining the SelectedIndex of the list box with expanders in it?

Simplified code for reference:

<Window x:Class="WpfApplication3.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    Loaded="Window_Loaded">
    <Grid Name="Root">
        <ScrollViewer>
            <ListBox SelectionChanged="ListBox_SelectionChanged" ItemsSource="{Binding}">
                <ItemsControl.ItemTemplate >
                    <DataTemplate>
                        <Border>
                            <Expander>
                                <Expander.Header>
                                    <TextBlock Text="{Binding Path=Name}"/>
                                </Expander.Header>
                                <Expander.Content>
                                    <StackPanel>
                                        <TextBlock Text="{Binding Path=Age}"/>
                                        <TextBlock Text="Line 2"/>
                                        <TextBlock Text="Line 3"/>
                                    </StackPanel>
                                </Expander.Content>
                            </Expander>
                        </Border>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ListBox>
        </ScrollViewer>
    </Grid>
</Window>

Simple class for Binding:

public class Person
{
    public string Name {
        get;
        set;
    }

    public int Age {
        get;
        set;
    }
}

Creating and populating the data for binding:

private void Window_Loaded(object sender, RoutedEventArgs e) {

    data = new ObservableCollection<Person>();

    data.Add(new Person {
        Name = "One",
        Age=10
    });

    data.Add(new Person {
        Name = "Two",
        Age = 20
    });

    data.Add(new Person {
        Name = "Three",
        Age = 30
    });

    Root.DataContext = data;
}

This is the event I need (really just the SelectedIndex I need)

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e) {
    ListBox box = (ListBox)sender;

    // This value is not set because events from Expander are not bubbled up to fire SelectionChanged Event
    int index = box.SelectedIndex;
}

回答1:

An alternate way which doesnt depends on the IsSelected, You can hook an Expanded/Collapsed event of expander to the code behind and use the following code to find out the ListBox index on which you clicked.

DependencyObject dep = (DependencyObject)e.OriginalSource;

while ((dep != null) && !(dep is ListViewItem))
{
   dep = VisualTreeHelper.GetParent(dep);
}

if (dep == null)
     return;

int index = yourListBox.ItemContainerGenerator.IndexFromContainer(dep);


回答2:

What you wanted is to get the Expander control controls the ListBox Selection. You can easily archive this by setting a TwoWay Binding on the IsExpanded property of the Expander to the immediate ListBoxItem which you clicked.

 <Expander IsExpanded="{Binding IsSelected,Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}">

UPDATE : If you need to avoid the automatic collapse when you select another item, make Listbox selection mode to multiple.

<ListBox SelectionMode="Multiple"


回答3:

Thanks Jobi. That's pretty clever. The rabbit hole of WPF keeps getting deeper and deeper.

Here is what I did based on your suggestion:

private void Expander_Expanded(object sender, RoutedEventArgs e) {
    DependencyObject dep = (DependencyObject)sender;

    while ((dep != null) && !(dep is ListBoxItem)) {
        dep = VisualTreeHelper.GetParent(dep);
    }

    if (dep == null)
        return;

    int index = PersonList.ItemContainerGenerator.IndexFromContainer(dep);

    PersonList.SelectedIndex = index;
}

private void Expander_Collapsed(object sender, RoutedEventArgs e) {
    DependencyObject dep = (DependencyObject)sender;

    while ((dep != null) && !(dep is ListBoxItem)) {
        dep = VisualTreeHelper.GetParent(dep);
    }

    if (dep == null)
        return;

    int index = PersonList.ItemContainerGenerator.IndexFromContainer(dep);

    if (PersonList.SelectedIndex == index)
        PersonList.SelectedIndex = -1;
}

I had to change the ListViewItem to ListBoxItem (I was using a ListBox).

Also, I used the index to select or de-select the ListBox.SelectedIndex. This give me the experience I was looking for.

  1. The first time someone expands an Expander, it selects the newly expanded ListBoxItem.

  2. If someone expands another Expander, the previous ListBoxItem is deselected, but remains expanded, the newly expanded ListBoxItem is selected.

  3. If someone collapses a selected Expander, the ListBoxItem is deselected.

  4. If there are several Expanders expanded, someone collapses a non-selected ListBoxItem expander, the previously selected ListBoxItem remains selected.

Thanks for the help - I think this is a very useful little code snippet for anyone who uses Expanders in a ListBox.