Привязка данных к SelectedItem в древовидном представлении WPF

241

Как я могу получить элемент, выбранный в WPF-дереве? Я хочу сделать это в XAML, потому что я хочу связать это.

Вы можете подумать, что это так, SelectedItemно, видимо, не существует он доступен только для чтения и поэтому непригоден для использования.

Вот что я хочу сделать:

<TreeView ItemsSource="{Binding Path=Model.Clusters}" 
            ItemTemplate="{StaticResource ClusterTemplate}"
            SelectedItem="{Binding Path=Model.SelectedCluster}" />

Я хочу связать SelectedItem к свойству на моей модели.

Но это дает мне ошибку:

Свойство SelectedItem доступно только для чтения и не может быть установлено из разметки.

Редактировать: Хорошо, это способ, которым я решил это:

<TreeView
          ItemsSource="{Binding Path=Model.Clusters}" 
          ItemTemplate="{StaticResource HoofdCLusterTemplate}"
          SelectedItemChanged="TreeView_OnSelectedItemChanged" />

и в codebehindfile моего xaml:

private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    Model.SelectedCluster = (Cluster)e.NewValue;
}
натрий
источник
51
Человек это отстой. Это меня тоже поразило. Я пришел сюда в надежде найти приличный путь и я просто идиот. Это первый раз, когда мне грустно, что я не идиот ..
Андрей Ринея
6
это действительно отстой и испортит обязательную концепцию
Delta
Надеюсь , что это может помочь кто - то связываться с древовидным выбранным элементом изменил вызов обратно на ICommand jacobaloysious.wordpress.com/2012/02/19/...
JACOB aloysious
9
С точки зрения связывания и MVVM, код позади не «запрещен», скорее код должен поддерживать представление. По моему мнению из всех других решений, которые я видел, код позади - намного лучший вариант, так как он все еще имеет дело с «связыванием» представления с моделью представления. Единственный минус в том, что если у вас есть команда, в которой работает дизайнер, работающий только на XAML, то код может быть поврежден / игнорирован. Это небольшая цена за решение, для реализации которого требуется 10 секунд.
nrjohnstone
Возможно, одно из самых простых решений: stackoverflow.com/questions/1238304/…
JoanComasFdz

Ответы:

240

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

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var item = e.NewValue as TreeViewItem;
        if (item != null)
        {
            item.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
    }

    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

Затем вы можете использовать это в своем XAML как:

<TreeView>
    <e:Interaction.Behaviors>
        <behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
    </e:Interaction.Behaviors>
</TreeView>

Надеюсь, это кому-нибудь поможет!

Стив Грейтрекс
источник
5
Как отметил Брент, мне также нужно было добавить Mode = TwoWay к привязке. Я не "Блендер", поэтому не был знаком с классом Behavior <> из System.Windows.Interactivity. Сборка является частью Expression Blend. Для тех, кто не хочет покупать / устанавливать пробную версию, чтобы получить эту сборку, вы можете скачать BlendSDK, который включает System.Windows.Interactivity. BlendSDK 3 для 3.5 ... Я думаю, что это BlendSDK 4 для 4.0. Примечание: это только позволяет вам получить, какой элемент выбран, но не позволяет вам установить выбранный элемент
Майк Роули,
4
Вы также можете заменить UIPropertyMetadata на FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Филиминджи
3
Это был бы подход к решению проблемы: stackoverflow.com/a/18700099/4227
bitbonk
2
@Lukas в точности как показано в фрагменте кода XAML выше. Просто замените {Binding SelectedItem, Mode=TwoWay}на{Binding MyViewModelField, Mode=TwoWay}
Стив Greatrex
4
@ Паскаль этоxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Стив Грейтрекс
46

Это свойство существует: TreeView.SelectedItem

Но это только для чтения, поэтому вы не можете назначить его через привязку, только получить его

Томас Левеск
источник
Я принимаю этот ответ, потому что там я нашел эту ссылку, которая позволяет мне ответить самостоятельно: msdn.microsoft.com/en-us/library/ms788714.aspx
Natrium,
1
Так можно ли это TreeView.SelectedItemповлиять на свойство модели, когда пользователь выбирает элемент (иначе OneWayToSource)?
Шимми Вайцхандлер
43

