views:

1963

answers:

3

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  A: 

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"
Jobi Joy
Ahh yes - that makes sense and it works!Thanks.
IUnknown
It does work now - but with an undesired side effect. When I expand a second Expander, the first one automatically collapses. Is there a way to do this without automatically collapsing the previous expanded item?
IUnknown
Just make SelectionMode="Multiple" on the ListBox
Jobi Joy
Ahh - sorry I should have mentioned that I tried that but it did not give me what I was looking for. Using SelectionMode="Multiple" does allow the previous items to stay expanded but it also leaves it selected. I would like to be able to expand a second or third Expander but only have the most recently selected ListBoxItem selected (no multi-selections allowed). The RelativeBind, as stated above, only allows for the ListBoxItem IsSelected to be set to false when the Expander is Collapsed.
IUnknown
+1  A: 

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);
Jobi Joy
A: 

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.

IUnknown
cool.. Yeah not just expander in a Listbox's DataTemplate.. Any control like Button/checkbox or anything you really need this kind of a hack.
Jobi Joy
I think your word "hack" is more appropriate then my work "code snippet". This solution above is not very pretty - it works - but not pretty. There should be a more elegant solution then walking the visual graph.
IUnknown