Edit this page

Bind RadTreeView to Self-Referencing Data

This tutorial will show you how to display in a RadTreeView flat, self-referencing data, loaded from a database, that has properties ID and ParentID (or similar) which define the hierarchy.

Consider the following very simple data object:

public class DataItem
{
    public int Id
    {
        get;
        set;
    }
    public int ParentId
    {
        get;
        set;
    }
    public string Text
    {
        get;
        set;
    }
    public DataItemCollection Owner
    {
        get;
        protected set;
    }
    internal void SetOwner( DataItemCollection collection )
    {
        this.Owner = collection;
    }
}
Public Class DataItem
Private _Id As Integer
    Public Property Id() As Integer
        Get
            Return _Id
        End Get
        Set(ByVal value As Integer)
            _Id = value
        End Set
        End Property

        Private _ParentId As Integer
        Public Property ParentId() As Integer
            Get
                Return _ParentId
            End Get
            Set(ByVal value As Integer)
                _ParentId = value
            End Set
        End Property

        Private _Text As String
        Public Property Text() As String
            Get
                Return _Text
            End Get
            Set(ByVal value As String)
                _Text = value
            End Set
        End Property

        Private _Owner As DataItemCollection
        Public Property Owner() As DataItemCollection
            Get
                Return _Owner
            End Get
            Protected Set(ByVal value As DataItemCollection)
                _Owner = value
            End Set
        End Property

        Friend Sub SetOwner(ByVal collection As DataItemCollection)
            Me.Owner = collection
        End Sub
    End Class

Those data objects are added into a special DataItemCollection class, that inherits ObservableCollection and overrides SetItem(), InsertItem(), RemoveItem() and ClearItems() methods. In each override we call AdoptItem() and DiscardItem(), respectively, which set the Owner property of the DataItem class:

public class DataItemCollection : ObservableCollection<DataItem>
{
    protected override void InsertItem( int index, DataItem item )
    {
        this.AdoptItem( item );
        base.InsertItem( index, item );
    }
    protected override void RemoveItem( int index )
    {
        this.DiscardItem( this[ index ] );
        base.RemoveItem( index );
    }
    protected override void SetItem( int index, DataItem item )
    {
        this.AdoptItem( item );
        base.SetItem( index, item );
    }
    protected override void ClearItems()
    {
        foreach ( DataItem item in this )
        {
            this.DiscardItem( item );
        }
        base.ClearItems();
    }
    private void AdoptItem( DataItem item )
    {
        item.SetOwner( this );
    }
    private void DiscardItem( DataItem item )
    {
        item.SetOwner( null );
    }
}
    Public Class DataItemCollection
        Inherits ObservableCollection(Of DataItem)
        Protected Overloads Overrides Sub InsertItem(ByVal index As Integer, ByVal item As DataItem)
            Me.AdoptItem(item)
            MyBase.InsertItem(index, item)
        End Sub

        Protected Overloads Overrides Sub RemoveItem(ByVal index As Integer)
            Me.DiscardItem(Me(index))
            MyBase.RemoveItem(index)
        End Sub

        Protected Overloads Overrides Sub SetItem(ByVal index As Integer, ByVal item As DataItem)
            Me.AdoptItem(item)
            MyBase.SetItem(index, item)
        End Sub

        Protected Overloads Overrides Sub ClearItems()
            For Each item As DataItem In Me
                Me.DiscardItem(item)
            Next
            MyBase.ClearItems()
        End Sub

        Private Sub AdoptItem(ByVal item As DataItem)
            item.SetOwner(Me)
        End Sub

        Private Sub DiscardItem(ByVal item As DataItem)
            item.SetOwner(Nothing)
        End Sub
    End Class

Normally when you load your data objects from a service in your application, you will have auto-generated partial classes, that are relatively easy to extend.

Now we are ready to data-bind our RadTreeView:

<example:HierarchyConverter x:Key="HierarchyConverter" />

<telerik:HierarchicalDataTemplate x:Key="ItemTemplate"
  ItemsSource="{Binding Converter={StaticResource HierarchyConverter}}">
    <TextBlock Text="{Binding Text}" />
</telerik:HierarchicalDataTemplate>

<telerik:RadTreeView x:Name="radTreeView"
 ItemTemplate="{StaticResource ItemTemplate}"
 ItemsSource="{Binding Converter={StaticResource HierarchyConverter}}"/>