Ответьте с прикрепленными свойствами и без внешних зависимостей, если необходимость когда-либо возникнет!

Вы можете создать присоединяемое свойство, которое является привязываемым и имеет геттер и сеттер:

public class TreeViewHelper
{
    private static Dictionary<DependencyObject, TreeViewSelectedItemBehavior> behaviors = new Dictionary<DependencyObject, TreeViewSelectedItemBehavior>();

    public static object GetSelectedItem(DependencyObject obj)
    {
        return (object)obj.GetValue(SelectedItemProperty);
    }

    public static void SetSelectedItem(DependencyObject obj, object value)
    {
        obj.SetValue(SelectedItemProperty, value);
    }

    // Using a DependencyProperty as the backing store for SelectedItem.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewHelper), new UIPropertyMetadata(null, SelectedItemChanged));

    private static void SelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        if (!(obj is TreeView))
            return;

        if (!behaviors.ContainsKey(obj))
            behaviors.Add(obj, new TreeViewSelectedItemBehavior(obj as TreeView));

        TreeViewSelectedItemBehavior view = behaviors[obj];
        view.ChangeSelectedItem(e.NewValue);
    }

    private class TreeViewSelectedItemBehavior
    {
        TreeView view;
        public TreeViewSelectedItemBehavior(TreeView view)
        {
            this.view = view;
            view.SelectedItemChanged += (sender, e) => SetSelectedItem(view, e.NewValue);
        }

        internal void ChangeSelectedItem(object p)
        {
            TreeViewItem item = (TreeViewItem)view.ItemContainerGenerator.ContainerFromItem(p);
            item.IsSelected = true;
        }
    }
}

Добавьте объявление пространства имен, содержащее этот класс, в ваш XAML и выполните привязку следующим образом (local - это то, как я назвал объявление пространства имен):

        <TreeView ItemsSource="{Binding Path=Root.Children}" local:TreeViewHelper.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}">

    </TreeView>

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

Bas
источник
4
+1, лучший ответ в этой теме imho. Не зависит от System.Windows.Interactivity и допускает двустороннюю привязку (устанавливается программно в среде MVVM). Отлично.
Крис Рэй
5
Проблема этого подхода заключается в том, что поведение начнет работать только после того, как выбранный элемент будет установлен один раз через привязку (то есть из ViewModel). Если начальное значение в виртуальной машине равно нулю, то привязка не будет обновлять значение DP и поведение не будет активировано. Вы можете исправить это, используя другой выбранный по умолчанию элемент (например, недопустимый элемент).
Марк
6
@Mark: просто используйте new object () вместо нулевого значения выше при создании экземпляра UIPropertyMetadata присоединенного свойства. Проблема должна исчезнуть тогда ...
barnacleboy
2
Я предполагаю, что приведение к TreeViewItem завершается неудачно, поскольку я использую шаблон HierarchicalDataTemplate, примененный к ресурсам по типу данных. Но если вы удалите ChangeSelectedItem, то привязка к модели представления и получение элемента будут работать нормально.
Кейси Себбен
1
У меня также возникают проблемы с приведением к TreeViewItem. В этот момент ItemContainerGenerator содержит только ссылки на корневые элементы, но мне нужно, чтобы он также мог получать элементы без полномочий root. Если вы передадите ссылку на один из них, произойдет сбой приведения и будет возвращен ноль. Не уверен, как это можно исправить?
Боб Tway
39

Ну, я нашел решение. Он перемещает беспорядок, так что MVVM работает.

Сначала добавьте этот класс:

public class ExtendedTreeView : TreeView
{
    public ExtendedTreeView()
        : base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(___ICH);
    }

    void ___ICH(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (SelectedItem != null)
        {
            SetValue(SelectedItem_Property, SelectedItem);
        }
    }

    public object SelectedItem_
    {
        get { return (object)GetValue(SelectedItem_Property); }
        set { SetValue(SelectedItem_Property, value); }
    }
    public static readonly DependencyProperty SelectedItem_Property = DependencyProperty.Register("SelectedItem_", typeof(object), typeof(ExtendedTreeView), new UIPropertyMetadata(null));
}

