WPF treeview itemselected moves incorrectly when deleting an item

天涯浪子 提交于 2020-01-11 10:37:36

问题


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.


回答1:


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).




回答2:


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.




回答3:


This works for me (thanks to investigations provided above)

protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            Focus();
        }
    }



回答4:


According to the answer provided by @Kirill I think the correct answer to this specific question would be the following code added to a class derived from TreeView.

protected override void OnItemsChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{      
    if (e.Action == NotifyCollectionChangedAction.Remove && SelectedItem != null)
    {
        var index = Items.IndexOf(SelectedItem);
        if (index + 1 < Items.Count)
        {
            var item = Items.GetItemAt(index + 1) as TreeViewItem;
            if (item != null)
            {
                item.IsSelected = true;
            }
        }
    }
}



回答5:


Based on the answers above, here's the solution that worked for me (it has fixed various other problems as well, such as the loss of focus after selecting an item via model etc.)

Note the OnSelected override (scroll all the way down) which actually did the trick.

This was compiled in VS2015 for Net 3.5.

using System.Windows;
using System.Windows.Controls;
using System.Collections.Specialized;

namespace WPF
{
    public partial class TreeViewEx : TreeView
    {
        #region Overrides

        protected override DependencyObject GetContainerForItemOverride()
        {
            return new TreeViewItemEx();
        }
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is TreeViewItemEx;
        }

        #endregion
    }
    public partial class TreeViewItemEx : TreeViewItem
    {
        #region Overrides

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

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is TreeViewItemEx;
        }
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Remove:
                    if (HasItems)
                    {
                        int newIndex = e.OldStartingIndex;
                        if (newIndex >= Items.Count)
                            newIndex = Items.Count - 1;
                        TreeViewItemEx item = ItemContainerGenerator.ContainerFromIndex(newIndex) as TreeViewItemEx;
                        item.IsSelected = true;
                    }
                    else
                        base.OnItemsChanged(e);
                    break;
                default:
                    base.OnItemsChanged(e);
                break;
            }
        }
        protected override void OnSelected(RoutedEventArgs e)
        {
            base.OnSelected(e);
            Focus();
        }

        #endregion
    }
}


来源:https://stackoverflow.com/questions/2030678/wpf-treeview-itemselected-moves-incorrectly-when-deleting-an-item

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!