Here is a problem.
I am displaying context menu on a button click and the menu command is bind to ICommand in the view model. Menu is displaying on the button click as well as on the right click. The problem is menu click is not firing when I click button and then click context menu, but I can confirm that menu is working when I right click on button and then click on menu.
<Button Grid.Row="3" Width="500" Height="30" Name="cmButton" >
Button with Context Menu
<Button.ContextMenu>
<ContextMenu DataContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Mode=Self}}" >
<MenuItem DataContext="{Binding}" Header="New Layout Element..." Command="{Binding Path=SubmitBtn}" />
</ContextMenu>
</Button.ContextMenu>
<Button.Style>
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<EventTrigger RoutedEvent="Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
I can confirm there is nothing wrong in my view model because command is firing when I do right click on button then click on context menu.
PlacementTarget is null when you manually set ContextMenu.IsOpen property because it is set to actual value only once it's open by right clicking on target control. (PopUpService class is responsible for setting this value to actual target).
Since PlacementTarget is null in case when you open it via Storyboard, binding is not able to resolve actual command it's binded to.
So, issue is you need to pass on the DataContext of Button to the MenuItem so that binding can be resolved. (MenuItem are not is same visual tree as that of button). That can be achieved via two ways:
Using x:Reference (available in WPF 4.0 and higher) but you need to declare dummy control so that it can be referenced to get DataContext with Visibility set to Collapsed.
<FrameworkElement x:Name="dummyControl" Visibility="Collapsed"/>
<Button Width="100" Height="30" Name="cmButton">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="New Layout Element..."
Command="{Binding Path=DataContext.SubmitBtn,
Source={x:Reference dummyControl}}" />
</ContextMenu>
</Button.ContextMenu>
</Button>
Another interesting thing is Freezable objects inherit DataContext even if they don't lie in VisualTree so we can use this feature to overcome situations where we need to inherit DataContext.
First we need to create class inheriting from Freezable and exposing DP which can be bind to:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy));
}
Now we can use it in XAML like this:
<Button Width="100" Height="30" Name="cmButton">
<Button.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}"/>
</Button.Resources>
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="New Layout Element..."
Command="{Binding Path=Data.SubmitBtn,
Source={StaticResource proxy}}" />
</ContextMenu>
</Button.ContextMenu>
</Button>
It's happened because the DataContext of ContextMenu is null, you just need set him on event click from Button. Look the sample:
XAML:
<Button Content="More..." Click="ButtonMoreClick" ContextMenu="{StaticResource ContextMenu1}"/>
BEHIND CODE
private void ButtonMoreClick(object sender, RoutedEventArgs e)
{
var menu = (sender as Button).ContextMenu;
menu.DataContext = DataContext;
menu.IsOpen = true;
}
I hope help
Related
I would like to get buttons like in AX, where some buttons only open a menu with more menu items underneath. I have found the code below and it works fine. But when I left-click on the button, it shows the context menu where the mouse pointer is. When I right-click on the button, it shows the context menu exactly under the button nicely. I would like the left-click to work like the right-click.
The problem is that ContextMenuService.Placement="Bottom" is only working when I right-click.
<Button Name="MainButton" Content="Button with ContextMenu" Width="150" Height="30" ContextMenuService.Placement="Bottom">
<Button.ContextMenu>
<ContextMenu x:Name="MainContextMenu" PlacementRectangle="{Binding RelativeSource={RelativeSource self}}">
<MenuItem Header="Do A" />
<MenuItem Header="Do B" />
<MenuItem Header="Do C" />
</ContextMenu>
</Button.ContextMenu>
<Button.Triggers>
<EventTrigger SourceName="MainButton" RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="MainContextMenu" Storyboard.TargetProperty="(ContextMenu.IsOpen)">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<system:Boolean>True</system:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
Using a Storyboard for setting a single property value is pretty odd. This works, but looks somewhat ugly.
But this is also the reason why your ContextMenuService.Placement="Bottom" setting doesn't work: you just don't invoke the ContextMenuService for opening the context menu via the Storyboard, so the PlacementTarget won't be set to your button.
I'd suggest you to create a simple attached property and re-use it in your views:
static class ContextMenuTools
{
public static readonly DependencyProperty OpenOnLeftClickProperty =
DependencyProperty.RegisterAttached(
"OpenOnLeftClick",
typeof(bool),
typeof(ContextMenuTools),
new PropertyMetadata(false, OpenOnLeftClickChanged));
public static void SetOpenOnLeftClick(UIElement element, bool value)
=> element.SetValue(OpenOnLeftClickProperty, value);
public static bool GetOpenOnLeftClick(UIElement element)
=> (bool)element.GetValue(OpenOnLeftClickProperty);
private static void OpenOnLeftClickChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is IInputElement element && (bool)e.NewValue)
{
element.PreviewMouseLeftButtonDown += ElementOnMouseLeftButtonDown;
}
}
private static void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is UIElement element
&& ContextMenuService.GetContextMenu(element) is ContextMenu contextMenu)
{
contextMenu.Placement = ContextMenuService.GetPlacement(element);
contextMenu.PlacementTarget = element;
contextMenu.IsOpen = true;
}
}
}
Usage might look like this:
<Button Content="Button with ContextMenu" ContextMenuService.Placement="Bottom"
yourNS:ContextMenuTools.OpenOnLeftClick="True">
<Button.ContextMenu>
<ContextMenu x:Name="MainContextMenu">
<MenuItem Header="Do A" />
</ContextMenu>
</Button.ContextMenu>
</Button>
Note the yourNS namespace - this is a XAML namespace mapped to the CLR namespace where the attached property class ContextMenuTools is located, e.g.:
xmlns:yourNS="clr-namespace:WpfApp1"
You could also use this attached property on any WPF control implementing IInputElement, not only on buttons.
This should do the trick instead of using a Button
<Menu>
<MenuItem Header="Options">
<MenuItem Header="Do A"/>
<MenuItem Header="Do B"/>
<MenuItem Header="Do C"/>
</MenuItem>
</Menu>
I have a ViewModel with a command 'OpenCommand', a flag 'IsConextMenuVisible' and an observable list 'Links'.
public ObservableList<string> Links { get; set; }
public bool IsContextMenuVisible { get; set; }
public ICommand OpenCommand { get; set; }
in XAML i want the following to work.
<ListBox ItemsSource="{Binding Links}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}">
<TextBlock.ContextMenu>
<ContextMenu Visibility="{Binding IsContextMenuVisible, Converter={StaticResource BoolToVisibiltyHiddenConverter}}">
<MenuItem Header="Open" Command="{Binding OpenCommand}" CommandParameter="{Binding}"/>
</ContextMenu>
</Textblock.ContextMenu>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I've already tried some binding expressions for the inner bindings on the ContextMenu, but nothing seems to work. Something like:
Visibility="{Binding Path=DataContext.IsContextMenuVisible,
Converter={StaticResource BoolToVisibilityCollapsedConverter},
RelativeSource={RelativeSource AncestorType=ListBox}}"
This is "problematic" as the kids say because the context menu isn't in the visual tree, so no flavor of RelativeSource is going to work.
You can often bind to properties of PlacementTarget, but in this case you need a visual ancestor of the PlacementTarget, and RelativeSource won't do an ancestor of something else.
In WPF, when there's a gap in the visual tree, the last ditch option is always a BindingProxy. Here's what that class looks like (including the URL of the StackOverflow question I stole it from -- that class has been copied and pasted around many, many questions and answers on this site):
// https://stackoverflow.com/questions/24452264/bindingproxy-binding-to-the-indexed-property
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
And you would use it like this. First create the BindingProxy as a resource, in a location where it can "see" the desired element:
<Window.Resources>
<local:BoolToVisibiltyHiddenConverter x:Key="BoolToVisibiltyHiddenConverter" />
<!-- {Binding} with no path will be the window's datacontext, the main viewmodel. -->
<local:BindingProxy Data="{Binding}" x:Key="MainViewModelBindingProxy" />
</Window.Resources>
And then use it for the Source of the binding. The desired DataContext will be the Data property of the proxy object, so provide paths relative to Data:
<TextBlock.ContextMenu>
<ContextMenu
Visibility="{Binding Data.IsContextMenuVisible,
Converter={StaticResource BoolToVisibiltyHiddenConverter},
Source={StaticResource MainViewModelBindingProxy}}"
>
<MenuItem
Header="Open"
Command="{Binding OpenCommand}"
CommandParameter="{Binding}"
/>
</ContextMenu>
</TextBlock.ContextMenu>
Now you've got another problem: The menu is still popping up. It just doesn't happen to be visible. If the user right clicks, it'll pop up invisibly, and suddenly appear when IsContextMenuVisible changes to true. That's not what you want.
You could omit the converter and just bind directly to ContextMenu.IsEnabled: It'll still pop up, but it'll be grayed out. This is consistent with common Windows UI practice.
You could also have a style trigger so that the TextBlock only has a ContextMenu when you want it to have one. Because that trigger is on the TextBlock, it's in the visual tree we can use a conventional RelativeSource for the binding.
<TextBlock Text="{Binding}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger
Binding="{Binding Data.IsContextMenuVisible,
RelativeSource={RelativeSource AncestorType=ListBox}}"
Value="True">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu >
<MenuItem
Header="Open"
Command="{Binding OpenCommand}"
CommandParameter="{Binding}"
/>
</ContextMenu>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
i wanted a contextmenu for my ListBoxItems.
So i created this:
<ListBox Name="listBoxName">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding UserName}" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="View" Name="MenuItemView" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
This works great. I have the contextmenu for all items, but if i want to add a click-eventhandler to the menuitem, like this:
<MenuItem Header="View" Name="MenuItemView" Click="MenuItemView_Click" />
I get a XamlParseException when the window is created.
InnerException:
The Object System.Windows.Controls.MenuItem cannot be converted to type System.Windows.Controls.Grid
It throws only the exception if i add a event-handler. The event-method is empty.
Edit:
Stacktrace of the InnerException:
at
Chat_Client.ChatWindow.System.Windows.Markup.IComponentConnector.Connect(Int32
connectionId, Object target) in
c:\XXX\Chat_Client\ChatWindow.xaml:Row
19.
at
MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetConnectionId(Object
root, Int32 connectionId, Object
instance)
Edit2:
Now i have to get the object I clicked with the contextmenu. First i tried this:
//MenuItem s = sender as MenuItem;
//ContextMenu cm = s.Parent as ContextMenu;
//Popup pu = cm.Parent as Popup;
//object o = pu.Parent;
But the Popup's parent is null.
Then i simply get the selectedItem from the ListBox. This works, but is there no way to get the ListBoxRow of the clicked Contextmenu?
I cannot reproduce your crash with VS2010 and WPF4.
You only need one context menu for all your items so you can extract it to a window resource, for example:
<Window.Resources>
<ContextMenu x:Key="ListBoxItemContextMenu">
<MenuItem Header="View" Name="MenuItemView" Click="MenuItemView_Click"/>
</ContextMenu>
</Window.Resources>
and then change your setter to refer to that one context menu:
<Setter Property="ContextMenu" Value="{StaticResource ListBoxItemContextMenu}"/>
and the event handler then works:
private void MenuItemView_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Clicked!");
}
I have a WPF application..In which I have an Image control in Xaml file.
On right click of this image I have a context menu.
I would like to have same to be displayed on "Left click" also.
How do I do this in MVVM way ?
Here is a XAML only solution.
Just add this style to your button.
This will cause the context menu to open on both left and right click. Enjoy!
<Button Content="Open Context Menu">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<EventTrigger RoutedEvent="Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem />
<MenuItem />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
You can do this by using the MouseDown event of an Image like this
<Image ... MouseDown="Image_MouseDown">
<Image.ContextMenu>
<ContextMenu>
<MenuItem .../>
<MenuItem .../>
</ContextMenu>
</Image.ContextMenu>
</Image>
And then show the ContextMenu in the EventHandler in code behind
private void Image_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
Image image = sender as Image;
ContextMenu contextMenu = image.ContextMenu;
contextMenu.PlacementTarget = image;
contextMenu.IsOpen = true;
e.Handled = true;
}
}
You can invent your own DependencyProperty which opens a context menu when image is clicked, just like this:
<Image Source="..." local:ClickOpensContextMenuBehavior.Enabled="True">
<Image.ContextMenu>...
</Image.ContextMenu>
</Image>
And here is a C# code for that property:
public class ClickOpensContextMenuBehavior
{
private static readonly DependencyProperty ClickOpensContextMenuProperty =
DependencyProperty.RegisterAttached(
"Enabled", typeof(bool), typeof(ClickOpensContextMenuBehavior),
new PropertyMetadata(new PropertyChangedCallback(HandlePropertyChanged))
);
public static bool GetEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(ClickOpensContextMenuProperty);
}
public static void SetEnabled(DependencyObject obj, bool value)
{
obj.SetValue(ClickOpensContextMenuProperty, value);
}
private static void HandlePropertyChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is Image) {
var image = obj as Image;
image.MouseLeftButtonDown -= ExecuteMouseDown;
image.MouseLeftButtonDown += ExecuteMouseDown;
}
if (obj is Hyperlink) {
var hyperlink = obj as Hyperlink;
hyperlink.Click -= ExecuteClick;
hyperlink.Click += ExecuteClick;
}
}
private static void ExecuteMouseDown(object sender, MouseEventArgs args)
{
DependencyObject obj = sender as DependencyObject;
bool enabled = (bool)obj.GetValue(ClickOpensContextMenuProperty);
if (enabled) {
if (sender is Image) {
var image = (Image)sender;
if (image.ContextMenu != null)
image.ContextMenu.IsOpen = true;
}
}
}
private static void ExecuteClick(object sender, RoutedEventArgs args)
{
DependencyObject obj = sender as DependencyObject;
bool enabled = (bool)obj.GetValue(ClickOpensContextMenuProperty);
if (enabled) {
if (sender is Hyperlink) {
var hyperlink = (Hyperlink)sender;
if(hyperlink.ContextMenu != null)
hyperlink.ContextMenu.IsOpen = true;
}
}
}
}
If you want to do this just in Xaml without using code-behind you can use Expression Blend's triggers support:
...
xmlns:i="schemas.microsoft.com/expression/2010/interactivity"
...
<Button x:Name="addButton">
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding Items}" />
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=addButton}" PropertyName="PlacementTarget" Value="{Binding ElementName=addButton, Mode=OneWay}"/>
<ei:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=addButton}" PropertyName="IsOpen" Value="True"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button.ContextMenu>
</Button>
you only need add the code into function Image_MouseDown
e.Handled = true;
Then it will not disappear.
Hey I came across the same problem looking for a solution which I didn't find here.
I don't know anything about MVVM so it's probably not MVVM conform but it worked for me.
Step 1: Give your context menu a name.
<Button.ContextMenu>
<ContextMenu Name="cmTabs"/>
</Button.ContextMenu>
Step 2: Double click the control object and insert this code. Order matters!
Private Sub Button_Click_1(sender As Object, e As Windows.RoutedEventArgs)
cmTabs.StaysOpen = True
cmTabs.IsOpen = True
End Sub
Step 3: Enjoy
This will react for left & right click. It's a button with a ImageBrush with a ControlTemplate.
you can bind the Isopen Property of the contextMenu to a property in your viewModel like "IsContextMenuOpen".
but the problem is your can't bind directly the contextmenu to your viewModel because it's not a part of your userControl hiarchy.So to resolve this you should bing the tag property to the dataontext of your view.
<Image Tag="{Binding DataContext, ElementName=YourUserControlName}">
<ContextMenu IsOpen="{Binding PlacementTarget.Tag.IsContextMenuOpen,Mode=OneWay}" >
.....
</ContextMenu>
<Image>
Good luck.
Interactivity is old and not support any more. The new approach for the implementation is:
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
<Button x:Name="ConvertVideoButton">
<Button.ContextMenu>
<ContextMenu VerticalContentAlignment="Top" >
<MenuItem Header="Convert 1" Command="{Binding ConvertMkvCommand}" />
<MenuItem Header="Convert 2" Command="{Binding ConvertMkvCommand}" />
</ContextMenu>
</Button.ContextMenu>
<b:Interaction.Triggers>
<b:EventTrigger EventName="Click">
<b:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=ConvertVideoButton}" PropertyName="PlacementTarget" Value="{Binding ElementName=ConvertVideoButton, Mode=OneWay}"/>
<b:ChangePropertyAction TargetObject="{Binding ContextMenu, ElementName=ConvertVideoButton}" PropertyName="IsOpen" Value="True"/>
</b:EventTrigger>
</b:Interaction.Triggers>
</Button>
XAML
<Button x:Name="b" Content="button" Click="b_Click" >
<Button.ContextMenu >
<ContextMenu >
<MenuItem Header="Open" Command="{Binding OnOpen}" ></MenuItem>
<MenuItem Header="Close" Command="{Binding OnClose}"></MenuItem>
</ContextMenu>
</Button.ContextMenu>
</Button>
C#
private void be_Click(object sender, RoutedEventArgs e)
{
b.ContextMenu.DataContext = b.DataContext;
b.ContextMenu.IsOpen = true;
}
I have a problem when binding a command in a context menu on a usercontrol that is on a tab page.
The first time I use the menu (right-click on the tab) it works great, but if I switch tab the command will use the databound instance that was used the first time.
If I put a button that is bound to the command in the usercontrol it works as expected...
Can someone please tell me what I'm doing wrong??
This is a test project that exposes the problem:
App.xaml.cs:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
CompanyViewModel model = new CompanyViewModel();
Window1 window = new Window1();
window.DataContext = model;
window.Show();
}
}
Window1.xaml:
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vw="clr-namespace:WpfApplication1"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="HeaderTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type vw:PersonViewModel}">
<vw:UserControl1/>
</DataTemplate>
</Window.Resources>
<Grid>
<TabControl ItemsSource="{Binding Path=Persons}"
ItemTemplate="{StaticResource HeaderTemplate}"
IsSynchronizedWithCurrentItem="True" />
</Grid>
</Window>
UserControl1.xaml:
<UserControl x:Class="WpfApplication1.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MinWidth="200">
<UserControl.ContextMenu>
<ContextMenu >
<MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/>
</ContextMenu>
</UserControl.ContextMenu>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0">The name:</Label>
<TextBox Grid.Column="1" Text="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</UserControl>
CompanyViewModel.cs:
public class CompanyViewModel
{
public ObservableCollection<PersonViewModel> Persons { get; set; }
public CompanyViewModel()
{
Persons = new ObservableCollection<PersonViewModel>();
Persons.Add(new PersonViewModel(new Person { Name = "Kalle" }));
Persons.Add(new PersonViewModel(new Person { Name = "Nisse" }));
Persons.Add(new PersonViewModel(new Person { Name = "Jocke" }));
}
}
PersonViewModel.cs:
public class PersonViewModel : INotifyPropertyChanged
{
Person _person;
TestCommand _testCommand;
public PersonViewModel(Person person)
{
_person = person;
_testCommand = new TestCommand(this);
}
public ICommand ChangeCommand
{
get
{
return _testCommand;
}
}
public string Name
{
get
{
return _person.Name;
}
set
{
if (value == _person.Name)
return;
_person.Name = value;
OnPropertyChanged("Name");
}
}
void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
TestCommand.cs:
public class TestCommand : ICommand
{
PersonViewModel _person;
public event EventHandler CanExecuteChanged;
public TestCommand(PersonViewModel person)
{
_person = person;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_person.Name = "Changed by command";
}
}
Person.cs:
public class Person
{
public string Name { get; set; }
}
The key thing to remember here is context menus are not part of the visual tree.
Therefore they don't inherit the same source as the control they belong to for binding. The way to deal with this is to bind to the placement target of the ContextMenu itself.
<MenuItem Header="Change" Command="{Binding
Path=PlacementTarget.ChangeCommand,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"
/>
The cleanest way I have found to bind commands to context menu items involves using a class called CommandReference. You can find it in the MVVM toolkit on Codeplex at WPF Futures.
The XAML might look like this:
<UserControl x:Class="View.MyView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ViewModel;assembly=MyViewModel"
xmlns:mvvm="clr-namespace:ViewModelHelper;assembly=ViewModelHelper"
<UserControl.Resources>
<mvvm:CommandReference x:Key="MyCustomCommandReference" Command="{Binding MyCustomCommand}" />
<ContextMenu x:Key="ItemContextMenu">
<MenuItem Header="Plate">
<MenuItem Header="Inspect Now" Command="{StaticResource MyCustomCommandReference}"
CommandParameter="{Binding}">
</MenuItem>
</MenuItem>
</ContextMenu>
</UserControl.Resources>
MyCustomCommand is a RelayCommand on the ViewModel. In this example, the ViewModel was attached to the view's datacontext in the code-behind.
Note: this XAML was copied from a working project and simplified for illustration. There may be typos or other minor errors.
I had the same issue recently with a ContextMenu located in a ListBox. I tried to bind a command the MVVM way without any code-behind. I finally gave up and I asked a friend for his help. He found a slightly twisted but concise solution.
He is passing the ListBox in the DataContext of the ContextMenu and then find the command in the view model by accessing the DataContext of the ListBox. This is the simplest solution that I have seen so far. No custom code, no Tag, just pure XAML and MVVM.
I posted a fully working sample on Github. Here is an excerpt of the XAML.
<Window x:Class="WpfListContextMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow" Height="350" Width="268">
<Grid>
<DockPanel>
<ListBox x:Name="listBox" DockPanel.Dock="Top" ItemsSource="{Binding Items}" DisplayMemberPath="Name"
SelectionMode="Extended">
<ListBox.ContextMenu>
<ContextMenu DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Show Selected" Command="{Binding Path=DataContext.ShowSelectedCommand}"
CommandParameter="{Binding Path=SelectedItems}" />
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</DockPanel>
</Grid>
</Window>
I prefer another solution.
Add context menu loader event.
<ContextMenu Loaded="ContextMenu_Loaded">
<MenuItem Header="Change" Command="{Binding Path=ChangeCommand}"/>
</ContextMenu>
Assign data context within the event.
private void ContextMenu_Loaded(object sender, RoutedEventArgs e)
{
(sender as ContextMenu).DataContext = this; //assignment can be replaced with desired data context
}
I found this method using the Tag property very useful when binding from a context menu deep inside a control template:
http://blog.jtango.net/binding-to-a-menuitem-in-a-wpf-context-menu
This makes it possible to bind to any datacontext available to the control that the context menu was opened from. The context menu can access the clicked control through "PlacementTarget". If the Tag property of the clicked control is bound to a desired datacontext, binding to "PlacementTarget.Tag" from inside the context menu will slingshot you directly to that datacontext.
I know this is already an old post, but I would like to add another solution for those one who are looking for different ways to do it.
I could not make the same solution to work in my case, since I was trying to do something else: open the context menu with a mouse click (just like a toolbar with a submenu attached to it) and also bind commands to my model. Since I was using an Event Trigger, the PlacementTarget object was null.
This is the solution I found to make it work only using XAML:
<!-- This is an example with a button, but could be other control -->
<Button>
<...>
<!-- This opens the context menu and binds the data context to it -->
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.DataContext">
<DiscreteObjectKeyFrame KeyTime="0:0:0" Value="{Binding}"/>
</ObjectAnimationUsingKeyFrames>
<BooleanAnimationUsingKeyFrames Storyboard.TargetProperty="ContextMenu.IsOpen">
<DiscreteBooleanKeyFrame KeyTime="0:0:0" Value="True"/>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Button.Triggers>
<!-- Here it goes the context menu -->
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Item 1" Command="{Binding MyCommand1}"/>
<MenuItem Header="Item 2" Command="{Binding MyCommand2}"/>
</ContextMenu>
</Button.ContextMenu>
</Button>