и добавьте это в свой xaml:

 <local:ExtendedTreeView ItemsSource="{Binding Items}" SelectedItem_="{Binding Item, Mode=TwoWay}">
 .....
 </local:ExtendedTreeView>
дельта
источник
3
Это единственная вещь, которая приблизилась к работе на меня до сих пор. Мне очень нравится это решение.
Рэйчел
1
Не знаю почему, но у меня это не сработало :( Мне удалось получить выбранный элемент из дерева, но не наоборот - изменить выбранный элемент из-за пределов дерева.
Erez
Было бы немного лучше установить свойство зависимости как BindsTwoWayByDefault, тогда вам не нужно было бы указывать TwoWay в XAML
Стивен Холт,
Это лучший подход. Он не использует ссылку на интерактивность, он не использует код позади, он не имеет утечки памяти, как некоторые поведения. Спасибо.
Александру Дику
Как уже упоминалось, это решение не работает с 2-х сторонней привязкой. Если вы установите значение в viewmodel, изменение не распространяется на TreeView.
Ричард Мур
25

Он отвечает немного больше, чем ожидает OP ... Но я надеюсь, что он может помочь кому-то, по крайней мере.

Если вы хотите выполнить ICommandвсякий раз, когда SelectedItemизменяется, вы можете привязать команду к событию, и использование свойства SelectedItemв ViewModelбольше не требуется.

Для этого:

1- Добавить ссылку на System.Windows.Interactivity

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

2- Привязать команду к событию SelectedItemChanged

<TreeView x:Name="myTreeView" Margin="1"
            ItemsSource="{Binding Directories}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <i:InvokeCommandAction Command="{Binding SomeCommand}"
                                   CommandParameter="
                                            {Binding ElementName=myTreeView
                                             ,Path=SelectedItem}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <TreeView.ItemTemplate>
           <!-- ... -->
    </TreeView.ItemTemplate>
</TreeView>
JiBéDoublevé
источник
3
Ссылка System.Windows.Interactivityможет быть установлена ​​из NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li
Я пытался решить эту проблему часами, я реализовал это, но моя команда не работает, не могли бы вы помочь мне?
Алфи
1
Microsoft представила XAML Behaviors для WPF в конце 2018 года. Его можно использовать вместо System.Windows.Interactivity. Это сработало для меня (пробовал с проектом .NET Core). Чтобы все это настроить, просто добавьте пакет nuget Microsoft.Xaml.Behaviors.Wpf и измените пространство имен на xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Чтобы получить больше информации - смотрите блог
rychlmoj
19

Это может быть выполнено «более приятным» способом, используя только привязку и EventToCommand библиотеки GalaSoft MVVM Light. В вашей виртуальной машине добавьте команду, которая будет вызываться при изменении выбранного элемента, и инициализируйте команду для выполнения любых необходимых действий. В этом примере я использовал RelayCommand и просто установлю свойство SelectedCluster.

public class ViewModel
{
    public ViewModel()
    {
        SelectedClusterChanged = new RelayCommand<Cluster>( c => SelectedCluster = c );
    }

    public RelayCommand<Cluster> SelectedClusterChanged { get; private set; } 

    public Cluster SelectedCluster { get; private set; }
}

Затем добавьте поведение EventToCommand в ваш xaml. Это действительно легко, используя смесь.

<TreeView
      x:Name="lstClusters"
      ItemsSource="{Binding Path=Model.Clusters}" 
      ItemTemplate="{StaticResource HoofdCLusterTemplate}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding SelectedClusterChanged}" CommandParameter="{Binding ElementName=lstClusters,Path=SelectedValue}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TreeView>
bstoney
источник
Это хорошее решение, особенно если вы уже используете инструментарий MvvmLight. Однако это не решает проблему установки выбранного узла, и древовидная структура обновляет выбор.
keft
12

Все к сложному ... Перейти с Caliburn Micro (http://caliburnmicro.codeplex.com/)

Посмотреть:

<TreeView Micro:Message.Attach="[Event SelectedItemChanged] = [Action SetSelectedItem($this.SelectedItem)]" />

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 
Devgig
источник
5
Да ... а где та часть, которая устанавливает SelectedItem в TreeView ?
13
Калибурн хорош и элегантен. Работает довольно легко для вложенных иерархий
Purusartha
8

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

Мотивация для привязки состоит в том, чтобы держать это хорошим & MVVM. Вероятное использование ViewModel состоит в том, чтобы иметь свойство с именем, таким как «CurrentThingy», и где-то еще, DataContext для некоторой другой вещи привязан к «CurrentThingy».

Вместо того, чтобы выполнять дополнительные необходимые шаги (например, настраиваемое поведение, сторонний контроль) для поддержки приятного связывания TreeView с моей моделью, а затем с чего-то другого с моей моделью, мое решение заключалось в использовании простого Element, связывающего другую вещь с TreeView.SelectedItem, вместо того, чтобы связывать другую вещь с моей ViewModel, тем самым пропуская дополнительную работу, необходимую.

XAML:

<TreeView x:Name="myTreeView" ItemsSource="{Binding MyThingyCollection}">
.... stuff
</TreeView>

<!-- then.. somewhere else where I want to see the currently selected TreeView item: -->

<local:MyThingyDetailsView 
       DataContext="{Binding ElementName=myTreeView, Path=SelectedItem}" />

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

Wes
источник
1
Что является локальным: MyThingyDetailsView? Я получаю это локально: MyThingyDetailsView содержит выбранный элемент, но как ваша модель представления получает эту информацию? Это выглядит как хороший, чистый способ сделать это, но мне нужно немного больше информации ...
Боб Хорн
local: MyThingyDetailsView - это просто UserControl, полный XAML, составляющий подробное представление об одном «штуковинном» экземпляре. Он встроен в середину другого представления в виде содержимого, а DataContext этого представления является текущим выбранным элементом древовидного представления с использованием привязки элемента.
Уэс
6

Вы также можете использовать свойство TreeViewItem.IsSelected

nabeelfarid
источник
Я думаю, что это может быть правильный ответ. Но я хотел бы увидеть пример или рекомендацию о том, как свойство IsSelected элементов передается в TreeView.
Anhoppe
3

Существует также способ создания привязываемого свойства XAML SelectedItem без использования Interaction.Behaviors.

public static class BindableSelectedItemHelper
{
    #region Properties

    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(BindableSelectedItemHelper),
        new FrameworkPropertyMetadata(null, OnSelectedItemPropertyChanged));

    public static readonly DependencyProperty AttachProperty = DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(BindableSelectedItemHelper), new PropertyMetadata(false, Attach));

    private static readonly DependencyProperty IsUpdatingProperty = DependencyProperty.RegisterAttached("IsUpdating", typeof(bool), typeof(BindableSelectedItemHelper));

    #endregion

    #region Implementation

    public static void SetAttach(DependencyObject dp, bool value)
    {
        dp.SetValue(AttachProperty, value);
    }

    public static bool GetAttach(DependencyObject dp)
    {
        return (bool)dp.GetValue(AttachProperty);
    }

    public static string GetSelectedItem(DependencyObject dp)
    {
        return (string)dp.GetValue(SelectedItemProperty);
    }

    public static void SetSelectedItem(DependencyObject dp, object value)
    {
        dp.SetValue(SelectedItemProperty, value);
    }

    private static bool GetIsUpdating(DependencyObject dp)
    {
        return (bool)dp.GetValue(IsUpdatingProperty);
    }

    private static void SetIsUpdating(DependencyObject dp, bool value)
    {
        dp.SetValue(IsUpdatingProperty, value);
    }

    private static void Attach(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            if ((bool)e.OldValue)
                treeListView.SelectedItemChanged -= SelectedItemChanged;

            if ((bool)e.NewValue)
                treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void OnSelectedItemPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            treeListView.SelectedItemChanged -= SelectedItemChanged;

            if (!(bool)GetIsUpdating(treeListView))
            {
                foreach (TreeViewItem item in treeListView.Items)
                {
                    if (item == e.NewValue)
                    {
                        item.IsSelected = true;
                        break;
                    }
                    else
                       item.IsSelected = false;                        
                }
            }

            treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void SelectedItemChanged(object sender, RoutedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            SetIsUpdating(treeListView, true);
            SetSelectedItem(treeListView, treeListView.SelectedItem);
            SetIsUpdating(treeListView, false);
        }
    }
    #endregion
}

