0

I need to bind Command and CommandParameter to two different classes from within a WPF ContextMenu MenuItem that is inside an ItemsControl.ItemTemplate. The problem is that ContextMenu has a separate visual tree than the rest of the window. I got it working in .NET 4.0+ but can't find a workaround that works in .NET 3.5.

Note: Even the working .NET 4.0+ workaround works at runtime but breaks both the Visual Studio 2013 Designer and the Blend Designer.

How can I bind the CommandParameter to a single CustomerViewModel and bind the Command to EditCustomersController in .NET 3.5?

View

<Window
    x:Class="MvvmLight1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MVVM Light Application"
    DataContext="{Binding EditCustomers, Source={StaticResource Locator}}">

    <ItemsControl
        x:Name="rootElement"
        ItemsSource="{Binding Customers}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBlock
                    Text="{Binding Name}">
                    <TextBlock.ContextMenu>
                        <ContextMenu
                            PlacementTarget="{Binding ElementName=myTextBlock}">
                            <MenuItem
                                Header="Capitalize Using RelativeSource (Does not work)"
                                CommandParameter="{Binding}"
                                Command="{Binding
                                    Path=DataContext.CapitalizeCommand,
                                    RelativeSource={RelativeSource
                                        FindAncestor,
                                        AncestorType={x:Type Window}}}" />
                            <MenuItem
                                Header="Lowercase Using RelativeSource (Does not work)"
                                CommandParameter="{Binding}"
                                Command="{Binding
                                    Path=DataContext.LowercaseCommand,
                                    RelativeSource={RelativeSource
                                        FindAncestor,
                                        AncestorType={x:Type Window}}}" />
                            <MenuItem
                                Header="Capitalize Using Source (Only works in .NET 4.0+)"
                                CommandParameter="{Binding}"
                                Command="{Binding
                                    Path=DataContext.CapitalizeCommand,
                                    Source={x:Reference Name=rootElement}}" />
                            <MenuItem
                                Header="Lowercase Using Source (Only works in .NET 4.0+)"
                                CommandParameter="{Binding}"
                                Command="{Binding
                                    Path=DataContext.LowercaseCommand,
                                    Source={x:Reference Name=rootElement}}" />
                        </ContextMenu>
                    </TextBlock.ContextMenu>
                </TextBlock>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>

Controller

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Windows.Input;

public class EditCustomersController : ViewModelBase
{
    public EditCustomersController()
    {
        this.CapitalizeCommand = new RelayCommand<CustomerViewModel>(this.Capitalize);
        this.LowercaseCommand = new RelayCommand<CustomerViewModel>(this.Lowercase);

        var c = new List<CustomerViewModel>();
        c.Add(new CustomerViewModel("Fred"));
        c.Add(new CustomerViewModel("Bob"));
        c.Add(new CustomerViewModel("Sue"));
        c.Add(new CustomerViewModel("Sally"));
        this.Customers = c;
    }

    public IEnumerable<CustomerViewModel> Customers { get; set; }

    public ICommand CapitalizeCommand { get; private set; }

    public ICommand LowercaseCommand { get; private set; }

    private void Capitalize(CustomerViewModel customer)
    {
        customer.Name = customer.Name.ToUpper();
    }

    private void Lowercase(CustomerViewModel customer)
    {
        customer.Name = customer.Name.ToLower();
    }
}

ViewModel

using GalaSoft.MvvmLight;

public class CustomerViewModel : ViewModelBase
{
    private string name;

    public CustomerViewModel(string name)
    {
        this.name = name;
    }

    public string Name
    {
        get
        {
            return this.name;
        }

        set
        {
            this.Set(() => this.Name, ref this.name, value);
        }
    }
}
Josh Graham
  • 1,188
  • 12
  • 17
  • 1
    The solution is the same regardless of .NET version. You need to set the `ContextMenu.DataContext` property by passing the object in the `PlacementTarget.Tag` property. See the solution in the [Can't get selected item context menu](http://stackoverflow.com/questions/21892834/cant-get-selected-item-context-menu/21893417#21893417) question, which links to two further questions with code examples. – Sheridan Sep 23 '14 at 13:59
  • Thanks for your input @Sheridan. Your answer [here](http://stackoverflow.com/a/18611364/2460879) does not apply to me because it relies on selection, which ItemsControl does not have. Your answer [here](http://stackoverflow.com/a/21564974/2460879) did not contain an example of using a different class for `CommandParameter` and `Command` but it did get me going in the right direction. – Josh Graham Sep 23 '14 at 15:36
  • You're welcome, although it does seem a little bit strange that you are trying to distance your solution from those linked answers. To take part of one answer and to apply it to your own situation is called extrapolation. That's all those links were supposed to provide and it seems that they did. I never intended on doing all of your work for you. – Sheridan Sep 23 '14 at 15:45

1 Answers1

0

Got it working in all versions of .NET by moving the ContextMenu into a resource and passing the top level DataContext through using the Tag property. Inside the ContextMenu MenuItem I can access both the current CustomerViewModel using {Binding} and the EditCustomersController using a binding with the Tag and RelativeSource.

View

<Window
    x:Class="MvvmLight1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MVVM Light Application"
    DataContext="{Binding EditCustomers, Source={StaticResource Locator}}">

    <Window.Resources>
        <ContextMenu
            x:Key="ItemContextMenu"
            Tag="{Binding PlacementTarget.Tag,
                RelativeSource={RelativeSource Self}}">
            <MenuItem
                Header="Capitalize"
                CommandParameter="{Binding}"
                Command="{Binding Tag.CapitalizeCommand,
                    RelativeSource={RelativeSource
                        FindAncestor,
                        AncestorType={x:Type ContextMenu}}}" />
        </ContextMenu>
    </Window.Resources>

    <ItemsControl
        ItemsSource="{Binding Customers}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBlock
                    Tag="{Binding
                        DataContext,
                        RelativeSource={RelativeSource
                            AncestorType={x:Type Window}}}"
                    Text="{Binding Name}"
                    ContextMenu="{StaticResource ItemContextMenu}">
                </TextBlock>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Window>
Josh Graham
  • 1,188
  • 12
  • 17