There is one non-standard thing: all ItemsSource bindings are made through a ValueConverter. This ValueConverter will create the "real" hierarchy for us:

public class HierarchyConverter : IValueConverter
{
    public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
    {
        // We are binding an item
        DataItem item = value as DataItem;
        if ( item != null )
        {
            return item.Owner.Where( i => i.ParentId == item.Id );
        }
        // We are binding the treeview
        DataItemCollection items = value as DataItemCollection;
        if ( items != null )
        {
            return items.Where( i => i.ParentId == 0 );
        }
        return null;
    }
    public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
    {
        throw new NotImplementedException();
    }
}
    Public Class HierarchyConverter
        Implements IValueConverter

        Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.Convert
            ' We are binding an item'
            Dim item As DataItem = TryCast(value, DataItem)
            If item IsNot Nothing Then
                Return item.Owner.Where(Function(i) i.ParentId = item.Id)
            End If

            ' We are binding the treeview'
            Dim items As DataItemCollection = TryCast(value, DataItemCollection)
            If items IsNot Nothing Then
                Return items.Where(Function(i) i.ParentId = 0)
            End If

            Return Nothing
        End Function

        Public Function ConvertBack(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object Implements IValueConverter.ConvertBack
            Throw New NotImplementedException()
        End Function

    End Class

When a DataItem object is passed as value, we are binding a TreeViewItem, so the Convert() method will return all DataItem objects from the Owner collection that have ParentID equal to the ID of the passed DataItem. When a DataItemCollection is passed, we are binding the RadTreeView, so the Convert() method will return the root-level DataItem objects, that have ParentID=0. Of course, it is up to you to decide whether you want a single, or separate converters for both of the cases. I did it in this way for simplicity, but if you want, you could split the code into two classes.

Finally, let's populate the treeview with some data:

this.DataContext = new DataItemCollection()
{
 new DataItem () { Text = "Item 1", Id = 1, ParentId = 0 },
 new DataItem () { Text = "Item 2", Id = 2, ParentId = 0 },
 new DataItem () { Text = "Item 3", Id = 3, ParentId = 0 },
 new DataItem () { Text = "Item 1.1", Id = 5, ParentId = 1 },
 new DataItem () { Text = "Item 1.2", Id = 6, ParentId = 1 },
 new DataItem () { Text = "Item 1.3", Id = 7, ParentId = 1 },
 new DataItem () { Text = "Item 2.1", Id = 8, ParentId = 2 },
 new DataItem () { Text = "Item 2.2", Id = 9, ParentId = 2 },
 new DataItem () { Text = "Item 2.3", Id = 10, ParentId = 2 },
 new DataItem () { Text = "Item 3.1", Id = 11, ParentId = 3 },
 new DataItem () { Text = "Item 3.2", Id = 12, ParentId = 3 },
 new DataItem () { Text = "Item 3.3", Id = 13, ParentId = 3 }
};
Me.DataContext = New DataItemCollection() From { 
    New DataItem () With {.Text = "Item 1", .Id = 1, .ParentId = 0}, 
    New DataItem () With {.Text = "Item 2", .Id = 2, .ParentId = 0}, 
    New DataItem () With {.Text = "Item 3", .Id = 3, .ParentId = 0}, 
    New DataItem () With {.Text = "Item 1.1", .Id = 5, .ParentId = 1}, 
    New DataItem () With {.Text = "Item 1.2", .Id = 6, .ParentId = 1}, 
    New DataItem () With {.Text = "Item 1.3", .Id = 7, .ParentId = 1}, 
    New DataItem () With {.Text = "Item 2.1", .Id = 8, .ParentId = 2}, 
    New DataItem () With {.Text = "Item 2.2", .Id = 9, .ParentId = 2}, 
    New DataItem () With {.Text = "Item 2.3", .Id = 10, .ParentId = 2}, 
    New DataItem () With {.Text = "Item 3.1", .Id = 11, .ParentId = 3}, 
    New DataItem () With {.Text = "Item 3.2", .Id = 12, .ParentId = 3}, 
    New DataItem () With {.Text = "Item 3.3", .Id = 13, .ParentId = 3} 
                                                }

Here is the result:

See Also