WPF - How to find item clicked on in menu handler method? - wpf

How can the handler method of a WPF menu item determine which item in a ListView was clicked on?
Edit:
The menu is a context menu which has been set for the ListView. The problem is to find which ListView item has been clicked on when the context menu item is selected.

Checkout ContextMenu.PlacementTarget, which that object you can walk up the visual tree (VisualTreeHelper.GetParent) until you find a ListViewItem.

Just in case anyone else has this problem, I ended up with something like:
private void ListViewItems_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var frameworkElement = e.OriginalSource as FrameworkElement;
var item = frameworkElement.DataContext as MyDataItem;
if(null == item)
{
return;
}
// TODO: Use item here...
}

If each of your data items has an IsSelected property that is bound to the ListViewItem.IsSelected property, then you just iterate through your data to find the selected ones:
<ListView ItemsSource="{Binding Items}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
And in your code:
public ICollection<DataItem> Items
{
get { return _items; }
}
public IEnumerable<DataItem> SelectedItems
{
get
{
foreach (var item in Items)
{
if (item.IsSelected)
yield return item;
}
}
}
private void DoSomethingWithSelectedItems()
{
foreach (var item in SelectedItems) ...
}

Related

WPF Drag and Drop multiple listviewitems from listview to a button

I have a listview which property SelectionMode is set to Extended.
Also I have a trash button. I am trying to perform delete operation on selected items on listview when user drags one or multiple selected items (using shift key if more than one item) and drop over delete button.
I have implemented it with a single selected item in listview and it works. Now I am trying to do the same with multiple selected items in listview without success.
Below code works when one single item is selected in listview and user drag and drop it from listview to delete button:
<ListView Margin="10" Name="lvUsers" ItemsSource="{Binding Path=Items}" SelectionMode="Extended">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<EventSetter Event="MouseMove" Handler="lstItems_MouseMove" />
</Style>
</ListView.ItemContainerStyle>
<!-- other stuff -->
</Listview>
<Button AllowDrop="True" Drop="btnDelete_Drop" Height="64" Width="64" Margin="10" Click="BtnDelete_Click" Content="Delete"/>
and the code-behind:
private void lstItem_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (e.Source != null)
{
DataModel selectedItem = (DataModel)lvUsers.SelectedItem;
DragDrop.DoDragDrop(lvUsers, selectedItem, DragDropEffects.Move);
}
}
}
private void btnDelete_Drop(object sender, DragEventArgs e)
{
Type myType = typeof(DataModel);
string modelns = myType.FullName;
DataModel selectedItem = e.Data.GetData(modelns) as DataModel;
MessageBox.Show(selectedItem.Name);
}
Each listviewitem on listview is of data type below:
public class DataModel
{
public string Name
{
get;
set;
}
public int Age
{
get;
set;
}
public string Mail
{
get;
set;
}
}
As an example, when user drops the dragged listviewitem over delete button I show a message box with the name of the person.
How can I do the same but instead of dragging and dropping one single item from listview, do the same for multiple selected listviewitems? Once dropped, within
btnDelete_Drop I want to iterate over all the listviews items dropped and do some stuff.
I have just implemented a solution that works. I have replaced methods lstItem_MouseMove and btnDelete_Drop in code-behind by these ones:
private void lstItem_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (e.Source != null)
{
List<DataModel> myList = new List<DataModel>();
foreach (DataModel Item in lvUsers.SelectedItems)
{
myList.Add(Item);
}
DataObject dataObject = new DataObject(myList);
DragDrop.DoDragDrop(lvUsers, dataObject, DragDropEffects.Move);
}
}
}
private void btnDelete_Drop(object sender, DragEventArgs e)
{
Type myType = typeof(List<DataModel>);
List<DataModel> selectedItems = e.Data.GetData(myType) as List<DataModel>;
string hello = "Hello ";
foreach (DataModel dm in selectedItems)
{
hello = hello + ", " + dm.Name;
}
MessageBox.Show(hello);
}
This is working but mayber there are any other better solution. Any suggestion will be welcome.

Expand/collapse groups in treeview by clicking the text

