0
votes

I'm trying to make Context Menu, which will have items depending on some data in code. So, i have simple class, determining single item of menu

class ContextMenuItem
{
    public string ItemHeader {get; set;}
    public Command ItemAction {get; set;
}

where Command is implementation of ICommand, and stores action, which will be fired once this item is selected. Then i have class, serving as DataContext

class SomeClass
{
    public List<ContextMenuItem> ContextMenuItems {get; set;}
    public string SomeProperty {get; set;}
    public string SomeAnotherProperty {get; set;}
}

So, ContextMenuItems is list of actions I need in my context menu, which can be generated using different approaches.

And I'm creating dynamic context menu, using this approach.

<ContextMenu ItemsSource="{Binding ContextMenuItems}">
    <ContextMenu.ItemContainerStyle>
        <Style TargetType="{x:Type MenuItem}">
            <Setter Property="Command" Value="{Binding ItemAction}"/>
            <Setter Property="Header" Value="{Binding ItemHeader}"/>
        </Style>
    </ContextMenu.ItemContainerStyle>
</ContextMenu>

So, i was suspecting this to work well. But, for some reason, binding works not the way I want it to.

<Setter Property="Command" Value="{Binding ItemAction}"/>
<Setter Property="Header" Value="{Binding ItemHeader}"/>

Somehow, data context for this lines is not ContextMenuItem, but SomeClass itself. So, i can bind SomeProperty and SomeAnotherProperty here, but not ItemHeader or ItemAction. And this ruins whole idea of dynamicaly created context menu.

So, how can i make this template recognize ContextMenuItem as its DataContext?

What i want to do can be accomplished using DataTemplate, but it gives us MenuItem inside MenuItem, and this is not good.

Update

Full xaml code involving ListBox

<ListBox Margin="5, 5" Background="White" ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <Grid Margin="3,1">
                <Grid.ContextMenu>
                    <ContextMenu ItemsSource="{Binding ContextMenuItems}">
                        <ContextMenu.ItemContainerStyle>
                            <Style TargetType="MenuItem">
                                <Setter Property="Command" Value="{Binding ItemAction}"/>
                                <Setter Property="Header" Value="{Binding ItemHeader}"/>
                            </Style>
                        </ContextMenu.ItemContainerStyle>
                    </ContextMenu>

                </Grid.ContextMenu>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="7*"/>
                </Grid.ColumnDefinitions>
                <CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
                <TextBlock Text="{Binding ObjectName}" Grid.Column="1" Margin="0,2"/>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>
2
I've set up a test app with your code and I can't duplicate your problems. It's all working as expected here. What's your ContextMenu attached to? Is it in any kind of DataTemplate?goobering
Well, it is. I have ListBox, with ItemsSource binded to list of SomeClass, and template to show its data correctly. Is Context Menu (or some template) inside DataTemplate something what we shouldn't do?lentinant
It's not a problem but it might change the binding code a little. Can you update the question with the XAML that shows the ContextMenu as you've placed it in your ListBox?goobering
Done. Now there is all code i'm using to create ListBox and its itemslentinant

2 Answers

0
votes

Refer below code. it is working fine for me.

<Window x:Class="BindingListBox_Learning.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <ListBox Margin="5, 5" Background="White"  ItemsSource="{Binding SwitchAgents, UpdateSourceTrigger=PropertyChanged}" HorizontalContentAlignment="Stretch">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid Margin="3,1">
                    <Grid.ContextMenu>
                        <ContextMenu ItemsSource="{Binding ContextMenuItems}">
                            <ContextMenu.ItemContainerStyle>
                                <Style TargetType="MenuItem">
                                    <Setter Property="Command" Value="{Binding ItemAction}"/>
                                    <Setter Property="Header" Value="{Binding ItemHeader}"/>
                                </Style>
                            </ContextMenu.ItemContainerStyle>
                        </ContextMenu>
                    </Grid.ContextMenu>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="1*"/>
                        <ColumnDefinition Width="7*"/>
                    </Grid.ColumnDefinitions>
                    <CheckBox IsChecked="{Binding Enabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,3"/>
                    <TextBlock Text="{Binding SomeProperty}" Grid.Column="1" Margin="0,2"/>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

 public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();           
    }
}

class MainViewModel
{
    public List<SomeClass> SwitchAgents { get; set; }
    public MainViewModel()
    {
        SwitchAgents = new List<SomeClass>();
        SomeClass obj = new SomeClass();
        obj.SomeProperty = "Test";
        List<ContextMenuItem> lst = new List<ContextMenuItem>();
        lst.Add(new ContextMenuItem() { ItemHeader = "Hi", ItemAction = new BaseCommand(MenuClick) });
        obj.ContextMenuItems = lst;
        SwitchAgents.Add(obj);
    }

    void MenuClick(object obj)
    {
        // Do Menu Click Stuff
    }
}

class ContextMenuItem
{
    public string ItemHeader { get; set; }
    public ICommand ItemAction { get; set; }
}

class SomeClass
{
    public List<ContextMenuItem> ContextMenuItems { get; set; }
    public string SomeProperty { get; set; }
    public string SomeAnotherProperty { get; set; }
}

public class BaseCommand : ICommand
{
    private Predicate<object> _canExecute;
    private Action<object> _method;
    public event EventHandler CanExecuteChanged;

    public BaseCommand(Action<object> method)
        : this(method, null)
    {
    }

    public BaseCommand(Action<object> method, Predicate<object> canExecute)
    {
        _method = method;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        if (_canExecute == null)
        {
            return true;
        }

        return _canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        _method.Invoke(parameter);
    }
}

Instead of BaseCommand you use RelayCommand from MVVMLight OR DelegateCommand from PRISM.

2
votes

There is a sneaky trick to making this work. Normally I'd just use a RelativeSource in the binding to have it tunnel up to something with a DataContext. The problem is that ContextMenu doesn't sit in the visual tree hierarchy, so RelativeSource has nothing to find.

The solution is outlined here: http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited

Copy/paste this class into your project somewhere:

public class BindingProxy : Freezable
{
    #region Overrides of Freezable

    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }

    #endregion

    public object Data
    {
        get { return (object)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}

Then reference the namespace of the BindingProxy at the top of your Window/UserControl/whatever:

xmlns:local="clr-namespace:INSERTYOURNAMESPACEHERE"

Add the BindingProxy as a resource to your ListBox:

<ListBox.Resources>
    <local:BindingProxy x:Key="proxy" Data="{Binding}" />
</ListBox.Resources>

And finally set the Source of your ContextMenu ItemsSource binding to the proxy:

<ContextMenu ItemsSource="{Binding Data.ContextMenuItems, Source={StaticResource proxy}}" >