C+Ctrl KeyBinding is not causing copying to happen - wpf

I have set up a ListBox like so:
<ListBox ItemsSource="{Binding Logs, Mode=OneWay}" x:Name="logListView">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=.}">
<TextBlock.InputBindings>
<KeyBinding Key="C"
Modifiers="Ctrl"
Command="Copy"/>
</TextBlock.InputBindings>
<TextBlock.CommandBindings>
<CommandBinding Command="Copy"
Executed="KeyCopyLog_Executed"
CanExecute="CopyLog_CanExecute"/>
</TextBlock.CommandBindings>
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Command="Copy">
<MenuItem.CommandBindings>
<CommandBinding Command="Copy"
Executed="MenuCopyLog_Executed"
CanExecute="CopyLog_CanExecute"/>
</MenuItem.CommandBindings>
</MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
As you can see, in the template, each TextBlock has a context menu that allows the user to copy the text. This works.
Also in the TextBlock there is an KeyBinding to ctrl+c and a CommandBinding to copy. When I press ctrl+c the method KeyCopyLog_Executed is not executed. I've checked with the debugger.
How should I be binding the keys to the TextBlock?

There are a couple of problems here.
First of all, KeyBindings will work only if currently focused element is located inside the element where KeyBindings are defined. In your case you have a ListBoxItem focused, but the KeyBindings are defined on the child element - TextBlock. So, defining a KeyBindings on a TextBlock will not work in any case, since a TextBlock cannot receive focus.
Second of all, you probably need to know what to copy, so you need to pass the currently selected log item as parameter to the Copy command.
Furthermore, if you define a ContextMenu on a TextBlock element it will be opened only if your right-click exactly on the TextBlock. If you click on any other part of the list item, it will not open. So, you need to define the ContextMenu on the list box item itself.
Considering all of that, what I believe you are trying to do can be done in the following way:
<ListBox ItemsSource="{Binding Logs, Mode=OneWay}"
x:Name="logListView"
IsSynchronizedWithCurrentItem="True">
<ListBox.InputBindings>
<KeyBinding Key="C"
Modifiers="Ctrl"
Command="Copy"
CommandParameter="{Binding Logs/}" />
</ListBox.InputBindings>
<ListBox.CommandBindings>
<CommandBinding Command="Copy"
Executed="CopyLogExecuted"
CanExecute="CanExecuteCopyLog" />
</ListBox.CommandBindings>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Command="Copy"
CommandParameter="{Binding}" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Here we define a KeyBinding on the ListBox itself and specify as a CommandParameter currently selected list box item (log entry).
CommandBinding is also defined at the ListBox level and it is a single binding for both right click menu and the keyboard shortcut.
The ContextMenu we define in the style for ListBoxItem and bind CommandParameter to the data item represented by this ListBoxItem (log entry).
The DataTemplate just declares a TextBlock with binding to current data item.
And finally, there is only one handler for the Copy command in the code-behind:
private void CopyLogExecuted(object sender, ExecutedRoutedEventArgs e) {
var logItem = e.Parameter;
// Copy log item to the clipboard
}
private void CanExecuteCopyLog(object sender, CanExecuteRoutedEventArgs e) {
e.CanExecute = true;
}

Thanks to Pavlov Glazkov for explaining that the key and command bindings need to go at the ListBox level, rather than the item template level.
This is the solution I now have:
<ListBox ItemsSource="{Binding Logs, Mode=OneWay}">
<ListBox.InputBindings>
<KeyBinding Key="C"
Modifiers="Ctrl"
Command="Copy"/>
</ListBox.InputBindings>
<ListBox.CommandBindings>
<CommandBinding Command="Copy"
Executed="KeyCopyLog_Executed"
CanExecute="CopyLog_CanExecute"/>
</ListBox.CommandBindings>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=.}">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Command="Copy">
<MenuItem.CommandBindings>
<CommandBinding Command="Copy"
Executed="MenuCopyLog_Executed"
CanExecute="CopyLog_CanExecute"/>
</MenuItem.CommandBindings>
</MenuItem>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Where KeyCopyLog_Executed is:
private void KeyCopyLog_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
if ((sender as ListBox).SelectedItem != null)
{
LogItem item = (LogItem) (sender as ListBox).SelectedItem;
Clipboard.SetData("Text", item.ToString());
}
}
and MenuCopyLog_Executed is:
private void MenuCopyLog_Executed(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
LogItem item = (LogItem) ((sender as MenuItem).TemplatedParent as ContentPresenter).DataContext;
Clipboard.SetData("Text", item.ToString());
}

Related

In WPF, How to get a Command Parameter from a specific item in a Collection View Source that is bound to a ListView?

