2

I want to bind a certain command to a menuItem. The said menu item is part of a ContextMenu that is defined inside an ItemTemplate.

Right now, what I have compiles and runs, but the command is never called. In the past, I had used a similar pattern to hook commands to buttons defined in an ItemTemplate with success.

Anyone has any idea how I could accomplish this?

XAML:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Wpf_treeView" x:Name="window" x:Class="Wpf_treeView.MainWindow"
    Title="MainWindow" Height="350" Width="525">
  <Grid>
    <TreeView HorizontalAlignment="Left" Height="299" Margin="10,10,0,0" VerticalAlignment="Top" Width="228" ItemsSource="{Binding DataInfosView}" >
      <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
          <TextBlock DockPanel.Dock="Left" Text="{Binding InfoValue}" TextAlignment="Left" >
            <TextBlock.ContextMenu>
              <ContextMenu>
                <MenuItem Header="{Binding InfoValue}" IsEnabled="False"/>
                <MenuItem Header="Add child" Command="{Binding AddChildCmd, ElementName=window}"/>
              </ContextMenu>
            </TextBlock.ContextMenu>
          </TextBlock>
        </HierarchicalDataTemplate>
      </TreeView.ItemTemplate>
    </TreeView>
  </Grid>
</Window>

C#:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;

namespace Wpf_treeView
{
    public partial class MainWindow : Window
    {
        private static readonly Random rnd = new Random();
        private List<InfoData> m_InfoData = new List<InfoData>();

        public ListCollectionView DataInfosView { get; private set; }

        public static readonly DependencyProperty AddChildProperty =
            DependencyProperty.Register("AddChildCmd", 
                                        typeof(ICommand),
                                        typeof(MainWindow));

        public ICommand AddChildCmd
        {
            get { return (ICommand) GetValue(AddChildProperty); }
            set { SetValue(AddChildProperty, value); }
        }

        public MainWindow()
        {
            AddChildCmd = new RoutedCommand();
            CommandManager.RegisterClassCommandBinding(
                GetType(), 
                new CommandBinding(AddChildCmd, AddChild));

            m_InfoData.Add(new InfoData(4));
            m_InfoData.Add(new InfoData(1));
            m_InfoData.Add(new InfoData(5));
            m_InfoData[1].Children.Add(new InfoData(3));
            m_InfoData[1].Children[0].Children.Add(new InfoData(7));

            DataInfosView = new ListCollectionView(m_InfoData);
            DataContext = this;

            InitializeComponent();
        }

        private void AddChild(object sender, RoutedEventArgs e)
        {
            ExecutedRoutedEventArgs args = (ExecutedRoutedEventArgs)e;
            InfoData info = (InfoData)args.Parameter;
            info.Children.Add(new InfoData(rnd.Next(0, 11)));
        }
    }

    class InfoData : INotifyPropertyChanged
    {
        private int infoValue;

        public int InfoValue
        {
            get { return infoValue; }
            set
            {
                if (value != infoValue)
                {
                    infoValue = value;
                    OnPropertyChanged();
                }
            }
        }

        public List<InfoData> Children { get; private set; }

