Silverlight TreeView - Drag & Drop with MVVM

Introduction

The Silverlight 4 TreeView control, when leveraged with the HierarchicalDataTemplate, allows easy binding of hierarchical data.  The Silverlight Toolkit's TreeViewDragDropTarget wrapper control provides Drag & Drop support to the TreeView.  Mixing the three together, you get pretty good out-of-the-box support for Drag & Drop reorganization of hierarchical data.  However, when searching online, I was unable to find an example of integrating this functionality with the MVVM pattern.  Since my application utilizes MVVM, I wanted to avoid putting code into the View itself.  So I began the investigation into how I could leverage the Expression interactivity libraries to facilitate the functionality that I needed in the application.  This article aims to shed some light on one possible solution to the problem that doesn’t rely on external toolkits or frameworks.

In my previous article, I talked about enabling Drag & Drop reordering of a ListBox control.  Many of the techniques I’m using here are derived from the investigation that I did for that article.  Extending the result to a TreeView control brought up a few new solutions, some of which I will likely use to update the previous article with a more robust solution that is much simpler.

Background

To setup the scenario, we have a basic Category maintenance application.  The goal of the application is to provide an administrative user with the ability to reorder the category ‘tree’, to add/remove categories and to edit details about the category in a standard master/detail layout.

The application will be backed by a WCF RIA Services backend, but I won’t spend much time on the details of the backend.  This article will focus on enabling the administrative scenario on the client.

The Data Model

The data model, as mentioned, is extremely simple.  A ‘Category’ entity has navigation properties to its parent and children categories, and has ‘CategoryId’, ‘Name’, ‘Description’ and ‘Sequence’ attributes.  The ‘Sequence’ attribute is used to order the categories within the parent category.  The entire EF4 diagram is shown below:

Data ModelData Model

 

The View

We’ll start by setting up our view and getting the basic UI layout in place based on the layout above.  Selecting a node in the Tree will bring up the ‘Edit’ screen for that node on the right-hand pane of the application.  Dragging a node around the tree will cause the node to ‘re-parent’ itself under the destination in the tree and submit any changes to the server in support of the reordering.  The view will also have a ‘Save’ button which forces a submission of changes to the server.  For the purposes of this example, I’ve left off the ‘Cancel’ button as it brings in a whole host of usage scenarios that I don’t want to address here.

I won’t reproduce the entire view code here in the interest of saving space, but I will call out the important pieces.  The actual TreeViewDragDropTarget + TreeView + HierarchicalDataTemplate that enable the drag & drop functionality are shown below:

 

    <!-- TreeView -->
    <StackPanel Grid.Row="1">
        <TextBlock Text="Categories:" />
        <toolkit:TreeViewDragDropTarget AllowDrop="True"
                                        HorizontalContentAlignment="Stretch"
                                        VerticalContentAlignment="Stretch">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="ItemDragComplete">
                    <ia:CallMethodAction TargetObject="{Binding}" MethodName="RealignItemParentage" />
                </i:EventTrigger>
            </i:Interaction.Triggers>

            <sdk:TreeView ItemsSource="{Binding Path=Categories}">
                <i:Interaction.Behaviors>
                    <helpers:TreeViewBoundSelectedItemBehavior TargetObject="{Binding}"
                                                                PropertyName="SelectedCategory" />
                </i:Interaction.Behaviors>

                <sdk:TreeView.ItemTemplate>
                    <sdk:HierarchicalDataTemplate ItemsSource="{Binding Path=Children}">
                        <TextBlock Text="{Binding Path=Category.Name}" />
                    </sdk:HierarchicalDataTemplate>
                </sdk:TreeView.ItemTemplate>
            </sdk:TreeView>
        </toolkit:TreeViewDragDropTarget>
    </StackPanel>

 