Затем вы можете использовать это в своем XAML как:

<TreeView  helper:BindableSelectedItemHelper.Attach="True" 
           helper:BindableSelectedItemHelper.SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
Павел Соломенчук
источник
3

Я перепробовал все решения этих вопросов. Никто не решил мою проблему полностью. Поэтому я думаю, что лучше использовать такой унаследованный класс с переопределенным свойством SelectedItem. Это будет отлично работать, если вы выберете элемент дерева из GUI и если вы установите это значение свойства в своем коде

public class TreeViewEx : TreeView
{
    public TreeViewEx()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(TreeViewEx_SelectedItemChanged);
    }

    void TreeViewEx_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }

    #region SelectedItem

    /// <summary>
    /// Gets or Sets the SelectedItem possible Value of the TreeViewItem object.
    /// </summary>
    public new object SelectedItem
    {
        get { return this.GetValue(TreeViewEx.SelectedItemProperty); }
        set { this.SetValue(TreeViewEx.SelectedItemProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public new static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(TreeViewEx),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedItemProperty_Changed));

    static void SelectedItemProperty_Changed(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        TreeViewEx targetObject = dependencyObject as TreeViewEx;
        if (targetObject != null)
        {
            TreeViewItem tvi = targetObject.FindItemNode(targetObject.SelectedItem) as TreeViewItem;
            if (tvi != null)
                tvi.IsSelected = true;
        }
    }                                               
    #endregion SelectedItem   

    public TreeViewItem FindItemNode(object item)
    {
        TreeViewItem node = null;
        foreach (object data in this.Items)
        {
            node = this.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (node != null)
            {
                if (data == item)
                    break;
                node = FindItemNodeInChildren(node, item);
                if (node != null)
                    break;
            }
        }
        return node;
    }

    protected TreeViewItem FindItemNodeInChildren(TreeViewItem parent, object item)
    {
        TreeViewItem node = null;
        bool isExpanded = parent.IsExpanded;
        if (!isExpanded) //Can't find child container unless the parent node is Expanded once
        {
            parent.IsExpanded = true;
            parent.UpdateLayout();
        }
        foreach (object data in parent.Items)
        {
            node = parent.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (data == item && node != null)
                break;
            node = FindItemNodeInChildren(node, item);
            if (node != null)
                break;
        }
        if (node == null && parent.IsExpanded != isExpanded)
            parent.IsExpanded = isExpanded;
        if (node != null)
            parent.IsExpanded = true;
        return node;
    }
} 
Евгений Бечкало
источник
Было бы намного быстрее, если бы UpdateLayout () и IsExpanded не вызывались для некоторых узлов. Когда не нужно вызывать UpdateLayout () и IsExpanded? Когда элемент дерева был посещен ранее. Как узнать это? ContainerFromItem () возвращает ноль для не посещенных узлов. Таким образом, мы можем расширить родительский узел только тогда, когда ContainerFromItem () возвращает null для детей.
CoperNick
3

