CommandBinding in ContextMenu - c#

I have a TreeView and have created a basic TreeItem type. Each TreeItem has a header, a TreeItem Collection for children and a collection for a possible context menu. The TreeItem class has those objects:
public delegate void dExecute(TreeItem item);
public dExecute ExecuteTarget { get; set; }
public object Tag { get; set; }
public string Header { get; set; }
public List<TreeItem> Children { get; set; }
public List<TreeItem> ContextMenu { get; set; }
The context menu uses again a HierarchicalDataTemplate to display TreeItem objects (I use the TreeItem class for the items in the treeview AND in the context menu). The context menu looks like this:
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}" Visibility="{Binding ShowContextMenu}" ItemsSource="{Binding ContextMenu}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Execute}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
</ContextMenu>
The context menu is rendered as I want it to be. I have created a context menu that I just attach to some of my items in the tree view. This is its content.
public List<TreeItem> ContextMenu
{
get
{
List<TreeItem> list = new List<TreeItem>();
TreeItem ti = new TreeItem("Some Action") { ExecuteTarget = targetMethod};
list.Add(ti);
ti = new TreeItem("test");
ti.Children.Add(new TreeItem("foo") { ExecuteTarget = targetMethod});
ti.Children.Add(new TreeItem("bar") { ExecuteTarget = targetMethod});
ti.Children.Add(new TreeItem("foo") { ExecuteTarget = targetMethod});
TreeItem ti2 = new TreeItem("inner"){ ExecuteTarget = targetMethod};
ti.Children.Add(ti2);
ti2.Children.Add(new TreeItem("foo") { ExecuteTarget = targetMethod});
list.Add(ti);
return list;
}
}
The context menu looks like this.
It looks as it should be. The commands work as they should. EXCEPT for the command on the highest level of the context menu. When I click on "Some Action" nothing happens. I assume that I have to add something to XAML, but I have no idea where.

<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}" Visibility="{Binding ShowContextMenu}" ItemsSource="{Binding ContextMenu}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Header}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Execute}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
<!-- this is what you're missing -->
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Execute}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>

Related

WPF command not working for submenu items in MVVM application

