Прив’язка даних до SelectedItem у WPF Treeview


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" />

і в кодібедфайла мого 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
Як зазначив Брент, мені також потрібно було додати модуль = TwoWay до прив'язки. Я не "Блендер", тому не був знайомий з класом Поведінка <> від System.Windows.Interactivity. Збірка є частиною суміші Expression. Для тих, хто не хоче купувати / встановлювати пробну версію, щоб отримати цю збірку, ви можете завантажити BlendSDK, який включає System.Windows.Interactivity. BlendSDK 3 для 3,5 ... Я думаю, що це BlendSDK 4 для 4.0. Примітка. Це дозволяє лише отримати обраний товар, не дозволяє встановити вибраний елемент
Майк Роулі

4
Ви також можете замінити UIPropertyMetadata на FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji

3
Це буде підхід до вирішення проблеми: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas точно так, як показано в фрагменті коду XAML вище. Просто замініть {Binding SelectedItem, Mode=TwoWay}на{Binding MyViewModelField, Mode=TwoWay}
Стів Гредрекс

4
@Pascal цеxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Стів Гредрекс

46

Ця властивість існує: TreeView.SelectedItem

Але він читається тільки тому, тому ви не можете призначити його за допомогою прив'язки, а лише отримати його


Я приймаю цю відповідь, тому що я знайшов це посилання, що нехай на мій власний відповідь: msdn.microsoft.com/en-us/library/ms788714.aspx
Натрій

1
То чи можу я TreeView.SelectedItemвпливати на властивість моделі, коли користувач вибирає елемент (ака OneWayToSource)?
Шиммі Вайцхандлер

43

Відповідь з прив’язаними властивостями та відсутністю зовнішніх залежностей, якщо потреба коли-небудь виникне!

Ви можете створити додане властивість, яке може бути в'язаним файлом, а також має getter та setter:

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 декларацію простору імен, що містить цей клас, і зв’яжіть так (місцевим чином я назвав декларацію простору імен):

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

    </TreeView>

Тепер ви можете прив’язати вибраний елемент, а також встановити його у своїй моделі перегляду, щоб змінити його програмно, якщо ця вимога виникне. Це, звичайно, якщо припустити, що ви реалізуєте INotifyPropertyChanged саме для цієї властивості.


4
+1, найкраща відповідь у цій темі imho. Немає залежності від System.Windows.Interactivity та дозволяє двостороннє прив'язування (встановлення програмно в середовищі MVVM). Ідеально.
Кріс Рей

5
Проблема такого підходу полягає в тому, що поведінка почне працювати лише після того, як вибраний елемент буде встановлений один раз через прив'язку (тобто від ViewModel). Якщо початкове значення в VM є нульовим, то прив'язка не оновить значення DP, і поведінка не буде активована. Ви можете виправити це за допомогою іншого вибраного за замовчуванням (наприклад, недійсного елемента).
Марк

6
@Mark: Просто використовуйте новий об'єкт () замість нуля, вказаного вище, при створенні екземплярів UIPropertyMetadata доданого ресурсу. Проблема повинна піти ...
barnacleboy

2
Передача на TreeViewItem не піддається мені, я вважаю, тому що я використовую HierarchicalDataTemplate, застосований з ресурсів за типом даних. Але якщо ви видалите ChangeSelectedItem, то прив'язка до перегляду та вилучення елемента справно працює.
Кейсі Себен

1
У мене також є проблеми з актором для TreeViewItem. У цей момент ItemContainerGenerator містить лише посилання на кореневі елементи, але мені це потрібно, щоб мати можливість отримувати й некорінні елементи. Якщо ви передаєте посилання на одне, команда не вдається і повертає нуль. Не знаєте, як це можна виправити?
Боб Твей

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
Це ТІЛЬКА річ, яка наблизилась до роботи для мене досі. Мені дуже подобається таке рішення.
Rachael