Моим требованием было решение на основе PRISM-MVVM, где требовалось TreeView, а связанный объект имеет тип Collection <> и, следовательно, нуждается в HierarchicalDataTemplate. BindableSelectedItemBehavior по умолчанию не сможет идентифицировать дочерний TreeViewItem. Чтобы это работало в этом сценарии.

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehavior;
        if (behavior == null) return;
        var tree = behavior.AssociatedObject;
        if (tree == null) return;
        if (e.NewValue == null)
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);
        var treeViewItem = e.NewValue as TreeViewItem;
        if (treeViewItem != null)
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            if (itemsHostProperty == null) return;
            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;
            if (itemsHost == null) return;
            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
            {
                if (WalkTreeViewItem(item, e.NewValue)) 
                    break;
            }
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue)
    {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }
        var itemsHostProperty = treeViewItem.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (itemsHostProperty == null) return false;
        var itemsHost = itemsHostProperty.GetValue(treeViewItem, null) as Panel;
        if (itemsHost == null) return false;
        foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
        {
            if (WalkTreeViewItem(item, selectedValue))
                break;
        }
        return false;
    }
    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

Это позволяет перебирать все элементы независимо от уровня.

Чайтанья Кадамати
источник
Спасибо! Это был единственный вариант, который подходит для моего сценария, который мало чем отличается от вашего.
Роберт
Работает очень хорошо, и не вызывают некоторые / расширенные привязки , чтобы спутать .
Расти
2

