Sunday, December 29, 2013

Toggling DataGrid Details

WPF version 4.5

The three default ways of displaying details for a DataGrid are VisibleWhenSelected, Visible, and Collapsed. None of these options are acceptable to our users. They want to be able to toggle a control on a grid row (ideally a [+]/[-] image) and toggle the details of the row on or off. They want to be able to have as many details open as they want. One detail at a time is not desirable. This blog introduces detail templates, row headers, and toggle buttons to achieve this.

A row header is the perfect place to put a toggle button. We could trap the click event on the button and toggle the detail visibility in code, but it's easier to bind the IsChecked property of the toggle button to it's containing row's detail visibility property using ancestor binding. Unfortunately the IsChecked property is a nullable Boolean (bool?) and the Visibility property uses the Visibility enum so we will need a simple converter to achieve the binding.

I bound the datagrid to the Products table of the AdventureWorks sample database for SQL Server 2012 in code. The datagrid shows the product name and number. The row detail shows the reorder point.

Let's start with the converter. Don't forget to compile the project as soon as you have written the converter and before you reference it in the XAML. Add a class called converters to your WPF application project. Let's assume the project is called WpfApplication8. I think the code is self-explanatory

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Globalization;
using System.Windows;

namespace WpfApplication8
{
    public class VisibilityToNullableBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is Visibility)
            {
                return (((Visibility)value) == Visibility.Visible);
            }
            else
            {
                return Binding.DoNothing;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is  bool?)
            {
                return (((bool?)value) == true ? Visibility.Visible : Visibility.Collapsed);
            }
            else if (value is bool)
            {
                return (((bool?)value) == true ? Visibility.Visible : Visibility.Collapsed);
            }
            else
            {
                return Binding.DoNothing;
            }
        }
    }
}

Now that we have our converter we can reference it in the XAML. Note the xmlns:local and the local:VisibilityToNullableBooleanConverter. Now we can reference the converter. The RowHeaderStyle causes the toggle button to be displayed at the top of the row, rather than in the middle.

We set  RowDetailsVisibilityMode to collapsed so that the details will not be displayed when a row is selected - only when the toggle button is checked.

<x:Class="WpfApplication8.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication8"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:VisibilityToNullableBooleanConverter x:Key="VisibilityToNullableBooleanConverter"/>
        <Style x:Key="RowHeaderStyle" TargetType="{x:Type DataGridRowHeader}">
            <Setter Property="VerticalAlignment" Value="Top"/>
            <Setter Property="Height" Value="25"/>
        </Style>
    </Window.Resources>
    <DataGrid Name="dg" AutoGenerateColumns="False" RowDetailsVisibilityMode="Collapsed" RowHeaderStyle="{StaticResource RowHeaderStyle}">
        <DataGrid.RowDetailsTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Reorder Point:" Margin="10,0,10,0"/>
                    <TextBlock Text="{Binding ReorderPoint}"/>
                </StackPanel>
            </DataTemplate>
        </DataGrid.RowDetailsTemplate>
        <DataGrid.RowHeaderTemplate>
            <DataTemplate>
                <ToggleButton  Width="15" Height="15" IsChecked="{Binding Path=DetailsVisibility, RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}}, Converter={StaticResource VisibilityToNullableBooleanConverter}}"/>
            </DataTemplate>
        </DataGrid.RowHeaderTemplate>
            <DataGrid.Columns>
            <DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
            <DataGridTextColumn Header="Product Number" Binding="{Binding ProductNumber}"/>
        </DataGrid.Columns>
    </DataGrid>
</Window>

The last thing we need is a little code-behind to fill a datatable and bind the grid to its default view.

using System.Data.SqlClient;

namespace WpfApplication8
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            string sConn = "Server=(local); Database=AdventureWorks2012;Trusted_Connection=true";
            SqlConnection conn = new SqlConnection(sConn);
            string sSQL = "SELECT Name, ProductNumber, ReorderPoint FROM [Production].[Product] ORDER BY Name";
            SqlCommand comm;
            SqlDataReader DR;
            System.Data.DataTable DT = new System.Data.DataTable();

            conn.Open();
            comm = new SqlCommand(sSQL, conn);
            DR = comm.ExecuteReader();
            DT.Load(DR);
            dg.ItemsSource = DT.DefaultView;
            DR.Close();
            conn.Close();
        }
    }
}

No comments:

Post a Comment