WPF: выберите TreeViewItem, пробитый за корневой уровень

Я пытаюсь выбрать TreeViewItem по ID, но у меня проблемы с его работой за первый (корневой) уровень. Я сделал так много чтения об этом, и я использую метод ниже.

private static bool SetSelected(ItemsControl parent, INestable itemToSelect) {
    if(parent == null || itemToSelect == null) {
        return false;
    }
    foreach(INestable item in parent.Items) {
        if(item.ID == itemToSelect.ID) { // just comparing instances failed
            TreeViewItem container = parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
            if(container != null) {
                container.IsSelected = true;
                container.Focus();
                return true;
            }
        }
        ItemsControl childControl = parent.ItemContainerGenerator.ContainerFromItem(item) as ItemsControl;
        if(SetSelected(childControl, itemToSelect))
            return true;
    }
    return false;
}

INestable - это интерфейс базового уровня, реализованный IGroup и IAccount:

public interface INestable {
        string ID { get; set; }
    ...
}
public interface IAccount : INestable { 
    ...
}
public interface IGroup : INestable { 
    public IList<INestable> Children
    ...
}

Я думаю, что это должно иметь какое-то отношение к datatemplates (возможно):

<HierarchicalDataTemplate DataType="{x:Type loc:IGroup}" ItemsSource="{Binding Children}" x:Key="TreeViewGroupTemplate">
<HierarchicalDataTemplate DataType="{x:Type loc:IAccount}" x:Key="TreeViewAccountTemplate">

The Template selector for the treeview returns thr group template for IGroups and the account template for IAccounts:
<conv:TreeTemplateSelector x:Key="TreeTemplateSelector" AccountTemplate="{StaticResource TreeViewAccountTemplate}" GroupTemplate="{StaticResource TreeViewGroupTemplate}"/>
<TreeView ItemTemplateSelector="{StaticResource TreeTemplateSelector}">

Он работает для всех элементов верхнего уровня, только ничего ниже, а отладка подтверждает parent.ItemContainerGenerator содержит элементы для всех уровней.

Я знаю, что есть много кода, но я сжигаю часы, пытаясь заставить это работать. Спасибо за любую помощь.:)

Ответ 1