Я предлагаю дополнение к поведению, предоставленному Стивом Грейтрексом. Его поведение не отражает изменений из источника, потому что это не может быть коллекция TreeViewItems. Таким образом, нужно найти TreeViewItem в дереве, для которого datacontext является выбранным значением из источника. TreeView имеет защищенное свойство с именем ItemsHost, которое содержит коллекцию TreeViewItem. Мы можем пройти через отражение и пройтись по дереву в поисках выбранного элемента.

private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehaviour;

        if (behavior == null) return;

        var tree = behavior.AssociatedObject;

        if (tree == null) return;

        if (e.NewValue == null) 
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);

        var treeViewItem = e.NewValue as TreeViewItem; 
        if (treeViewItem != null)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

            if (itemsHostProperty == null) return;

            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;

            if (itemsHost == null) return;

            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
                if (WalkTreeViewItem(item, e.NewValue)) break;
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue) {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }

        foreach (var item in treeViewItem.Items.OfType<TreeViewItem>())
            if (WalkTreeViewItem(item, selectedValue)) return true;

        return false;
    }

Таким образом, поведение работает для двусторонних привязок. В качестве альтернативы, можно переместить получение ItemsHost в метод OnAttached поведения, сохраняя издержки на использование отражения каждый раз, когда привязка обновляется.

Артур Нуньес
источник
2

WPF MVVM TreeView SelectedItem

... является лучшим ответом, но не упоминает способ получить / установить SelectedItem во ViewModel.

  1. Добавьте логическое свойство IsSelected в ItemViewModel и свяжите его с ним в установщике стилей для TreeViewItem.
  2. Добавьте свойство SelectedItem в вашу ViewModel, используемую в качестве DataContext для TreeView. Это недостающая часть в решении выше.
    'ItemVM ...
    Публичная собственность выбрана как логическая
        Получить
            Вернуться _func.SelectedNode Is Me
        Конец получить
        Set (значение As Boolean)
            Если значение IsSelected, то
                _func.SelectedNode = If (значение, я, ничего)
            End If
            RaisePropertyChange ()
        Конец набора
    Конечная недвижимость
    'TreeVM ...
    Открытое свойство SelectedItem As ItemVM
        Получить
            Возврат _selectedItem
        Конец получить
        Установить (значение как ItemVM)
            Если _selectedItem является значением, то
                Возвращение
            End If
            Dim prev = _selectedItem
            _selectedItem = значение
            Если предыдущая не ничего
                prev.IsSelected = False
            End If
            Если _selectedItem не ничего, то
                _selectedItem.IsSelected = True
            End If
        Конец набора
    Конечная недвижимость
<TreeView ItemsSource="{Binding Path=TreeVM}" 
          BorderBrush="Transparent">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>
JustinMichel
источник
1

После изучения Интернета в течение дня я нашел свое собственное решение для выбора элемента после создания нормального дерева в обычной среде WPF / C #

private void BuildSortTree(int sel)
        {
            MergeSort.Items.Clear();
            TreeViewItem itTemp = new TreeViewItem();
            itTemp.Header = SortList[0];
            MergeSort.Items.Add(itTemp);
            TreeViewItem prev;
            itTemp.IsExpanded = true;
            if (0 == sel) itTemp.IsSelected= true;
            prev = itTemp;
            for(int i = 1; i<SortList.Count; i++)
            {

                TreeViewItem itTempNEW = new TreeViewItem();
                itTempNEW.Header = SortList[i];
                prev.Items.Add(itTempNEW);
                itTempNEW.IsExpanded = true;
                if (i == sel) itTempNEW.IsSelected = true;
                prev = itTempNEW ;
            }
        }