How do I do so that it is possible to expand/collaps groups in the TreeView simply by clicking on the text, instead of clicking the arrow to the left.
You should create style for your Tree Item with next setter:
<Style x:Key="TreeItemStyle"
TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded"
Value="{Binding Path=IsExpanded, Mode=TwoWay}"/>
</Style>
Then add to you group view data class observable property named IsExpanded:
private bool _isExpanded;
public bool IsExpanded
{
get
{
return this._isExpanded;
}
set
{
if (this._isExpanded != value)
{
this._isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
}
}
Then intercept hyper link click event and set IsExpanded as true:
private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
var dc = ((Hyperlink)sender).DataContext;
if (dc is GroupViewData)
{
((GroupViewData)dc).IsExpanded = true;
}
}
Of course, the best way is to use commands instead of click handlers, but I don't know composition of your presentation model so can't provide proper solution. I just must say that in our projects with alike requirements we successfully avoid any view code behind. God bless WPF!

WPF Get the referenced item from a ContextMenu on a ListViewItem

I have a ListView with ContextMenu on each ListViewItem that has Click event,
how can I detect in the event handler which Item was clicked in this ContextMenu?
I need the item ID.
<Style TargetType="{x:Type ListViewItem}">
.
.
.
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="tv:TreeListViewItem">
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Open in current tab" Click="MenuItemCurrentTab_Click"/>
<MenuItem Header="Open in new tab" Click="MenuItemNewTab_Click"/>
</ContextMenu>
</Grid.ContextMenu>
See this thread..
Following the same way as the answer from the link you would
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Open in current tab"
Click="MenuItemCurrentTab_Click"
CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Parent}"/>
...
private void MenuItemCurrentTab_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = sender as MenuItem;
if (menuItem != null)
{
ContextMenu parentContextMenu = menuItem.CommandParameter as ContextMenu;
if (parentContextMenu != null)
{
ListViewItem listViewItem = parentContextMenu.PlacementTarget as ListViewItem;
}
}
}
UPDATE
Add this to get the parent ListViewItem from the Grid
public T GetVisualParent<T>(object childObject) where T : Visual
{
DependencyObject child = childObject as DependencyObject;
while ((child != null) && !(child is T))
{
child = VisualTreeHelper.GetParent(child);
}
return child as T;
}
private void MenuItemCurrentTab_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = sender as MenuItem;
if (menuItem != null)
{
ContextMenu parentContextMenu = menuItem.CommandParameter as ContextMenu;
if (parentContextMenu != null)
{
Grid grid = parentContextMenu.PlacementTarget as Grid;
ListViewItem listViewItem = GetVisualParent<ListViewItem>(grid);
}
}
}
private void MenuItemCurrentTab_Click(object sender, RoutedEventArgs e)
{
MenuItem menuItem = (MenuItem)e.Source;
ContextMenu menu = (ContextMenu)menuItem.Parent;
ListViewItem item = (ListViewItem)menu.PlacementTarget;
// do something with item
}
But it's probably better idea to create single ContextMenu, give it proper name, and use it for all list view items.
A recurring problem, with many attempts to solve but all have their drawbacks. The accepted answer here, for instance, supposes that each ListViewItem has its own ContextMenu. This works but, especially with a larger number of list items, has a considerable cost in XAML complexity and can be slow. And really isn't necessary at all. If we only use a single ContextMenu on the ListView itself, some other solutions suggest to use
<MenuItem CommandParameter="{Binding PlacementTarget.SelectedItem, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
which seems to solve the problem at first sight (PlacementTarget points to the ListView, its SelectedItem points to the list item, so the menu item handler can use the CommandParameter to get the originating list item), but, unfortunately, fails if the ListView has multiple selection enabled (SelectedItem will point to one of the items selected but not necessarily the one currently clicked) or if we use ListView.PreviewMouseRightButtonDown to disable the selection on right-click (which is, arguably, the only logical thing to do with multiple selections).
There is, however, an approach that has all the benefits:
single ContextMenu on the ListView itself;
works with all selection schemes, single, multiple, disabled;
even with multiple selection, it will pass the currently hovered item to the handler.
Consider this ListView:
<ListView ContextMenuOpening="ListView_ContextMenuOpening">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Menu1" Click="Menu1_Click" CommandParameter="{Binding Parent, RelativeSource={RelativeSource Self}}" />
</ContextMenu>
</ListView.ContextMenu>
</ListView>
The CommandParameter is used to pass the parent of the MenuItem, ie. the ContextMenu itself. But the main trick comes in the menu opening handler:
private void ListView_ContextMenuOpening(object sender, ContextMenuEventArgs e) {
var menu = (e.Source as FrameworkElement).ContextMenu;
menu.Tag = (FrameworkElement)e.OriginalSource;
}
Inside this handler, we still know the original source of the event, the root FrameworkElement of the list item DataTemplate. Let's store it in the Tag of the menu for later retrieval.
private void Menu1_Click(object sender, RoutedEventArgs e) {
if (sender is MenuItem menu)
if (menu.CommandParameter is ContextMenu context)
if (context.Tag is FrameworkElement item)
if (item.DataContext is DataType data) {
//process data
}
}
In the menu click handler, we can look up the original ContextMenu we stored in the command parameter, from that we can look up the root FrameworkElement of the list item that we stored just before, and finally get the object stored in the list item (of type DataType).
ListViewItem item = myListView.SelectedItem as ListViewItem;
Seems to work just fine as the item is selected when you right-click it.

