I am trying to create a file tree using a TreeView where I can bind a collection to. In order to bind to a TreeView you need to use a HierarchicalDataTemplate. This automatically uses the default TreeViewItem as its base. But I have no direct access to this TreeViewItem to add stuff to. This is why I am accessing the properties of this TreeViewItem using a style and setters.
Now when I add a ContextMenu through this style, all the commands and events that are assigned to the MenuItems won’t fire. The ContextMenu still shows up fine when I right click the MenuItem.
<TreeView ItemsSource="{Binding FileTree}">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu Width="250">
<!-- This command never gets executed when the menu item is clicked -->
<MenuItem Header="New File" Command="{Binding NewFile}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:FileTreeModel}"
ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding FileName, Converter={StaticResource FileNameToIconConverter}}"
Width="14" Height="12" Margin="5,0,5,0" SnapsToDevicePixels="True"
Stretch="Fill" />
<TextBlock Text="{Binding Path=FileName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
public class FileTreeModel
{
public string? FileName { get; set; }
public string? Path { get; set; }
public IList<FileTreeModel> Children { get; set; } = [];
}
public ObservableCollection<FileTreeModel> FileTree { get; }
public ICommand NewFile { get; }
public ExplorerViewModel(INewFileService newFileService)
{
FileTree = [];
NewFile = new RelayCommand(() =>
{
Console.WriteLine("New file"); // Never gets ran
newFileService.NewFile();
});
}
4
Without knowing how you set the Data Context, it is impossible to give an exact answer to your question.
One of the possible variants:
<Window -------------------
-------------------
DataContext="{DynamicResource vm}">
<Window.Resources>
<local:FilesTreeViewModel x:Key="vm"/>
</Window.Resources>
----------------------------
<ContextMenu Width="250">
<!-- This command never gets executed when the menu item is clicked -->
<MenuItem Header="New File"
Command="{Binding NewFile, Source={StaticResource vm}}"/>
</ContextMenu>
----------------------------
The solution I have found was changing TreeView.Resources
to TreeView.ItemContainerStyle
. This means it will now try to find the command inside the container model, instead of the view model. This is a very hacky solution, and I would still like to figure out a way that allows me to have the command inside the view model.
<TreeView ItemsSource="{Binding FileTree}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu Width="250">
<!-- It will now try to find the NewFile command inside FileTreeModel, because of ItemContainerStyle -->
<MenuItem Header="New File" Command="{Binding NewFile}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type model:FileTreeModel}"
ItemsSource="{Binding Path=Children}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding FileName, Converter={StaticResource FileNameToIconConverter}}"
Width="14" Height="12" Margin="5,0,5,0" SnapsToDevicePixels="True"
Stretch="Fill" />
<TextBlock Text="{Binding Path=FileName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
public class FileTreeModel
{
public string? FileName { get; set; }
public string? Path { get; set; }
public IList<FileTreeModel> Children { get; set; } = [];
public ICommand NewFileCommand { get; }
public FileTreeModel()
{
NewFile = new RelayCommand(() =>
{
Console.WriteLine("New file");
});
}
}