During a recent work I found some trouble working with the TreeView control in a Model-View-ViewModel scenario. As usually happen, the standard controls are designed to work in an event-driven behavior and this non always marries with a correct MVVM implementation. After some work, I found a way to change the TreeView and transform it to be lazy loadable.
Download Source Code
The Model-View-ViewModel pattern has been introduced in Silverlight by few time and this platform lacks a complete support to the pattern by the standard controls. In Windows Presentation Foundation there is an extended commanding support since the first releases of the platform that enable handling many behavior of the controls and respond easily to the user interaction. This is not true for Silverlight, where the elective model is event-driven and this often cause the need to write code in the codebehind of the page instead of relying on commands and ViewModel. Also if we can rely on Prism's DelegateCommand class, to create some of the missing commands and enhance the support to the pattern, there are some cases where it is not possible to connect directly a command to an event and catch it in the ViewModel.
In a recent work I did, one of this not-unusual cases taken me some time to reach a solution. I was working with a SDK's TreeView control, trying to make it lazy loaded in the ViewModel and also to catch the selection of an item to publish the SelectedItem to other ViewModels.
Understanding the problem
Before starting to analyze the solutions I will propose, it is better to understand where is the problem I would like to resolve. If you scan the events of a TreeView control you will discover that it miss a series of item-level events. The sole event that have this scope is the SelectedItemChanged event we can use to detect if the user changed the currently selected item.
But what if I need to detect when the user expands or collapses a node? The treeview completely miss this kind of events. In an event-driven application is still possible to detect the expand/collapse action because the TreeViewItem class exposes the Expanded and Collapsed event so we can write something like this:
TreeView tv = new TreeView();
TreeViewItem item = new TreeViewItem();
item.Expanded += new RoutedEventHandler(item_Expanded);
item.Collapsed += new RoutedEventHandler(item_Collapsed);
tv.Items.Add(item);
This solution besides being complicated is also invalid in a MVVM application because violate a key concept of separation of concerns between the user interface and the Business logic. Using this tecnique we will have to attach every single item of the TreeView to an event handler to catch the actions and this has to be done into the codebehind of the page.
So, the problem is that we cannot gain access to the events using DataBinding, because the TreeViewItem class is only a wrapper that is generated by the DataBinding but is not exposed by a template or some properties of the control. This apply not only to the Expanded and Collapsed events, but also to the mouse events, like MouseLeftButtonUp and other else and virtually every other item-level event.
Anatomy of a TreeView
Similar to many other controls that generates its content from the data source, the TreeView inherits his behavior from the ItemsControl. This flexible control - you may use directly into the xaml or indirectly with some major controls - is substantially capable of taking the elements of a list, attached to the ItemsSource property and apply a template to generate some UI elements. When you use the ItemsControl as base of TreeView (or also of a ListBox), the control wraps every element of the source collection with a specialized item that is responsible to handle item-specific properties and behaviors.
The TreeViewItem class is generated by the TreeView control and - as we said in the previous paragraph - it contains some key events like Expanded and Collapsed but also is a subclass of ItemsControl itself. This is the way the TreeView shows a hierarchical structure because every item in the control is binded to the list of children that the item must expose to continue the hierarchy.
The interesting part of this structure is that we can change the container class overriding a method of the ItemsControl. The ItemsControl contains some methods we can use to manage the lifetime of the container:
1) PrepareContainerForItemOverride()
2) ClearContainerForItemOverride()
3) GetContainerForItemOverride()
Solving the problem
Now we know all the required elements to solve the problem I've illustrated previously. What we would like to do is catch an event, as an example the Expanded event, and bubble it to the main TreeView control that finally will raises and event. This let us creating a specific command and bind it to the ItemExpanded event.
To do this we will create a new LazyTreeView control inherited by the TreeView and then we override the GetContainerForItemOverride method to issue another kind of wrapper. Here is the simplified code:
public class LazyTreeView : TreeView
{
public event EventHandler ItemExpanded;
public event EventHandler ItemClicked;
protected override DependencyObject GetContainerForItemOverride()
{
LazyTreeViewItem item = new LazyTreeViewItem();
// Expanded
item.ItemExpanded += (s, e) => this.RaiseEvent(this.ItemExpanded, s);
// Clicked
item.ItemClicked += (s, e) => this.RaiseEvent(this.ItemClicked, s);
return item;
}
private void RaiseEvent(EventHandler handler, object sender)
{
if (handler != null)
handler(sender, EventArgs.Empty);
}
}
In the GetContainerForItemOverride method we did two things. First of all we instantiate a LazyTreeViewItem class as wrapper. Then we attach the Expanded event to raise the ItemExpanded on the TreeView. Of course while the TreeView is gerarchical this code simple handles the event of the first level but we can create an item-specific class to attach the other levels:
public class LazyTreeViewItem : TreeViewItem
{
public event EventHandler ItemExpanded;
public event EventHandler ItemClicked;
protected override DependencyObject GetContainerForItemOverride()
{
LazyTreeViewItem item = new LazyTreeViewItem();
// Expanded
item.ItemExpanded += (s, e) => this.RaiseEvent(this.ItemExpanded, s);
// Clicked
item.ItemClicked += (s, e) => this.RaiseEvent(this.ItemClicked, s);
return item;
}
protected override void OnExpanded(RoutedEventArgs e)
{
this.RaiseEvent(this.ItemExpanded, this);
base.OnExpanded(e);
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
this.RaiseEvent(this.ItemClicked, this);
e.Handled = true;
base.OnMouseLeftButtonUp(e);
}
private void RaiseEvent(EventHandler handler, object sender)
{
if (handler != null)
handler(sender, EventArgs.Empty);
}
}
In this snippet I show the LazyTreeViewItem class. It is almost similar to the previous snippet but adds the override of OnExpanded method. It is required to catch the Expanded event of the item and start the bubbling. When the user expand an item the event is catched and an ItemExpanded event is raised. It then start navigating the hierarchy while it arrives to the LazyTreeView.
Use of the new control
Now that we have prepared our pretty new LazyTreeView we can try to use it to make a lazy-loaded TreeView. In the example attached to this article I use a Ria Service to get files and directories from the server filesystem and populates the nodes on demand when the user expand an element. In the xaml I use the LazyTreeView with a HierarchicalDataTemplate to the data source bind hierarchically.
<code:LazyTreeView ItemsSource="{Binding Root}"
code:ItemExpanded.Command="{Binding ItemExpandedCommand}">
<code:LazyTreeView.ItemTemplate>
<sw:HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding PayLoad.Name}" />
</sw:HierarchicalDataTemplate>
</code:LazyTreeView.ItemTemplate>
</code:LazyTreeView>
As you can see the ItemExpanded event is binded to a command of the ViewModel. When the command is raised the ViewModel is responsible of connecting to the Ria Services and get the content of the expanded node. The command behavior I used to create the ItemExpanded command automatically assign the source element to the CommandParameter so the destination command gets a reference to the expanded element and its ViewModel. (Remember I use Prism to do Model-View-ViewModel programming)
public class ItemClickedCommandBehavior : CommandBehaviorBase<LazyTreeView>
{
public ItemClickedCommandBehavior(LazyTreeView targetObject)
: base(targetObject)
{
targetObject.ItemClicked += new EventHandler(targetObject_ItemClicked);
}
void targetObject_ItemClicked(object sender, EventArgs e)
{
base.CommandParameter = ((FrameworkElement)sender).DataContext;
base.ExecuteCommand();
}
}
When the ViewModel receive the ItemExpanded command and gets the elements from the filesystem it wraps them with a HierarchicalViewModel. This class contains the item and adds it a Children collection and a IsLoaded property. This property is useful to avoid duplicated loading of the elements. After the items has been loaded the flag is set to true and then none will try to load it again.
So the trick is done but we have another little thing to notice. When the ViewModel creates the HierarchicalViewModel instances it is required to load every instance with a dummy child instance. The scope of this instance is to show the expansion cross that is displayed only if a node has at least one child. I load every item with a dummy FileSystemItem with the Text set to "loading...". This cause the user to see this message when it open the node and then it is replaced by the content coming from the server.
if (item.Type == FileSystemItemType.Folder)
vm.Children.Add(new HierarchicalViewModel<FileSystemItem>(new FileSystemItem { Name = "loading..." }));
Recap
When you work with the Model-View-ViewModel pattern you must have always in mind that not always you can apply the pattern correctly if you are using the standard controls. The controls are designed to work in an event-driven world so they may require to write some code in the codebehind. It isn't not a bad thing but for sure modifying the controls to make them compliant with the pattern is often possible and let you have more reusable and tested code.