Question

I've been working off of this Q/A and a bunch of others to try and figure this out, but I must be missing something simple: Bind Items to MenuItem -> use Command

I created this little test application to try and understand context menus and learn about how to wire up the click events to relay commands in the ViewModel, and get access to the currently selected item from the context menu.

Here is the XAML:

<Window x:Class="ContextMenuTest_01.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="105" Width="525"
    WindowStartupLocation="CenterScreen"
    xmlns:local="clr-namespace:ContextMenuTest_01.ViewModels"
    DataContext="MainWindowViewModel">
<Window.Resources>
    <ObjectDataProvider x:Key="MainWindowViewModel" ObjectType="{x:Type local:MainWindowViewModel}" IsAsynchronous="True"/>
</Window.Resources>

<!-- CONTEXT MENU -->
<Window.ContextMenu>
    <ContextMenu DataContext="MainWindowViewModel" Name="MainWindowContextMenu" PresentationTraceSources.TraceLevel="High">
        <MenuItem Header="Skins" ItemsSource="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins}">
            <MenuItem.ItemContainerStyle>
                <Style TargetType="MenuItem">
                    <Setter Property="Header" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins.SkinName}"/>
                    <Setter Property="Command" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=ContextMenuClickCommand}"/>
                    <Setter Property="CommandParameter" Value="{Binding Path=SkinName}"/>
                </Style>
            </MenuItem.ItemContainerStyle>
        </MenuItem>
    </ContextMenu>
</Window.ContextMenu>
</Window>

ViewModel:

using ContextMenuTest_01.Models;
using System.Collections.ObjectModel;
using ContextMenuTest_01.CommandBase;
using System.Windows;

namespace ContextMenuTest_01.ViewModels
{
/// <summary>Main Window View Model</summary>
class MainWindowViewModel
{
    #region Class Variables
    /// <summary>The skins</summary>
    private ObservableCollection<SkinItem> skins = new ObservableCollection<SkinItem>();
    #endregion Class Variables

    public RelayCommand<object> ContextMenuClickCommand{ get; private set; }

    #region Properties
    /// <summary>Gets the skins.</summary>
    /// <value>The skins.</value>
    public ObservableCollection<SkinItem> Skins
    {
        get { return this.skins; }
        private set { this.skins = value; }
    }
    #endregion Properties

    /// <summary>Initializes a new instance of the <see cref="MainWindowViewModel"/> class.</summary>
    public MainWindowViewModel()
    {
        ContextMenuClickCommand = new RelayCommand<object>((e) => OnMenuItemClick(e));

        skins.Add(new SkinItem("Skin Item 1"));
        skins.Add(new SkinItem("Skin Item 2"));
        skins.Add(new SkinItem("Skin Item 3"));
    }

    /// <summary>Called when [menu item click].</summary>
    public void OnMenuItemClick(object selected)
    {
        MessageBox.Show("Got to the ViewModel! YAY!!!");
    }
}
}

So the function OnMenuItemClick will get hit in the debugger and the message box is shown if I change the following three lines in the ViewModel:

Remove the from the Relay Command definition:

public RelayCommand ContextMenuClickCommand{ get; private set; }

Remove the and (e) from where the RelayCommand is created:

ContextMenuClickCommand = new RelayCommand(() => OnMenuItemClick());

Remove the (object selected) from the OnMenuItemClick public function:

public void OnMenuItemClick()

Then everything works but of course I don't have the currently selected item. So what am I missing in the XAML that would command parameter from passing the argument SkinName to the RelayCommand?

Also if I leave off the line:

<Setter Property="Header" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins.SkinName}"/>

Then I get the following in my context menu:

     Skins -> ContextMenuTest_01.Models.SkinItem

              ContextMenuTest_01.Models.SkinItem

              ContextMenuTest_01.Models.SkinItem

Which tells me the binding is working correctly, it's just not displaying it correctly, which is why I tried to insert the

<Setter Property="Header"....

but of course that's not working as I would expect it to.

Thanks for your time! Any ideas would be helpful!

I don't have anything in the code behind which is the way it should be when following MVVM. Here is my skinItem class, not much to speak of, but I figured I would show it before someone asks about it:

using System.Windows.Input;
using System.Windows.Media;

namespace ContextMenuTest_01.Models
{
/// <summary>A small data structure to hold a single skin item.</summary>
public class SkinItem
{
    #region Class Variables
    /// <summary>The skin name</summary>
    private string skinName;
    /// <summary>The base skin name</summary>
    private string baseSkinName;
    /// <summary>The skin path</summary>
    private string skinPath;
    /// <summary>The action to be taken when switching skins.</summary>
    private ICommand action;
    /// <summary>The icon of the skin.</summary>
    private Brush icon;
    #endregion Class Variables

