Hierarchical Grid with WPF

17 באפריל 2010

תגיות: , , ,
8 תגובות

A TreeView handles hierarchical data well, and a DataGrid handles tabular data. But what about hierarchical tabular data?

A simple example of such a source is a System.IO.DirectoryInfo. Each item can have zero or more children of the same type, enumerated by EnumerateDirectories.

I would like to have it displayed like this:

image

Solution

You can download the source code for this solution here.

The inspiration for this solution came from Delay's Blog by David Anson. I think David's idea of measuring the indentation at each level and compensating for it in the template for that level is clever indeed.

In this solution, I offer two improvements over the SimpleTreeGridUX presented in the article:

  1. Every level in the hierarchy uses the same template (a problem raised by David in the article).
  2. The number of levels may be arbitrary (not hardcoded in the XAML).

As in SimpleTreeGridUX, I use an empty TreeViewItem to measure the incremental indentation at every level. However, instead of adding a varying number of columns in the template of each level to compensate, I add one column whose width is bound to a function of the level.

The function is implemented in a MultiValueConverter called LevelConverter.

To demonstrate how it works, here is the solution with ShowGridLines=True for all grids:

image

As you can see, the width of the second column on each row depends on the level of that row in the tree.

To review the code, lets start with the DirectoryRecord which is my ViewModel representation of a DirectoryInfo.

Apart from some rather obvious code that extracts file and folder information from DirectoryInfo, I added two properties that are needed by the LevelConverter.

       #region Level

 

        // Returns the number of nodes in the longest path to a leaf

 

        public int Depth

        {

            get

            {

                int max;

                if (Items.Count == 0)

                    max = 0;

                else

                    max = (int)Items.Max(r => r.Depth);

                return max + 1;

            }

        }

 

        private DirectoryRecord parent;

 

        // Returns the maximum depth of all siblings

 

        public int Level

        {

            get

            {

                if (parent == null)

                    return Depth;

                return parent.Level-1;

            }

        }

 

        #endregion

The level converter uses the Level and the size of the measured TreeViewItem to return the Width of the column that compensates for the indentation. Here is the code:

    public class LevelConverter : DependencyObject, IMultiValueConverter

    {

        public object Convert(

            object[] values, Type targetType,

            object parameter, CultureInfo culture)

        {

            int level = (int)values[0];

            double indent = (double)values[1];

            return indent * level;

        }

 

        public object[] ConvertBack(

            object value, Type[] targetTypes,

            object parameter, CultureInfo culture)

        {

            throw new NotImplementedException();

        }

    }

Here is the XAML for the solution:

    <Grid Grid.IsSharedSizeScope="True"

         Name="treeGrid">

        <Grid.RowDefinitions>

            <!– Header row –>

            <RowDefinition Height="Auto" />

            <!– Row for data –>

            <RowDefinition />

        </Grid.RowDefinitions>

 

        <Grid.Resources>

 

            <!– Converts the level in the tree to the width of the spacer column –>

            <local:LevelConverter x:Key="levelConverter" />

 

            <!– Template for directory information at all levels –>

            <HierarchicalDataTemplate ItemsSource="{Binding Items}"

                                     DataType="{x:Type local:DirectoryRecord}">

                <Grid ShowGridLines="False">

 

                    <!– All column widths are shared except for column 1 which is sized

                         to compensate for different indentation at each level –>

                    <Grid.ColumnDefinitions>

                        <ColumnDefinition SharedSizeGroup="rowHeaderColumn"/>

                        <ColumnDefinition></ColumnDefinition>

                        <ColumnDefinition SharedSizeGroup="column1"/>

                        <ColumnDefinition SharedSizeGroup="column2"/>

                    </Grid.ColumnDefinitions>

                    <TextBlock Grid.Column="0"

                              Text="{Binding Info.Name}"></TextBlock>

                    <Rectangle Grid.Column="1">

                        <Rectangle.Width>

                            <MultiBinding Converter="{StaticResource levelConverter}">

                                <Binding Path="Level"></Binding>

                                <Binding ElementName="treeViewItemToMeasure"

                                        Path="ActualWidth"></Binding>

                            </MultiBinding>

                        </Rectangle.Width>

                    </Rectangle>

                    <TextBlock Grid.Column="2"

                              Text="{Binding Info.LastAccessTime}"></TextBlock>

                    <TextBlock Grid.Column="3"

                              Text="{Binding Files.Count}"></TextBlock>

                </Grid>

            </HierarchicalDataTemplate>

        </Grid.Resources>

 

        <!– Tree view with one item for the header row –>

 

        <TreeView BorderThickness="0"

                 ScrollViewer.HorizontalScrollBarVisibility="Disabled">

            <TreeViewItem>

                <TreeViewItem.Header>

                    <Grid ShowGridLines="False">

                        <Grid.ColumnDefinitions>

                            <ColumnDefinition SharedSizeGroup="rowHeaderColumn"/>

                            <ColumnDefinition />

                            <ColumnDefinition SharedSizeGroup="column1"/>

                            <ColumnDefinition SharedSizeGroup="column2"/>

                        </Grid.ColumnDefinitions>

                        <TextBlock Grid.Column="0"

                                  Text="Name"></TextBlock>

                        <TreeViewItem Grid.Column="1">

                            <TreeViewItem.Header>

                                <TreeViewItem Name="treeViewItemToMeasure"

                                             Padding="0"></TreeViewItem>

                            </TreeViewItem.Header>

 

                            <!– Set the width of Column 1 to the same width as the top level

                                 in the data –>

                            <TreeViewItem.Width>

                                <MultiBinding Converter="{StaticResource levelConverter}">

                                    <Binding Path="Level"></Binding>

                                    <Binding ElementName="treeViewItemToMeasure"

                                            Path="ActualWidth"></Binding>

                                </MultiBinding>

                            </TreeViewItem.Width>

                        </TreeViewItem>

                        <TextBlock Grid.Column="2"

                                  Text="LastAccessTime"></TextBlock>

                        <TextBlock Grid.Column="3"

                                  Text="File Count"></TextBlock>

                    </Grid>

                </TreeViewItem.Header>

            </TreeViewItem>

        </TreeView>

 

        <!– Tree view that will display hierarchical data rows –>

 

        <TreeView Grid.Row="1"

                 BorderThickness="0"

                 ItemsSource="{Binding}"></TreeView>

    </Grid>