My Goal: Right click on a specific item in my ListView, a context menu pops up, select a command, followed by me running a function based on which item's context menu was selected.
My ListView's ItemsSource is bounded to a CollectionViewSource, whose source is an ObservableCollection of "Items".
(ListView, binds -> CollectionViewSource, source -> ObservableCollection of class "Item")
What I tried to do is add a general ContextMenu to all of the "Items" in the listview, and when the context menu item is selected for an item in the ListView, then a command is run. I was able to get the command to run in general, but I haven't been able to get any information/parameters about the specific item whose context menu was chosen.
In this example, "Item" class has a string called host, and I want to pass host string to the RefundRequestCommand but I havent been able to pass any CommandParameters.
I've read some stuff about creating a data template and using that, but haven't been successful. Can anyone guide me / help me out?
Here is some code for reference:
ListView:
<ListView x:Name="ordersList" Margin="0,10,10,0" BorderThickness="2" Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Stretch" ItemsSource="{Binding Source={StaticResource cvsOrders}}" SelectionChanged="ordersList_SelectionChanged" SelectedIndex="0" SelectionMode="Extended">
<ListView.Resources>
<local:RefundRequestCommand x:Key="refund"></local:RefundRequestCommand>
</ListView.Resources>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="test" Command="{StaticResource refund}" CommandParameter="{Binding host}"></MenuItem>
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
<GridView>
<GridViewColumn Width="140">
<GridViewColumnHeader Name="OrderNumber" Click="sortClick" Tag="orderNumber" Content="Order Number" />
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding orderNumber}" TextAlignment="Center"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
//On and On.....
Command:
class RefundRequestCommand : ICommand
{
TreeViewFilter treeViewFilter;
public void Execute(object parameter)
{
string host = (string)parameter;
Console.WriteLine(host); //FOR TESTING
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
Actually you are setting the ContextMenu for ListView but you want to pass ListViewItem over there. You should set the context menu for ListViewItem. Try this.
<local:RefundRequestCommand x:Key="refund"/>
<Style x:Key="MyLVItemStyle" TargetType="ListViewItem">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="test"
Command="{StaticResource refund}"
CommandParameter="{Binding host}">
</MenuItem>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
and use it it you listview like
<ListView x:Name="ordersList" Margin="0,10,10,0" BorderThickness="2" Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Stretch"
ItemsSource="{Binding Rectangles}" SelectedIndex="0" SelectionMode="Extended" ItemContainerStyle="{StaticResource MyLVItemStyle}">
......
Also you remove the style applied in ListView and it should work.

Detect what TreeView node was right-clicked

I am working on a WPF project and I have added a TreeView to it. I have also created a ContextMenu to the TreeView as below:
<TreeView Name="treeView" ItemsSource="{Binding Elements}">
<TreeView.ContextMenu>
<ContextMenu Name="treeViewContextMenu">
<MenuItem Header="First option"/>
<MenuItem Header="Second Option/>
</ContextMenu>
</TreeView.ContextMenu>
.... </TreeView>
Since I add the treeView nodes dinamically, how can I detect what node was right-clicked in order to open the contextMenu?
Hope someone can help me, thanks in advance
Assuming that I loaded my treeview item's dynamically..
<TreeView Name="treeView" ContextMenuClosing="treeView_ContextMenuClosing">
<TreeView.ContextMenu>
<ContextMenu Name="treeViewContextMenu">
<MenuItem Header="First option"/>
<MenuItem Header="Second Option"/>
</ContextMenu>
</TreeView.ContextMenu>
<TreeViewItem Header="Hello 1"/>
<TreeViewItem Header="Hello 2"/>
</TreeView>
MainWindow.xaml.cs
private void treeView_ContextMenuClosing(object sender, ContextMenuEventArgs e)
{
//Sender should let me determine who sent it from my children/parent
var parent = sender as TreeView;
var children = parent.SelectedItem as TreeViewItem;
MessageBox.Show(children.Header.ToString());
}
It's up to you if you want to know the object when the ContextMenu is Closed/Open or whatever event like when the MenuItem is Clicked.

context menu for removing items in listview

I have a ListView which displays a list of string values. I want to add a context menu entry for each item in the list to remove the selected item. My XAML looks like this:
<ListView x:Name="itemsListView" ItemsSource="{Binding MyItems}">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Remove"
Command="{Binding RemoveItem}"
CommandParameter="{Binding ElementName=itemsListView, Path=SelectedItem}" />
</ContextMenu>
</ListView.ContextMenu>
</ListView>
The problem is that the CommandParameter value is always null. I've added an additional button to remove the selected item to check if my command works. The button has exactly the same binding and removing items via the button works. The button looks like this:
<Button Content="Remove selected item"
Command="{Binding RemoveItem}"
CommandParameter="{Binding ElementName=itemsListView, Path=SelectedItem}"/>
The command looks like this:
private ICommand _removeItem;
public ICommand RemoveItem
{
get { return _removeItem ?? (_removeItem = new RelayCommand(p => RemoveItemCommand((string)p))); }
}
private void RemoveItemCommand(string item)
{
if(!string.IsNullOrEmpty(item))
MyItems.Remove(item);
}
Any ideas why the selected item is null when opening the context menu? Maybe a focus problem of the listview?
H.B. is right. but you can also use RelativeSource Binding
<ListView x:Name="itemsListView" ItemsSource="{Binding MyItems}">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Remove"
Command="{Binding RemoveItem}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}" />
</ContextMenu>
</ListView.ContextMenu>
</ListView>
ContextMenus are disconnected, you cannot use ElementName bindings. One workaround would be using Binding.Source and x:Reference which requires you to extract parts that use it to be in the resources (due to cyclical dependency errors). You can just put the whole context menu there.
An example:
<ListBox Name="lb" Height="200">
<ListBox.Resources>
<ContextMenu x:Key="cm">
<MenuItem Header="{Binding ActualHeight, Source={x:Reference lb}}" />
</ContextMenu>
</ListBox.Resources>
<ListBox.ContextMenu>
<StaticResource ResourceKey="cm" />
</ListBox.ContextMenu>
</ListBox>
This work for me CommandParameter="{Binding}"

TreeView ContextMenu MVVM Binding

I currently have a UserControl that uses the MVVM model.
In that control there is a TreeView, which displays some items. I have added a HierarchicalDataTemplate for this TreeView and in that template is a ContextMenu for the Items.
In the ViewModel, which is DataContext of the control (named RestoresTreeViewControl) is a command I want to bind one of the menu items to. However what I have done doesn't seem to be working. I am getting the usual can't find source for binding reference.
Here is the bit of code for the datatemplate that tried to bind the EditDatabaseCommand to one of the menu items.
<HierarchicalDataTemplate DataType="{x:Type model:Database}" >
<StackPanel>
<TextBlock Text="{Binding Name}" >
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit" Command="{Binding ElementName=RestoresTreeViewControl, Path=DataContext.EditDatabaseCommand}" />
<MenuItem Header="Delete"/>
<Separator/>
<MenuItem Header="Test Connection"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
Here is a section of the ViewModel where the command is.
public ICommand EditDatabaseCommand { get; private set; }
Unfortunately the ContextMenu is not in the VisualTree, so it's not going to see your DataContext. What you can do is something like this (copied from here: MVVM binding command to contextmenu item)
<Button Height="40" Margin="0,2,0,0" CommandParameter="{Binding Name}"
Tag="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type UserControl}}}" Command = "{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}},
Path=DataContext.ConnectCommand}">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Remove"
CommandParameter="{Binding Name}"
Command="{Binding Path=PlacementTarget.Tag.DataContext.RemoveCommand,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"/>
</ContextMenu>
</Button.ContextMenu>
So simply use PlacementTarget.Tag to find your ViewModel.
You can try tracing the binding:
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
...
{binding ... diag:PresentationTraceSources.TraceLevel="High"}
However requiring the users (even if it is just yourself) of your control to name each instance of "RestoresTreeViewControl" rather burdensome.
Try:
{Binding Path=... RelativeSource={ FindAncestor, AncestorType={x:TheRestoresTreeViewControlType}} }
That probably has to do with the inheritance context.
See: Binding WPF ContextMenu MenuItem to UserControl Property vs ViewModel Property

ElementName Binding from MenuItem in ContextMenu

Has anybody else noticed that Bindings with ElementName do not resolve correctly for MenuItem objects that are contained within ContextMenu objects? Check out this sample:
<Window x:Class="EmptyWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
x:Name="window">
<Grid x:Name="grid" Background="Wheat">
<Grid.ContextMenu>
<ContextMenu x:Name="menu">
<MenuItem x:Name="menuItem" Header="Window" Tag="{Binding ElementName=window}" Click="MenuItem_Click"/>
<MenuItem Header="Grid" Tag="{Binding ElementName=grid}" Click="MenuItem_Click"/>
<MenuItem Header="Menu" Tag="{Binding ElementName=menu}" Click="MenuItem_Click"/>
<MenuItem Header="Menu Item" Tag="{Binding ElementName=menuItem}" Click="MenuItem_Click"/>
</ContextMenu>
</Grid.ContextMenu>
<Button Content="Menu"
HorizontalAlignment="Center" VerticalAlignment="Center"
Click="MenuItem_Click" Tag="{Binding ElementName=menu}"/>
<Menu HorizontalAlignment="Center" VerticalAlignment="Bottom">
<MenuItem x:Name="anotherMenuItem" Header="Window" Tag="{Binding ElementName=window}" Click="MenuItem_Click"/>
<MenuItem Header="Grid" Tag="{Binding ElementName=grid}" Click="MenuItem_Click"/>
<MenuItem Header="Menu" Tag="{Binding ElementName=menu}" Click="MenuItem_Click"/>
<MenuItem Header="Menu Item" Tag="{Binding ElementName=anotherMenuItem}" Click="MenuItem_Click"/>
</Menu>
</Grid>
</Window>
All of the bindings work great except for the bindings contained within the ContextMenu. They print an error to the Output window during runtime.
Any one know of any work arounds? What's going on here?
I found a much simpler solution.
In the code behind for the UserControl:
NameScope.SetNameScope(contextMenu, NameScope.GetNameScope(this));
As said by others, the 'ContextMenu' is not contained in the visual tree and an 'ElementName' binding won't work. Setting the context menu's 'NameScope' as suggested by the accepted answer only works if the context menu is not defined in a 'DataTemplate'. I have solved this by using the {x:Reference} Markup-Extension which is similar to the 'ElementName' binding but resolves the binding differently, bypassing the visual tree. I consider this to be far more readable than using 'PlacementTarget'. Here is an example:
<Image Source="{Binding Image}">
<Image.ContextMenu>
<ContextMenu>
<MenuItem Header="Delete"
Command="{Binding Source={x:Reference Name=Root}, Path=DataContext.RemoveImage}"
CommandParameter="{Binding}" />
</ContextMenu>
</Image.ContextMenu>
</Image>
According to the MSDN-documentation
x:Reference is a construct defined in XAML 2009. In WPF, you can use
XAML 2009 features, but only for XAML that is not WPF markup-compiled.
Markup-compiled XAML and the BAML form of XAML do not currently
support the XAML 2009 language keywords and features.
whatever that means... Works for me, though.
Here's another xaml-only workaround. (This also assumes you want what's inside the DataContext, e.g., you're MVVMing it)
Option one, where the parent element of the ContextMenu is not in a DataTemplate:
Command="{Binding PlacementTarget.DataContext.MyCommand,
RelativeSource={RelativeSource AncestorType=ContextMenu}}"
This would work for OP's question. This won't work if you are inside of a DataTemplate. In these cases, the DataContext is often one of many in a collection, and the ICommand you wish to bind to is a sibling property of the collection within the same ViewModel (the DataContext of the Window, say).
In these cases, you can take advantage of the Tag to temporarily hold the parent DataContext which contains both the collection AND your ICommand:
class ViewModel
{
public ObservableCollection<Derp> Derps { get;set;}
public ICommand DeleteDerp {get; set;}
}
and in the xaml
<!-- ItemsSource binds to Derps in the DataContext -->
<StackPanel
Tag="{Binding DataContext, ElementName=root}">
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem
Header="Derp"
Command="{Binding PlacementTarget.Tag.DeleteDerp,
RelativeSource={RelativeSource
AncestorType=ContextMenu}}"
CommandParameter="{Binding PlacementTarget.DataContext,
RelativeSource={RelativeSource AncestorType=ContextMenu}}">
</MenuItem>
Context menus are tricky to bind against. They exist outside the visual tree of your control, hence they can't find your element name.
Try setting the datacontext of your context menu to its placement target. You have to use RelativeSource.
<ContextMenu
DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}"> ...
After experimenting a bit, I discovered one work around:
Make top level Window/UserControl implement INameScope and set NameScope of ContextMenu to the top level control.
public class Window1 : Window, INameScope
{
public Window1()
{
InitializeComponent();
NameScope.SetNameScope(contextMenu, this);
}
// Event handlers and etc...
// Implement INameScope similar to this:
#region INameScope Members
Dictionary<string, object> items = new Dictionary<string, object>();
object INameScope.FindName(string name)
{
return items[name];
}
void INameScope.RegisterName(string name, object scopedElement)
{
items.Add(name, scopedElement);
}
void INameScope.UnregisterName(string name)
{
items.Remove(name);
}
#endregion
}
This allows the context menu to find named items inside of the Window. Any other options?
I'm not sure why resort to magic tricks just to avoid a one line of code inside the eventhandler for the mouse click you already handle:
private void MenuItem_Click(object sender, System.Windows.RoutedEventArgs e)
{
// this would be your tag - whatever control can be put as string intot he tag
UIElement elm = Window.GetWindow(sender as MenuItem).FindName("whatever control") as UIElement;
}

Resources