Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using MVVM, how can a ContextMenu ViewModel find the ViewModel that opened the ContextMenu?

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 point 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.

like image 839
Scott Whitlock Avatar asked Feb 21 '10 15:02

Scott Whitlock


2 Answers

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 Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}
like image 139
JerKimball Avatar answered Sep 21 '22 16:09

JerKimball


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 one ContextMenu open at a time.

like image 22
Scott Whitlock Avatar answered Sep 21 '22 16:09

Scott Whitlock