1
Не знаю, чому, але мені це не вийшло :( Мені вдалося дістати вибраний елемент з дерева, але не навпаки - змінити вибраний предмет поза дерева.
Ерез

Було б трохи акуратніше встановити властивість залежності як BindsTwoWayByDefault, тоді вам не потрібно буде вказувати TwoWay у XAML
Стівен Холт

Це найкращий підхід. Він не використовує посилання на інтерактивність, не використовує код позаду, не має витоку пам’яті, як деякі поведінки. Дякую.
Олександру Діку

Як згадувалося, це рішення не працює з двостороннім зв'язуванням. Якщо встановити значення в моделі перегляду, зміна не поширюється на TreeView.
Річард Мур

25

Це відповідає трохи більше, ніж очікує ОП ... Але я сподіваюся, що це могло б допомогти хоча б комусь.

Якщо ви хочете виконувати 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>

3
Посилання System.Windows.Interactivityможе бути встановлена з NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Лі

Я годинами намагаюся вирішити цю проблему, це я реалізував, але моя команда не працює, будь ласка, чи можете ви мені допомогти?
Альфі

1
Компанія Microsoft представила XAML Behaviors for WPF наприкінці 2018 року. Її можна використовувати замість System.Windows.Interactivity. Він працював для мене (спробував проект .NET Core). Щоб налаштувати речі, просто додайте пакет нульових програм Microsoft.Xaml.Behaviors.Wpf , змініть простір імен на xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Для отримання додаткової інформації - будь ласка, дивіться щоденник
rychlmoj

19

Це може бути досягнуто "приємнішим" способом, використовуючи лише зв'язування та EventToCommand бібліотеки GalaSoft MVVM Light. У свій VM додайте команду, яка буде викликана, коли вибраний елемент буде змінено, і ініціалізуйте команду для виконання будь-яких дій, необхідних. У цьому прикладі я використав 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>

Це приємне рішення, особливо якщо ви вже використовуєте інструментарій MvvmLight. Однак це не вирішує проблему встановлення вибраного вузла, і перегляд дерева оновить вибір.
кефт

12

Все до складного ... Перейдіть з Caliburn Micro (http://caliburnmicro.codeplex.com/)

Вид:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
Так ... і де частина, яка встановлює SelectedItem на TreeView ?
mnn

Калібр приємний і елегантний. Працює досить легко для вкладених ієрархій
Пурусарта

8

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

Мотивація зв’язування полягає в тому, щоб це було добре і MVVM. Ймовірне використання ViewModel полягає в тому, щоб мати властивість w / ім'я, наприклад "CurrentThingy", а деінде, DataContext в якійсь іншій речі пов'язаний з "CurrentThingy".

Замість того, щоб пройти необхідні додаткові кроки (наприклад, власна поведінка, сторонній контроль), щоб підтримати приємне прив'язування від TreeView до моєї моделі, а потім від чогось іншого до моєї моделі, моє рішення полягало в тому, щоб використовувати простий елемент, що зв'язує іншу річ 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}" />

Звичайно, це чудово для читання обраного на даний момент предмета, але не встановлення його, що все, що мені потрібно.


1
Що місцевого значення: MyThingyDetailsView? Я розумію, що локальний: MyThingyDetailsView містить вибраний елемент, але як ваша модель перегляду отримує цю інформацію? Це виглядає як приємний, чистий спосіб зробити це, але мені потрібно лише трохи більше інформації ...
Боб Хорн

локальний: MyThingyDetailsView - це просто UserControl, повний XAML, який складається з детальної інформації про один "штучний" екземпляр. Він вбудований в середину іншого подання як вміст, w / DataContext цього перегляду - це обраний на даний момент елемент перегляду дерева, використовуючи прив'язку елементів.
Уес

6

Можливо, ви також зможете використовувати властивість TreeViewItem.IsSelected


Я думаю, це може бути правильною відповіддю. Але я хотів би побачити приклад або рекомендацію щодо найкращої практики щодо того, як властивість IsSelected елементів переноситься до TreeView.
anhoppe

3

Існує також спосіб створити властивість XAML bindable 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. Він буде відмінно працювати, якщо ви виберете елемент дерева з графічного інтерфейсу і якщо ви встановите це значення властивості у своєму коді

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 () повертає null для не відвідуваних вузлів. Таким чином, ми можемо розширити батьківський вузол лише тоді, коли 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 у дереві, котрий контекст даних є вибраним джерелом від джерела. У 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;
    }

Таким чином поведінка працює для двосторонніх прив’язок. Крім того, можливо перемістити придбання ItemHost на метод OnAttached Behavior, заощадивши накладні витрати на використання відображення кожного разу, коли прив'язка оновлюється.


2

WPF MVVM TreeView SelectedItem

... є кращою відповіддю, але не згадує спосіб отримати / встановити SelectedItem у ViewModel.

  1. Додайте бутовий властивість IsSelected до вашої ItemViewModel і зв’яжіть його в Setter Setter для TreeViewItem.
  2. Додайте властивість SelectedItem у свій ViewModel, який використовується як DataContext для TreeView. Це відсутній шматок у розчині вище.
    'ItemVM ...
    Громадська власність обрана як булева
        Отримайте
            Повернути _func.SelectedNode Is Me
        Кінець Get
        Встановити (значення як булева)
            Якщо значення "Вибір" Тоді
                _func.SelectedNode = If (значення, Me, Nothing)
            Кінець Якщо
            RaisePropertyChange ()
        Кінцевий набір
    Кінцева власність
    'TreeVM ...
    Вибране суспільне надбання як предмет VM
        Отримайте
            Повернути _selectedItem
        Кінець Get
        Встановити (значення як ItemVM)
            Якщо _selectedItem є значення Тоді
                Повернення
            Кінець Якщо
            Dim prev = _selectedItem
            _selectedItem = значення
            Якщо prev IsNot нічого тоді
                prev.IsSelected = Неправдиво
            Кінець Якщо
            Якщо _selectedItem IsNot нічого тоді
                _selectedItem.IsSelected = Істинно
            Кінець Якщо
        Кінцевий набір
    Кінцева власність
<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>

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 років, але проблема все ще існує ....

Первісне запитання полягало в тому, щоб "відновити" вибраний елемент. Мені також потрібно було "отримати" вибраний елемент в моєму переглядімоделі (не встановлювати його). З усіх відповідей у ​​цій темі один із "Уес" є єдиним, який по-різному підходить до проблеми: Якщо ви можете використовувати "Вибраний елемент" як ціль для прив'язки даних, використовуйте його як джерело для прив'язки даних. Wes зробив це для іншого властивості перегляду, я зроблю це для властивості viewmodel:

Нам потрібні дві речі:

  • Створіть властивість залежності у моделі перегляду (у моєму випадку типу "MyObject", оскільки мій вигляд дерева пов'язаний з об'єктом типу "MyObject")
  • Прив’яжіть з Treeview.SelectedItem до цього властивості у конструкторі View (так, це за кодом, але, ймовірно, ви також вкладете свій контекст даних)

Модель перегляду:

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

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}" />  

0

Я пропоную це рішення (яке я вважаю найпростішим і витоком пам'яті є вільним), яке ідеально підходить для оновлення вибраного елемента ViewModel від вибраного елемента View.

Зауважте, що зміна вибраного елемента з ViewModel не оновить вибраний елемент Перегляду.

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}" >
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.