WPF:TreeViewItem绑定到一个ICommand

11

我正在创建我的第一个WPF MVVM应用程序。

基本上,我的问题是我在WPF窗口上放置了一个TreeView(System.Windows.Controls.TreeView),我决定将其绑定到CommandViewModel项的ReadOnlyCollection,这些项由DisplayString、Tag和RelayCommand组成。

现在,在XAML中,我已经成功地将ReadOnlyCollection绑定到了TreeView上。在UI中,我可以查看它并且一切看起来很好。

现在的问题是,我需要将RelayCommand绑定到TreeViewItem的Command上,但据我所知,TreeViewItem没有Command属性。这是否迫使我在IsSelected属性中或者在后台的TreeView_SelectedItemChanged方法中实现它,还是有一种神奇的方法可以在WPF中自动完成这个过程?

以下是我的代码:

<TreeView BorderBrush="{x:Null}" 
      HorizontalAlignment="Stretch" 
      VerticalAlignment="Stretch">
<TreeView.Items>
    <TreeViewItem
        Header="New Commands"
        ItemsSource="{Binding Commands}"
        DisplayMemberPath="DisplayName"
        IsExpanded="True">
    </TreeViewItem>
</TreeView.Items>

最理想的情况是我只需要这样做:

<TreeView BorderBrush="{x:Null}" 
      HorizontalAlignment="Stretch" 
      VerticalAlignment="Stretch">
<TreeView.Items>
    <TreeViewItem
        Header="New Trade"
        ItemsSource="{Binding Commands}"
        DisplayMemberPath="DisplayName"
        IsExpanded="True"
        Command="{Binding Path=Command}">
    </TreeViewItem>
</TreeView.Items>

有没有人有解决方案,允许我使用我拥有的RelayCommand基础设施。

谢谢大家,非常感谢!

理查德

6个回答

29

我知道这个问题早就有人回答过了,但是由于那些答案并不理想,所以我想发表我的看法。我使用一种方法,可以让我不需要采用任何“样式按钮花招”,甚至不需要使用代码后台,而是将所有分离保留在MVVM中。在你的TreeView中添加以下xaml:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="SelectedItemChanged">
        <i:InvokeCommandAction Command="{Binding TreeviewSelectedItemChanged}" CommandParameter="{Binding ElementName=treeView, Path=SelectedItem}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

在你的XAML头部添加:

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

然后你需要在你的项目中添加对上述程序集的引用。

之后,一切都像在任何其他命令上(比如按钮等)一样正常运作。


+1 - 这是一个很棒的答案,因为它不是一个代码后置解决方案。然而,这不是一个开箱即用的解决方案 :(。 - TheCloudlessSky
System.Windows.Interactivity 可以再分发吗? - AMissico
是的,请查看"C:\Program Files (x86)\Microsoft SDKs\Expression\Blend.NETFramework\v4.0\Redist.en.txt"。 - AMissico
你可以使用NuGet获取这些库。 - SouthShoreAK
你可以在Visual Studio中直接从GAC添加引用,不需要使用Blend。由于DLL已被引用,因此它将自动包含在您的安装程序中(假设您正在使用常规安装程序项目或ClickOnce)。 - BrainSlugs83
显示剩余3条评论

2
感谢您对问题的贡献,是的,我确实说过我不想要一个代码后置解决方案,然而当时我仍然非常认为我只是错过了什么......因此我最终使用了TreeView_SelectedItemChanged事件。
尽管威尔的方法似乎是一个好的解决方法,但对于我的个人情况,我决定使用代码后置。原因是这样View和XAML将保持不变,就像TreeViewItem有一个“Command”属性可以绑定我的命令一样。现在我不必更改模板或视图,我只需要添加代码和TreeView_SelectedItemChanged事件即可。
我的解决方案:
  private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (sender != null)
        {
            var treeView = sender as TreeView;
            if (treeView != null)
            {
                var commandViewModel = treeView.SelectedItem as CommandViewModel;
                if (commandViewModel != null)
                {
                    var mi = commandViewModel.Command.GetType().GetMethod("Execute");
                    mi.Invoke(commandViewModel.Command, new Object[] {null});
                }
            }
        }
    }

我已经将RelayCommand附加到了TreeViewItem上,现在我只需手动调用特定RelayCommand的“Execute”方法。

如果这种做法完全错误,请让我知道...

谢谢!


1
我相信使用ASP.NET TreeView,您可以扩展TreeView和TreeViewItem以添加此类功能。 您可以更改TVI以添加所需内容,然后覆盖TV中的方法以创建您的TVI的新实例。 我不知道WPF TreeView中是否有相同的模式; 我检查过,但不是那么彻底。 - user1228

