使用 MVVM 获取选定的 TreeViewItem
Posted
技术标签:
【中文标题】使用 MVVM 获取选定的 TreeViewItem【英文标题】:Get Selected TreeViewItem Using MVVM 【发布时间】:2012-02-26 22:10:54 【问题描述】:所以有人建议使用 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>
这可以按环境(开发、QA、产品)正确显示服务器。但是,我在 SO 上找到了各种获取所选项目的方法,其中许多方法令人费解且困难。是否有一种简单 方法可以将所选项目添加到我的视图模型中?
注意:TreeView 上有一个SelectedItem
属性,但它是只读的。令我沮丧的是只读就可以了。我不想通过代码更改它。但是我不能使用它,因为编译器抱怨它是只读的。
还有一个看似优雅的建议来做这样的事情:
<ContentPresenter Content="Binding ElementName=treeView1, Path=SelectedItem" />
我问了这个问题:“您的视图模型如何获取此信息?我知道ContentPresenter
持有所选项目,但我们如何将其传递给视图模型?”但是还没有答案。
所以,我的总体问题是:“有没有一种简单的方法可以将所选项目添加到我的视图模型中?”
【问题讨论】:
【参考方案1】:要做你想做的事,你可以修改TreeView
的ItemContainerStyle
:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsSelected" Value="Binding IsSelected, Mode=TwoWay"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
您的视图模型(树中每个项目的视图模型)必须公开一个布尔值 IsSelected
属性。
如果您希望能够控制特定的 TreeViewItem
是否被扩展,您也可以为该属性使用 setter:
<Setter Property="IsExpanded" Value="Binding IsExpanded, Mode=TwoWay"/>
然后,您的视图模型必须公开一个布尔值 IsExpanded
属性。
请注意,这些属性是双向的,因此如果用户在树中选择一个节点,则视图模型的 IsSelected
属性将设置为 true。另一方面,如果您在视图模型上将IsSelected
设置为true,则将选择该视图模型的树中的节点。扩展也是如此。
如果树中的每个项目都没有视图模型,那么你应该得到一个。没有视图模型意味着您将模型对象用作视图模型,但要使其正常工作,这些对象需要 IsSelected
属性。
要在您的父视图模型(您绑定到 TreeView
并且具有一组子视图模型的视图模型)上公开一个 SelectedItem
属性,您可以这样实现它:
public ChildViewModel SelectedItem
get return Items.FirstOrDefault(i => i.IsSelected);
如果您不想跟踪树上每个单独项目的选择,您仍然可以使用TreeView
上的SelectedItem
属性。但是,为了能够做到“MVVM 风格”,您需要使用混合行为(可作为各种 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>
您必须在返回 ICommand
的 TreeView
的 DataContext
上添加属性 SetSelectedItemCommand
。当树视图的选定项更改时,将调用命令上的Execute
方法,并将选定项作为参数。创建命令的最简单方法可能是使用DelegateCommand
(谷歌它以获取实现,因为它不是 WPF 的一部分)。
在不使用笨重命令的情况下允许双向绑定的一种可能更好的替代方法是使用 Steve Greatrex 在 Stack Overflow 上提供的 BindableSelectedItemBehavior。
【讨论】:
但是视图模型不只是绑定到 IsSelected 吗?它实际上是如何获得价值的? @BobHorn:在 MVVM 中,您可以将模型对象包装在视图模型对象中,或者使您的模型对象如此丰富以至于它们可以充当视图模型。如果您将Foo
对象包装在FooViewModel
中,并向此视图模型添加IsSelected
属性,您会发现选择很容易处理。 TreeView
控件通过TreeViewItem
对象而不是TreeView
控件本身公开选择,您需要在视图模型中进行镜像。
@BobHorn:没有什么与此绑定,但您的问题是关于如何使用 MVVM 和数据绑定获取树视图的选定项。我假设您在主视图模型中有一些操作需要选定的项目才能处理。
@BobHorn:要么直接从子视图模型中 IsSelected
属性的设置器调用父视图模型,要么让父视图模型订阅来自子视图模型。或者您可以使用更松散耦合的事件聚合器设计。
@UB3571:恐怕我不明白你的问题的细节。如果您在 Stack Overflow 上将其作为一个真正的问题提出可能会更好。如果您正在寻找一种允许视图模型进行通信的方法,您可以使用事件聚合器。我认为避免混合行为没有任何价值,因为这允许您修改视图的行为,而不会因引入代码而失去工具支持。【参考方案2】:
我可能会使用 SelectedItemChanged
事件在您的 VM 上设置相应的属性。
【讨论】:
@BobHorn: 不一定,但不管怎样,人们对背后的代码太着迷了,这没什么大不了的…… 是的,你可能是对的,但如果我要让这个树视图垃圾成为突破点,我会被诅咒的......哈哈。 你知道,在经历了上面所有的对话之后,我真的可能最终会这样做。这要简单得多。我所要做的就是将它添加到树视图控件中:SelectedItemChanged="TreeView_SelectedItemChanged"。然后在后面的代码中,对于那个方法,我只需要一行代码: ((ApplicationServerViewModel)DataContext).SelectedApplicationServer = e.NewValue as ApplicationServer; 我只是在与工作人员讨论这个场景......让代码隐藏处理事件,然后使用 datacontext 属性执行所需的 VM 方法是完全可以的。我已经看到了疯狂的解决方法,他们所做的只是复制代码隐藏中的事件处理程序已经做的事情。请记住,代码隐藏是 XAML 的部分类!是同等水平的!如果您可以在 xaml 中进行绑定或使用某些附加属性来执行此操作,那么在代码隐藏中处理事件同样有效! @AshbyEngineer:当然,视图可以直接访问虚拟机,而不是相反(如果您希望按照模式的意图进行严格的解耦)。【参考方案3】:根据 Martin 的回答,我制作了一个简单的应用程序,展示了如何应用建议的解决方案。
示例代码使用 Cinch V2 框架来支持 MVVM,但可以轻松更改为使用您喜欢的框架。
对于那些感兴趣的人,在 GitHub 上here is the code
希望对你有帮助。
【讨论】:
【参考方案4】:聚会有点晚了,但对于现在遇到这种情况的人来说,我的解决方案是:
-
添加对“System.Windows.Interactivity”的引用
将以下代码添加到您的树形视图元素中。
<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,而无需使用后面的代码或一些冗长的工作。
【讨论】:
【参考方案5】:也迟到了,但作为 MVVMLight 用户的替代方案:
-
将 TreeViewItem 绑定到您的 ViewModel 以获取 IsSelected 属性的更改。
创建发送 SelectedItem ViewModel 或 Model 项的 MVVMLight 消息(如 PropertyChangeMessage)
注册您的 TreeView 主机(或其他一些 ViemModel,如有必要)以收听此消息。
整个实现速度非常快,运行良好。
这里是 IsSelected 属性(SourceItem 是所选 ViewModel 项的 Model 部分):
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
这里是虚拟机主机代码:
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
【讨论】:
以上是关于使用 MVVM 获取选定的 TreeViewItem的主要内容,如果未能解决你的问题,请参考以下文章
在选定的剑道网格 mvvm 中获取剑道下拉列表值/文本/索引
如何使用 MVVM 应用程序在 WPF 中以编程方式设置 DataGrid 的选定项?