views:

555

answers:

2

I'm using MVVM to bind views to objects in a Tree. I have a base class that implements the items in my tree, and that base class has a ContextMenu property:

    public IEnumerable<IMenuItem> ContextMenu
    {
        get
        {
            return m_ContextMenu;
        }
        protected set
        {
            if (m_ContextMenu != value)
            {
                m_ContextMenu = value;
                NotifyPropertyChanged(m_ContextMenuArgs);
            }
        }
    }
    private IEnumerable<IMenuItem> m_ContextMenu = null;
    static readonly PropertyChangedEventArgs m_ContextMenuArgs =
        NotifyPropertyChangedHelper.CreateArgs<AbstractSolutionItem>(o => o.ContextMenu);

The View that binds to the base class (and all derived classes) implements a ContextMenu that binds to that property:

<ContextMenu x:Name="contextMenu" ItemsSource="{Binding Path=(local:AbstractSolutionItem.ContextMenu)}"
             IsEnabled="{Binding Path=(local:AbstractSolutionItem.ContextMenuEnabled)}"
             ItemContainerStyle="{StaticResource contextMenuStyle}"/>

Each item in the menu is bound to an IMenuItem object (a ViewModel for the menu items). When you click on the menu item, it uses Commands to execute a command on the base object. This all works great.

However, once the command is executing on the IMenuItem class, it sometimes needs to get a reference to the object that the user right clicked on to bring up the context menu (or the ViewModel of that object, at least). That's the whole points of a context menu. How should I go about passing the reference of the tree item ViewModel to the MenuItem ViewModel? Note that some context menus are shared by many objects in the tree.

+1  A: 

I solved this by handling the ContextMenuOpening event on the parent control (the one that owned the ContextMenu in the View). I also added a Context property to IMenuItem. The handler looks like this:

    private void stackPanel_ContextMenuOpening(
        object sender, ContextMenuEventArgs e)
    {
        StackPanel sp = sender as StackPanel;
        if (sp != null)
        {
            // solutionItem is the "context"
            ISolutionItem solutionItem =
                sp.DataContext as ISolutionItem;
            if (solutionItem != null) 
            {
                IEnumerable<IMenuItem> items = 
                    solutionItem.ContextMenu as IEnumerable<IMenuItem>;
                if (items != null)
                {
                    foreach (IMenuItem item in items)
                    {
                        // will automatically set all 
                        // child menu items' context as well
                        item.Context = solutionItem;
                    }
                }
                else
                {
                    e.Handled = true;
                }
            }
            else
            {
                e.Handled = true;
            }
        }
        else
        {
            e.Handled = true;
        }
    }

This takes advantage of the fact that there can only be on ContextMenu open at a time.

Scott Whitlock
+3  A: 

There's a DP on the ContextMenu object called "PlacementTarget" - this will be set to the UI element the context menu is attached to - you can even use it as a Binding source, so you could pass it along to your Command via CommandParameter:

http://msdn.microsoft.com/en-us/library/system.windows.controls.contextmenu.placementtarget.aspx

edit: in your case, you'd want the VM of the PlacementTarget, so your binding would probably look more like:

{Binding Source=PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}
JerKimball