简体   繁体   中英

Click Event not firing on ListBoxItem ContextMenu

I'm trying to add a ContextMenu to ListBoxItem , but the Click event is not fired.

I tried ContextMenu.MenuItem.Click event. I also tried binding Command , but a binding error appears in the output window like:

"Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=NavigateCommand;"

Here is complete sample code.

XAML

<ListBox>
    <ListBoxItem>1</ListBoxItem>
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}"
               BasedOn="{StaticResource {x:Type ListBoxItem}}">
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu MenuItem.Click="ContextMenu_Click">
                        <MenuItem Header="Navigate"
                                  Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window, AncestorLevel=1}, Path=NavigateCommand}"
                                  Click="NavigateItemClick" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

CODE BEHIND

public MainWindow()
{
    InitializeComponent();
    NavigateCommand = new DelegateCommand(Navigate);
}

public DelegateCommand NavigateCommand { get; set; }


private void Navigate()
{
    MessageBox.Show("Command Worked");
}

private void NavigateItemClick(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Item Click Worked");
}

private void ContextMenu_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Any Item click Worked");
}
        

Is there any way to invoke the Click event handler or bind the Command ?

The ContextMenu is not part of the same visual tree as your ListBox , as it is displayed in a separate window. Therefore using a RelativeSource or ElementName binding does not work directly . However, you can work around this issue by binding the Window (where you define the NavigateCommand in code-behind) with a RelativeSource binding to the Tag property of ListBoxItem . This works, since they are part of the same visual tree. The Tag property is a general purpose property that you can assign anything to.

Gets or sets an arbitrary object value that can be used to store custom information about this element.

Then use the PlacementTarget property of the ContextMenu as indirection to access the Tag of the ListBoxItem that it was opened for.

<Style TargetType="{x:Type ListBoxItem}"
       BasedOn="{StaticResource {x:Type ListBoxItem}}">
   <Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
   <Setter Property="ContextMenu">
      <Setter.Value>
         <ContextMenu>
            <MenuItem Header="Navigate"
                      Command="{Binding PlacementTarget.Tag.NavigateCommand, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
         </ContextMenu>
      </Setter.Value>
   </Setter>
</Style>

In essence, you bind the Window data context to the Tag of ListBoxItem which is the PlacementTarget of the ContextMenu , that can then bind the NavigateCommand through the Tag property.

Adding a Click event handler is also possible, but in case the ContextMenu is defined in a Style , you have to add it differently, otherwise it will not work and you get this strange exception.

Unable to cast object of type System.Windows.Controls.MenuItem to type System.Windows.Controls.Button .

Add a MenuItem style with an event setter for Click , where you assign the event handler.

<Style TargetType="{x:Type ListBoxItem}"
       BasedOn="{StaticResource {x:Type ListBoxItem}}">
   <Setter Property="ContextMenu">
      <Setter.Value>
         <ContextMenu>
            <MenuItem Header="Navigate">
               <MenuItem.Style>
                  <Style TargetType="MenuItem">
                     <EventSetter Event="Click" Handler="MenuItem_OnClick"/>
                  </Style>
               </MenuItem.Style>
            </MenuItem>
         </ContextMenu>
      </Setter.Value>
   </Setter>
</Style>

I don't think it is necessary to write "hackish" code. I think you should refine your code instead.

A context menu should only operate on its current UI context, hence the name context menu. When you right click your code editor, you won't find eg an "Open File" menu item - this would be out of context.

The context of the menu is the actual item. Therefore, the command should be defined in the data model of the item as the context menu should only define commands that target the items.

Note that the ContextMenu is always implicitly hosted inside a Popup (as Child of Popup ). The child elements of Popup can never part of the visual tree (since the content is rendered inside a dedicated Window instance and Window can't be a child of another element, Window is always the root of its own visual tree), therefore tree traversal using Binding.RelativeSource can't work.
You can reference ContextMenu.PlacementTarget (instead of DataContext ) to get the actual ListBoxItem (the placement target). The DataContext of the ListBoxItem is the data model (WindowItem class in the example below).

For example to reference the eg OpenWindowCommand of the underlying data model of the current ListBoxItem from inside the ContextMenu use:

"{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.DataContext.OpenWindowCommand}"

WindowItem.cs
Create a data model for the items in the list. This data model exposes all the commands to operate on the item itself.

class WindowItem
{
  public WindowItem(string windowName) => this.Name = windowName;

  public string Name { get; }

  public ICommand OpenWindowCommand => new RelayCommand(ExecuteOpenWindow);

  private void ExecuteOpenWindow(object commandParameter)
  {
    // TODO::Open the window associated with this instance
    MessageBox.Show($"Window {this.Name} is open");
  }

  public override string ToString() => this.Name;
}

MainWindow.xaml.cs
Initialize the ListBox from your MainWindow using data binding:

partial class MainWindow : Window
{
  public ObservableCollection<WindowItem> WindowItems { get; }

  public MainWindow()
  {
    InitializeComponent();
    this.DataContext = this;

    var windowItems = new 
    this.WindowItems = new ObservableCollection<WindowItem> 
    {
      new WindowItem { Name = "Window 1" },
      new WindowItem { Name = "Window 2" }
    }
  }
}

MainWindow.xaml

<Window>
  <Window.Resources>

    <!-- Command version -->
    <ContextMenu x:Key="ListBoxItemContextMenu">
      <MenuItem Header="Open"
                Command="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.DataContext.OpenWindowCommand}" />
    </ContextMenu>

    <!-- Optionally define a Click handler in code-behind (MainWindow.xaml.cs) -->
    <ContextMenu x:Key="ListBoxItemContextMenu">
      <MenuItem Header="Open"
                Click="OpenWindow_OnMenuItemClick" />
    </ContextMenu>
  </Window.Resources>

  <ListBox ItemsSource="{Binding WindowItems}">
    <ListBox.ItemContainerStyle>
      <Style TargetType="ListBoxItem">
        <Setter Property="ContextMenu" Value="{StaticResource ListBoxItemContextMenu}">
        </Setter>
      </Style>
    </ListBox.ItemContainerStyle>
  </ListBox>
</Window>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM