I have a tree view representing certain items. This tree is always two levels deep. The right-click menu for the child items has a "move up" command. The UI allows you to move a child item up even if it’s the first item of its parent, as long as there is another item at the parent level, above the selected item’s parent.
The obvious way to do this is to get the selected item’s parent and see if there are items above it. However, getting the selected item’s parent in WPF is anything but trivial. Again, the obvious (for a WPF beginner, anyway) approach is to get the TreeViewItem
for the selected item, which has a Parent
property. Unfortunately, this is also hard to do.
Taking the hint from someone who says it’s hard because I’m doing it wrong, I decided to ask those more experienced with WPF: what’s the right, non-hard way to do this? Logically it’s trivial, but I can’t figure out the correct way to deal with the WPF APIs.
You are absolutely right that doing this kind of thing with the Wpf TreeView is painful. A key part of the reason for that is the flexibility that Wpf gives you - you could have overriden the ItemContainerGenerator in a custom TreeView and your tree view might not actually contain TreeViewItem objects, for example. i.e. there isn't that same fixed hierarchy that you find in a comparable Winforms control.
It really seems counter intuitive at first and it's a real shame that MS didn't spend more time explaining how to make this kind of thing work in a way that doesn't lead to frustration.
We've had huge success with Wpf since embracing MVVM - to the point where we always create a ViewModel for classes bound to the UI, without exception - it's just that much easier to wire in new functionality later down the line.
If you have an underlying viewmodel (or even model item if you must) that your tree view is bound to, and think of the treeview as just an observer, you will get along much better with the Wpf TreeView and other Wpf controls too. In practical terms for a tree bound hierarchy, you would have a hierarchy of viewmodel objects that your TreeView is visualizing - where each child has a handle back to it's parent, and each parent has a collection of child viewmodels. You would then have Hierarchical data template for each item, where the ItemsSource is the ChildCollection. You then fire off your "MoveUp" command against the ViewModel, and it takes care of making the change - if you are using collections based on ObservableCollection (or that implement INotifyCollectionChanged) then the TreeView updates automagically to reflect the new hierarchy.
Driving the functionality from the ViewModel, and seeing the UI as just a thin layer reflecting the ViewModel hierarchy and properties makes for code that can be unit tested to a high degree - with no code in the code-behind, you can often test your ViewModel functionality without any UI at all which makes for much better quality code in the long run.
The natural response for us when we started with Wpf was that ViewModels were overkill, but our experience (having started off without them in many places) is that they start paying off pretty rapidly in Wpf and are without doubt worth the extra effort to get your head around.
One thing you might not have hit yet, which we found really painful, was setting the selected item on a treeview - now that's not something for the faint of heart :)
The "right" way would be to forget the UI manifestation of your problem and instead think about how your model should represent it. You do have a model behind your UI, right?
Your UI would then just bind to the appropriate properties on your model.
I may be missing something here, but what I would do is pass the SelectedIndex as a command parameter to the binding for the command's CanExecute method. Then just use that to decide whether the command is enabled or not.
The problem may be that datacontext of the context menu doesnt change after loading because the contextmenu isnt in the visual tree. I usually use this method to expose the datacontext to items not in the visual tree via a static resource. I actually wrote an answer to a question about this earlier today.
I really think I'm missing something. Could you explain why this wouldn't work?
Edit
Ok I read abit about TreeViews and still didn't really understand what the issue was. So I went ahead and made an example and managed to get it to work.
My first step was reading This article by Josh Smith about treeviews. It talks about making viewmodels for each item type and exposing properties like IsSelected and IsExpanded, which you then bind to in the xaml. This allows you to access properties of the treeviewitem in the viewmodels.
After reading this I set to work:
Firstly I made a small datastructure which shows some kind of hierarchy to put into the tree view. I picked movies.
Next Step is to create a viewmodel for the TreeViewItems, which holds all the properties that relate to managing the tree view stuff i.e. IsExpanded, IsSelected etc.
The important thing to note is that they all have a parent and child.
This is how we are going to keep track of whether we are the first or last item in the parents collection.
After this we create our viewmodels for each Model. They all inherit from TreeViewItemModel, as they are all going to be treeviewitems.
Then I created the MainWindowViewModel, which will create a collection of these viewmodels (which is bound to the TreeView) as well as implement the commands the menus use, and the logic for how they are enabled.
It is important to note here that I have a SelectedItem property. I got this item by subscribing to all the viewmodel's property changed event and then getting the one that is selected. I use this item to check whether it is the first of last item in its parents Children collection.
Also note in the command enabling methods how I decide whether the item is in the root or not. This is important because my mainwindowviewmodel is not a TreeViewItemViewModel, and does not implement a Children property. Obviously for your program you will need another way of sorting out the root. You may want to put a boolean variable in the TreeViewItemViewModel called root, which you can just set to true if the item has no parent.
Lastly, here is the xaml of the MainWindow, where we bind to the properties.
Note the style inside the treeview for the treeviewitem. This is where we bind all the TreeViewItem properties to those created in the TreeviewItemViewModel.
The contextmenu's MenuItems's command property is bound to the commands, via a DataContextBridge (similar to an ElementSpy, both Josh Smith creations). This is because the contextmenu is out of the visual tree and therefore has trouble binding to the viewmodel.
Also note that I have a different HierarchicalDataTemplate for each of the viewmodel types I created. This allows me to bind to different properties for the different types that will be displayed in the treeview.