Получить выбранный TreeViewItem, используя MVVM

Поэтому кто-то предложил использовать WPF TreeViewи я подумал: "Да, это похоже на правильный подход". Теперь, часами и часами позже, я просто не могу поверить, как трудно было использовать этот элемент управления. Благодаря многим исследованиям я смог заставить работать элемент управления TreeView, но я просто не могу найти "правильный" способ передачи выбранного элемента в модель представления. Мне не нужно устанавливать выбранный элемент из кода; Мне просто нужна моя модель представления, чтобы знать, какой элемент выбран пользователем.

До сих пор у меня есть этот XAML, который сам по себе не очень интуитивен. Это все в теге UserControl.Resources:

<CollectionViewSource x:Key="cvs" Source="{Binding ApplicationServers}">
    <CollectionViewSource.GroupDescriptions>
        <PropertyGroupDescription PropertyName="DeploymentEnvironment"/>
    </CollectionViewSource.GroupDescriptions>
</CollectionViewSource>

<!-- Our leaf nodes (server names) -->
<DataTemplate x:Key="serverTemplate">
    <TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>

<!-- Note: The Items path refers to the items in the CollectionViewSource group (our servers).
           The Name path refers to the group name. -->
<HierarchicalDataTemplate x:Key="categoryTemplate"
                          ItemsSource="{Binding Path=Items}"
                          ItemTemplate="{StaticResource serverTemplate}">
    <TextBlock Text="{Binding Path=Name}" FontWeight="Bold"/>
</HierarchicalDataTemplate>

И вот дерево:

<TreeView DockPanel.Dock="Bottom" ItemsSource="{Binding Source={StaticResource cvs}, Path=Groups}"
              ItemTemplate="{StaticResource categoryTemplate}">
            <Style TargetType="TreeViewItem">
                <Setter Property="IsSelected" Value="{Binding Path=IsSelected}"/>
            </Style>
        </TreeView>

Это правильно показывает серверы по среде (dev, QA, prod). Тем не менее, я нашел различные способы получения выбранного элемента на SO, и многие из них запутаны и сложны. Есть ли простой способ получить выбранный элемент в моей модели представления?

Примечание: есть SelectedItem свойство на TreeView`, но оно доступно только для чтения. Что меня расстраивает, так это то, что только для чтения это нормально; Я не хочу менять это с помощью кода. Но я не могу использовать его, потому что компилятор жалуется, что он только для чтения.

Было также на первый взгляд элегантное предложение сделать что-то вроде этого:

<ContentPresenter Content="{Binding ElementName=treeView1, Path=SelectedItem}" />

И я задал этот вопрос: "Как ваша модель представления может получить эту информацию? ContentPresenter содержит выбранный элемент, но как мы можем передать это модели представления?"Но ответа пока нет.

Итак, мой общий вопрос: "Есть ли простой способ получить выбранный элемент в моей модели представления?"

4 ответа

Решение

Чтобы сделать то, что вы хотите, вы можете изменить ItemContainerStyle из TreeView:

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

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

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

<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>

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

Обратите внимание, что эти свойства работают в обоих направлениях, поэтому, если пользователь выбирает узел в дереве, IsSelected свойство view-модели будет установлено в true. С другой стороны, если вы установите IsSelected в true для модели представления будет выбран узел в дереве для этой модели представления. И аналогично с расширенным.

Если у вас нет модели представления для каждого элемента в дереве, тогда вы должны получить ее. Отсутствие модели представления означает, что вы используете объекты модели в качестве модели представления, но для этого для работы этих объектов требуется IsSelected имущество.

Разоблачить SelectedItem свойство вашей родительской модели представления (та, которую вы связываете с TreeView и это имеет коллекцию дочерних моделей представления) вы можете реализовать это так:

public ChildViewModel SelectedItem {
  get { return Items.FirstOrDefault(i => i.IsSelected); }
}

Если вы не хотите отслеживать выбор каждого отдельного элемента в дереве, вы все равно можете использовать SelectedItem собственность на TreeView, Однако, чтобы иметь возможность делать это в стиле MVVM, вам нужно использовать поведение Blend (доступно в виде различных пакетов NuGet - поиск по "смешиванию интерактивности").

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

<TreeView x:Name="treeView">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="SelectedItemChanged">
      <i:InvokeCommandAction
        Command="{Binding SetSelectedItemCommand}"
        CommandParameter="{Binding SelectedItem, ElementName=treeView}"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
</TreeView>

Вам нужно будет добавить недвижимость SetSelectedItemCommand на DataContext из TreeView возвращая ICommand, Когда выбранный элемент древовидного представления изменяет Execute метод в команде вызывается с выбранным элементом в качестве параметра. Самый простой способ создать команду - это использовать DelegateCommand (Google, чтобы получить реализацию, поскольку она не является частью WPF).

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

Я бы, наверное, использовал SelectedItemChanged событие, чтобы установить соответствующее свойство на вашей виртуальной машине.

Основываясь на ответе Мартина, я сделал простое приложение, показывающее, как применить предложенное решение.

В примере кода используется инфраструктура Cinch V2 для поддержки MVVM, но его можно легко изменить, чтобы использовать платформу, которую вы предпочитаете.

Для тех, кто заинтересован, вот код на GitHub

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

Несколько поздно на вечеринку, но для тех, кто сталкивается с этим сейчас, мое решение было:

  1. Добавить ссылку на 'System.Windows.Interactivity'
  2. Добавьте следующий код в свой элемент дерева. <i:Interaction.Triggers> <i:EventTrigger EventName="SelectedItemChanged"> <i:InvokeCommandAction Command="{Binding SomeICommand}" CommandParameter="{Binding ElementName=treeviewName, Path=SelectedItem}" /> </i:EventTrigger> </i:Interaction.Triggers>

Это позволит вам использовать стандартную привязку MVVM ICommand для доступа к SelectedItem без необходимости использования кода или какой-либо длительной работы.

Также опоздал на вечеринку, но в качестве альтернативы для пользователей MVVMLight:

  1. Свяжите TreeViewItem с ViewModel, чтобы получить изменения свойства IsSelected.
  2. Создайте сообщение MVVMLight (например, PropertyChangeMessage), отправив элемент SelectedItem ViewModel или Model
  3. Зарегистрируйте хост TreeView (или другие ViemModels, если необходимо), чтобы прослушать это сообщение.

Вся реализация очень быстрая и работает отлично.

Здесь свойство IsSelected (SourceItem является частью модели выбранного элемента ViewModel):

       Public Property IsSelected As Boolean
        Get
            Return _isSelected
        End Get
        Set(value As Boolean)
            If Me.HasImages Then
                _isSelected = value
                OnPropertyChanged("IsSelected")
                Messenger.Default.Send(Of SelectedImageFolderChangedMessage)(New SelectedImageFolderChangedMessage(Me, SourceItem, "SelectedImageFolder"))
            Else
                Me.IsExpanded = Not Me.IsExpanded
            End If
        End Set
    End Property

и вот код хоста VM:

    Messenger.Default.Register(Of SelectedImageFolderChangedMessage)(Me, AddressOf NewSelectedImageFolder)

    Private Sub NewSelectedImageFolder(msg As SelectedImageFolderChangedMessage)
        If msg.PropertyName = "SelectedImageFolder" Then
            Me.SelectedFolderItem = msg.NewValue
        End If
    End Sub
Другие вопросы по тегам