Expanded all WPF Treeview Items

I'm trying to iterate through my Treeview, expanding all nodes however it runs into an InvalidCastException when ran;
Unable to cast object of type 'System.Data.DataRowView' to type 'System.Windows.Controls.TreeViewItem'.
My Code;
foreach (TreeViewItem treeitem in thetreeView.Items)
{
treeitem.IsExpanded = true;
}
Any ideas? I want to fire this from a button.
just add this style
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.ItemContainerStyle>
for code please go through this link may be this can help u
http://bea.stollnitz.com/blog/?p=55
I've found an "Hackish" solution for that.
It does not involved with inheritance like the solution suggested here (by Kishore Kumar)
I've added two buttons - "Collapse all" and "Expand All".
Code Behind:
private void btnCollapseAll_Click(object sender, RoutedEventArgs e)
{
foreach (var item in treeView.Items)
{
DependencyObject dObject = treeView.ItemContainerGenerator.ContainerFromItem(item);
CollapseTreeviewItems(((TreeViewItem)dObject));
}
}
private void btnExpandAll_Click(object sender, RoutedEventArgs e)
{
foreach (var item in treeView.Items)
{
DependencyObject dObject = treeView.ItemContainerGenerator.ContainerFromItem(item);
((TreeViewItem)dObject).ExpandSubtree();
}
}
private void CollapseTreeviewItems(TreeViewItem Item)
{
Item.IsExpanded = false;
foreach (var item in Item.Items)
{
DependencyObject dObject = treeView.ItemContainerGenerator.ContainerFromItem(item);
if (dObject != null)
{
((TreeViewItem)dObject).IsExpanded = false;
if (((TreeViewItem)dObject).HasItems)
{
CollapseTreeviewItems(((TreeViewItem)dObject));
}
}
}
}
My solution is based on this
Bag of tricks has a demo called "TreeView Expand" that has a tree view with expand all and collapse all buttons (and some more)

WPF Drag & drop from ListBox with SelectionMode Multiple

