tags:

views:

380

answers:

2

I have a treeview bound to an object tree. When I remove an object from the object tree, it is removed correctly from the tree view, but the treeview's default behaviour is to jump the selecteditem up to the deleted item's parent node. How can I change this so it jumps to the next item instead?

EDIT:

I updated my code with Aviad's suggestion. Here is my code..

public class ModifiedTreeView : TreeView
{
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            if (e.OldStartingIndex - 1 > 0)
            {
                ModifiedTreeViewItem item = 
                    this.ItemContainerGenerator.ContainerFromIndex(
                    e.OldStartingIndex - 2) as ModifiedTreeViewItem;

                item.IsSelected = true;
            }
        }
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ModifiedTreeViewItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is ModifiedTreeViewItem;
    }
}

public class ModifiedTreeViewItem : TreeViewItem
{
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            if (e.OldStartingIndex > 0)
            {
                ModifiedTreeViewItem item =
                    this.ItemContainerGenerator.ContainerFromIndex(
                    e.OldStartingIndex - 1) as ModifiedTreeViewItem;

                item.IsSelected = true;
            }
        }
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ModifiedTreeViewItem();
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is ModifiedTreeViewItem;
    }
}

The code above does not work unless I debug it, or in some way slow down the OnItemsChanged method. For example, if I put a thread.sleep(500) at the bottom of the OnItemsChanged method, it works, otherwise it does not. Any idea what I'm doing wrong? This is really strange.

A: 

The behavior you mention is controlled by a virtual method in the Selector class called OnItemsChanged (reference: Selector.OnItemsChanged Method) - In order to modify it, you should derive from TreeView and override that function. You might use reflector to base your implementation on the existing implementation, although it's pretty straightforward.

Here's the code for the treeview override TreeView.OnItemsChanged extracted using reflector:

protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
    switch (e.Action)
    {
        case NotifyCollectionChangedAction.Add:
        case NotifyCollectionChangedAction.Move:
            break;

        case NotifyCollectionChangedAction.Remove:
        case NotifyCollectionChangedAction.Reset:
            if ((this.SelectedItem == null) || this.IsSelectedContainerHookedUp)
            {
                break;
            }
            this.SelectFirstItem();
            return;

        case NotifyCollectionChangedAction.Replace:
        {
            object selectedItem = this.SelectedItem;
            if ((selectedItem == null) || !selectedItem.Equals(e.OldItems[0]))
            {
                break;
            }
            this.ChangeSelection(selectedItem, this._selectedContainer, false);
            return;
        }
        default:
            throw new NotSupportedException(SR.Get("UnexpectedCollectionChangeAction", new object[] { e.Action }));
    }
}

Alternatively, you might hook into the collection NotifyCollectionChanged event from one of your code-behind classes and explicitly change the current selection before the event reaches the TreeView (I'm not sure of this solution though because I am not sure of the order in which event delegates are called - the TreeView might get to process the event before you do - but it might work).

Aviad P.
+1  A: 

Original answer

In my original answer I guessed that you may be encountering a bug in WPF and gave a generic workaround for this kind of situation, which was to replace item.IsSelected = true; with:

Disptacher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
{
  item.IsSelected = true;
}));

I explained that the reason this kind of workaround does the trick 90% of the time is that it delays the selection until almost all current operations have finished processing.

When I actually tried the code you posted in your other question I discovered that it was indeed a bug in WPF but found a more direct and reliable workaround. I'll explain how I diagnosed the problem and then describe the workaround.

Diagnosis

I added a SelectedItemChanged handler with a breakpoint in it, and looked at the stack trace. This made it obvious where the problem lies. Here are selected portions of the stack trace:

...
System.Windows.Controls.TreeView.ChangeSelection
...
System.Windows.Controls.TreeViewItem.OnGotFocus
...
System.Windows.Input.FocusManager.SetFocusedElement
System.Windows.Input.KeyboardNavigation.UpdateFocusedElement
System.Windows.FrameworkElement.OnGotKeyboardFocus
System.Windows.Input.KeyboardFocusChangedEventArgs.InvokeEventHandler
...
System.Windows.Input.InputManager.ProcessStagingArea
System.Windows.Input.InputManager.ProcessInput
System.Windows.Input.KeyboardDevice.ChangeFocus
System.Windows.Input.KeyboardDevice.TryChangeFocus
System.Windows.Input.KeyboardDevice.Focus
System.Windows.Input.KeyboardDevice.ReevaluateFocusCallback
...

As you can see, KeyboardDevice has a ReevaluateFocusCallback private or internal method which changes the focus to the parent of the deleted TreeViewItem. This causes a GotFocus event which causes the parent item to be selected. This all happens in the background after your event handler returns.

Solution

Normally in this case I would tell you to just manually .Focus() the TreeViewItem you are selecting. That is difficult here because in a TreeView there is no easy way to get from an arbitrary data item to the corresponding container (there are separate ItemContainerGenerators at each level).

So I think your best solution is to force the focus to the parent node (just where you don't want it to end up), then set IsSelected in the child's data. That way the input manager will never decide it needs to move the focus on its own: It will find the focus already set to a valid IInputElement.

Here is some code to do that:

      if(child != null)
      {
        SomeObject parent = child.Parent;

        // Find the currently focused element in the TreeView's focus scope
        DependencyObject focused =
          FocusManager.GetFocusedElement(
            FocusManager.GetFocusScope(tv)) as DependencyObject;

        // Scan up the VisualTree to find the TreeViewItem for the parent
        var parentContainer = (
          from element in GetVisualAncestorsOfType<FrameworkElement>(focused)
          where (element is TreeViewItem && element.DataContext == parent)
                || element is TreeView
          select element
          ).FirstOrDefault();

        parent.Children.Remove(child);
        if(parent.Children.Count > 0)
        {
          // Before selecting child, first focus parent's container
          if(parentContainer!=null) parentContainer.Focus();
          parent.Children[0].IsSelected = true;
        }
      }

This also requires this helper method:

private IEnumerable<T> GetVisualAncestorsOfType<T>(DependencyObject obj) where T:DependencyObject
{
  for(; obj!=null; obj = VisualTreeHelper.GetParent(obj))
    if(obj is T)
      yield return (T)obj;
}

This should be more reliable than using Dispatcher.BeginInvoke because it will work around this particular problem without making any assumptions about input queue ordering, Dispatcher priorities, and so forth.

Ray Burns
http://stackoverflow.com/questions/2053993/wpf-treeview-selecteditem-moves-incorrectly-why-doesnt-this-workHere is a link to the full code. I took out the inherited treeview / treeviewitem because it seems to have the same effect. Any insight would be really helpful.
Oh, by the way I tried your fix, this works most of the time. Every now and then it still jumps up to the parent node though which can't happen. Thanks anyway :)
Thanks for posting your code. It was easy to find the problem, which was focus-related. I've added an update to my answer that explains what was going on and how to fix it.
Ray Burns