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 メソッドを遅くしない限り機能しません。たとえば、OnItemsChanged メソッドの最後に thread.sleep(500) を配置すると機能しますが、それ以外の場合は機能しません。私が間違っていることは何か分かりますか?これは本当に奇妙です。

4

5 に答える 5

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 に答える
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ます。これにより、親アイテムが選択されるイベントが発生します。これはすべて、イベント ハンドラーが戻った後にバックグラウンドで行われます。ReevaluateFocusCallbackTreeViewItemGotFocus

解決

通常、この場合、手動で選択するように指示し.Focus()ますTreeViewItemTreeViewここでは、任意のデータ項目から対応するコンテナーに簡単に取得する方法がないため、これは困難です(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 の優先順位などについて何の想定もせずにこの特定の問題を回避できるため、使用するよりも信頼性が高いはずです。

于 2010-01-13T02:52:03.363 に答える
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または、コード ビハインド クラスの 1 つからコレクション イベントにフックし、イベントが に到達する前に現在の選択を明示的に変更するTreeViewこともできます (ただし、イベント デリゲートの順序がわからないため、この解決策はわかりません)。が呼び出されTreeViewます - あなたが行う前にイベントを処理するかもしれません - しかし、それはうまくいくかもしれません)。

于 2010-01-08T22:19:28.443 に答える
0

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

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

これは、Net 3.5 用に VS2015 でコンパイルされました。

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 に答える