I have a menu which is built from a collection at runtime. This is all working as shown.
But if the menu contains child items (Child1, Child2 etc) the ReactiveCommand MenuCommand is never called.
If I remove all children from the menu so that the menu only contains parent items then MenuCommand is called. I am fairly new to WPF. I have re-created the problem in a sample app (code below). There are no visible binding errors in VS.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
public class Service
{
public Service(string menuHeading, string menuSubHeading)
{
MenuHeading = menuHeading;
MenuSubHeading = menuSubHeading;
}
public string MenuHeading { get; set; }
public string MenuSubHeading { get; set; }
}
public static class MenuBuilder
{
public static ReactiveList<MenuItem> Build(ReactiveList<Service> services)
{
ReactiveList<MenuItem> menuItems = new ReactiveList<MenuItem>();
foreach (var service in services)
{
AddOrUpdate(menuItems, service);
}
return menuItems;
}
private static void AddOrUpdate(ReactiveList<MenuItem> menu, Service service)
{
if (menu.Any((_ => _.Header.ToString() == service.MenuHeading)))
{
var item = menu.FirstOrDefault(x => x.Header.ToString() == service.MenuHeading);
item.Items.Add(new MenuItem() { Header = service.MenuSubHeading });
//if above line removed MenuCommand works
}
else
{
menu.Add(new MenuItem() { Header = service.MenuHeading });
var item = menu.FirstOrDefault(x => x.Header.ToString() == service.MenuHeading);
item.Items.Add(new MenuItem() { Header = service.MenuSubHeading });
//if above line removed MenuCommand works
}
}
}
public class MainWindowViewModel : ReactiveObject
{
public MainWindowViewModel()
{
MenuCommand = ReactiveCommand.Create<Object>(selectedItem => OnMenuItemSelected(selectedItem));
MenuCommand.Execute().Subscribe();
}
public ReactiveCommand<Object, Unit> MenuCommand { get; }
private ReactiveList<MenuItem> servicesMenu;
private ReactiveList<Service> Services = new ReactiveList<Service>()
{
new Service("Parent1", "Child1"),
new Service("Parent2", "Child1"),
new Service("Parent2", "Child2"),
};
public ReactiveList<MenuItem> ServicesMenu
{
get
{
if (servicesMenu == null)
{
servicesMenu = MenuBuilder.Build(Services);
return servicesMenu;
}
else
{
return servicesMenu;
}
}
}
private void OnMenuItemSelected(Object selectedItem)
{
//This method is only called when the menu does not contain any child items
}
}
<Grid>
<StackPanel Orientation="Vertical">
<Button Name="Button" Content="Button" Padding="5" HorizontalAlignment="Left"
Tag="{Binding RelativeSource={RelativeSource Self}, Path=DataContext}">
<Button.ContextMenu>
<ContextMenu x:Name="MainMenu" ItemsSource="{Binding ServicesMenu}"
DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command"
Value="{Binding DataContext.MenuCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}}" />
<Setter Property="CommandParameter"
Value="{Binding RelativeSource={RelativeSource Self}}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
</StackPanel>
</Grid>
Updated XAML after suggestions form Glenn
<Grid>
<StackPanel Orientation="Vertical">
<Button Name="Button" Content="Button" Padding="5" HorizontalAlignment="Left"
Tag="{Binding RelativeSource={RelativeSource Self}, Path=DataContext}">
<Button.ContextMenu>
<ContextMenu x:Name="MainMenu" ItemsSource="{Binding ServicesMenu}"
DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Header}" />
<Setter Property="Command" Value="{Binding Command}" />
<!--<Setter Property="Command" Value="{Binding MenuCommand}" /> was also tried-->
<Setter Property="CommandParameter" Value="{Binding}" />
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
</StackPanel>
</Grid>
I suspect this is because child items placement target wouldn't be the Button like you expect, it would be the parent MenuItem.
One way I've gotten around this in the past is using MVVM approach for these type of menu items.
Create a Menu Item VM (you call them Service above) for your items (similar to what you already doing). In the VM have a Command property and pass in your command as part of it's constructor. Then you can just do {Binding MenuCommand} from your Item Container Style.
Also don't create the MenuItem's directly in your ViewModel, instead just bind direct to the Services. I would also recommend creating your sub-services as a ObservableCollection directly inside your Service, then in your item container set the ItemsSource property to bind to the sub-children of your Services.

Create Hierarchical ContextMenu dynamically MVVM

I would like to create dynamically Hierarchical ContextMenu from data in ViewModel.
In ViewMode, I defined ContextMenuAction:
public class ContextMenuAction : ViewModelBase
{
public string Header { get; set; }
public ICommand Action { get; set; }
public Brush Icon { get; set; }
public ObservableCollection<ContextMenuAction> SubActions { get; set; } = new ObservableCollection<ContextMenuAction>();
}
In View:
<ContextMenu ItemsSource="{Binding Path=PlacementTarget.Tag.Actions, RelativeSource={RelativeSource Self}}">
<ContextMenu.ItemTemplate >
<DataTemplate DataType="MenuItem">
<MenuItem/>
</DataTemplate>
</ContextMenu.ItemTemplate>
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="ItemsSource" Value="{Binding SubActions}"/>
<Setter Property="Header" Value="{Binding Header}" />
<Setter Property="Command" Value="{Binding Action}"/>
</Style>
</ContextMenu.ItemContainerStyle>
This is result, there no text in context menu.
I already check output window to check binding, all bindings work, there is no exception.
Please help me to find out the reason, thank in advance!
You should define a HierarchicalDataTemplate:
<ContextMenu ItemsSource="{Binding Path=PlacementTarget.Tag.Actions, RelativeSource={RelativeSource Self}}">
<ContextMenu.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:ContextMenuAction}" ItemsSource="{Binding SubActions}">
<TextBlock Text="{Binding Header}" />
</HierarchicalDataTemplate>
</ContextMenu.ItemTemplate>
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command" Value="{Binding Action}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>

WPF: context menu in TreeViewItem binding to root

