Как выбрать элемент TreeView из кода

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

Вот код, который я использую для выбора TreeViewItem. Я что-то упускаю?

public static void SetSelectedItem(this TreeView control, object item)
{
    try
    {
        var dObject = control.ItemContainerGenerator.ContainerFromItem(item);

        //uncomment the following line if UI updates are unnecessary
        ((TreeViewItem)dObject).IsSelected = true;

        MethodInfo selectMethod = typeof(TreeViewItem).GetMethod("Select",
            BindingFlags.NonPublic | BindingFlags.Instance);

        selectMethod.Invoke(dObject, new object[] { true });
    }
    catch { }
}

person Sergej Andrejev    schedule 02.06.2009    source источник
comment
TreeView от WPF жертвует каждым юзабилити, чтобы получить функции, которые большинству людей не нужны большую часть времени ...   -  person Roman Starkov    schedule 26.03.2012


Ответы (6)


Другой вариант - использовать привязку. Если у вас есть объект, с которым вы используете привязку для получения текста каждого TreeViewItem (например), вы можете создать стиль, который также связывает свойство IsSelected:

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

Это предполагает, что связанный объект имеет свойство IsSelected типа bool. Затем вы можете выбрать TreeViewItem, установив IsSelected на true для соответствующего объекта.

Тот же подход можно использовать со свойством IsExpanded для управления раскрытием или сворачиванием TreeViewItem.

person Andy    schedule 03.06.2009
comment
Да, я знал об этом. Но похоже, что это вводит связывание кода. В любом случае хорошо, что у вас есть здесь ответ. Люди, которые зайдут на эту страницу, могут предпочесть ваш путь моему - person Sergej Andrejev; 03.06.2009
comment
@Andy: Как я могу это сделать в Silverlight? Я пробую этот код получить ошибку Cannot set read-only property ''.. - person Navid Farhadi; 21.07.2011
comment
@Navid Я не уверен, сможешь ли ты. Я мало что сделал с Silverlight, но не думаю, что TreeViewItem.IsSelected является DependencyProperty в Silverlight. Вы не можете использовать привязку для свойств, которые не являются DependencyProperty. - person Andy; 21.07.2011
comment
Похоже, у меня не работает; программный выбор всегда автоматически сбрасывается на то, что было выбрано ранее, если до этого ничего не было выбрано. Я создал связанный вопрос с образец кода. - person O. R. Mapper; 09.12.2012
comment
@ Энди, спасибо. вы спасли меня. - person New Developer; 30.11.2014
comment
Для правильной работы этого кода независимо от того, выбран элемент уже или нет, важен Mode=TwoWay. - person hfann; 25.10.2020

Вы можете использовать следующее расширение TreeView, которое, как я считаю, является более простым решением:

public static class TreeViewExtension
{
    public static bool SetSelectedItem(this TreeView treeView, object item)
    {
        return SetSelected(treeView, item);
    }

    private static bool SetSelected(ItemsControl parent, object child)
    {
       if (parent == null || child == null)
          return false;

       TreeViewItem childNode = parent.ItemContainerGenerator
       .ContainerFromItem(child) as TreeViewItem;

       if (childNode != null)
       {
          childNode.Focus();
          return childNode.IsSelected = true;
       }

       if (parent.Items.Count > 0) 
       {
          foreach (object childItem in parent.Items)
          {
             ItemsControl childControl = parent
               .ItemContainerGenerator
               .ContainerFromItem(childItem) 
               as ItemsControl;

             if (SetSelected(childControl, child))
               return true;
          }
       }

      return false;
   }
}

Для получения дополнительной информации прочтите эту статью в блоге; http://decompile.it/blog/2008/12/11/selecting-an-item-in-a-treeview-in-wpf/

person kbisang    schedule 03.03.2015
comment
Работает и не требует изменения структуры данных, но совершенно неэффективен. - person Diogo Gomes; 29.03.2016
comment
Работает очень хорошо, спасибо, дружище! - person Bravo; 07.12.2018

Попробовав разные решения, я попал на этот сайт. Чжоу Юн показывает, как программно развернуть все узлы TreeView. В его методе есть две основные идеи:

  • ContainerFromItem вернет контейнер, только если элемент является прямым потомком элемента. В TreeView это означает, что будет возвращен только дочерний контейнер первого уровня, и вам нужно вызвать ContainerFromItem для дочернего TreeViewItem, чтобы получить контейнер со следующего уровня.
  • Для работы ContainerFromItem необходимо создать дочерние визуальные элементы TreeViewItem, и это происходит только при раскрытии TreeViewItem. Это означает, что для выбора TreeViewItem необходимо развернуть все элементы, предшествующие требуемому элементу. На практике это означает, что нам нужно будет указать путь к элементу, который мы хотим выбрать, а не только к элементу.

Вот код, который у меня получился