карма
источник
1

Это также можно сделать с помощью свойства IsSelected элемента TreeView. Вот как мне это удалось,

public delegate void TreeviewItemSelectedHandler(TreeViewItem item);
public class TreeViewItem
{      
  public static event TreeviewItemSelectedHandler OnItemSelected = delegate { };
  public bool IsSelected 
  {
    get { return isSelected; }
    set 
    { 
      isSelected = value;
      if (value)
        OnItemSelected(this);
    }
  }
}

Затем в ViewModel, которая содержит данные, к которым привязан TreeView, просто подпишитесь на событие в классе TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

И, наконец, реализовать этот обработчик в той же ViewModel,

private void TreeViewItemSelected(TreeViewItem item)
{
  //Do something
}

И обязательна, конечно,

<Setter Property="IsSelected" Value="{Binding IsSelected}" />    
Фахад Оваис
источник
Это на самом деле недооцененное решение. Изменяя свой образ мышления и привязывая свойство IsSelected каждого элемента древовидной структуры, и запуская события IsSelected, вы получаете возможность использовать встроенную функциональность, которая хорошо работает с двухсторонним связыванием. Я перепробовал много предложенных решений этой проблемы, и это первое, что сработало. Просто немного сложнее подключиться. Спасибо.
Ричард Мур
1

Я знаю, что этой теме 10 лет, но проблема все еще существует ....

Первоначальный вопрос был «извлечь» выбранный элемент. Мне также нужно было «получить» выбранный элемент в моей модели просмотра (не установить его). Из всех ответов в этой теме ответ «Уэс» является единственным, который по-разному подходит к проблеме: если вы можете использовать «Выбранный элемент» в качестве цели для привязки данных, используйте его в качестве источника для привязки данных. Уэс сделал это с другим свойством view, я сделаю это с свойством viewmodel:

Нам нужны две вещи:

  • Создайте свойство зависимости в viewmodel (в моем случае типа «MyObject», так как мое древовидное представление связано с объектом типа «MyObject»)
  • Привязать из Treeview.SelectedItem к этому свойству в конструкторе View (да, это код позади, но, вероятно, вы также будете инициализировать свой datacontext)

ViewModel:

public static readonly DependencyProperty SelectedTreeViewItemProperty = DependencyProperty.Register("SelectedTreeViewItem", typeof(MyObject), typeof(MyViewModel), new PropertyMetadata(OnSelectedTreeViewItemChanged));

    private static void OnSelectedTreeViewItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as MyViewModel).OnSelectedTreeViewItemChanged(e);
    }

    private void OnSelectedTreeViewItemChanged(DependencyPropertyChangedEventArgs e)
    {
        //do your stuff here
    }

    public MyObject SelectedWorkOrderTreeViewItem
    {
        get { return (MyObject)GetValue(SelectedTreeViewItemProperty); }
        set { SetValue(SelectedTreeViewItemProperty, value); }
    }

Просмотр конструктора:

Binding binding = new Binding("SelectedItem")
        {
            Source = treeView, //name of tree view in xaml
            Mode = BindingMode.OneWay
        };

        BindingOperations.SetBinding(DataContext, MyViewModel.SelectedTreeViewItemProperty, binding);
Nils
источник
0

(Давайте просто согласимся с тем, что TreeView явно обанкротился в связи с этой проблемой. Привязка к SelectedItem была бы очевидна. Вздох )

Мне нужно было решение для правильного взаимодействия со свойством IsSelected TreeViewItem, поэтому вот как я это сделал:

// the Type CustomThing needs to implement IsSelected with notification
// for this to work.
public class CustomTreeView : TreeView
{
    public CustomThing SelectedCustomThing
    {
        get
        {
            return (CustomThing)GetValue(SelectedNode_Property);
        }
        set
        {
            SetValue(SelectedNode_Property, value);
            if(value != null) value.IsSelected = true;
        }
    }

