WPF ListView with Check Boxes and No Clipping

     

    Do you need a simple data-bound WPF ListView with check boxed items without text trimming ellipsis and without header columns? I very often do. In this blog, I would like to share with you a simple way to build this control that I call CheckBoxListView.

     

    What We Want

    • Data-bound WPF ListView with a check box at the beginning of each item
    • No column header
    • Item text not clipped and no text-trimming ellipsis
    • Horizontal scrollbar to automatically sized to show the widest item

     

     image

     

    image

     

     

    What We Don't Want

     

     image

     

     

    What We Need to Do

    • Add the Check Boxes
      • To add the check box to each list view item, we must first override the ListView.View to be a GridView and create two GridViewColumns: one for the check box and the other for the item text.
      • Here the XAML code for our ListView:

     

            <ListView Name="myListView">

                <ListView.View>

                    <GridView>

                        <GridView.ColumnHeaderContainerStyle>

                            <Style TargetType="{x:Type GridViewColumnHeader}">

                                <Setter Property="Visibility"

                                        Value="Collapsed"/>

                            </Style>

                        </GridView.ColumnHeaderContainerStyle>

                        <GridViewColumn>

                            <GridViewColumn.CellTemplate>

                                <DataTemplate>

                                    <CheckBox Margin="0"

                                              VerticalAlignment="Center"

                                              IsChecked="{Binding IsChecked}"/>

                                </DataTemplate>

                            </GridViewColumn.CellTemplate>

                        </GridViewColumn>

                        <GridViewColumn>

                            <GridViewColumn.CellTemplate>

                                <DataTemplate>

                                    <TextBlock Margin="0"

                                               Text="{Binding Text}"

                                               Loaded="TextBlock_Loaded"/>

                                </DataTemplate>

                            </GridViewColumn.CellTemplate>

                        </GridViewColumn>

                    </GridView>

                </ListView.View>

            </ListView>

     

    • Hide the Column Headers
      • To hide the column headers, we need to collapse the GridViewColumnHeader.

     

            <ListView Name="myListView">

                <ListView.View>

                    <GridView>

                        <GridView.ColumnHeaderContainerStyle>

                            <Style TargetType="{x:Type GridViewColumnHeader}">

                                <Setter Property="Visibility"

                                        Value="Collapsed"/>

                            </Style>

                        </GridView.ColumnHeaderContainerStyle>

                        <GridViewColumn>

                            ...

                        </GridViewColumn>

                        <GridViewColumn>

                            ...

                        </GridViewColumn>

                    </GridView>

                </ListView.View>

            </ListView>

     

    • Resize the Column Header to the Widest Item
      • In the code-behind, implement the TextBlock Loaded event handler. In this handler, measure the text and resize the (invisible) header column to the widest rendered item. If the widest item is wider than the available width, the list view automatically shows a horizontal scrollbar
      • Since we hook the TextBlock Loaded event, only rendered ListViewItems will be called. This is an important optimization! So if you a have list of 100,000 items and then you grab the scrollbar thumb and quickly scroll from top to bottom, only a small subset of the items is rendered and consequently measured - Not all 100,000 items! As more items come into view, the header column will be resized to the widest item to appropriately show it.

     

            private void TextBlock_Loaded(object sender, RoutedEventArgs e)

            {

                GridView gridView = myListView.View as GridView;

                if (gridView != null && gridView.Columns.Count >= 2)

                {

                    // Calculate the item's desired text width and increase the

                    // text column's width to match the widest text

                    TextBlock tb = (TextBlock)sender;

                    tb.Measure(new Size(Double.MaxValue, Double.MaxValue));

                    double newWidth = tb.DesiredSize.Width;

                    GridViewColumnCollection columns = gridView.Columns;

                    if (newWidth > columns[1].Width ||

                        double.IsNaN(columns[1].Width))

                    {

                        columns[1].Width = newWidth;

                    }

                }

            }

     

    • Remove the Item Cell Padding for Good Looks
      • The cell content presenter has a default margin of about 6 units. Removing this margin makes the gap between the check box and the text look "perfect" on Vista.

     

            private void TextBlock_Loaded(object sender, RoutedEventArgs e)

            {

                GridView gridView = myListView.View as GridView;

                if (gridView != null && gridView.Columns.Count >= 2)

                {

                    ...

     

                    // Remove the text block cell's content presenter built-in

                    // margin for better-looking spacing

                    ContentPresenter contentPresenter = VisualTreeHelper.GetParent(tb) as ContentPresenter;

                    if (contentPresenter != null)

                    {

                        contentPresenter.Margin = new Thickness(0);

                    }

                }

            }

     

    Hey, What About Data-Binding?

    Even though there are a lot of resources on this topic, here is one way that you can encapsulate an item data source and bind the sources to the List View.

     

    First define a class to hold the data for each list view item:

     

        /// <summary>

        /// Encapsulate item data source for a CheckBoxListView item

        /// </summary>

        public class CheckBoxListViewItemSource : INotifyPropertyChanged

        {

            public CheckBoxListViewItemSource(String text)

            {

                m_text = text;

            }

     

            public bool IsChecked

            {

                get { return m_checked; }

                set

                {

                    if (m_checked == value) return;

                    m_checked = value;

                    RaisePropertyChanged("Checked");

                }

            }

     

            public String Text

            {

                get { return m_text; }

                set

                {

                    if (m_text == value) return;

                    m_text = value;

                    RaisePropertyChanged("Text");

                }

            }

     

            public override string ToString()

            {

                return Text;

            }

     

            public event PropertyChangedEventHandler PropertyChanged;

     

            private void RaisePropertyChanged(string propName)

            {

                PropertyChangedEventHandler eh = PropertyChanged;

                if (eh != null)

                {

                    eh(this, new PropertyChangedEventArgs(propName));

                }

            }

     

            private bool m_checked;

            private String m_text;

        }

     

    Then create a list to hold those item data sources:

    In the following sample, I created this list in the constructor of the application window.

     

            public Window1()

            {

                InitializeComponent();

     

                // Fill the list via data-bound item sources

                ObservableCollection<CheckBoxListViewItemSource> itemSources = new ObservableCollection<CheckBoxListViewItemSource>();

                myListView.ItemsSource = itemSources;

                int ii = 1;

                for (int i = 0; i < 10; i++)

                {

                    itemSources.Add(new CheckBoxListViewItemSource(String.Format("Short Item {0}", ii++)));

                }

                for (int i = 0; i < 5; i++)

                {

                    itemSources.Add(new CheckBoxListViewItemSource(String.Format("Long Item {0} with long long long long long long long long long text", ii++)));

                }

            }

     

    Then bind that list to the List View:

     

            public Window1()

            {

                InitializeComponent();

     

                // Fill the list via data-bound item sources

                ObservableCollection<CheckBoxListViewItemSource> itemSources = new ObservableCollection<CheckBoxListViewItemSource>();

                myListView.ItemsSource = itemSources;

                int ii = 1;

                for (int i = 0; i < 10; i++)

                {

                    itemSources.Add(new CheckBoxListViewItemSource(String.Format("Short Item {0}", ii++)));

                }

                for (int i = 0; i < 5; i++)

                {

                    itemSources.Add(new CheckBoxListViewItemSource(String.Format("Long Item {0} with long long long long long long long long long text", ii++)));

                }

            }

     

     

     

     

    That's it for now. Next time, we'll look at a way to set focus to a list view item.

     

    -Tan