#1,217 – Using Multiple HierarchicalDataTemplates in a TreeView

I showed last time how to use a HierarchicalDataTemplate as the ItemTemplate for a TreeView. This allowed us to specify the look and feel of each node in the tree and also dictated how the main data item would be traversed to generate the tree.

If you have some hierarchical data in which the items are not all of the same type, you can specify more than one HierarchicalDataTemplate for the TreeView, based on the underlying type of each node.

Let’s assume that we have both a Breed and a Dog class. Breed objects contain information about a breed and in turn have a Dogs property that is a list of individual dogs of that breed.

In the example below, we have a TreeView whose ItemsSource is bound to a list of breeds. We then include a template to use for each Breed item and a different template to use for each Dog item in the tree. We also indicate that the tree should be expanded by looking at the Dogs property in each breed instance.

        <TreeView Grid.Row="0" Margin="5" ItemsSource="{Binding Breeds}">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type loc:Breed}" ItemsSource="{Binding Dogs}">
                    <StackPanel Orientation="Horizontal" Margin="0,5,0,0">
                        <TextBlock Text="Breed:"/>
                        <TextBlock Margin="5,0,0,0" FontWeight="Bold" Text="{Binding Name}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>

                <HierarchicalDataTemplate DataType="{x:Type loc:Dog}">
                    <StackPanel Orientation="Horizontal" Margin="0,2,0,0">
                        <TextBlock Text="{Binding}"/>
                    </StackPanel>
                </HierarchicalDataTemplate>
            </TreeView.Resources>
        </TreeView>

Note that we specify a DataType for each HierarchicalDataTemplate. As the tree is constructed, the appropriate template will be used, based on whether a node is a breed or a dog.

Below, we can see how this will look at runtime. Breed nodes show the breed’s name. Dog nodes show the dog’s name and age. We just have a TextBlock bound to the Dog instance as a whole, which causes the dog’s ToString method to get invoked. In our case, we’ve overridden ToString to display the dog’s name and age.

#1,216 – Creating a Custom ItemTemplate in a TreeView

If we just want each node in a TreeView control to contain text, we can use a single TreeViewItem in the HierarchicalDataTemplate and bind its Header property to the text that we want displayed. However, we can use an Panel we like in the data template.

The example below builds on our last post, but using a Grid in a Border for each person in the tree.

<TreeView Grid.Row="0" Margin="5" ItemsSource="{Binding Royal}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <Border Background="AliceBlue" Margin="0,2,0,0" Padding="4,2,8,2" 
                    BorderBrush="DimGray" BorderThickness="1"
                    CornerRadius="3">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>

                    <TextBlock Grid.Row="0" Grid.Column="0" FontWeight="Bold"
                               Text="{Binding Name}"/>
                    <TextBlock Grid.Row="1" Grid.Column="0" FontStyle="Italic"
                               Text="{Binding BirthDeath}" Margin="5,0,0,0"/>
                    <Image Grid.RowSpan="2" Grid.Column="2"
                           Height="20" Width="20" Margin="5,0"
                           Source="Crown.png"
                           Visibility="{Binding KingOrQueen, Converter={StaticResource BooleanToVisibilityConverter}}"/>
                </Grid>
            </Border>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

