1
votes

I have a TreeView created using HierarchicalDataTemplate with help of this famous article.

Each node in my treeview has a different contextMenu. So I created a property for treeView that return for me the object of every node selected. Then I used the code below to display my ContextMenu. But the contextMenu is always empty.

<view:MyTreeView ItemsSource="{Binding MyNode}" 
SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" >
    <TreeView.Resources>
      <ContextMenu x:Key="MyContextMenu" ItemsSource="{Binding ContextMenuItem}"/>
       <DataTemplate DataType="{x:Type local:ChildViewModel}">
         <StackPanel Orientation="Horizontal" ContextMenu="{StaticResource MyContextMenu}">
//...
         </StackPanel>
       </DataTemplate>
   </TreeView.Resources>
</view:MyTreeView>

PrincipalViewModel: (No relation with ChildViewModel)

private ICommand _editMapCommand;

    public ICommand EditMapCommand
    {
        get
        {
            return _editMapCommand;
        }
        set
        {
            SetProperty(ref _editMapCommand, value, () => EditMapCommand);
            OnPropertyChanged("EditMapCommand");

        }
    }

    private ICommand _removeMapCommand;

    public ICommand RemoveMapCommand
    {
        get
        {
            return _removeMapCommand;
        }
        set
        {
            SetProperty(ref _removeMapCommand, value, () => RemoveMapCommand);
            OnPropertyChanged("RemoveMapCommand");

        }
    }
 private ObservableCollection<MenuItem> _contextMenuMap;
    public ObservableCollection<MenuItem> ContextMenuMap
    {
        get
        {
            return _contextMenuMap;
        }
        set
        {
            SetProperty(ref _contextMenuMap, value, () => ContextMenuMap);
            OnPropertyChanged("ContextMenuMap");

        }
    }
private object _selectedItem;
    public object SelectedItem
    {
        get
        {
            return _selectedItem;
        }

        set
        {
            SetProperty(ref _selectedItem, value, () => SelectedItem);
            OnPropertyChanged("SelectedItem");
            Fill(_selectedItem);
        }
    }
 private void FillPropertyCard(object obj)
    {
        PcEditable = false;
        if (obj is MyObject)
        {
            ContextMenuMap = new ObservableCollection<MenuItem>();
            EditMapCommand = new DelegateCommand<CancelEventArgs>(OnEditMapCommandExecute, OnEditMapCommandCanExecute);
            RemoveMapCommand = new DelegateCommand<CancelEventArgs>(OnRemoveMapCommandExecute, OnRemoveMapCommandCanExecute);
            ContextMenuMap.Add(new MenuItem() { Header = "editHeader", Command = EditMapCommand });
            ContextMenuMap.Add(new MenuItem() { Header = "removeHeader", Command = RemoveMapCommand });
}

I believe I'm missing something in relation with binding.

NB: when debugging, I found in xaml that the value of ContextMenuMap changed as expected but always nothing is displayed.

1

1 Answers

8
votes

You have to proxy the binding. ContextMenus are popups, so they're not part of the same visual tree and thus don't inherit the DataContext. You can read more about this on Thomas Levesque's article 'How to bind to data when the DataContext is not inherited', he also provides the source code for a BindingProxy class. Add that to your project and then modify your ContextMenu to use it:

<local:BindingProxy x:Key="MyBindingProxy" Data="{Binding}" />
<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Source={StaticResource MyBindingProxy}, Path=Data}" ItemsSource="{Binding ContextMenuMap}" />

There are quite a few other problems with the code you pasted though, for starters you're binding your context menu's items to ContextMenuItem when I'm fairy sure you meant ContextMenuMap. Also ContextMenuMap shouldn't be a collection of MenuItem, you should never declare view controls in your view models. Change the ContextMenuMap to be a collection of strings instead; the context menus MenuItems will be created automatically.

EDIT: Sorry Sadok, I wasn't seriously suggesting you use a collection of strings in your application, I was just using that to illustrate the overall point of how data binding works in a case like this. In a real-world app you would create a view model for your menu items, just as you would for other types of views. A simple one might only need the header text, the ICommands (which you're currently setting up as separate properties) and maybe support for a CanExecute handler:

public class MenuItemViewModel
{
    public string Header { get; private set; }
    public ICommand Command { get; private set; }

    public MenuItemViewModel(string header, Action execute, Func<bool> canExecute = null)
    {
        this.Header = header;
        this.Command = new RelayCommand(execute, canExecute);
    }
}

Menus would then be set up in your code like this:

// set up the menu
this.ContextMenuMap = new ObservableCollection<MenuItemViewModel>
{
    new MenuItemViewModel("New", OnNew),
    new MenuItemViewModel("Open", OnOpen),
    new MenuItemViewModel("Save", OnSave, CanSave)
};

// menu command handlers
private void OnNew() { /* ... */ }
private void OnOpen() { /* ... */ }
private void OnSave() { /* ... */ }
private bool CanSave() { /* ... */ return false; }

Or if you prefer you could use anonymous functions where appropriate:

this.ContextMenuMap = new ObservableCollection<MenuItemViewModel>
{
    new MenuItemViewModel("Cut", () => { /* cut code here */ }),
    new MenuItemViewModel("Copy", () => { /* copy code here */ }),
    new MenuItemViewModel("Paste", () => { /* paste code here */ }, () => false)
};

The only other change is to let your XAML know how to use this view model. As I mentioned below, you can set DisplayMemberPath to specify the field to use for text and you can use a style setter to specify the command field:

<ContextMenu x:Key="MyContextMenu" DataContext="{Binding Source={StaticResource MyBindingProxy}, Path=Data}" ItemsSource="{Binding ContextMenuMap}" DisplayMemberPath="Header">
    <ContextMenu.Resources>
        <Style TargetType="{x:Type MenuItem}">
            <Setter Property="Command" Value="{Binding Command}" />
        </Style>
    </ContextMenu.Resources>
</ContextMenu>