views:

2285

answers:

2

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?

A: 

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.

arconaut
I'd read that at http://stackoverflow.com/questions/662164/wpf-context-menu-doesnt-bind-to-right-databound-item but it doesn't explain why ListBox works yet ItemsControl doesn't. What is it that ListBox does that makes its context menu item commands bindable? I've had a look in .NET Reflector, but haven't turned anything up.
Drew Noakes
+3  A: 

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>
Cameron MacFarland
Thanks Cameron, I'll give this a shot today. I'd still like to know what exactly the ListBox adds to enable the routed command to tunnel up from its items as I'm sure that ItemsControl's items are still part of the logical tree and that routed events should travel up ok.
Drew Noakes
Yeah I was up to 2am digging through Reflector trying to find out what made ListBox so special. This morning I realised this would be a better solution :P
Cameron MacFarland