Проблема в том, что вложенные ItemContainerGenerators не генерируются в начале, они генерируются по требованию. И тем более, они генерируются в отдельном потоке, поэтому вы должны слушать StatusChanged на генераторе, чтобы убедиться, что он готов = (

Некоторые люди предлагают играть с Dispatcher (как в этом сообщении Bea). Я попытался реализовать решение Dispatcher, но по какой-то причине это не сработало... генераторы все еще пусты = (

Итак, я закончил с другим, где вы специально попросите дерево обновить его макет, что вызывает генерацию для расширенных узлов. Вот окончательный метод... вы можете проверить его немного, чтобы убедиться, что он соответствует вашим потребностям. Он может свернуть некоторые узлы, которые были расширены до запуска.

    private static bool SetSelected(TreeView treeView, ItemsControl parentControl, INestable itemToSelect)
    {
        if (parentControl == null || itemToSelect == null)
        {
            return false;
        }
        foreach (INestable item in parentControl.Items)
        {
            TreeViewItem container = parentControl.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;

            if (item.ID == itemToSelect.ID)
            { // just comparing instances failed
                    container.IsSelected = true;
                    container.Focus();
                    return true;
            }
            container.IsExpanded = true;
            treeView.UpdateLayout();
            WaitForPriority(DispatcherPriority.Background);
            if (SetSelected(treeView, container, itemToSelect))
                return true;
            else
                container.IsExpanded = false;
        }
        return false;
    }

Ответ 2

Я думаю, что это не работает, так как элемент свернут и его контейнер не создается. Таким образом, попытка выбрать TreeViewItem напрямую - определенно не лучший способ.

Вместо этого мы используем подход MVVM. Каждый объект viewmodel должен иметь свойство IsSelected. Затем вы привязываете TreeViewItem.IsSelected к нему.

В вашем случае это будет похоже на это

CS:

public interface INestable : INotifyPropertyChanged
{
  string ID { get; set; }

  // Make sure you invoke PropertyChanged in setter
  bool IsSelected { get; set; } 

  event PropertyChangedEventHandler PropertyChanged;
  ...
}

XAML:

<TreeView ...>
  <TreeView.ItemContainerStyle>
   <Style TargetType="{x:Type TreeViewItem}">
     <Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=TwoWay}" />
   </Style>
  </TreeView.ItemContainerStyle>
</TreeView>

Теперь вы можете пройти через свою модель и установить там свойство IsSelected.

Вы также можете отслеживать свойство IsExpanded таким же образом...

Чтобы получить дополнительную информацию о TreeView, прочитайте эту замечательную статью Джоша Смита: Упрощение WPF TreeView с помощью шаблона ViewModel

Надеюсь, что это поможет.

Ответ 3

Хотя принятый ответ будет работать большую часть времени. Возможно, это не работает, потому что объект создается в другом потоке, который не контролируется диспетчером.

Как упоминалось ранее, проблема заключается в том, что TreeViewItem создается в другом потоке из диспетчерского.

Я лично считаю, что правильное решение более сложное. Я знаю, что плохо слышать, но я действительно думаю, что это так. Мой код должен работать при виртуализации или нет. Кроме того, вы можете удалить любую ссылку, не необходимую (я не подтвердил).

Мое решение основано на модели данных, где каждый node наследует от одного и того же корня: объект MultiSimBase, но это не является обязательным требованием.

Все начинается с SetSelectedTreeViewItem(), которые активируют (+ задавать фокус и отображают) вновь добавленный элемент.

Надеюсь, что это может помочь или вдохновить некоторых... Счастливое кодирование!!!

Код формы:

    //  ******************************************************************
    private List<MultiSimBase> SetPathListFromRootToNode(MultiSimBase multiSimBase, List<MultiSimBase> listTopToNode = null)
    {
        if (listTopToNode == null)
        {
            listTopToNode = new List<MultiSimBase>();
        }

        listTopToNode.Insert(0, multiSimBase);
        if (multiSimBase.Parent != null)
        {
            SetPathListFromRootToNode(multiSimBase.Parent, listTopToNode);
        }

        return listTopToNode;
    }

    // ******************************************************************
    private void SetSelectedTreeViewItem(MultiSimBase multiSimBase)
    {
        List<MultiSimBase> listOfMultiSimBasePathFromRootToNode = SetPathListFromRootToNode(multiSimBase);

        TreeViewStudy.SetItemHierarchyVisible(listOfMultiSimBasePathFromRootToNode, (tvi) =>
                                                                                    {
                                                                                        tvi.IsSelected = true;
                                                                                        tvi.Focus();
                                                                                        tvi.BringIntoView();
                                                                                    });
    }

И теперь общий код:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;

namespace HQ.Util.Wpf.WpfUtil
{
    public static class TreeViewExtensions
    {
        public delegate void OnTreeViewVisible(TreeViewItem tvi);

        private static void SetItemHierarchyVisible(ItemContainerGenerator icg, IList listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
        {
            Debug.Assert(icg != null);

            if (icg != null)
            {
                if (listOfRootToNodePath.Count == 0) // nothing to do
                    return;

                TreeViewItem tvi = icg.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
                if (tvi != null) // Due to threading, always better to verify
                {
                    listOfRootToNodePath.RemoveAt(0);

                    if (listOfRootToNodePath.Count == 0)
                    {
                        if (onTreeViewVisible != null)
                            onTreeViewVisible(tvi);
                    }
                    else
                    {
                        if (!tvi.IsExpanded)
                            tvi.IsExpanded = true;

                        SetItemHierarchyVisible(tvi.ItemContainerGenerator, listOfRootToNodePath, onTreeViewVisible);
                    }
                }
                else
                {
                    ActionHolder actionHolder = new ActionHolder();
                    EventHandler itemCreated = delegate(object sender, EventArgs eventArgs)
                                    {
                                        var icgSender = sender as ItemContainerGenerator;
                                        tvi = icgSender.ContainerFromItem(listOfRootToNodePath[0]) as TreeViewItem;
                                        if (tvi != null) // Due to threading, it is always better to verify
                                        {
                                            SetItemHierarchyVisible(icg, listOfRootToNodePath, onTreeViewVisible);

                                            actionHolder.Execute();
                                        }
                                    };

                    actionHolder.Action = new Action(() => icg.StatusChanged -= itemCreated);
                    icg.StatusChanged += itemCreated;
                    return;
                }
            }
        }

        // ******************************************************************
        /// <summary>
        /// You cannot rely on this method to be synchronous. If you have any action that depend on the TreeViewItem 
        /// (last item of collectionOfRootToNodePath) to be visible, you should set it in the 'onTreeViewItemVisible' method.
        /// This method should work for Virtualized and non virtualized tree.
        /// </summary>
        /// <param name="treeView">TreeView where  an item has to be set visible</param>
        /// <param name="collectionOfRootToNodePath">Any of collection that implement ICollection like a generic List.
        /// The collection should have every objet of the path to the targeted item from the top to the target.
        /// For example for an apple tree: AppleTree (index 0), Branch4, SubBranch3, Leaf2 (index 3)</param>
        /// <param name="onTreeViewVisible">Optionnal</param>
        public static void SetItemHierarchyVisible(this TreeView treeView, IList listOfRootToNodePath, OnTreeViewVisible onTreeViewVisible = null)
        {
            ItemContainerGenerator icg = treeView.ItemContainerGenerator;
            if (icg == null)
                return; // Is tree loaded and initialized ???

            SetItemHierarchyVisible(icg, listOfRootToNodePath, onTreeViewVisible);
        }

и

    using System;

namespace HQ.Util.Wpf.WpfUtil
{
    // Requested to unsubscribe into an anonymous method that is a delegate used for a one time execution
    // http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/df2773eb-0cc1-4f3a-a674-e32f2ef2c3f1/
    public class ActionHolder
    {
        public void Execute()
        {
            if (Action != null)
            {
                Action();
            }
        }

        public Action Action { get; set; }
    }
}