I have TreeView control and I need to bind property from root (window/usercontrol) DataContext in context menu in that treeview.
<TextBox Text="{Binding Header}"></TextBox>
<TreeView ItemsSource="{Binding Items}" Grid.Row="1">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{{ BINDING TO HEADER PROPERTY FROM WINDOW DATACONTEXT}}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
public ObservableCollection<string> Items { get; set; }
public string Header { get { return _header; } set { _header = value; } }
I've tried multiple things: I've added x:Name="WindowRoot" to Window and {Binding Header, ElementName=WindowRoot} but it didn't work, I've tried multiple FindAncestor and RelativeSource but it didn't work.
Can someone help me?
Edit:
This is simplified case, in my normal application I use Unity + Prism, so ViewModel is AutoDiscovered (prism:ViewModelLocator.AutoWireViewModel="True") and it generally works. By "works" I mean: TreeView shows items from my collection, so it is connected, the problem is with context menu binding only.
In this simplified example I have ugly and simple code-behind, because I only want to test this ContextMenu binding:
public partial class MainWindow : Window
{
public ObservableCollection<string> Items { get; set; }
private string _header = "testtest";
public string Header { get { return _header ; } set { _header = value; } }
public MainWindow()
{
Items = new ObservableCollection<string>();
Items.Add("ItemTest");
InitializeComponent();
this.DataContext = this;
}
}
You could bind the Tag property of the TreeViewItem to the parent window or user control using a {RelativeSource} and then bind the Header property of the MenuItem to the Tag property of the PlacementTarget of the ContextMenu:
<TreeView x:Name="tv" ItemsSource="{Binding Items}" Grid.Row="1">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType=Window}}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{Binding PlacementTarget.Tag.DataContext.Header, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
The reason you cannot bind directly to any property of the window from the MenuItem is that a ContextMenu resides in a different element tree than the parent window or user control.

How to access the right datacontext

I'm building a treeview with HierarchicalDataTemplates and would like to have some nodes expaned. The Nodes have a property "IsNodeExpanded" that I'd like to bind the property IsExpanded to.
Where I run into troubles is binding to this property. E.g. this
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding DataContext.IsNodeExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}">
</Setter>
</Style>
will bind to a property IsNodeExpanded defined on my MainViewModel while this
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding IsNodeExpanded}">
</Setter>
</Style>
will have no effect at all because, I guess, the binding has a datacontext problem.
How can I refer to the right datacontext?
For completeness, here is my TreeView
<TreeView ItemsSource="{Binding _questions}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Question}"
ItemsSource="{Binding Converter={StaticResource QuestionConverter}}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:MainOption}"
ItemsSource="{Binding MainOptions}">
<StackPanel Orientation="Horizontal">
<CheckBox Content="{Binding Path=Name}"
IsChecked="{Binding Path=IsSelected}"
Command="{Binding DataContext.ToggleSelectedMetaItem, RelativeSource={RelativeSource FindAncestor, AncestorType=Window}}"
CommandParameter="{Binding Path=MetaItemId}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding DataContext.IsNodeExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType=TreeView}}">
</Setter>
</Style>
</TreeView.ItemContainerStyle>
EDIT:
public class MainOption
{
public string Name { get; set; }
public int MetaItemId { get; set; }
public bool IsSelected { get; set; }
public MainOption()
{
this.IsSelected = true;
}
}
public class Question
{
public string Name { get; set; }
public List<MainOption> MainOptions { get; set; }
public Question()
{
MainOptions = new List<MainOption>();
}
}
In ItemContainerStyle you should not define relative source when binding to IsNodeExpanded property. if itemssource _questions is list of Question, then {Binding IsNodeExpanded} binds to IsNodeExpanded property of Question class. I quess you are missing the property.
here is xaml:
<TreeView ItemsSource="{Binding Questions}">
<TreeView.Resources>
<DataTemplate x:Key="OptionTemplate" DataType="l:MainOption">
<CheckBox IsChecked="{Binding IsSelected}" Content="{Binding Name}" />
</DataTemplate>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="l:Question" ItemsSource="{Binding MainOptions}"
ItemTemplate="{StaticResource OptionTemplate}" ItemContainerStyle="{x:Null}">
<!-- DataContext of type Question -->
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsNodeExpanded}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
and C# code:
public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel()
{
Questions = Enumerable.Range(1, 10)
.Select(i => new Question
{
Name = "Question " + i,
MainOptions = Enumerable.Range(1, 5)
.Select(j => new MainOption {Name = "Option " + i})
.ToList()
})
.ToList();
}
public List<Question> Questions { get; private set; }
}
public class Question
{
public string Name { get; set; }
public List<MainOption> MainOptions { get; set; }
public bool IsNodeExpanded { get; set; }
public Question()
{
IsNodeExpanded = true;
MainOptions = new List<MainOption>();
}
}
the point is, that you set Style and ItemTemplate of root items in threeview. In HierarchicalItemTemplate ItemSource is used to generate subitems, ItemContaierStyle affects style of subitems and ItemTemplate affects datatempalte of subitems

