Accessing controls in a WPF ItemTemplate

Programmatically accessing a control in an ItemTemplate is not so straightforward. We clearly need to call the FindName method, but not before we first find the receiver of that call...

Let's say we have a listbox databound to a list of Book instances. The Book class looks like this:

namespace DockOfTheBay
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
 
    /// <summary>
    /// Sample book class.
    /// </summary>
    public class Book
    {
        /// <summary>
        /// Gets or sets the title of the book.
        /// </summary>
        public string Title { get; set; }
 
        /// <summary>
        /// Get or sets a value indicating whether the book is still on sale.
        /// </summary>
        public bool Discontinued { get; set; }
    }
}

Books are displayed via an itemtemplate that contains a checkbox bound to the Discontinued property. For one or another reason, we want to programmatically enable or disable that checkbox. For demo purposes, let's implement a button that toggles the IsEnabled property of all checkboxes.

This is the XAML for the demo form:

<Window x:Class="DockOfTheBay.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DockOfTheBay"
        Title="ItemTemplate Access" 
        Height="160" Width="200" 
        ResizeMode="NoResize">
 
    <Window.Resources>
        <DataTemplate x:Key="BookTemplate" DataType="{x:Type local:Book}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="auto"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="auto" SharedSizeGroup="Title" />
                    <ColumnDefinition Width="auto" SharedSizeGroup="Discontinued" />
                </Grid.ColumnDefinitions>
                <TextBlock Text="{Binding Path=Title}" 
                           Grid.Row="0" Grid.Column="0" Margin="2"/>
                <CheckBox x:Name="DiscontinuedCheckBox"
                          IsChecked="{Binding Discontinued}" 
                          IsEnabled="False" 
                          Grid.Row="0" Grid.Column="1"
                          Margin="2"/>
            </Grid>
        </DataTemplate>
    </Window.Resources>
 
    <Grid Grid.IsSharedSizeScope="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ListBox x:Name="BookListBox"
                 ItemTemplate="{StaticResource BookTemplate}" 
                 Grid.Row="0" Grid.Column="0" 
                 HorizontalAlignment="Center" 
                 Margin="4" BorderThickness="0" />
        <Button x:Name="ToggleButton" 
                Click="ToggleButton_Click" 
                Content="Toggle Checkbox Access"
                Grid.Row="1" Grid.Column="0" 
                HorizontalAlignment="Center"
                Margin="4" />
    </Grid>
</Window>


The form looks like this:


Let's implement the button's event handler. We have to iterate through the listbox's items. Thanks to data binding, these items are all Book -not ListBoxItem- instances. We gain access to the surrounding user interface element through a ItemContainerGenerator.ContainerFromItem call. The Content property of that listBoxItem refers back to the Book instance, so in order to access the child controls we have to follow another path: that of its ContentPresenter. We find it by walking through the VisualTree, looking for an instance of the appropriate type. The next extension method can be very useful here:

namespace DockOfTheBay
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows;
    using System.Windows.Media;
 
    /// <summary>
    /// Extension methods to the DependencyObject class.
    /// </summary>
    public static class DependencyObjectExtensions
    {
        /// <summary>
        /// Find a child of a specific type in the Visual Tree.
        /// </summary>
        public static T FindVisualChild<T>(this DependencyObject obj) where T : DependencyObject
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                if (child != null && child is T)
                {
                    return (T)child;
                }
                else
                {
                    T grandChild = child.FindVisualChild<T>();
                    if (grandChild != null)
                    {
                        return grandChild;
                    }
                }
            }
 
            return null;
        }
    }
}

Eventually, the ContentTemplate of that presenter is the DataTemplate instance to which we'll send the FindName call. Here's the C# for the entire form:

namespace DockOfTheBay
{
    using System.Collections.Generic;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
 
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        /// <summary>
        /// Initializes a new instance of the Window1 class.
        /// </summary>
        public Window1()
        {
            InitializeComponent();
            this.FillListBox();
        }
 
        /// <summary>
        /// Fills the listbox with some sample books.
        /// </summary>
        private void FillListBox()
        {
            List<Book> books = new List<Book>();
            books.Add(new Book { Title = "WPF Unleashed ", Discontinued = false });
            books.Add(new Book { Title = "WinForms in action", Discontinued = true });
            books.Add(new Book { Title = "The iBook for Dummies", Discontinued = false });
            books.Add(new Book { Title = "Essential WPF", Discontinued = false });
            this.BookListBox.ItemsSource = books;
        }
 
        /// <summary>
        /// Toggle the access to the CheckBox.
        /// </summary>
        /// <param name="sender">Sender of the event (button).</param>
        /// <param name="e">Event arguments.</param>
        private void ToggleButton_Click(object sender, RoutedEventArgs e)
        {
            // Iterate through Books
            foreach (var item in this.BookListBox.Items)
            {
                // Get the ListBoxItem around the Book
                ListBoxItem listBoxItem =
                    this.BookListBox.ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem;
 
                // Get the ContentPresenter
                ContentPresenter presenter = listBoxItem.FindVisualChild<ContentPresenter>();
 
                // Get the Template instance
                DataTemplate template = presenter.ContentTemplate;
 
                // Find the CheckBox within the Template
                CheckBox checkBox = template.FindName("DiscontinuedCheckBox", presenter) as CheckBox;
                checkBox.IsEnabled = !checkBox.IsEnabled;
            }
        }
    }
}

2 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Great! Love this solution, thank you very much.

    ReplyDelete