(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

Make the TreeView control to be MVVM compliant

(6 votes)
Andrea Boschin
>
Andrea Boschin
Joined Nov 17, 2009
Articles:   91
Comments:   9
More Articles
7 comments   /   posted on Jan 05, 2010

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.


Subscribe

Comments

  • -_-

    RE: Make the TreeView control to be MVVM compliant


    posted by jsp3536 on Jan 05, 2010 19:51
    That is a nice solution.  One thing to think about is if your application uses theming then now the tree control will not be themed since it has been derived from a the base tree control.  If you still require theming you can just wire up the events on the treecontrol like normal and then in the events call the commands that are on the view model.
  • -_-

    RE: Make the TreeView control to be MVVM compliant


    posted by marcos on Nov 03, 2010 02:53

    Hi Andrea !

    I would like to give you congratulations for such a nice article !. I plugged your generic solution into a Silverlight Navigation application so now my app users will be able to navigate to the application's pages through the TreeView navigation system.

    Your generic implementation and clean code are amazing ! I'm actually getting used to the MVVM pattern (I come from the Java Swing MVC) could you give me a clue from where I can learn more about MVVM ?

    Thank you very much !
    --marcos

     

  • -_-

    RE: Make the TreeView control to be MVVM compliant


    posted by Andrea Boschin on Nov 03, 2010 10:03
    thanks marcos, unfortunately there is not any "official manual" of MVVM since it is a young matter. You have to follow blogs and twits and try it by yourself, and perhaps contributing to the pattern yourself :)
  • -_-

    RE: Make the TreeView control to be MVVM compliant


    posted by Bill Gan on May 25, 2011 08:08
    nice article!
  • rookie07

    Re: Make the TreeView control to be MVVM compliant


    posted by rookie07 on Dec 19, 2011 16:00

    very nice article!

    i'm getting slowly into the mvvm thing...so there is still a problem which i have to solve. supose i want to bind a TextBox.Text to the ItemClickedCommad how to get this done?

     i added a propety to the MainPageViewModel like:

    public HierarchicalViewModel<CMSSTRUCT> CurrentViewModel { get; set; }

    in order to connect the data to the event: 

     private void ClickItem(HierarchicalViewModel<CMSSTRUCT> item)
            {
                CurrentViewModel = item;
            }

     

    in the XAML i thought i could do something like:

     <TextBox Grid.Column="2" Grid.Row="1" Height="23" HorizontalAlignment="Left" Name="textBoxText" VerticalAlignment="Top" Width="280" Text="{Binding CurrentViewModel.PayLoad.CMSSTRUCT_NAME, FallbackValue='loding failed!'}" />

    but it seems that the textbox is never updated...? what am i doing wrong?

    thanx!

  • rookie07

    Re: Make the TreeView control to be MVVM compliant


    posted by rookie07 on Dec 19, 2011 16:46

    very nice article!

    i'm getting slowly into the mvvm thing...so there is still a problem which i have to solve. supose i want to bind a TextBox.Text to the ItemClickedCommad how to get this done?

     i added a propety to the MainPageViewModel like:

    public HierarchicalViewModel<CMSSTRUCT> CurrentViewModel { get; set; }

    in order to connect the data to the event: 

     private void ClickItem(HierarchicalViewModel<CMSSTRUCT> item)
            {
                CurrentViewModel = item;
            }

     

    in the XAML i thought i could do something like:

     <TextBox Grid.Column="2" Grid.Row="1" Height="23" HorizontalAlignment="Left" Name="textBoxText" VerticalAlignment="Top" Width="280" Text="{Binding CurrentViewModel.PayLoad.CMSSTRUCT_NAME, FallbackValue='loding failed!'}" />

    but it seems that the textbox is never updated...? what am i doing wrong?

    thanx!

  • rookie07

    Re: Make the TreeView control to be MVVM compliant


    posted by rookie07 on Dec 19, 2011 17:20

    ok, i think i figured it out. i coded a simple helper class derived from INotifyPropertyChanged and bound it to the TextBox - may be i've got something wrong, but i thought the notification behaviour would be automatically be added because ViewModel is indirectly derived from it???

        public class HierarchicalViewModelNotifier : INotifyPropertyChanged
        {
            private HierarchicalViewModel<CMSSTRUCT> _currentViewModel = null;
            public HierarchicalViewModel<CMSSTRUCT> CurrentViewModel { get { return _currentViewModel; } set { _currentViewModel = value; Changed("CurrentViewModel"); } }
            public event PropertyChangedEventHandler PropertyChanged;
            private void Changed(string propertyName)
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

      public class MainPageViewModel : ViewModel
        {

    public HierarchicalViewModelNotifier ViewModelNotifier { get; set; } 

    ...

    public MainPageViewModel()
            {
                ViewModelNotifier = new HierarchicalViewModelNotifier();
                ViewModelNotifier.CurrentViewModel = Root.FirstOrDefault();

    ...

    }

    private void ClickItem(HierarchicalViewModel<CMSSTRUCT> item)
            {
                ViewModelNotifier.CurrentViewModel = item;
            } 

     and in the XAML i did it like this way:

    <TextBox Grid.Column="2" Grid.Row="1" Height="23" HorizontalAlignment="Left" Name="textBoxText" VerticalAlignment="Top" Width="280" Text="{Binding ViewModelNotifier.CurrentViewModel.PayLoad.CMSSTRUCT_NAME, FallbackValue='Selektion konnte nicht ermittelt werden!', Mode=TwoWay}" />

    so let's look where i might stuck next ;o)

     

     

Add Comment

Login to comment:
  *      *