I am trying to make design a WPF TreeView where the ContextMenu activates on particular nodes.
In my example, despite my best efforts, I cannot keep the ContextMenu of a BarNode from appearing when it's children 'FooNode's are clicked.
C#:
public abstract class NodeBase
{
public NodeBase[] ChildNodes { get; set; }
}
public class FooNode : NodeBase
{
}
public class BarNode : NodeBase
{
}
public class ExampleModel : BaseModel
{
private NodeBase[] _nodes;
public NodeBase[] Nodes
{
get
{
_nodes = new NodeBase[]
{
new FooNode(),
new BarNode()
{
ChildNodes = new NodeBase[]
{
new FooNode(),
new FooNode()
}
}
};
return _nodes;
}
}
public ExampleModel()
{
}
}
public class TreeViewStyleSelector : StyleSelector
{
public Style FooNodeStyle { get; set; }
public Style BarNodeStyle { get; set; }
public override Style SelectStyle(object item, DependencyObject container)
{
var fooNode = item as FooNode;
if (fooNode != null)
{
return FooNodeStyle;
}
var barNode = item as BarNode;
if (barNode != null)
{
return BarNodeStyle;
}
return base.SelectStyle(item, container);
}
}
XAML
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Nodes="clr-namespace:UnderstandingWPFTreeView.Nodes"
xmlns:Models="clr-namespace:UnderstandingWPFTreeView.Models"
xmlns:Common="clr-namespace:UnderstandingWPFTreeView.Common"
x:Class="UnderstandingWPFTreeView.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<Models:ExampleModel/>
</Window.DataContext>
<Window.Resources>
<ContextMenu x:Key="testContextMenu">
<MenuItem Header="Test Context Item"></MenuItem>
<MenuItem Header="Test Context Item"></MenuItem>
</ContextMenu>
<Style TargetType="{x:Type TreeViewItem}" x:Key="FooNodeStyle">
</Style>
<Style TargetType="{x:Type TreeViewItem}" x:Key="BarNodeStyle">
<Setter Property="ContextMenu" Value="{StaticResource testContextMenu}" />
</Style>
<Common:TreeViewStyleSelector
x:Key="treeViewStyleSelector"
FooNodeStyle="{StaticResource ResourceKey=FooNodeStyle}"
BarNodeStyle="{StaticResource ResourceKey=BarNodeStyle}" />
</Window.Resources>
<StackPanel HorizontalAlignment="Left" Height="320" VerticalAlignment="Top" Width="517">
<TreeView Height="100">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type Nodes:BarNode}" ItemsSource="{Binding Path=ChildNodes}">
<TextBlock Text="Bar" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type Nodes:FooNode}" ItemsSource="{Binding Path=ChildNodes}">
<TextBlock Text="Foo" />
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeViewItem Header="Testing" ItemsSource="{Binding Nodes}" ItemContainerStyleSelector="{StaticResource ResourceKey=treeViewStyleSelector}"/>
</TreeView>
</StackPanel>
</Window>
I asked the same question of the MSDN Forums and got an answer that I can confirm as working.
http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/7dd183bc-d616-4ec4-8b2a-0b438c9a115c
Placing the ContextMenu objects on the TextBlock gives the same visual appearance, minus the effect of passing the ContextMenu down the chain of TreeNodes.
<HierarchicalDataTemplate DataType="{x:Type local:BarNode}" ItemsSource="{Binding Path=ChildNodes}">
<TextBlock Text="Bar" ContextMenu="{StaticResource testContextMenu}" />
</HierarchicalDataTemplate>
Related
I have the following code:
Template.XAML
<Style TargetType="{x:Type HeaderedContentControl}">
<Setter Property="Header">
<Setter.Value>
<ContentControl Foreground="Red"
FontFamily="Segoe UI"
Margin="0,0,0,20"
Content="{Binding Tag, RelativeSource={RelativeSource AncestorType=HeaderedContentControl}}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Setter.Value>
</Setter>
</Style>
<DataTemplate DataType="{x:Type local:ViewModel}">
<HeaderedContentControl xmlns:sys="clr-namespace:System;assembly=mscorlib"
Tag="{Binding Header}"
Background="SteelBlue"
BorderBrush="DarkSlateBlue">
</HeaderedContentControl>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel2}">
<HeaderedContentControl xmlns:sys="clr-namespace:System;assembly=mscorlib"
Tag="{Binding Header}"
Background="SteelBlue"
BorderBrush="DarkSlateBlue">
</HeaderedContentControl>
</DataTemplate>
Windows.XAML:
<Window.DataContext>
<local:WindowsVM x:Name="viewModel"/>
</Window.DataContext>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Templates.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<StackPanel Orientation="Horizontal">
<TreeView SelectedItemChanged="TreeView_SelectedItemChanged" ItemsSource="{Binding AllContents}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate>
<Label Content="{Binding Header}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<StackPanel Orientation="Vertical">
<ContentControl Content="{Binding SelectedItem}" />
</StackPanel>
</StackPanel>
Windows.XAML.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
viewModel.SelectedItem = (ViewModel)e.NewValue;
}
}
ViewModel.cs
public class WindowsVM : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public WindowsVM()
{
AllContents = new ObservableCollection<ViewModel>();
AllContents.Add(new ViewModel("Item 1"));
AllContents.Add(new ViewModel2("Item 2")); //using ViewModel("Item 2") will show the Header as it should
SelectedItem = AllContents.First();
}
private ViewModel _selectedItem;
public ViewModel SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
}
}
private ObservableCollection<ViewModel> _allContents;
public ObservableCollection<ViewModel> AllContents
{
get { return _allContents; }
set
{
_allContents = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AllContents)));
}
}
}
public class ViewModel:INotifyPropertyChanged
{
private string _header;
public event PropertyChangedEventHandler PropertyChanged;
public string Header
{
get { return _header; }
set
{
_header = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Header)));
}
}
public ViewModel(string header)
{
Header = header;
}
}
public class ViewModel2 : ViewModel
{
public ViewModel2(string header) : base(header)
{
}
}
If you run the application, the "Item 1" will be shown as the header of the selected item, as it should.
If you click on the second tree node, I would expect "Item 2" shown as the header of the selected item, but it is not.
Here comes the interesting part, if for "Item 2", it is of the type ViewModel instead of ViewModel2, then the "Item 2" header will be shown. How come this is happening? Is this a WPF treeview bug?
I would also welcome workaround, in the spirit of MVVM model.
You should bind the Header property of the HeaderedContentControl to your Header source property and then define a HeaderTemplate in the HeaderedContentControl style.
If you implement Templates.xaml like this, your example works as expected:
<Style TargetType="{x:Type HeaderedContentControl}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<Label Foreground="Red"
FontFamily="Segoe UI"
Margin="0,0,0,20"
Content="{Binding}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate DataType="{x:Type local:ViewModel}">
<HeaderedContentControl Header="{Binding Header}"
Background="SteelBlue"
BorderBrush="DarkSlateBlue">
</HeaderedContentControl>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ViewModel2}">
<HeaderedContentControl Header="{Binding Header}"
Background="SteelBlue"
BorderBrush="DarkSlateBlue">
</HeaderedContentControl>
</DataTemplate>
The following code produces a TreeView as seen below. When you right click any of the child nodes (not parents), I would like a simple context menu to display.
Here is the code I am using to create the tree view. I need to use the HierarchicalDataTemplate so the solution must include that.
XAML
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d">
<Grid>
<TreeView ItemsSource="{Binding Parents}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Parent}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Child}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
CODE
using System.Collections.Generic;
using System.Windows;
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel
{
public ViewModel()
{
Parents = new List<Parent>();
Parents.Add(new Parent()
{
Name = "Parent A",
Children = new List<Child>() {
new Child() { Name = "Child A" },
new Child() { Name = "Child B" }
}
});
Parents.Add(new Parent()
{
Name = "Parent B",
Children = new List<Child>() {
new Child() { Name = "Child C" },
new Child() { Name = "Child D" }
}
});
}
public List<Parent> Parents { get; set; }
}
public class Parent
{
public Parent() { Children = new List<Child>(); }
public string Name { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public string Name { get; set; }
}
}
SAMPLE CONTEXT MENU
<ContextMenu x:Key ="ArchiveFaxNodePopupMenu">
<MenuItem Header="Delete" />
</ContextMenu>
Thanks for the help!
UPDATE
Here is the updated XAML that makes the content menu work for the child node types only (thanks to #EdPlunket for the answer)
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d">
<Grid>
<TreeView ItemsSource="{Binding Parents}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Parent}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Child}">
<TextBlock Text="{Binding Name}">
<TextBlock.Resources>
<ContextMenu x:Key ="ArchiveFaxNodePopupMenu">
<MenuItem Header="Delete" />
</ContextMenu>
</TextBlock.Resources>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="ContextMenu" Value="{StaticResource ArchiveFaxNodePopupMenu}" />
</Style>
</TextBlock.Style>
</TextBlock>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
</Window>
This should do it.
<TreeView ItemsSource="{Binding Parents}">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="ContextMenu" Value="{StaticResource ArchiveFaxNodePopupMenu}" />
</Style>
</TreeView.ItemContainerStyle>
<!-- resources etc. -->
If you want different context menus for the different types of items, put them on the TextBlocks in the templates.
I have the "simple" task to have a ContextMenu on a TreeView(Element) that is done in MVVM-way.
When searching the web I found some solutions that I could bring to work with buttons etc. but not with the TreeView. I think the problem is with setting the ItemsSource-Property of TreeView that gives every single item an own DataContext.
Here's my little Test-App where you can see the principle working for button but not for the TreeView-Elements:
MainWindow.xaml:
<Window x:Class="ContextMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid >
<StackPanel>
<TextBlock Text="{Binding MyText}" />
<Button Tag="{Binding DataContext,RelativeSource={RelativeSource Mode=Self}}" Content="Click me">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="{Binding PlacementTarget.Tag.MyText,
RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=ContextMenu}}" />
</ContextMenu>
</Button.ContextMenu>
</Button>
<TreeView ItemsSource="{Binding MyList}" Tag="{Binding DataContext, RelativeSource={RelativeSource Mode=Self}}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Name}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="{Binding Path=PlacementTarget.Tag.MyText,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}" />
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
</Grid>
</Window>
Codebehind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowVM();
}
}
MainWindowVM.cs:
public class MainWindowVM
{
public string MyText { get; set; }
public ObservableCollection<TreeElement> MyList { get; set; }
public MainWindowVM()
{
MyText = "This is my Text!";
MyList = new ObservableCollection<TreeElement>();
MyList.Add(new TreeElement("String 1"));
MyList.Add(new TreeElement("String 2"));
}
}
public class TreeElement
{
public string Name { get; set; }
public TreeElement(string Name)
{
this.Name = Name;
}
}
Thanks for your help!!
Joerg
You are close.
What you are doing:
you set the Tag of TreeView to its own DataContext. This is
unnecessary.
you try to get Tag.MyText from your ContextMenu.PlacementTarget - which is TextBlock. The TextBlock has no Tag set.
What you should do:
set the Tag of the TextBlock to DataContext of the Window
(Window is TextBlock ancestor and you should look it up via
RelativeSource Mode=FindAncestor).
the second part is OK - you have TextBlock.Tag set in the first step.
I have a ListBox:
<ListBox Name="lbsfHolder"
ItemsSource="{Binding UISupportingFunctions}"
SelectedItem="{Binding Path=SelectedSupportedFunction, Mode=TwoWay}"
SelectionMode="Multiple"
IsSynchronizedWithCurrentItem="True"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<controls:SupportingFunction GotFocus="SupportingFunction_GotFocus"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In the ViewModel I have:
private SupportingFunction _selectedSupportedFunction;
public SupportingFunction SelectedSupportedFunction
{
get { return _selectedSupportedFunction; }
set
{
_selectedSupportedFunction = value;
NotifyPropertyChanged("SelectedSupportedFunction");
}
}
But when I'm trying to select any item in list box nothing happens. The SelectedItem is null for the ListBox and for SelectedValue, too. Do I need to add some special code to make this work?
UPD:
I've changed views a bit, now I have:
<UserControl x:Class="RFM.UI.WPF.Controls.SupportingFunction">
<Grid>
<ListBox Name="supportingFunctions"
ItemsSource="{Binding UISupportingFunctions}"
SelectedItem="{Binding Path=SelectedSupportedFunction, Mode=TwoWay}"
SelectionMode="Multiple"
IsSynchronizedWithCurrentItem="True"
HorizontalContentAlignment="Stretch">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition />
<ColumnDefinition Width="30" />
</Grid.ColumnDefinitions>
<TextBox Name="tbsfName" Grid.Column="0" Text="{Binding Path=Title, Mode=TwoWay}"></TextBox>
<TextBox Name="tbsfExperssion" Grid.Column="1" Text="{Binding Path=Expression}" HorizontalAlignment="Stretch"></TextBox>
<Button Name="bsfDel" Grid.Column="2">Del</Button>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
</UserControl>
In Page where this control placed:
<StackPanel Name="spSupportingFunctions">
<StackPanel Name="spsfOperations" Orientation="Horizontal">
<Button Name="bsfAdd" Width="30" Command="commands:CustomCommands.AddSupportingFunction">Add</Button>
</StackPanel>
<controls:SupportingFunction DataContext="{Binding Self}" />
</StackPanel>
at code behind of this Page
public PlotDataPage()
{
DataContext = new PlotDataViewModel();
InitializeComponent();
}
and this is the full listing of PlotDataViewModel
public class UISupportingFunction : ISupportingFunction
{
public string Title { get; set; }
public string Expression { get; set; }
}
public class PlotDataViewModel : INotifyPropertyChanged
{
public PlotDataViewModel Self
{
get
{
return this;
}
}
private ObservableCollection<UISupportingFunction> _supportingFunctions;
public ObservableCollection<UISupportingFunction> UISupportingFunctions
{
get
{
return _supportingFunctions;
}
set
{
_supportingFunctions = value;
NotifyPropertyChanged("UISupportingFunctions");
}
}
private UISupportingFunction _selectedSupportedFunction;
public UISupportingFunction SelectedSupportedFunction
{
get
{
return _selectedSupportedFunction;
}
set
{
_selectedSupportedFunction = value;
NotifyPropertyChanged("SelectedSupportedFunction");
}
}
public PlotDataViewModel()
{
UISupportingFunctions = new ObservableCollection<UISupportingFunction>();
}
public void CreateNewSupportingFunction()
{
UISupportingFunctions.Add(new UISupportingFunction() { Title = Utils.GetNextFunctionName() });
}
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
public event PropertyChangedEventHandler PropertyChanged;
}
I'm just calling the CreateNewSupportingFunction() method when I click Add button. Everything looks fine - the items is add and I see them. But when I'm clicking on one of the TextBoxes and then to the bsfDel button right to each item I'm getting null in SelectedSupportedFunction.
Maybe it is because of focus event have been handling by TextBox and not by ListBox?
It's either your ItemsSource UISupportingFunctions is not a SupportingFunction object or you did not set the View's Datacontext to your ViewModel.
ViewModel.xaml.cs
this.DataContext = new ViewModelClass();
In my application, I have a view (ListView) and a view model. Inside view model, I have 2 properties: first is a list of items, and the second is a command. I want to display items (from first property) inside ListView. In addition, I want to have for each one a context menu, where clicking on it will activate a command (from second property).
Here is a code of my view model:
public class ViewModel
{
public IEnumerable Items
{
get
{
return ...; //returns a collection of items
}
}
public ICommand MyCommand //this is a command, I want to be able execute from context menu of each item
{
get
{
return new DelegateCommand(new Action<object>(delegate(object parameter)
{
//here code of the execution
}
), new Predicate<object>(delegate(object parameter)
{
//here code of "can execute"
}));
}
}
Now the XAML part:
<ListView ItemsSource="{Binding Items}">
<ListView.Resources>
<commanding:CommandReference x:Key="myCommand" Command="{Binding MyCommand}"/>
</ListView.Resources>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Name}"
/>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem
Header="Remove from workspace"
Command="{StaticResource myCommand}"
CommandParameter="HERE I WANT TO PASS THE DATA CONTEXT OF THE ListViewItem"
/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
</ListView>
The problem: until I actually opening context menu, the PlacementTarget of the context menu is null. I need somehow to receive data context of the clicked ListViewItem into "CanExecute" of the command, BEFORE the command being called - and I truly wish to make everything in the XAML, without handling any callbacks in code behind.
Thank you in advance.
If you are looking for ListViewItem's DataContext you can do this:
CommandParameter="{Binding}"
Edit - Here is what I tried:
public partial class MainWindow : Window
{
private ObservableCollection<Person> list = new ObservableCollection<Person>();
public MainWindow()
{
InitializeComponent();
list.Add(new Person() { Name = "Test 1"});
list.Add(new Person() { Name = "Test 2"});
list.Add(new Person() { Name = "Test £"});
list.Add(new Person() { Name = "Test 4"});
this.DataContext = this;
}
public static ICommand MyCommand //this is a command, I want to be able execute from context menu of each item
{
get
{
return new DelegateCommand<Person>(
a => Console.WriteLine(a.Name),
a => true);
}
}
public ObservableCollection<Person> Items
{
get
{
return this.list;
}
}
}
public class Person
{
public string Name { get; set; }
}
And the xaml:
<Window x:Class="ListView1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ListView1="clr-namespace:ListView1" Title="MainWindow" Height="350" Width="525">
<Grid>
<ListView ItemsSource="{Binding Items}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="Remove from workspace" Command="{x:Static ListView1:MainWindow.MyCommand}" CommandParameter="{Binding}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
</ListView>
</Grid>
</Window>