Bind RadTreeView to Self-Referencing Data
This tutorial will show you how to display a RadTreeView with 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:
Example 1: Defining the DataItem class
public class DataItem
{
public int Id
{
get;
set;
}
public int ParentId
{
get;
set;
}
public string Text
{
get;
set;
}
public DataItemCollection OwnerCollection
{
get;
protected set;
}
internal void SetOwnerCollection(DataItemCollection collection)
{
this.OwnerCollection = collection;
}
}
Public Class DataItem
Inherits ViewModelBase
Public Property Id As Integer
Public Property ParentId As Integer
Public Property Text As String
Public Property OwnerCollection As DataItemCollection
Friend Sub SetOwnerCollection(ByVal collection As DataItemCollection)
Me.OwnerCollection = collection
End Sub
End Class
Those data objects are added into a special DataItemCollection class, that inherits ObservableCollection
Example 2: Defining DataItemCollection
public class DataItemCollection : ObservableCollection<DataItem>
{
public DataItemCollection()
: base()
{
}
public DataItemCollection(IEnumerable<DataItem> collection)
: base(collection)
{
}
public DataItem AssociatedItem
{
get;
protected set;
}
public void SetAssociatedItem(DataItem item)
{
this.AssociatedItem = item;
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (DataItem item in e.NewItems)
{
if (this.AssociatedItem != null && item.ParentId != this.AssociatedItem.Id)
{
item.ParentId = this.AssociatedItem.Id;
}
}
}
}
}
Public Class DataItemCollection
Inherits ObservableCollection(Of DataItem)
Public Sub New()
End Sub
Public Sub New(ByVal collection As IEnumerable(Of DataItem))
End Sub
Public Property AssociatedItem As DataItem
Public Sub SetAssociatedItem(ByVal item As DataItem)
Me.AssociatedItem = item
End Sub
Protected Overrides Sub OnCollectionChanged(ByVal e As NotifyCollectionChangedEventArgs)
MyBase.OnCollectionChanged(e)
If e.Action = NotifyCollectionChangedAction.Add Then
For Each item As DataItem In e.NewItems
If Me.AssociatedItem IsNot Nothing AndAlso item.ParentId <> Me.AssociatedItem.Id Then
item.ParentId = Me.AssociatedItem.Id
End If
Next
End If
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 3: Defining the resources
<example:HierarchyConverter x:Key="HierarchyConverter" />
<HierarchicalDataTemplate x:Key="ItemTemplate"
ItemsSource="{Binding Converter={StaticResource HierarchyConverter}}">
<TextBlock Text="{Binding Text}" />
</HierarchicalDataTemplate>
Example 4: Defining the RadTreeView
<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:
Example 5: Defining the HierarchyConverter
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)
{
var children = item.OwnerCollection.Where(i => i.ParentId == item.Id);
var collection = new DataItemCollection(children);
collection.SetAssociatedItem(item);
return collection;
}
// We are binding the treeview
DataItemCollection items = value as DataItemCollection;
if (items != null)
{
var children = items.Where(i => i.ParentId == 0);
return new DataItemCollection(children);
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Public Class HierarchyConverter
Inherits IValueConverter
Public Function Convert(ByVal value As Object, ByVal targetType As Type, ByVal parameter As Object, ByVal culture As CultureInfo) As Object
Dim item As DataItem = TryCast(value, DataItem)
If item IsNot Nothing Then
Dim children = item.OwnerCollection.Where(Function(i) i.ParentId = item.Id)
Dim collection = New DataItemCollection(children)
collection.SetAssociatedItem(item)
Return collection
End If
Dim items As DataItemCollection = TryCast(value, DataItemCollection)
If items IsNot Nothing Then
Dim children = items.Where(Function(i) i.ParentId = 0)
Return New DataItemCollection(children)
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
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. It is done in this way for simplicity, but if you want, you could split the code into two classes.
Example 6: Populating the RadTreeView
public MainWindow()
{
InitializeComponent();
var source = 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, }
};
foreach (var item in source)
{
item.SetOwnerCollection(source);
}
this.DataContext = source;
}
Public Sub MainWindow()
InitializeComponent()
Dim source = 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}}
For Each item In source
item.SetOwnerCollection(source)
Next
Me.DataContext = source
End Sub
Image 1: Self-Referencing RadTreeView
You can check out this example in the RadTreeView SDK examples or in the SDK Samples Browser that provides a more convenient approach in exploring and executing the examples in the Telerik XAML SDK repository. The SDK Samples Browser application is available for download from this link.