删除项目时,WPF树视图项目选择移动不正确

时间:2010-01-08 20:55:49

标签: c# wpf data-binding treeview selecteditem

我有一个绑定到对象树的树视图。当我从对象树中删除一个对象时,它会从树视图中正确删除,但树视图的默认行为是将selecteditem跳转到已删除项的父节点。如何改变这一点,以便它跳转到下一个项目?

编辑:

我用Aviad的建议更新了我的代码。这是我的代码..

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;
    }
}

除非我调试它,否则上面的代码不起作用,或者以某种方式减慢OnItemsChanged方法的速度。例如,如果我将一个thread.sleep(500)放在OnItemsChanged方法的底部,它就可以工作,否则就不行。知道我做错了什么吗?这真的很奇怪。

5 个答案:

答案 0 :(得分:1)

您提到的行为由名为Selector的{​​{1}}类中的虚拟方法控制(参考:Selector.OnItemsChanged Method) - 为了修改它,您应该从{{1并覆盖该函数。您可以使用反射器将您的实现基于现有实现,尽管它非常简单。

以下是使用反射器提取的树视图覆盖OnItemsChanged的代码:

TreeView

或者,您可以从一个代码隐藏类挂接到集合TreeView.OnItemsChanged事件,并在事件到达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 })); } } 之前显式更改当前选择(我不确定此解决方案)虽然因为我不确定调用事件委托的顺序 - NotifyCollectionChanged可能会在你做之前处理事件 - 但它可能有效。)

答案 1 :(得分:1)

原始回答

在我原来的回答中,我猜测你可能遇到了WPF中的一个错误,并为这种情况提供了一个通用的解决方法,即将item.IsSelected = true;替换为:

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

我解释说,这种解决方法在90%的时间内完成这一操作的原因是它会延迟选择,直到几乎所有当前操作都已完成处理。

当我真正尝试你在其他问题中发布的代码时,我发现它确实是WPF中的一个错误,但却找到了更直接,更可靠的解决方法。我将解释如何诊断问题,然后描述解决方法。

<强>诊断

我添加了一个带有断点的SelectedItemChanged处理程序,并查看了堆栈跟踪。这使问题显而易见。以下是堆栈跟踪的选定部分:

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

正如您所看到的,KeyboardDevice有一个ReevaluateFocusCallback私有或内部方法,可将焦点更改为已删除TreeViewItem的父级。这会导致GotFocus事件导致选择父项。这一切都发生在事件处理程序返回后的后台。

<强>解决方案

通常在这种情况下,我会告诉您只需手动.Focus()您选择的TreeViewItem。这里很难,因为在TreeView中没有简单的方法可以从任意数据项到相应的容器(每个级别都有单独的ItemContainerGenerators)。

所以我认为你最好的解决方案是强制关注到父节点(就在你不想让它结束的地方),然后设置 IsSelected 孩子的数据。这样输入管理器永远不会决定它需要自己移动焦点:它会发现焦点已经设置为有效的IInputElement

以下是一些代码:

      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;
        }
      }

这也需要这个辅助方法:

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;
}

这应该比使用Dispatcher.BeginInvoke更可靠,因为它可以解决这个特定问题,而无需对输入队列排序,Dispatcher优先级等做任何假设。

答案 2 :(得分:1)

这对我有用(感谢上面提供的调查)

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

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

答案 3 :(得分:0)

根据@Kirill提供的答案,我认为这个特定问题的正确答案是将以下代码添加到从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;
            }
        }
    }
}

答案 4 :(得分:0)

基于上面的答案,这里有适用于我的解决方案(它还修复了其他各种问题,例如通过模型选择项目后失去焦点等)。

请注意 OnSelected 覆盖(完全向下滚动),这实际上是诀窍。

这是在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
    }
}