How to bind to event in parent viewmodel from child viewmodel

I am currently following this guide for setting up a TreeView with checkboxes. In my code, the tree "FooViewModel" is initiated in my MainViewModel and bound to the TreeView as an ItemsSource. I want to be able to subscribe to some event in the MainViewModel that will trigger when something is checked or unchecked. That way I can iterate through the "FooViewModel" and check which nodes have IsChecked = True. How do I create this event binding?
This is the code I have:
<Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="False" />
<Setter Property="IsSelected" Value="{Binding IsInitiallySelected, Mode=OneTime}" />
<Setter Property="KeyboardNavigation.AcceptsReturn" Value="True" />
<Setter Property="xn:VirtualToggleButton.IsVirtualToggleButton" Value="True" />
<Setter Property="xn:VirtualToggleButton.IsChecked" Value="{Binding IsChecked}" />
<Setter Property="Focusable" Value="False" />
</Style>
<xn:TreeView ItemsSource="{Binding CollectionFooViewModel}" ItemContainerStyle="{StaticResource TreeViewItemStyle}">
<xn:TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False" IsChecked="{Binding IsChecked}" VerticalAlignment="Center"/>
<ContentPresenter Content="{Binding Name, Mode=OneTime}" Margin="2,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</xn:TreeView.ItemTemplate>
</xn:TreeView>
I figured if there's a way to bind "IsChecked" to two properties (one in FooViewModel, another in MainViewModel) I would have my answer.
Lots of ways to achieve this. One would be some kind of a pub/sub (messaging) implementation or maybe just bunch of Action delegates? Something like...
MainWindow
<Window x:Class="WpfApplication1.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Height="300"
Width="250">
<TreeView ItemsSource="{Binding CollectionFooViewModel}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children, Mode=OneTime}">
<StackPanel Orientation="Horizontal">
<CheckBox Focusable="False"
IsChecked="{Binding IsChecked}"
VerticalAlignment="Center"/>
<ContentPresenter Content="{Binding Name, Mode=OneTime}"
Margin="2,0"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Window>
DataContext
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
Action<MyItem> action = item => Console.WriteLine(#"MyItem was clicked");
CollectionFooViewModel = new ObservableCollection<MyItem>()
{
new MyItem()
{
Name = "MyItem1",
Children = new List<MyItem>()
{
new MyItem()
{
Name = "MySubItem1",
IsChecked = false,
Action = item => Console.WriteLine(#"{0} invoked action", item.Name)
},
new MyItem()
{
Name = "MySubItem2",
IsChecked = true,
Action = item => Console.WriteLine(#"{0} state is {1} ", item.Name, item.IsChecked)
},
},
Action = action
}
};
}
public ObservableCollection<MyItem> CollectionFooViewModel { get; set; }
}
public class MyItem : ViewModelBase
{
private bool _isChecked;
public string Name { get; set; }
public bool IsChecked
{
get { return _isChecked; }
set
{
_isChecked = value;
if (Action != null)
Action.BeginInvoke(this, null, null);
}
}
public IEnumerable<MyItem> Children { get; set; }
public Action<MyItem> Action { get; set; }
}
Which gives you the following...
...and spits this out to console when clicked in order.
MyItem was clicked
MySubItem1 invoked action
MySubItem2 state is False
Of course, in your case, you might want to pass concrete method to delegate.
Try adding "OnPropertyEventChanged" method call in the setter for your model, here is an example of what I mean:
https://msdn.microsoft.com/en-us/library/ms743695%28v=vs.110%29.aspx

Categories

Resources