public static void SelectItem(this ItemsControl parentContainer, List<object> path)
{
    var head = path.First();
    var tail = path.GetRange(1, path.Count - 1);
    var itemContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(head) as TreeViewItem;

    if (itemContainer != null && itemContainer.Items.Count == 0)
    {
        itemContainer.IsSelected = true;

        var selectMethod = typeof(TreeViewItem).GetMethod("Select", BindingFlags.NonPublic | BindingFlags.Instance);
        selectMethod.Invoke(itemContainer, new object[] { true });
    }
    else if (itemContainer != null)
    {
        itemContainer.IsExpanded = true;

        if (itemContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
        {
            itemContainer.ItemContainerGenerator.StatusChanged += delegate
            {
                SelectItem(itemContainer, tail);
            };
        }
        else
        {
            SelectItem(itemContainer, tail);
        }
    }
}
person Sergej Andrejev    schedule 02.06.2009
comment
Кажется, мне нужно иметь элемент, который я хочу выбрать. Однако обычно этот самый объект недоступен; у вас просто есть идентификатор этого объекта, верно? Я не уверен, как вызвать ContainerFromItem, не имея фактического экземпляра элемента. - person O. R. Mapper; 07.12.2012

В моем случае (у меня была такая же проблема), но было неуместно использовать привязку к свойству IsSelected объекта Data, а также мне не удавалось легко получить путь к элементу дерева, поэтому следующий код отлично справился с задачей:

  private void SelectTreeViewItem(object item)
    {
        try
        {
            var tvi = GetContainerFromItem(this.MainRegion, item);

            tvi.Focus();
            tvi.IsSelected = true;

            var selectMethod =
                typeof(TreeViewItem).GetMethod("Select",
                System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

            selectMethod.Invoke(tvi, new object[] { true });
        }
        catch { }
    }

  private TreeViewItem GetContainerFromItem(ItemsControl parent, object item)
    {
        var found = parent.ItemContainerGenerator.ContainerFromItem(item);
        if (found == null)
        {
            for (int i = 0; i < parent.Items.Count; i++)
            {
                var childContainer = parent.ItemContainerGenerator.ContainerFromIndex(i) as ItemsControl;
                TreeViewItem childFound = null;
                if (childContainer != null && childContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                {
                    childContainer.ItemContainerGenerator.StatusChanged += (o, e) =>
                        {
                             childFound = GetContainerFromItem(childContainer, item);
                        };
                }
                else
                {
                     childFound = GetContainerFromItem(childContainer, item);                            
                }
                if (childFound != null)
                    return childFound;                 
            }
        }
        return found as TreeViewItem;
    }
person Mladen Nikolov    schedule 20.12.2013

Да, метод ContainerFromItem ничего не возвращает, даже когда вы вызываете его из прямого родительского TreeViewItem.

Возможно, вам придется немного переделать дизайн. Если вы создаете все как явный TreeViewItem, вы сможете сохранить ссылку на него и установить для него IsSelected.

person RandomEngy    schedule 02.06.2009

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

Чтобы это работало, основной ViewModel нужны элементы, свойство для текущего выбранного элемента и свойство команды, которое будет вызываться при изменении текущего выбранного элемента:

public class MainViewModel : ViewModelBase
{
    // the currently selected node, can be changed programmatically
    private Node _CurrentNode;
    public Node CurrentNode
    {
        get { return this._CurrentNode; }
        set { this._CurrentNode = value; RaisePropertyChanged(() => this.CurrentNode); }
    }

    // called when the user selects a new node in the tree view
    public ICommand SelectedNodeChangedCommand { get { return new RelayCommand<Node>(OnSelectedNodeChanged); } }
    private void OnSelectedNodeChanged(Node node)
    {
        this.CurrentNode = node;
    }

    // list of items to display in the tree view
    private ObservableCollection<Node> _Items;
    public ObservableCollection<Node> Items
    {
        get { return this._Items; }
        set { this._Items = value; RaisePropertyChanged(() => this.Items); }
    }
}

TreeView требуется триггер события для вызова SelectedNodeChangedCommand при изменении выбора и DataTrigger в стиле TreeViewItem, чтобы элементы управления выбирались при программном изменении значения CurrentNode в коде:

<TreeView x:Name="treeView" ItemsSource="{Binding Items}"
            xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
            xmlns:cmd ="http://www.galasoft.ch/mvvmlight">
        <TreeView.Resources>

            <conv:EqualityConverter x:Key="EqualityConverter" />

            <Style TargetType="TreeViewItem">
                <Setter Property="IsExpanded" Value="True" />
                <Setter Property="IsSelected" Value="False" />
                <Style.Triggers>
                    <!-- DataTrigger updates TreeViewItem selection when vm code changes CurrentNode -->
                    <DataTrigger Value="True">
                        <DataTrigger.Binding>
                            <MultiBinding Converter="{StaticResource EqualityConverter}">
                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type TreeView}}" Path="DataContext.CurrentNode" />
                                <Binding />
                            </MultiBinding>
                        </DataTrigger.Binding>
                        <Setter Property="IsSelected" Value="True" />
                    </DataTrigger>
                </Style.Triggers>
            </Style>


            <!-- *** HierarchicalDataTemplates go here ***  -->

        </TreeView.Resources>

        <!-- EventTrigger invokes SelectedNodeChangedCommand when selection is changed by user interaction -->
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="SelectedItemChanged">
                <cmd:EventToCommand Command="{Binding SelectedNodeChangedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=TreeView}, Path=SelectedItem}"  />
            </i:EventTrigger>
        </i:Interaction.Triggers>

    </TreeView>

DataTrigger работает, определяя, когда значение CurrentNode совпадает с Node для текущего элемента списка. К сожалению, DataTriggers не может привязать свое значение, поэтому вместо этого он должен тестировать EqualityConverter, который просто выполняет простое сравнение:

    public class EqualityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values[0] == values[1];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
person Mark Feldman    schedule 06.08.2015