In the meantime I solved my special scenario by creating MoveSelectionUp()
and MoveSelectionDown()
extension methods on the TreeView
. I copied the implementation over from some private methods in the Toolkit's control code and made slight changes were private or protected methods were accessed. Thanks to all the extensions methods that are available in the toolkit now it's not so difficult anymore.
Because it is largely not mine, I hereby provide the code below if future visitors stumble upon the same issue.
I'd leave this question open however because I'd still like to know, in a more general fashion, if events can be forwarded in the DependencyObject framework.
TreeViewExtensions:
using System;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
static public class TreeViewExtensions
{
static public void SetSelectedContainerIfValid(this TreeView self, TreeViewItem itm)
{
Contract.Requires(self != null);
if (itm != null)
{
self.SetSelectedContainer(itm);
}
}
static public void MoveSelectionUp(this TreeView self)
{
Contract.Requires(self != null);
var itm = self.GetSelectedContainer();
if (itm == null)
{
self.SetSelectedContainerIfValid(self.GetContainers().LastOrDefault());
}
else
{
self.SetSelectedContainerIfValid(itm.GetContainerAbove());
}
}
static public void MoveSelectionDown(this TreeView self)
{
Contract.Requires(self != null);
var itm = self.GetSelectedContainer();
if (itm == null)
{
self.SetSelectedContainerIfValid(self.GetContainers().FirstOrDefault());
}
else
{
self.SetSelectedContainerIfValid(itm.GetContainerBelow());
}
}
}
TreeViewItemExtensions:
using System;
using System.Diagnostics.Contracts;
using System.Windows;
using System.Windows.Controls;
static public class TreeViewItemExtensions
{
static public TreeViewItem GetContainerBelow(this TreeViewItem self)
{
return TreeViewItemExtensions.GetContainerBelow(self, true);
}
/// <summary>
/// Find the next focusable TreeViewItem below this item.
/// </summary>
/// <param name="recurse">
/// A value indicating whether the item should recurse into its child
/// items when searching for the next focusable TreeViewItem.
/// </param>
/// <returns>The next focusable TreeViewItem below this item.</returns>
static public TreeViewItem GetContainerBelow(this TreeViewItem self, bool recurse)
{
Contract.Requires(self != null);
// Look for the next item in the children of this item (if allowed)
if (recurse && self.IsExpanded && self.HasItems)
{
TreeViewItem item = self.ItemContainerGenerator.ContainerFromIndex(0) as TreeViewItem;
if (item != null)
{
return item.IsEnabled ?
item :
item.GetContainerBelow(false);
}
}
// Look for the next item in the siblings of this item
ItemsControl parent = self.GetParentTreeViewItem() as ItemsControl ?? self.GetParentTreeView();
if (parent != null)
{
// Get the index of this item relative to its siblings
TreeViewItem item = null;
int index = parent.ItemContainerGenerator.IndexFromContainer(self);
int count = parent.Items.Count;
// Check for any siblings below this item
while (index++ < count)
{
item = parent.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
if (item != null && item.IsEnabled)
{
return item;
}
}
// If nothing else was found, try to find the next sibling below
// the parent of this item
TreeViewItem parentItem = self.GetParentTreeViewItem();
if (parentItem != null)
{
return parentItem.GetContainerBelow(false);
}
}
return null;
}
/// <summary>
/// Find the last focusable TreeViewItem contained by this item.
/// </summary>
/// <returns>
/// The last focusable TreeViewItem contained by this item.
/// </returns>
static public TreeViewItem GetLastContainer(this TreeViewItem self)
{
Contract.Requires(self != null);
TreeViewItem item = self;
TreeViewItem lastItem = null;
int index = -1;
// Walk the children of the current item
while (item != null)
{
// Ignore any disabled items
if (item.IsEnabled)
{
// If the item has no children, it must be the last
if (!item.IsExpanded || !item.HasItems)
{
return item;
}
// If the item has children, mark it as the last known
// focusable item so far and walk into its child items,
// starting from the last item and moving toward the first
lastItem = item;
index = item.Items.Count - 1;
}
else if (index > 0)
{
// Try searching for the previous item's sibling
index--;
}
else
{
// Stop searching if we've run out of children
break;
}
// Move to the item's previous sibling
item = lastItem.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
}
return lastItem;
}
/// <summary>
/// Find the previous focusable TreeViewItem above this item.
/// </summary>
/// <returns>
/// The previous focusable TreeViewItem above this item.
/// </returns>
static public TreeViewItem GetContainerAbove(this TreeViewItem self)
{
Contract.Requires(self != null);
ItemsControl parent = self.GetParentTreeViewItem() as ItemsControl ?? self.GetParentTreeView();
if (parent == null)
{
return null;
}
// Get the index of the current item relative to its siblings
int index = parent.ItemContainerGenerator.IndexFromContainer(self);
// Walk the previous siblings of the item to find a focusable item
while (index-- > 0)
{
// Get the sibling
TreeViewItem item = parent.ItemContainerGenerator.ContainerFromIndex(index) as TreeViewItem;
if (item != null && item.IsEnabled)
{
// Get the last focusable descendent of the sibling
TreeViewItem last = item.GetLastContainer();
if (last != null)
{
return last;
}
}
}
return parent as TreeViewItem;
}
}