I've almost got this working apart from one little annoying thing...
Because the ListBox selection happens on mouse down, if you start the drag with the mouse down when selecting the last item to drag it works fine, but if you select all the items to drag first and then click on the selection to start dragging it, the one you click on gets unselected and left behind after the drag.
Any thoughts on the best way to get around this?
<DockPanel LastChildFill="True">
<ListBox ItemsSource="{Binding SourceItems}"
SelectionMode="Multiple"
PreviewMouseLeftButtonDown="HandleLeftButtonDown"
PreviewMouseLeftButtonUp="HandleLeftButtonUp"
PreviewMouseMove="HandleMouseMove"
MultiSelectListboxDragDrop:ListBoxExtension.SelectedItemsSource="{Binding SelectedItems}"/>
<ListBox ItemsSource="{Binding DestinationItems}"
AllowDrop="True"
Drop="DropOnToDestination"/>
<DockPanel>
...
public partial class Window1
{
private bool clickedOnSourceItem;
public Window1()
{
InitializeComponent();
DataContext = new WindowViewModel();
}
private void DropOnToDestination(object sender, DragEventArgs e)
{
var viewModel = (WindowViewModel)
e.Data.GetData(typeof(WindowViewModel));
viewModel.CopySelectedItems();
}
private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var sourceElement = (FrameworkElement)sender;
var hitItem = sourceElement.InputHitTest(e.GetPosition(sourceElement))
as FrameworkElement;
if(hitItem != null)
{
clickedOnSourceItem = true;
}
}
private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e)
{
clickedOnSourceItem = false;
}
private void HandleMouseMove(object sender, MouseEventArgs e)
{
if(clickedOnSourceItem)
{
var sourceItems = (FrameworkElement)sender;
var viewModel = (WindowViewModel)DataContext;
DragDrop.DoDragDrop(sourceItems, viewModel, DragDropEffects.Move);
clickedOnSourceItem = false;
}
}
}
I've found a very simple way to enable Windows Explorer like drag/drop behaviour when having multiple items selected. The solution replaces the common ListBox with a little derived shim that replaces the ListBoxItem with a more intelligent version. This way, we can encapsulate the click state at the right level and call into the protected selection machinery of the ListBox. Here is the relevant class. For a complete example, see my repo on github.
public class ListBoxEx : ListBox
{
protected override DependencyObject GetContainerForItemOverride()
{
return new ListBoxItemEx();
}
class ListBoxItemEx : ListBoxItem
{
private bool _deferSelection = false;
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (e.ClickCount == 1 && IsSelected)
{
// the user may start a drag by clicking into selected items
// delay destroying the selection to the Up event
_deferSelection = true;
}
else
{
base.OnMouseLeftButtonDown(e);
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (_deferSelection)
{
try
{
base.OnMouseLeftButtonDown(e);
}
finally
{
_deferSelection = false;
}
}
base.OnMouseLeftButtonUp(e);
}
protected override void OnMouseLeave(MouseEventArgs e)
{
// abort deferred Down
_deferSelection = false;
base.OnMouseLeave(e);
}
}
}
So...having become the proud owner of a tumbleweed badge, I've got back on to this to try & find a way around it. ;-)
I'm not sure I like the solution so I'm still very much open to any better approaches.
Basically, what I ended up doing is remember what ListBoxItem was last clicked on & then make sure that gets added to the selected items before a drag. This also meant looking at how far the mouse moves before starting a drag - because clicking on a selected item to unselect it could sometimes result in it getting selected again if mouse bounce started a little drag operation.
Finally, I added some hot tracking to the listbox items so, if you mouse down on a selected item it'll get unselected but you still get some feedback to indicate that it will get included in the drag operation.
private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var source = (FrameworkElement)sender;
var hitItem = source.InputHitTest(e.GetPosition(source)) as FrameworkElement;
hitListBoxItem = hitItem.FindVisualParent<ListBoxItem>();
origPos = e.GetPosition(null);
}
private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e)
{
hitListBoxItem = null;
}
private void HandleMouseMove(object sender, MouseEventArgs e)
{
if (ShouldStartDrag(e))
{
hitListBoxItem.IsSelected = true;
var sourceItems = (FrameworkElement)sender;
var viewModel = (WindowViewModel)DataContext;
DragDrop.DoDragDrop(sourceItems, viewModel, DragDropEffects.Move);
hitListBoxItem = null;
}
}
private bool ShouldStartDrag(MouseEventArgs e)
{
if (hitListBoxItem == null)
return false;
var curPos = e.GetPosition(null);
return
Math.Abs(curPos.Y-origPos.Y) > SystemParameters.MinimumVerticalDragDistance ||
Math.Abs(curPos.X-origPos.X) > SystemParameters.MinimumHorizontalDragDistance;
}
XAML changes to include hot tracking...
<Style TargetType="ListBoxItem">
<Setter Property="Margin" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Border Background="{TemplateBinding Background}" />
<Border Background="#BEFFFFFF" Margin="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition /><RowDefinition />
</Grid.RowDefinitions>
<Border Margin="1" Grid.Row="0" Background="#57FFFFFF" />
</Grid>
</Border>
<ContentPresenter Margin="8,5" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="PowderBlue" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsSelected" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#5FB0E0E6" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
One option would be not to allow ListBox or ListView to remove selected items until MouseLeftButtonUp is triggered.
Sample code:
List<object> removedItems = new List<object>();
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count > 0)
{
ListBox box = sender as ListBox;
if (removedItems.Contains(e.RemovedItems[0]) == false)
{
foreach (object item in e.RemovedItems)
{
box.SelectedItems.Add(item);
removedItems.Add(item);
}
}
}
}
private void ListBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (removedItems.Count > 0)
{
ListBox box = sender as ListBox;
foreach (object item in removedItems)
{
box.SelectedItems.Remove(item);
}
removedItems.Clear();
}
}
I'm surprised that the behaviour difference between ListBox and the Windows Explorer has not been taken care of after 4 years across 3 major updates of the .NET framework.
I ran in to this problem back in Silverlight 3. I ended up overriding the mouse down and mouse up event handler to fully simulate the Windows Explorer behaviour.
I don't have the source code any more but the logic should be:
When mouse down
if the target item is not selected, clear existing selection
if Ctrl key is down, add target item to selection
if Shift key is down
if there is a previously selected item, add all items between target item and previous item to selection
else only add target item to selection
if the target item is selected de-select only if Ctrl key is down
When mouse up (on the same item)
if the target item is selected
if Ctrl key is down, remove item from selection
if Shift key is down
if there is a previously selected item, remove all items between target item and previous item from selection
else only remove target item from selection
However
This should really be Microsoft's job to update the behaviour to be consistent to the operating system and to be more intuitive. I've submitted it as a bug to Microsoft if any body wants to vote for it: http://connect.microsoft.com/VisualStudio/feedback/details/809192/
I had a similar problem. I Started with the basic implementation from https://www.c-sharpcorner.com/uploadfile/dpatra/drag-and-drop-item-in-listbox-in-wpf/
and Modified to something like this:
ListBox dragSource = null;
ObservableCollection<String> dragItems;
private void ListBox_Drop(object sender, DragEventArgs e)
{
ListBox parent = (ListBox)sender;
// check if the Items are from an different source
if(dragSource != parent)
{
// Add and remove the Items of both sources
foreach (var item in dragItems)
{
((ObservableCollection<String>)dragSource.ItemsSource).Remove(item);
((ObservableCollection<String>)parent.ItemsSource).Add(item);
}
}
}
private void ListBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// Check if Modifiers for Selection modes are pressed
if(Keyboard.Modifiers != ModifierKeys.Control && Keyboard.Modifiers != ModifierKeys.Shift)
{
ListBox parent = (ListBox)sender;
dragSource = parent;
object data = GetDataFromListBox(dragSource, e.GetPosition(parent));
dragItems = new ObservableCollection<String>();
for(int i = 0; i < parent.SelectedItems.Count; i++)
{
dragItems.Add(parent.SelectedItems[i] as String);
}
//If the Data is currently selected drop whole selection
if(dragItems.Contains(data as String))
{
DragDrop.DoDragDrop(parent, parent.SelectedItems, DragDropEffects.Move);
}
// The data is not selected, so clear selection and try to drop the current Item
else
{
dragItems.Clear();
dragItems.Add(data as String);
parent.SelectedItems.Clear();
DragDrop.DoDragDrop(parent, data, DragDropEffects.Move);
}
}
}
private static object GetDataFromListBox(ListBox source, Point point)
{
UIElement element = source.InputHitTest(point) as UIElement;
if (element != null)
{
object data = DependencyProperty.UnsetValue;
while (data == DependencyProperty.UnsetValue)
{
data = source.ItemContainerGenerator.ItemFromContainer(element);
if (data == DependencyProperty.UnsetValue)
{
element = VisualTreeHelper.GetParent(element) as UIElement;
}
if (element == source)
{
return null;
}
}
if (data != DependencyProperty.UnsetValue)
{
return data;
}
}
return null;
}
Hope this helps anyone to sumples across this Thread

Resources