2

我有一个绑定到对象树的树视图。当我从对象树中删除一个对象时,它会从树视图中正确删除,但树视图的默认行为是将选定项向上跳转到已删除项的父节点。我怎样才能改变它,让它跳到下一个项目呢?

编辑:

我根据 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 方法的底部,它可以工作,否则它不会。知道我做错了什么吗?这真的很奇怪。

4

5 回答 5

2

您提到的行为由Selector名为OnItemsChanged(参考:)的类中的虚拟方法控制Selector.OnItemsChanged Method- 为了修改它,您应该从该函数派生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事件,并在事件到达之前显式更改当前选择TreeView(我不确定这个解决方案,因为我不确定事件委托的顺序被调用 -TreeView可能会在你之前处理事件 - 但它可能会起作用)。

于 2010-01-08T22:19:28.443 回答
2

原始答案

在我最初的回答中,我猜您可能在 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因为它将解决这个特定问题,而无需对输入队列顺序、调度程序优先级等进行任何假设。

于 2010-01-13T02:52:03.363 回答
2

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

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

        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            Focus();
        }
    }
于 2011-06-30T07:01:55.213 回答
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;
            }
        }
    }
}
于 2014-08-26T12:42:28.550 回答
0

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

请注意实际上起到了作用的OnSelected覆盖(一直向下滚动)。

这是在 VS2015 中为 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
    }
}
于 2016-03-03T03:04:17.217 回答