A couple things here, first, the ‘AllowDrop’ property is set to True, as we want to be able to drag and drop the items within the tree.  Secondly, I’ve added a trigger to the ‘ItemDragComplete’ event.  The ItemDragComplete event is fired when the entire drag/drop process is done.  The event order is a little different with the TreeView, and it was my investigation here that will lead to a better solution for the ListBox timing issue I mentioned in my previous article.  The order of events looks like this:

  1. … various other events …
  2. [if destination is same parent node as source] ItemDroppedOnSource
  3. CollectionChanged [Remove]
  4. CollectionChanged [Add]
  5. ItemDragComplete

The key here is that my ListBox solution of setting a flag in the ItemDroppedOnSource event simply won’t work with the TreeView.  This finding led me to revisit my previous solution and I discovered that there was no need to set a flag at all, the CollectionChanged events [all of them] are taken care of during the completion of the Drop operation, so I simply needed to wait until ItemDragComplete was fired and all the collection reordering/realignment is done.  At that point, I invoke my re-alignment code to clean up the underlying Category objects and submit the changes to the server.

The second key item is the custom behavior that I wrote to handle the binding for the SelectedItem property of the TreeView class.  The problem here is that the SelectedItem property on the TreeView is read-only, which means that you cannot do a TwoWay binding as you would expect, in fact, you can’t really bind the SelectedItem property AT ALL.  If you try to do so [at least in my limited testing], Silverlight generates a fatal exception and ‘white screens’.  To work around this, I’ve adapted code from this blog post into a custom behavior that handles the binding between the ViewModel property and the TreeView property.  Hopefully future versions of the Toolkit will fix this problem.

The ViewModel

The ViewModel is pretty straightforward.  The only pieces that I’ll call out are the CategoryTreeViewItem nested class and the RealignItemParentage method.  We have a ‘child’ view model, the CategoryTreeViewItem class, which wraps the individual Category entities and serves as our ItemsSource for the TreeView.  The ViewModel handles creating the root nodes at the MainPageViewModel level, and each CategoryTreeViewItem handles populating it’s Children collection with the appropriate CategoryTreeViewItems based on its Children categories.

The RealignItemParentage method zips through the CategoryTreeViewItem(s) and sets their associated ‘Category’ children to the correct parent category and sequence.  Then a call to Save() invokes SubmitChanges to save data to the database.

 

    public class MainPageViewModel : BaseViewModel
    {

        #region 'Child' View Models
        public class CategoryTreeViewItem : BaseViewModel
        {
            public CategoryTreeViewItem()
            {
                Children = new ObservableCollection<CategoryTreeViewItem>();
            }

            public CategoryTreeViewItem(Category category)
                : this()
            {
                Category = category;

                foreach (Category child in category.Children.OrderBy(i=> i.Sequence).ThenBy(i => i.Name))
                {
                    Children.Add(new CategoryTreeViewItem(child));
                }
            }

            public Category Category
            {
                get;
                set;
            }

            public ObservableCollection<CategoryTreeViewItem> Children { get; set; }

            public void RealignItemParentage()
            {
                foreach (CategoryTreeViewItem cat in Children)
                {
                    cat.Category.Sequence = (byte)Children.IndexOf(cat);
                    cat.Category.Parent = this.Category;

                    cat.RealignItemParentage();
                }
            }

        }
        #endregion

        /// more code ...

        #region Public Methods
        public void RealignItemParentage()
        {
            foreach (CategoryTreeViewItem cat in Categories)
            {
                cat.Category.Sequence = Categories.IndexOf(cat);
                cat.Category.Parent = null;

                cat.RealignItemParentage();
            }

            Save();
        }

        /// yet more code ...

 

Conclusion

In all, linking the Silverlight Toolkit’s TreeView Drag & Drop with an MVVM structure is fairly straightforward, and I was surprised that I didn’t find more examples online in my searches.  The example code below contains a fully working application, with reordering, updates and category addition.  Adding deletion of the currently selected category should be a fairly straightforward extension that I’ll leave to the reader.

The source code can be found here: TreeViewMVVM.zip (172K)

NOTE:  If you plan to run the example code, you’ll need to update the connection string in Web.config to point to your properly qualified database path (look for [[YOUR PATH HERE]]).  Thanks for reading!