    public static DependencyProperty SelectedNode_Property =
        DependencyProperty.Register(
            "SelectedCustomThing",
            typeof(CustomThing),
            typeof(CustomTreeView),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.None,
                SelectedNodeChanged));

    public CustomTreeView(): base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(SelectedItemChanged_CustomHandler);
    }

    void SelectedItemChanged_CustomHandler(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SetValue(SelectedNode_Property, SelectedItem);
    }

    private static void SelectedNodeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as CustomTreeView;
        var newNode = e.NewValue as CustomThing;

        treeView.SelectedCustomThing = (CustomThing)e.NewValue;
    }
}

С этим XAML:

<local:CustonTreeView ItemsSource="{Binding TreeRoot}" 
    SelectedCustomThing="{Binding SelectedNode,Mode=TwoWay}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
        </Style>
    </TreeView.ItemContainerStyle>
</local:CustonTreeView>
Эрик Йоргенсен
источник
0

Я предлагаю вам свое решение, которое предлагает следующие функции:

  • Поддерживает 2 способа привязки

  • Автоматически обновляет свойства TreeViewItem.IsSelected (согласно SelectedItem)

  • Нет TreeView подклассов

  • Элементы, привязанные к ViewModel, могут быть любого типа (даже нулевые)

1 / Вставьте следующий код в вашу CS:

public class BindableSelectedItem
{
    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached(
        "SelectedItem", typeof(object), typeof(BindableSelectedItem), new PropertyMetadata(default(object), OnSelectedItemPropertyChangedCallback));

    private static void OnSelectedItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as TreeView;
        if (treeView != null)
        {
            BrowseTreeViewItems(treeView, tvi =>
            {
                tvi.IsSelected = tvi.DataContext == e.NewValue;
            });
        }
        else
        {
            throw new Exception("Attached property supports only TreeView");
        }
    }

    public static void SetSelectedItem(DependencyObject element, object value)
    {
        element.SetValue(SelectedItemProperty, value);
    }

    public static object GetSelectedItem(DependencyObject element)
    {
        return element.GetValue(SelectedItemProperty);
    }

    public static void BrowseTreeViewItems(TreeView treeView, Action<TreeViewItem> onBrowsedTreeViewItem)
    {
        var collectionsToVisit = new System.Collections.Generic.List<Tuple<ItemContainerGenerator, ItemCollection>> { new Tuple<ItemContainerGenerator, ItemCollection>(treeView.ItemContainerGenerator, treeView.Items) };
        var collectionIndex = 0;
        while (collectionIndex < collectionsToVisit.Count)
        {
            var itemContainerGenerator = collectionsToVisit[collectionIndex].Item1;
            var itemCollection = collectionsToVisit[collectionIndex].Item2;
            for (var i = 0; i < itemCollection.Count; i++)
            {
                var tvi = itemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
                if (tvi == null)
                {
                    continue;
                }

                if (tvi.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
                {
                    collectionsToVisit.Add(new Tuple<ItemContainerGenerator, ItemCollection>(tvi.ItemContainerGenerator, tvi.Items));
                }

                onBrowsedTreeViewItem(tvi);
            }

            collectionIndex++;
        }
    }

}

2 / Пример использования в вашем файле XAML

<TreeView myNS:BindableSelectedItem.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" />  
Kino101
источник
0

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

Обратите внимание, что изменение выбранного элемента из ViewModel не будет обновлять выбранный элемент View.

public class TreeViewEx : TreeView
{
    public static readonly DependencyProperty SelectedItemExProperty = DependencyProperty.Register("SelectedItemEx", typeof(object), typeof(TreeViewEx), new FrameworkPropertyMetadata(default(object))
    {
        BindsTwoWayByDefault = true // Required in order to avoid setting the "BindingMode" from the XAML
    });

    public object SelectedItemEx
    {
        get => GetValue(SelectedItemExProperty);
        set => SetValue(SelectedItemExProperty, value);
    }

    protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemEx = e.NewValue;
    }
}

Использование XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Kino101
источник