In WPF, SelectionChanged does not mean that the selection changed

Windows Presentation Foundation's Routed Events can lead to unexpected or at least nonintuitive behavior when using TabControls that contain ListViews and/or ComboBoxes. A routed event generally bubbles from the control that raised it, up the whole element tree until the root. On its way up it invokes handlers on multiple listeners. This makes a lot of sense in the ButtonBase.Click Event: if a button is clicked, then its containing element is also clicked.

By design, the Selector.SelectionChanged Event is such a routed event. TabItem, ListBox, and ComboBox all inherit from Selector, so if you put them in a hierarchy they will register on each other's events. A ComboBox that appears via a template in a ListBox will raise the SelectionChanged event of that ListBox - even if the user didn't select a new ListBoxItem. If you put that ListBox in a TabControl, then the SelectionChanged on that TabControl will also be fired - even if the user didn't select a new TabItem.

Enough talking: let's build a small demo. First we build a Window with a TabControl that has a ComboBox in its first TabItem:

XAML

<Window x:Class="DockOfTheBay.SelectorSampleWindow"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
       Title="Selector sample" Height="200" Width="400" >
    <Grid>
        <TabControl
           x:Name="MainTabControl"
           SelectionChanged="MainTabControl_SelectionChanged" >
            <TabItem Header="Courses" >
                <TabItem.Content>
                    <ListBox x:Name="ListBox1" />
                </TabItem.Content>
            </TabItem>
            <TabItem Header="Classrooms" >
            </TabItem>
        </TabControl>
    </Grid>
</Window>

C#

/// <summary>
/// The selection in the main tab control was changed.
/// </summary>
/// <param name="sender">Sender of the event: the Main Tab.</param>
/// <param name="e">Event arguments.</param>
private void MainTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    // First Tab
    if (this.MainTabControl.SelectedIndex == 0)
    {
        // (Re-)Populate ListBox
        this.ListBox1.Items.Clear();
        this.ListBox1.Items.Add("Building Windows Applications with WPF, LINQ and WCF");
        this.ListBox1.Items.Add("Building Cloud based Enterprise Applications for Windows Azure");
        this.ListBox1.Items.Add("Designing Data Warehouses using Dimensional Modeling");
        this.ListBox1.Items.Add("Upgrade to SharePoint 2010");
    }
}



Switching from one tab to another behaves nicely. Unfortunately, clicking in the ListBox now also triggers the SelectionChanged event from the TabControl itself, resulting in unexpected behavior:



Before diving into solutions, let's make it worse by implementing a very popular pattern. When the user navigates to a new TabItem, we populate a ListBox, and programatically select its first item. In theory there's nothing wrong with this, in practice it creates an infinite loop (well, it's not really infinite: it stops when you're out of stack space):



We can solve this by letting the child controls prevent the event from propagating -via the Handled property- like this:

XAML

<TabItem Header="Courses" >
    <TabItem.Content>
        <ListBox x:Name="ListBox1"
                SelectionChanged="ListBox1_SelectionChanged" />
    </TabItem.Content>
</TabItem>

C#

/// <summary>
/// The selection in the listbox was changed.
/// </summary>
/// <param name="sender">Sender of the event: the ListBox.</param>
/// <param name="e">Event arguments.</param>
private void ListBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    // Stop the event from bubbling.
    e.Handled = true;
}

Of course, not all child controls will have an event handler or even need one (e.g. a ComboBox bound to a property of a Business Entity). Moreover, the parent control can not depend on the implementation of his children. Fortunately the parent control can decide to ignore all bubbled events from child controls like this:

C#

/// <summary>
/// The selection in the main tab control was changed.
/// </summary>
/// <param name="sender">Sender of the event: the Main Tab.</param>
/// <param name="e">Event arguments.</param>
private void MainTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    // Ignore Routed Events from children
    if (e.OriginalSource == this.MainTabControl)
    {
        // First Tab
        if (this.MainTabControl.SelectedIndex == 0)
        {
            // (Re-)Populate ListBox
            this.ListBox1.Items.Clear();
            this.ListBox1.Items.Add("Building Windows Applications with WPF, LINQ and WCF");
            this.ListBox1.Items.Add("Building Cloud based Enterprise Applications for Windows Azure");
            this.ListBox1.Items.Add("Designing Data Warehouses using Dimensional Modeling");
            this.ListBox1.Items.Add("Upgrade to SharePoint 2010");

            // Select first item (no more 'Kaboom')
            this.ListBox1.SelectedIndex = 0;
        }
    }
}

In practice this means that you should program this check on OriginalSource not only in every TabControl (because its TabItems can contain ListBoxes and ComboBoxes), but also in every ListBox (because its template can contain ComboBoxes).

4 comments:

  1. Thank you, well described and very useful.

    ReplyDelete
  2. Thanks! I used "if(sender == e.OriginalSource)" instead, in order to recycle the method.

    ReplyDelete
  3. thanks, how to do that in xaml for mvvm?

    ReplyDelete