Avalon Simple Tree View, Part 2

In my last post, I walked through creating a simple hierarchy using styles. Now, let's bring it to life. We want to add expand/collapse to our TreeViewItem (HeaderedItemsControl), but IsExpanded is not a property on HIC. We need to add one.

Let's go into the Window1.xaml.cs file and add two classes as peers to Window1. TreeView, which inherits from ItemsControl and TreeViewItem, which inherits from HeaderedItemsControl.

public partial class Window1 : Window { ... } 

public class TreeView : ItemsControl { } 

public class TreeViewItem : HeaderedItemsControl
{

}

Now things get fun. To enable expand/collapse we need to add a property to TreeViewItem...but not just a regular CLR property. To enable data binding and animation in Avalon, we need to create a DependencyProperty (DP). DPs are public, static, readonly. All DPs should follow the naming pattern [Property Name]Property. You register it with DependencyProperty.Register (clever, huh?). The first parameter is the name of the DP, in this case "IsExpanded". The second parameter is the type of the property, bool. The third is the owning class, TreeViewItem. And the fourth is FrameworkPropertyMetadata. The simplest override for this class just takes the default value of the property. In this case it's false.

public class TreeViewItem : HeaderedItemsControl
{
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.Register(
"IsExpanded",
typeof(bool),
typeof(TreeViewItem),
new FrameworkPropertyMetadata(false)); 

      public bool IsExpanded
{
get { return (bool)GetValue(TreeViewItem.IsExpandedProperty); }
set { SetValue(TreeViewItem.IsExpandedProperty, value); }
}
}

Notice that we also create a regular CLR property for IsExpanded. While strictly speaking this is not required for XAML, it's a good idea for people who will use this control from code. Since the property engine in Avalon stores the value for us, we need to call a couple of functions to set/get the value.

Now back to the XAML.

We want to refer to our classes in XAML, but they are not part of the default control set in Avalon. We need to add an XML mapping and an associated XML namespace. The mapping goes at the top of the file. Notice it refers to the CLR namespace of the project "AvalonApplication1". We also define the XML namespace to map to, in this case "app". We also need to define another mapping that we will use in our controls. This goes in the Window element.

<?Mapping XmlNamespace="app" ClrNamespace="AvalonApplication1" ?>
<Window
      x:Class="AvalonApplication1.Window1"
      xmlns="https://schemas.microsoft.com/winfx/avalon/2005"
      xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"
      Text="TreeView"
      xmlns:tv="app">

Now we can refer to our new controls with the tv: prefix. Let's change our ItemsControl to tv:TreeView and our HeaderedItemsControls to tv:TreeViewItem. Make sure to change the references in the styles, too.

What's our game plan? We'd like to expand/collapse the child nodes based upon the IsExpanded property. We'd like to bind the IsExpanded property to the IsChecked property of the CheckBox. We'd also like to show/hide the checkbox based upon the existence of children.

Starting with the CheckBox. First we bind the IsChecked property of the CheckBox to the IsExpanded property of the StyledParent. (The StyledParent in this case is the TreeViewItem.) Next we set the Visibility to Hidden.  Finally we give the CheckBox a StyleID so we can change the Visibility from a trigger.

<CheckBox Grid.Column="0" Grid.Row="0"
      IsChecked = "*Bind(Path=IsExpanded;RelativeSource=/StyledParent)"
      Visibility="Hidden"
      x:StyleID="ChildrenToggle"/>

The StackPanel is similar: Set the Visibility and give it a StyleID. Notice that we use Collapsed for the Visibility. This tells layout to not only not draw it, but also to not give it any space. Make sense?

<StackPanel  Grid.Column="1" Grid.Row="1"
            IsItemsHost="True"
            Visibility="Collapsed"
            x:StyleID="ExpandSite"/>

Add a VisualTriggers section to the Style element. Our first PropertyTrigger will control the visibility of the CheckBox. TreeViewItem is an HeaderedItemsControl. HeaderedItemsControl has a bool property HasItems. We want to change the Visibility of ChildrenToggle (our CheckBox) to Visible when HasItems = True. Similarly, we want to make ExpandSite (our StackPanel with the child elements) visible when IsExpanded (our sole new property) is true.

<Style.VisualTriggers>
<PropertyTrigger Property="HasItems" Value="True">
            <Set PropertyPath="Visibility" Value="Visible" Target="ChildrenToggle"/>
</PropertyTrigger>
<PropertyTrigger Property="IsExpanded" Value="True">
<Set PropertyPath="Visibility" Value="Visible" Target="ExpandSite" />
</PropertyTrigger>
</Style.VisualTriggers>

The effect is pretty cool. DataBinding pushes the value from the CheckBox to our new DependencyProperty. The DP pushes it's value to the ExpandSite and we have a functioning control.

Just to show that the added property can be set in XAML, I've set IsExpanded="True" on a couple of the TreeViewItems. Update: earlier I said that creating a CLR property for the new DP was not strictly required. Actually, if you want to be able to set the property in markup, you have to create a standard get/set. FYI.

<tv:TreeView Width="200" Height="200">
<tv:TreeViewItem Header="Iowa">
<tv:TreeViewItem Header="Okoboji"/>
<tv:TreeViewItem Header="Milford"/>
<tv:TreeViewItem Header="Des Moines"/>
</tv:TreeViewItem>
<tv:TreeViewItem Header="Washington" IsExpanded="True">
<tv:TreeViewItem Header="Seattle" IsExpanded="True">
<tv:TreeViewItem Header="Brasa"/>
<tv:TreeViewItem Header="HaNa"/>
<tv:TreeViewItem Header="Mama's"/>
</tv:TreeViewItem>
<tv:TreeViewItem Header="Redmond"/>
</tv:TreeViewItem>
<tv:TreeViewItem Header="California">
</tv:TreeViewItem>
</tv:TreeView>