WPF MVVM TreeView SelectedItem

Это не может быть так сложно. TreeView в WPF не позволяет вам устанавливать SelectedItem, говоря, что свойство ReadOnly. У меня есть заполнение TreeView, даже обновление при изменении коллекции данных.

Мне просто нужно знать, какой элемент выбран. Я использую MVVM, поэтому нет кода или переменной для ссылки на treeview. Это единственное решение, которое я нашел, но это очевидный взлом, он создает еще один элемент в XAML, который использует привязку ElementName, чтобы установить себя в выбранный элемент treeviews, который вы также должны привязать свою модель Viewmodel. Несколько других вопросов, но никаких других рабочих решений не задано.

Я видел этот вопрос, но используя предоставленный ответ дает компиляцию ошибок, по какой-то причине я не могу добавить ссылку на blend sdk System.Windows.Interactivity к моему проекту. В нем говорится: "Неизвестная система ошибок. Windows не была предварительно загружена", и я еще не понял, как это пройти.

Для бонусных очков: почему, черт возьми, Microsoft создала этот элемент свойства SelectedItem ReadOnly?

Ответ 1

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

Эскиз:

<TreeView ItemsSource="{Binding TreeData}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>
public class TViewModel : INotifyPropertyChanged
{
    private static object _selectedItem = null;
    // This is public get-only here but you could implement a public setter which
    // also selects the item.
    // Also this should be moved to an instance property on a VM for the whole tree, 
    // otherwise there will be conflicts for more than one tree.
    public static object SelectedItem
    {
        get { return _selectedItem; }
        private set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnSelectedItemChanged();
            }
        }
    }

    static virtual void OnSelectedItemChanged()
    {
        // Raise event / do other things
    }

    private bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_isSelected != value)
            {
                _isSelected = value;
                OnPropertyChanged("IsSelected");
                if (_isSelected)
                {
                    SelectedItem = this;
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Ответ 2

Вы можете создать прикрепленное свойство, которое может быть привязано и имеет 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 и привяжите его следующим образом (local - это то, как я назвал объявление пространства имен):

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

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

Ответ 3

Очень необычный, но довольно эффективный способ решения этой проблемы в MVVM-приемлемом способе:

  • Создайте сглаженный контент ContentControl на том же самом представлении TreeView. Назовите его соответствующим образом и привяжите его содержимое к некоторому свойству SelectedSomething в viewmodel. Этот ContentControl будет "удерживать" выделенный объект и обрабатывать его привязку, OneWayToSource;
  • Прослушайте SelectedItemChanged в TreeView и добавьте обработчик в коде для установки вашего ContentControl.Content в новый выбранный элемент.

XAML:

<ContentControl x:Name="SelectedItemHelper" Content="{Binding SelectedObject, Mode=OneWayToSource}" Visibility="Collapsed"/>
<TreeView ItemsSource="{Binding SomeCollection}"
    SelectedItemChanged="TreeView_SelectedItemChanged">

Код за:

    private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemHelper.Content = e.NewValue;
    }

ViewModel:

    public object SelectedObject  // Class is not actually "object"
    {
        get { return _selected_object; }
        set
        {
            _selected_object = value;
            RaisePropertyChanged(() => SelectedObject);
            Console.WriteLine(SelectedObject);
        }
    }
    object _selected_object;

Ответ 4

Используйте OneWayToSource режим привязки. Это не работает. См. Править.

Изменить. Похоже, что это ошибка или "по дизайну" поведения Microsoft, согласно этому вопросу; однако есть некоторые обходные пути. Кто-нибудь из них работает для вашего TreeView?

Проблема Microsoft Connect: https://connect.microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings

Отправлено Microsoft 1/10/2010 в 2:46 вечера

Мы не можем сделать это в WPF сегодня, по той же причине, мы не можем поддерживать привязки к свойствам, которые не являются DependencyProperties. Время выполнения per-instance состояние привязки сохраняется в BindingExpression, которое мы сохраняем в EffectiveValueTable для целевого объекта DependencyObject. Если целевое свойство не является DP или DP доступно только для чтения, нет места для хранения выражения BindingExpression.

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

Спасибо за ваши отзывы.

Ответ 5

Я решил использовать комбинацию кода позади и код viewmodel. xaml выглядит так:

<TreeView 
                    Name="tvCountries"
                ItemsSource="{Binding Path=Countries}"
                ItemTemplate="{StaticResource ResourceKey=countryTemplate}"   
                    SelectedValuePath="Name"
                    SelectedItemChanged="tvCountries_SelectedItemChanged">

Код за

private void tvCountries_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        var vm = this.FindResource("vm") as ViewModels.CoiEditorViewModel;
        if (vm != null)
        {
            var treeItem = sender as TreeView;
            vm.TreeItemSelected = treeItem.SelectedItem;
        }
    }

И в viewmodel есть объект TreeItemSelected, доступ к которому вы можете получить в viewmodel.

Ответ 6

Вы всегда можете создать DependencyProperty, который использует ICommand и прослушивать событие SelectedItemChanged в TreeView. Это может быть немного проще, чем привязка IsSelected, но я предполагаю, что вы все равно закроете привязку IsSelected по другим причинам. Если вы просто хотите привязываться к IsSelected, вы всегда можете отправлять свой элемент при каждом изменении IsSelected. Затем вы можете слушать эти сообщения в любом месте вашей программы.