Finally, this is the code that sets the data context for the mark up:

        void MainWindow_Loaded(object sender, RoutedEventArgs e)

        {

            string folder = @"..\..\";

            DirectoryRecord root = new DirectoryRecord(new DirectoryInfo(folder));

            treeGrid.DataContext = new DirectoryRecord[] { root };

        }

Enjoy!

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

8 תגובות

  1. Naresh Bhatia19 במאי 2010 ב 2:45

    Excellent post David! This is exactly what I was looking for. I do have couple of questions:

    1) For the header row, you calculate the width of column 1 by specifying Path=Level and ElementName="treeViewItemToMeasure". I don't understand how this sets the width to the same width as the top level in the data. Are both TreeView's bound to the same data somehow? Also, why do you have the nested TreeViewItem in the header row?

    2) In your use case the data context is set up at window load time. In my use case, the data context is not bound at window load time, but at a later time based on an event. This is causing my header row column 1 to be calculated with wrong information (at window load time) and is not being recalculated when I set the data context. Is there a way around this? Moreover, I am using MVVM, so the event processing is in the ViewModel, I don't have access to the TreeView to ask it to recalculate.

    Thanks again for this very helpful post.

    הגב
  2. Steve14 בספטמבר 2010 ב 21:26

    I like this control.
    One question:
    How do I get rid of the Yellow/Blue dashed line?

    Great job!

    הגב
  3. Marzio18 בספטמבר 2010 ב 15:19

    Hi,
    I tried to add GridSplitter on but they didn't work. Do you have any idea for properly add GridSplitter?

    PS: Great job, I found your post very helpful 😉 הגב

  4. Sutikshan dubey1 בדצמבר 2010 ב 17:46

    Hi,
    How to make subsequent columns , editable?

    הגב
  5. Alexandre10 בנובמבר 2011 ב 16:32

    Hi,

    I've used the this grid you defined above, defining an Info class of my own instead of using DirectoryInfo.
    I wanted to know if it is possible to add another column to this grid where I can add a Control (ComboBox, TextBox, DatePicker) that is defined in my info class.

    I've seen this article http://zamjad.wordpress.com/2010/02/03/apply-control-template-conditionally/ but I am having problems to apply it to your grid.

    If you can help it would be appreciated.

    Best Regards,

    Alexandre

    הגב
  6. james11 באפריל 2012 ב 11:23

    In your use case the data context is set up at window load time. In my use case, the data context is not bound at window load time, but at a later time based on an event.

    הגב
  7. seylom26 באפריל 2012 ב 20:51

    Great article!
    How would you make this code more generic though if you wanted to support variable number of columns?

    הגב