        public InfoData(int infoValue)
        {
            InfoValue = infoValue;
            Children = new List<InfoData>();
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(
            [CallerMemberName] string propertyName = null)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
joce
  • 9,624
  • 19
  • 56
  • 74
  • are you getting binding errors for the command? – Gordon Allocman Mar 16 '16 at 18:16
  • Nope. Everything compiles and runs fine. The menu shows up fine as well. It's just that the command isn't being called. BTW, I would be fine with dealing with an event (Click) as well, but I would need to have access to the item one which the menu was created, which I'm not sure how to have access to . – joce Mar 16 '16 at 18:19
  • 2
    A binding error won't cause any compile time issues or cause any display issues, it will just show up in the output window when using the debugger, they can easily be missed but are highly useful for debugging xaml issues such as binding. – Gordon Allocman Mar 16 '16 at 18:21
  • HA! I didn't know about that. And yes, there's a binding error! :-/ `System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=window'. BindingExpression:Path=AddChildCmd; DataItem=null; target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')` – joce Mar 16 '16 at 18:24
  • Ah I figured that would be the case, I think ContextMenu is not part of the Visual Tree so binding is a bit different, I need to look into it and I can get back to you. Glad I could make you aware of the binding errors though, those are extremely helpful when doing any sort of data bindings because they show things as simple as typos to more complex issues – Gordon Allocman Mar 16 '16 at 18:26
  • FYI, I also tried ``, with other and higher values for `AncestorLevel`, all without success. I keep getting the same binding errors. – joce Mar 16 '16 at 18:28
  • I found this very helpful [answer](http://stackoverflow.com/a/3668699/2455627) and I wanted to apply it to your code, but since you are both binding to what would be the natural context and a relative source higher up in the tree, I am not sure how to go about that, hopefully the answer will give you some insight on how to go about it – Gordon Allocman Mar 16 '16 at 18:37
  • Ok here is another [answer](http://stackoverflow.com/a/26383463/2455627) that covers binding to the window itself, with these two you should be able to piece it together – Gordon Allocman Mar 16 '16 at 18:39
  • alright I tried to combine the two answers to solve your issue (see my answer). If that does not work I try to fix it or delete it – Gordon Allocman Mar 16 '16 at 18:54

1 Answers1

1

Alright this should work:

<TextBlock DockPanel.Dock="Left"
                           Text="{Binding InfoValue}"
                           TextAlignment="Left"
                           Tag="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
                    <TextBlock.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="{Binding InfoValue}"
                                      IsEnabled="False" />
                            <MenuItem Header="Add child"
                                      Command="{Binding Path=Parent.PlacementTarget.Tag.AddChildCmd, RelativeSource={RelativeSource Self}}" 
                                      CommandParameter="{Binding}" />
                        </ContextMenu>
                    </TextBlock.ContextMenu>
                </TextBlock>

The ContextMenu doesn't exist in the regular Visual Tree, so you aren't able to walk up the tree to get to the main data context. By using the Tag you are able to "pass in" the Main Window's data context to the context menu. For some more information on binding with context menu's see this answer as well as this one as they provide some good explanations as to what is going on

Community
  • 1
  • 1
Gordon Allocman
  • 768
  • 7
  • 23
  • It works more than before, but it still doesn't work. :-/ The menu item displaying the item value displays nothing. And the command *IS* called, but try as I may, I can't get the currently selected item to be passed as a param (I guess this issue is similar to the one where I can't display the item value). This makes me think I might not be declaring the `ContextMenu` at the right place. – joce Mar 16 '16 at 19:21
  • was there a binding error for the menu item that didn't display? – Gordon Allocman Mar 16 '16 at 19:27
  • I updated my code to the bindings should work now. You have another issue with null reference exceptions in your AddChild method, but you should be able to figure that out – Gordon Allocman Mar 16 '16 at 19:37
  • W00T!!! Thank you so much @GordonAllocman! You saved the day! Also, I've just updated your answer to add the proper `CommandParameter`. – joce Mar 16 '16 at 19:42
  • 1
    No problem, I was just getting around to working out the paramter myself haha. One thing to note is there is a bug when you add children likely due to not calling propertychanged or using an observable collection. good luck with the rest of your program! – Gordon Allocman Mar 16 '16 at 19:46
  • HA! I was forcing a refresh of the `ListCollectionView` upon adding a child, but it was collapsing the entire tree. `ObservableCollection` fixed it! It's beautiful! You've saved the day twice now! – joce Mar 16 '16 at 19:57
  • 1
    Yeah if possible ObservableCollection's are preferred over lists if you plan on binding (there are obviously times when you make an exception) because they handle the synchronization between view(window) and viewmodel(code-behind) for you – Gordon Allocman Mar 16 '16 at 20:00
  • Also, FYI in your answer, I just simplified the bindings for Header for the menu item that just displays the item value and for the CommandParameter of the menu item that has the command. – joce Mar 16 '16 at 20:02
  • Sounds good, I added some explanation and links in case other users run into this question – Gordon Allocman Mar 16 '16 at 20:37