Tuesday, July 14, 2009

Determining which ListViewItem was clicked on in a ListView when executing a ContextMenu MenuItem

So I had need of utilizing a context menu inside a WPF ListView. I wanted to be able to right click an item, bring up the menu, then make a selection from the menu, and execute the appropriate method, using information from the item that was clicked. Seems simple, no?

In fact, I was unable to find any examples that did exactly that. Well, not entirely true. There were several people who suggested using the mouse right click event to monitor and record which item was clicked, and then reading that record. Messy. Ugly. Very non-WPF like. So I poked around a little bit more.

In the end, I now have two solutions - one utilizing commands, and another utilizing the DataContext. Both are clean and simple, and both work great for me.

First, the command solution. This one is a little more involved, but is certainly a little more powerful too. Via the CanExecute method you could dynamically determine which menu options in the context menu were usable, based on what was clicked. I don't do that here yet, but I'm going to be utilzing that feature in the future, which is why I've actually implemented this method instead of the DataContext one.


So I decided to try and implement a command solution.  I'm pretty pleased with how it's working now.



First, created my command:



    public static class CustomCommands

    {

        public static RoutedCommand DisplayMetadata = new RoutedCommand();

    }




Next in my custom listview control, I added a new command binding to the constructor:



    public SortableListView()

    {

        CommandBindings.Add(new CommandBinding(CustomCommands.DisplayMetadata, DisplayMetadataExecuted, DisplayMetadataCanExecute));

    }




And also there, added the event handlers:





    public void DisplayMetadataExecuted(object sender, ExecutedRoutedEventArgs e)

    {

        var nbSelectedItem = (MyItem)e.Parameter;



        // do stuff with selected item

    }



    public void DisplayMetadataCanExecute(object sender, CanExecuteRoutedEventArgs e)

    {

        e.CanExecute = true;

        e.Handled = true;

    }



I was already using a style selector to dynamically assign styles to the listview items, so instead of doing this in the xaml, I have to set the binding in the codebehind.  You could do it in the xaml as well though:





    public override Style SelectStyle(object item, DependencyObject container)

    {

        ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(container);

        MyItem selectedItem = (MyItem)item;

        Style s = new Style();



        var listMenuItems = new List<MenuItem>();

        var mi = new MenuItem();

        mi.Header= "Get Metadata";

        mi.Name= "cmMetadata";

        mi.Command = CustomCommands.DisplayMetadata;

        mi.CommandParameter = selectedItem;

        listMenuItems.Add(mi);



        ContextMenu cm = new ContextMenu();

        cm.ItemsSource = listMenuItems;



        // Global styles

        s.Setters.Add(new Setter(Control.ContextMenuProperty, cm));

           

        // other style selection code



        return s;

    }



I like the feel of this solution much better than attempting to set a field on mouse click and try to access what was clicked that way.

 


Pretty nice! The other solution, using the DataContext, is virtually identical - in fact it requires even less code! You don't need to generate the commands or modify the constructor of your control. All you need to do is set the DataContext for the MenuItem to your item, and you're set. For me, that means doing it in the style selector. But it's quite easily done in the XAML as well if you aren't using a dynamic selection method like I am.

public override Style SelectStyle(object item, DependencyObject container) { ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(container); MyItem selectedItem = (MyItem)item; Style s = new Style(); var listMenuItems = new List(); var mi = new MenuItem(); mi.Header= "Get Metadata"; mi.Name= "cmMetadata"; mi.Click = cmMetadata_Click; mi.DataContext = selectedItem listMenuItems.Add(mi); ContextMenu cm = new ContextMenu(); cm.ItemsSource = listMenuItems; // Global styles s.Setters.Add(new Setter(Control.ContextMenuProperty, cm)); // other style selection code return s; }

private void cmMetadata_Click(object sender, RoutedEventArgs e) { var nbSelectedItem = (sender as MenuItem).DataContext as MyItem; // do stuff with selected item }

That's it! Notice the only difference is the lack of the command. And instead of setting the Command and CommandParameter properties for my menu item, I instead set the DataContext property and bind the click event.

I hope this code saves you the troubles I had figuring this out!

UPDATE: Yes I see that the code is extraordinarily malformatted. Blogspot is relly a poor place to post code samples, it appears.


No comments:

Post a Comment