ItemControls don’t properly select items or raise events when bound to a data source


About a month ago, I saw two unrelated questions where an ItemsControl was not behaving correctly when clicking on an item: Events weren’t being raised, items weren’t getting selected; chaos ensued.  In both cases, the developer was adding an items container to the data template.  (As a refresher, ListBoxItem, ListViewItem, and TreeViewItem are examples of item containers.)  They were doing something like the following:


 


        <Grid>


            <Grid.Resources>


                <src:ItemsForSale x:Key=”Data”/>


            </Grid.Resources>


            <ListBox ItemsSource=”{StaticResource Data}”>


                <ListBox.ItemTemplate>


                  <DataTemplate>


                        <ListBoxItem Padding=”3,8,3,8″ MouseDoubleClick=”ListBoxItem_MouseDoubleClick” >


                            <TextBlock Text=”{Binding Path=Description}”/>


                        </ListBoxItem>


                    </DataTemplate>


 


                </ListBox.ItemTemplate>


 


            </ListBox>


        </Grid>


 


When the correct way to implement the ListBox is something like the following:


 


        <Grid>


            <Grid.Resources>


                <src:ItemsForSale x:Key=”Data”/>


            </Grid.Resources>


            <ListBox ItemsSource=”{StaticResource Data}”>


                <ListBox.ItemContainerStyle>


                    <Style  TargetType=”ListBoxItem”>


                        <Setter Property=”Padding” Value=”3,8,3,8″/>


                        <EventSetter Event=”MouseDoubleClick” Handler=”ListBoxItem_MouseDoubleClick”/>


                    </Style>


                </ListBox.ItemContainerStyle>


                <ListBox.ItemTemplate>


                    <DataTemplate>


                        <TextBlock Text=”{Binding Path=Description}”/>


                    </DataTemplate>


 


                </ListBox.ItemTemplate>


 


            </ListBox>


        </Grid>


 


In the second example, properties and events for item containers are set on a style, and the data template contains only elements that go in each item container.  You see, when the ItemsControl is bound to a data source, an items container is implicitly created for each item in the source, so in the first example above two ListBoxItem objects are made for each item: the one that is implicitly created and the one in the data template. In the second example, only the implicitly created ListBoxItem is present, and the Padding and event handler from the style is applied to each ListBoxItem.


 


For full disclosure, I must say that when I experimented with the first example, I got inconsistent results.  Sometimes I couldn’t select an item but I could get the double click event to occur and sometimes I could select the item but not raise the event.  So be forewarned: Don’t put item containers in data templates; only weird things happen if you do!


 


Comments (5)

  1. kenneth@kryger.name says:

    How would then go about implementing a data bound TabControl, where each TabItem has a combo-box, that is bound to a collection?

    E.g. a TabControl is bound a ObservableCollection<ClassA> where each ClassA-object has a OberservableCollection<ClassB> to which the ComboBox is bound?

    Regards,

    Kenneth

  2. kenneth@kryger.name says:

    Setting IsSynchronizedWithCurrentItem="True" on the ComboBox solved the problem… 🙂

  3. rob@finitesolutions.com says:

    This is a nice point and I’ll try to remember it and all, but if the arrangement of code in the "incorrect" example doesn’t actually work, then the compiler needs to reject the declaration of event handlers in a data template, at *least* with a warning that the programmer needs to use a setter instead.

    Any chance something like that is already in the SP1 Beta?

  4. wcsdkteam says:

    The issue isn’t that there’s an event handler in the DataTemplate, the issue is that there’s a ListBoxItem in the DataTemplate.  For example, something like this is entirely acceptable and works:

         <DataTemplate x:Key="ItemTemplate" >

           <StackPanel Orientation="Horizontal" >

             <TextBlock Text="Customer Name" Margin="5"/>

             <TextBox Width="100" Margin="5" Text="{Binding Name}"/>

             <Button Content="Save Customer" Click="saveCustomer_Click"/>

           </StackPanel>

         </DataTemplate>

        <ListBox Name="customerList"  ItemTemplate="{StaticResource ItemTemplate}"

                     ItemsSource="{Binding}"/>

    I don’t know if it’s really possible for the compiler to check for the presence of certain elements in a DataTemplate.  I’ll pass this along to the product team, though.

    Thanks,

    Carole

  5. wcsdkteam says:

    Regarding the compiler rejecting a ListBoxItem in a DataTemplate, one of the developerd on the product team had the following comment.

    Carole

    As far as the DataTemplate is concerned, ListBoxItem is a perfectly valid root element. It is within the context of being an ItemTemplate on an ItemsControl where the root element also satisfies IsItemItsOwnerContainer do we want to do something special. This is where it’s a little tricky. The ItemsControl technically does not expand the template, so it doesn’t really have the instance of the root of the template. Some ContentPresenter has it, but the ItemsControl doesn’t have a reference to it either. The same goes for the container, ListBoxItem. It doesn’t actually have a reference to its own ContentPresenter or root element of its ContentTemplate. Now, the ContentPresenter could go to its TemplatedParent, in this case ListBoxItem, and check if the root element of its template is of the same type. It could also try to do some checks to determine if the templated parent is a ContentControl based container generated from an ItemsControl. Once all that is done, and let’s assume it has determined that its root element is of the same type as the template parent, it could throw an exception or whatever we deemed appropriate (and no, this can’t be detected at compile time).

    So, to sum up:

    ·         Detectable at runtime, not compile time

    ·         Must be 100% sure that it’s invalid, perhaps there’s something else we could do

    ·         Not addressed in 3.5 SP1

    Ben