2


0

アイテムを削除すると、選択したWPFツリービューアイテムが正しく移動しない

オブジェクトツリーにバインドされたツリービューがあります。 オブジェクトツリーからオブジェクトを削除すると、ツリービューから正しく削除されますが、ツリービューのデフォルトの動作では、選択したアイテムを削除したアイテムの親ノードまでジャンプします。 代わりに次の項目にジャンプするようにこれを変更するにはどうすればよいですか?

編集:

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メソッドの速度を落とさない限り機能しません。 たとえば、OnItemsChangedメソッドの下部にthread.sleep(500)を配置すると、機能しますが、そうでない場合は機能しません。 何がおかしいのですか? これは本当に奇妙です。

5 Answer


1


あなたが言及する動作は、 OnItemsChanged`と呼ばれる Selector`クラスの仮想メソッドによって制御されます(参照:http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.selector.onitemschanged .aspx [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 }));
    }
}

または、コードビハインドクラスの1つからコレクションの NotifyCollectionChanged`イベントにフックし、イベントが TreeView`に到達する前に現在の選択を明示的に変更することもできます(この解決策はわかりませんが、イベントデリゲートが呼び出される順序-`TreeView`は、実行する前にイベントを処理できる可能性がありますが、動作する可能性があります。


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`には、削除された TreeViewItem`の親にフォーカスを変更する `ReevaluateFocusCallback`プライベートまたは内部メソッドがあります。 これにより、親アイテムが選択される「GotFocus」イベントが発生します。 これはすべて、イベントハンドラーが戻った後、バックグラウンドで発生します。

溶液

通常、この場合、選択した TreeViewItem`を手動で .Focus() `するように指示します。 「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(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 GetVisualAncestorsOfType(DependencyObject obj) where T:DependencyObject
{
  for(; obj!=null; obj = VisualTreeHelper.GetParent(obj))
    if(obj is T)
      yield return (T)obj;
}

これは、 `Dispatcher.BeginInvoke`を使用するよりも信頼性が高いはずです。なぜなら、入力キューの順序、Dispatcherの優先順位などについて何も仮定せずにこの特定の問題を回避できるからです。


1


これは私のために機能します(上記の調査のおかげで)

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

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


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


0


上記の答えに基づいて、ここで私のために働いた解決策があります(モデルを介してアイテムを選択した後のフォーカスの喪失など、他のさまざまな問題も修正しました)

実際にトリックを行った* OnSelected *オーバーライド(下方向にスクロール)に注意してください。

これは、VS 3.5で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
    }
}