1
我会将TreeViewItem的Header设置为按钮,然后对按钮进行皮肤处理,使其看起来不像按钮,并执行我的命令绑定。您可能需要通过DataTemplate来完成这个过程,或者您可能需要更改TreeViewItem本身的模板。虽然我从未尝试过,但这是我完成类似任务(例如选项卡页标题)的方式。
这是我所说的一个例子(您可以将其放入Kaxaml中并进行调试):
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Page.Resources>
      <Style x:Key="ClearButan" TargetType="Button">
         <Setter Property="Template">
            <Setter.Value>
               <ControlTemplate TargetType="Button">              
                 <Border Name="border"
                     Padding="4"
                     Background="transparent">
                     <Grid >
                     <ContentPresenter HorizontalAlignment="Center"
                                    VerticalAlignment="Center">
                     </ContentPresenter>
                     </Grid>
                 </Border>
               </ControlTemplate>
            </Setter.Value>
         </Setter>
      </Style>
   </Page.Resources>
   <Grid>
      <TreeView>
         <TreeViewItem>
            <Button Style="{StaticResource ClearButan}">
            easy peasy
            </Button>
         </TreeViewItem>
      </TreeView>
   </Grid>
</Page>

我为按钮创建了一个新的清晰样式。然后,我只需将按钮放入 TVI 中并设置其样式。当然,您也可以使用数据模板来完成同样的事情。


嗨,我看到你正在尝试做什么,并感谢你的快速回复,但这真的是最好的解决方案吗?我在网上搜索了很久也找不到一个好的方法来完成它!必须说这很让人恼火,因为这并没有内置。但至少你提供了一种实现方式,现在该怎样让标题显示按钮和绑定文本呢? - Richard
1
感谢Will的更新,我看到这可能会起作用。我已经走了另一条路,但非常感谢你的帮助。希望这能帮助其他遇到此问题的人。 - Richard

1

这是一个很好的例子,说明了在WPF中,MVVM非常被忽视。你期望某些GUI元素有命令支持,但实际并没有,所以你被迫按照Will的示例所示进行复杂的过程,才能将命令附加到某个东西上。

希望他们在WPF 2.0中解决这个问题 :-)


5
MVVM 是在 WPF 之后出现的,因此它并不是一个“事后想法”,而是一种将 MVC 原则应用于 WPF 的方式。此外,这更多地是WPF TreeView的缺陷,TreeView 并不是为这种用途而开发的,这在我的看法中有点短视。 - user1228

0

我通过常见的标签属性从Richard改进了一个好的解决方案:

MyView.xaml:

 <TreeView SelectedItemChanged="TreeView_SelectedItemChanged" Tag="{Binding SelectTreeViewCommand}" >
                  <TreeViewItem Header="Item1" IsExpanded="True"   Tag="Item1"  />
                  <TreeViewItem Header="Item2" IsExpanded="True">
                      <TreeViewItem Header="Item21"  Tag="Item21"/>
                  </TreeViewItem>
              </TreeView>

MyView.xaml.cs

    private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        var treeView = (TreeView)sender;
        var command = (ICommand)treeView.Tag;
        TreeViewItem selectedItem = (TreeViewItem)treeView.SelectedItem;
        if (selectedItem.Tag != null)
        {
            command.Execute(selectedItem.Tag);
        }
    }

MyViewModel.cs

      public RelayCommand selectTreeViewCommand;
      [Bindable(true)]
      public RelayCommand SelectTreeViewCommand => selectTreeViewCommand ?? (selectTreeViewCommand = new RelayCommand(CanSelectTreeViewCommand, ExecuteSelectTreeViewCommand));

      private void ExecuteSelectTreeViewCommand(object obj)
      {
          Console.WriteLine(obj);
      }

      private bool CanSelectTreeViewCommand(object obj)
      {
          return true;
      }

0
Shaggy13spe提供的答案非常好。但是,我还需要额外的时间来理解它,所以我将扩展答案。
整个TreeView xaml可以如下所示:
<TreeView x:Name="treeView" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Tree}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <i:InvokeCommandAction Command="{Binding FilterMeetingsCommand}" CommandParameter="{Binding  ElementName=treeView, Path=SelectedItem}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Path=Nodes}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}"></TextBlock>
                <TextBlock Text="{Binding Id}"></TextBlock>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

在我的视图中,我有一个集合。
public ObservableCollection<TreeNode> Tree { get; set; }

TreeNode 被定义为一个简单的类:

public class TreeNode
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<TreeNode> Nodes { get; set; }

    public TreeNode(string name)
    {
        this.Name = name;
        this.Nodes = new List<TreeNode>();
    }
}

第一个重要点: CommandParameter 不绑定到 ViewModel 上的属性,而是传递给方法。因此,该方法应该如下所示:
private async void FilterMeeting(object parameter){}

第二个重要的点:如果你将传递所选项(在我的情况下,对象将是TreeNode类型),并且你有分层结构,那么你将面临事件冒泡。因此,选择一个项目将为该特定项目和所有父级触发事件。要解决这个问题,你需要理解在ViewModel中只能传递一个对象到方法中(而不是标准事件处理程序中的两个对象),并且这个对象需要是一个事件。
在这种情况下,将XAML更改为以下内容(PassEventArgsToCommand="True"在这里很重要)
<TreeView x:Name="treeView" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Tree}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <i:InvokeCommandAction Command="{Binding FilterMeetingsCommand}" PassEventArgsToCommand="True"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Path=Nodes}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}"></TextBlock>
                <TextBlock Text="{Binding Id}"></TextBlock>
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

然后在您的处理方法中,您将不会收到模型对象,而是事件参数,其中包含一个模型对象。

Event with selected element


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接