WPF TreeView выбранный элемент перемещается неправильно при удалении элемента - PullRequest
2 голосов
/ 08 января 2010

У меня есть дерево, привязанное к дереву объектов. Когда я удаляю объект из дерева объектов, он корректно удаляется из представления дерева, но поведение дерева по умолчанию состоит в том, чтобы перепрыгивать выбранный элемент до родительского узла удаленного элемента. Как я могу изменить это, чтобы вместо этого перейти к следующему элементу?

EDIT:

Я обновил свой код по предложению Авиад. Вот мой код ..

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 ]

1 голос
/ 13 января 2010

Оригинальный ответ

В своем первоначальном ответе я догадался, что вы можете столкнуться с ошибкой в ​​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).

Так что я думаю, что ваше лучшее решение - это заставить focus к родительскому узлу (именно там, где вы не хотите, чтобы он заканчивался), затем установить 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, потому что оно будет обходить эту конкретную проблему, не делая никаких предположений об упорядочении входной очереди, приоритетах диспетчера и т. Д.

1 голос
/ 30 июня 2011

Это работает для меня (благодаря исследованиям, приведенным выше)

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

        if (e.Action == NotifyCollectionChangedAction.Remove)
        {
            Focus();
        }
    }
1 голос
/ 09 января 2010

Упомянутое вами поведение контролируется виртуальным методом в классе 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 }));
    }
}

Кроме того, вы можете подключиться к событию collection NotifyCollectionChanged из одного из ваших классов code-behind и явно изменить текущий выбор до того, как событие достигнет TreeView (хотя я не уверен в этом решении, потому что я не уверен в порядке, в котором вызываются делегаты события - TreeView может обработать событие раньше, чем вы, - но это может сработать).

0 голосов
/ 03 марта 2016

Основываясь на ответах выше, вот решение, которое сработало для меня (оно также исправило различные другие проблемы, такие как потеря фокуса после выбора элемента через модель и т.

Обратите внимание на 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
    }
}
0 голосов
/ 26 августа 2014

В соответствии с ответом, предоставленным @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;
            }
        }
    }
}
...