Here’s what the final result looks like (after I expand a few of the TreeView nodes:

#1,215 – Binding a TreeView to a Hierarchical Data Source

Below is a very simple example of how you might use a TreeView control to display a set of hierarchical data.

Let’s say that we have a Person object that looks like the following. (INPCBase is just a class that implements INotifyPropertyChanged and includes a SetField method).

    public class Person : INPCBase
    {
        public Person(string name, int birth, int? death)
        {
            Name = name;
            Birth = birth;
            Death = death;

            Children = new List<Person>();
        }

        private string _name;
        public string Name
        {
            get { return _name; }
            set { SetField(ref _name, value); }
        }

        private int _birth;
        public int Birth
        {
            get { return _birth; }
            set { SetField(ref _birth, value); }
        }

        private int? _death;
        public int? Death
        {
            get { return _death; }
            set { SetField(ref _death, value); }
        }

        private List<Person> _children;
        public List<Person> Children
        {
            get { return _children; }
            private set { SetField(ref _children, value); }
        }

        public override string ToString()
        {
            string death = Death.HasValue ? Death.Value.ToString() : "";
            return string.Format($"{Name} ({Birth} - {death})");
        }
    }

Notice that we have a Children property that in turn contains other Person objects. Also note that we override ToString so that the default string representation for a Person includes their name and birth/death.

Now let’s assume that we have some ViewModel with a property called Royal that is a list of Person objects. The property might look like:

        private List<Person> _royal;
        public List<Person> Royal
        {
            get { return _royal; }
            set { SetField(ref _royal, value); }
        }

Let’s assume that we initialize this list as follows:

            var p1 = new Person("George V", 1865, 1936);

            var p2 = new Person("Edward VIII", 1894, 1974);
            var p3 = new Person("George VI", 1895, 1952);
            var p4 = new Person("Mary", 1897, 1965);
            var p5 = new Person("Henry", 1900, 1974);
            var p6 = new Person("George", 1902, 1942);
            var p7 = new Person("John", 1905, 1919);

            var p8 = new Person("Elizabeth II", 1926, null);
            var p9 = new Person("Margaret", 1865, 1936);

            var p10 = new Person("Richard", 1944, null);

            var p11 = new Person("Edward", 1935, null);
            var p12 = new Person("Michael", 1942, null);

            var p13 = new Person("Charles", 1948, null);
            var p14 = new Person("Anne", 1950, null);
            var p15 = new Person("Andrew", 1960, null);
            var p16 = new Person("Edward", 1964, null);

            p1.Children.AddRange(new [] { p2, p3, p4, p5, p6, p7});
            p3.Children.AddRange(new[] { p8, p9 });
            p5.Children.Add(p10);
            p6.Children.AddRange(new[] { p11, p12 });
            p8.Children.AddRange(new[] { p13, p14, p15, p16 });

            Royal = new List<Person>() { p1 };

To wire this hierarchical data source up to a TreeView control, we use a HierarchicalDataTemplate in XAML, as shown below.

        <TreeView Grid.Row="0" Margin="5" ItemsSource="{Binding Royal}">
            <TreeView.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                    <TreeViewItem Header="{Binding}"/>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>

There are several things to note about this XAML fragment:

  • We set the ItemsSource of the TreeView to our top-level property (a list of Person, which contains one person).
  • We tell the HierarchicalDataTemplate to use the Children property to traverse the hierarchy
  • We indicate that each node should be a TreeViewItem object, with text (Header) that just binds to the Person object. This causes the default ToString method of the object to be used for the text

The final result is a nice little family tree:

1215-01

#1,203 – Create a Reusable Group of Elements with a ContentPresenter

There are several ways to achieve reuse in WPF. You can create a reusable control by creating shared data and control templates for a single type of control. You can also create custom controls, providing custom code and layout.

One simple way to reuse a group of controls is by using a ContentPresenter with a reusable ContentTemplate.

In the example below, we have a small grid displaying a person’s name and address. To use this set of controls in multiple places in an application, we place them in a DataTemplate and then use a ContentPresenter wherever we want the set of controls to appear.

The DataTemplate is defined in the resources section for a window. The main body of the Window includes two instances of the set of controls. Note that we need to set the Content of each ContentPresenter to the bound data context of the window. Not shown here, but assumed, is that our Window is bound to an object having MyName and Address properties.

    <Window.Resources>
        <DataTemplate x:Key="MyGridContentTemplate">
            <Grid Margin="8">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>

                <TextBlock Text="Your name is:"/>
                <TextBlock Grid.Column="1" Margin="8,0,0,0"
                           FontWeight="Bold"
                           Text="{Binding MyName}"/>

                <TextBlock Grid.Row="1" Text="Your address is:"/>
                <TextBlock Grid.Row="1" Grid.Column="1" Margin="8,0,0,0"
                           FontWeight="Bold"
                           Text="{Binding Address}"/>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <ContentPresenter Grid.Row="0" ContentTemplate="{StaticResource MyGridContentTemplate}" Content="{Binding}"/>
        
        <Separator Grid.Row="1"/>

        <ContentPresenter Grid.Row="2" ContentTemplate="{StaticResource MyGridContentTemplate}" Content="{Binding}"/>
    </Grid>

Here’s what the application looks like at run-time:

#1,198 – Selectively Enabling Child Elements in a Disabled Panel

When you set IsEnabled to false in a panel, all child elements in that panel are disabled. You cannot selectively enabled child elements in the panel.

You may, however, want to selectively enable child elements in a panel. (E.g. Disable entire panel, then set IsEnabled=True, IsReadOnly=True on TextBox controls so that you can copy text).

One possible solution is to define a new control that inherits from TextBox and does not coerce the value of IsEnabled.

    public class CanEnableTextBox : TextBox
    {
        static CanEnableTextBox()
        {
            CanEnableTextBox.IsEnabledProperty.OverrideMetadata(typeof(CanEnableTextBox),
                new System.Windows.UIPropertyMetadata(true,
                    new PropertyChangedCallback(IsEnabledPropertyChanged),
                    new CoerceValueCallback(CoerceIsEnabled)));

        }

        private static void IsEnabledPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
        {
            // Overriding PropertyChanged results in merged metadata, which is what we want--
            // the PropertyChanged logic in UIElement.IsEnabled will still get invoked.
        }

        private static object CoerceIsEnabled(DependencyObject source, object value)
        {
            return value;
        }
    }

You can now use this control in a panel that has IsEnabled set to false and you’ll be able to set IsEnabled on the child TextBox.

#1,194 – DesiredSize of Child Elements Includes Margins

After the measure phase, during which a custom element calls the Measure method on each of its child elements, each child element will set its DesiredSize property to indicate how much space it wants.

The DesiredSize of a child element accounts for any margins that have been set on that child.  Assume that we have the following XAML (MyElement is a custom element with a single child).

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <loc:MyElement x:Name="Left">
            <loc:MyElement.Child>
                <Label Content="Doowahditty"/>
            </loc:MyElement.Child>
        </loc:MyElement>

        <loc:MyElement Grid.Column="1" x:Name="Right">
            <loc:MyElement.Child>
                <Label Content="Doowahditty" Margin="10"/>
            </loc:MyElement.Child>
        </loc:MyElement>
    </Grid>

We can see that the child element uses a margin.
1194-001

We can look at the value of the DesiredSize property after Measure has been called on the child element.  We can see that in the second case, the desired size is larger, indicating that the child wants to add a margin.

1194-002

 

 

#1,193 – MeasureOverride and Margins

During the measure phase, the MeasureOverride method is called on an element, indicating the size available to the control.  If a Margin has been set on the control, the available size passed in to MeasureOverride will have already been adjusted for that margin.

Below, we include two instances of MyElement in a Grid, setting a margin of 15 on the second element.

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <loc:MyElement/>
        <loc:MyElement Grid.Column="1" Margin="15"/>
    </Grid>

At run-time, we see that the second element is smaller.
1193-001
If we instrument MyElement to report the value of the Size parameter that is passed to it, we see that MeasureOverride gets a smaller size passed in for the second instance.  The Size has been adjusted for a uniform margin of 15, subtracting 30 from both the width and the height.  (We see the same values passed in to ArrangeOverride).

1193-002