WPF - Why do ContextMenu items work for ListBox but not ItemsControl?

不打扰是莪最后的温柔 提交于 2020-01-04 02:57:13

问题


Items in a list have context menus. The context menu items are bound to routed commands.

The context menu items work correctly if the list control is a ListBox, but as soon as I downgrade it to an ItemsControl it no longer works. Specifically the menu items are always greyed out. The CanExecute callback in my CommandBinding is not being called either.

What is it about ListBox that allows context menu items with commands to bind correctly?

Here are some excerpts from a sample app I put together to highlight the problem:

<!-- Data template for items -->
<DataTemplate DataType="{x:Type local:Widget}">
  <StackPanel Orientation="Horizontal">
    <StackPanel.ContextMenu>
      <ContextMenu>
        <MenuItem Header="UseWidget" 
                  Command="{x:Static l:WidgetListControl.UseWidgetCommand}"
                  CommandParameter="{Binding}" />
      </ContextMenu>
    </StackPanel.ContextMenu>
    <TextBlock Text="{Binding Path=Name}" />
    <TextBlock Text="{Binding Path=Price}" />
  </StackPanel>
</DataTemplate>

<!-- Binding -->
<UserControl.CommandBindings>
  <CommandBinding Command="{x:Static l:WidgetListControl.UseWidgetCommand}" 
                  Executed="OnUseWidgetExecuted" 
                  CanExecute="CanUseWidgetExecute" />
</UserControl.CommandBindings>

<!-- ItemsControl doesn't work... -->
<ItemsControl ItemsSource="{Binding Path=Widgets}" />

<!-- But change it to ListBox, and it works! -->
<ListBox ItemsSource="{Binding Path=Widgets}" />

Here's the C# code for the view model and data item:

public sealed class WidgetListViewModel
{
    public ObservableCollection<Widget> Widgets { get; private set; }

    public WidgetViewModel()
    {
        Widgets = new ObservableCollection<Widget>
            {
                new Widget { Name = "Flopple", Price = 1.234 },
                new Widget { Name = "Fudge", Price = 4.321 }
            };
    }
}

public sealed class Widget
{
    public string Name { get; set; }
    public double Price { get; set; }
}

Here's the C# code-behind for the control:

public partial class WidgetListControl
{
    public static readonly ICommand UseWidgetCommand 
        = new RoutedCommand("UseWidget", typeof(WidgetListWindow));

    public WidgetListControl()
    {
        InitializeComponent();
    }

    private void OnUseWidgetExecuted(object s, ExecutedRoutedEventArgs e)
    {
        var widget = (Widget)e.Parameter;
        MessageBox.Show("Widget used: " + widget.Name);
    }

    private void CanUseWidgetExecute(object s, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute = true;
        e.Handled = true;
    }
}

Just to reiterate the question -- what is is that ListBox provides that allows for it's context menu item commands to bind correctly, and is there some way I can get this working for ItemsControl?


回答1:


Ok, the main issue I see is that an ItemsControl doesn't have a concept of the selected item, so you can't select an item for the DataTemplate to be bound to.

I can't remember where I saw it, but a good rule to follow when writing WPF is to use the control that gives you the behavior you need and then style it to look like what you want.

So thinking about this, you want the behaviour of a ListBox, but the look of an ItemsControl, so why don't you style the ListBoxItems to not show the difference between selected and non-selected.

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="HorizontalContentAlignment" Value="{Binding Path=HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
    <Setter Property="VerticalContentAlignment" Value="{Binding Path=VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
    <Setter Property="Padding" Value="2,0,0,0"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Border SnapsToDevicePixels="true" x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}">
                    <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>



回答2:


It's probably related to the fact that items in the ContextMenu popup are not int the same visual tree as the rest of your UserControl (basically, popup is a separate window). That's why the CommandBindings don't work.

But for now I don't have an idea how to fix this without specifing CommandBindings within the ContextMenu.



来源:https://stackoverflow.com/questions/833607/wpf-why-do-contextmenu-items-work-for-listbox-but-not-itemscontrol

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!