    #region Constructors
    /// <summary>Initializes a new instance of the <see cref="SkinItem"/> class.</summary>
    public SkinItem() { }

    /// <summary>Initializes a new instance of the <see cref="SkinItem" /> class.</summary>
    /// <param name="newSkinName">The name of the new skin.</param>
    /// <param name="baseSkinName">Name of the base skin.</param>
    /// <param name="newSkinPath">Optional Parameter: The new skin path.</param>
    /// <param name="newSkinAction">Optional Parameter: The new skin action to be taken when switching to the new skin.</param>
    /// <param name="newSkinIcon">Optional Parameter: The new skin icon.</param>
    public SkinItem(string newSkinName, string baseSkinName = "", string newSkinPath = "", ICommand newSkinAction = null, Brush newSkinIcon = null)
    {
        if (newSkinName != "")
            this.skinName = newSkinName;

        if (baseSkinName != "")
            this.baseSkinName = baseSkinName;

        if (newSkinPath != "")
            this.skinPath = newSkinPath;

        if (newSkinAction != null)
            this.action = newSkinAction;

        if (newSkinIcon != null)
            this.icon = newSkinIcon;
    }
    #endregion Constructors

    #region Properties
    /// <summary>Gets or sets the name of the skin.</summary>
    /// <value>The name of the skin.</value>
    public string SkinName
    {
        get { return this.skinName; }
        set
        {
            if (this.skinName != value)
            {
                this.skinName = value;
                //OnPropertyChanged(() => this.SkinName);
            }
        }
    }

    /// <summary>Gets or sets the name of the base skin.</summary>
    /// <value>The name of the base skin.</value>
    public string BaseSkinName
    {
        get { return this.baseSkinName; }
        set
        {
            if (this.baseSkinName != value)
            {
                this.baseSkinName = value;
                //OnPropertyChanged(() => this.BaseSkinName);
            }
        }
    }

    /// <summary>Gets or sets the skin path.</summary>
    /// <value>The skin path.</value>
    public string SkinPath
    {
        get { return this.skinPath; }
        set
        {
            if (this.skinPath != value)
            {
                this.skinPath = value;
                //OnPropertyChanged(() => this.SkinPath);
            }
        }
    }

    /// <summary>Gets or sets the action.</summary>
    /// <value>The action.</value>
    public ICommand Action
    {
        get { return this.action; }
        set
        {
            if (this.action != value)
            {
                this.action = value;
                //OnPropertyChanged(() => this.Action);
            }
        }
    }

    /// <summary>Gets or sets the icon.</summary>
    /// <value>The icon.</value>
    public Brush Icon
    {
        get { return this.icon; }
        set
        {
            if (this.icon != value)
            {
                this.icon = value;
                //OnPropertyChanged(() => this.Icon);
            }
        }
    }
    #endregion Properties
}
}

Oh and I am using Galasoft MVVM-Light generic RelayCommand which is supposed to take parameters, can be found here: http://mvvmlight.codeplex.com/SourceControl/latest#GalaSoft.MvvmLight/GalaSoft.MvvmLight%20%28NET35%29/Command/RelayCommandGeneric.cs

Was it helpful?

Solution

I'm having a little trouble understanding exactly what you're looking for. But in running your code I see that the names of the Skins are not showing in the context menu. If you remove the source so your setting looks like this for the header:

<!-- CONTEXT MENU -->
<Window.ContextMenu>
    <ContextMenu DataContext="MainWindowViewModel" Name="MainWindowContextMenu" PresentationTraceSources.TraceLevel="High">
        <MenuItem Header="Skins" ItemsSource="{Binding Source={StaticResource MainWindowViewModel}, Path=Skins}">
            <MenuItem.ItemContainerStyle>
                <Style TargetType="MenuItem">
                    <Setter Property="Header" Value="{Binding Path=SkinName}"/>
                    <Setter Property="Command" Value="{Binding Source={StaticResource MainWindowViewModel}, Path=ContextMenuClickCommand}"/>
                    <Setter Property="CommandParameter" Value="{Binding Path=SkinName}"/>
                </Style>
            </MenuItem.ItemContainerStyle>
        </MenuItem>
    </ContextMenu>
</Window.ContextMenu>

That will fix your problem. Since you are setting the source on the MenuItem, you change the datacontext for the items within. So you don't need to specify the source again.

Edit:

Also I changed the Path from Skins.SkinName to SkinName

Now I see the text for the items in the menu and when I click on say "Skin Item 1", the value of selected in OnMenuItemClick is "Skin Item 1".

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top