Home > Back-end >  Command binding in ContextMenu returns error
Command binding in ContextMenu returns error

Time:03-18

I have a ListBox with items of type ThemeLayer and I want to add context menu functionality to those items.

The ListBox XAML definition is as follows:

<ListBox x:Name="DataLayerList" Grid.Row="2" Grid.ColumnSpan="2" MaxHeight="800"
         Width="{Binding ActualWidth, ElementName=PanelGrid}" Margin="0,10,0,0" 
         ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Auto" 
         ScrollViewer.HorizontalScrollBarVisibility="Disabled"
         HorizontalAlignment="Left" SelectionMode="Extended" IsTextSearchEnabled="True"
         ItemsSource="{Binding LayersFiltered, Mode=TwoWay}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Image Source="{Binding Image}" Height="16" Width="16" Margin="5,0"/>
                <TextBlock Text="{Binding Path=Name}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Header="Add to current / new Map" 
                                  Command="{Binding AddServiceFromContext}">
                            <MenuItem.Icon>
                                <Image Source="{DynamicResource AddContent16}" Width="16"
                                       RenderOptions.BitmapScalingMode="HighQuality"/>
                            </MenuItem.Icon>
                        </MenuItem>
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </Style>
    </ListBox.ItemContainerStyle>
</ListBox>

The definition of AddServiceFromContext is in my ViewModel:

public ICommand AddServiceFromContext
{
    get
    {
        return new RelayCommand(() =>
        {
            IEnumerable<ThemeLayer> selectedThemeLayers = LayersFiltered.Where(i => i.IsSelected);
        }, true);
    }
}

As soon as I call and visualize the context menu the following error shows up in the XAML Binding Failures:

ThemeLayer AddServiceFromContext MenuItem.Command ICommand AddServiceFromContext property not found on object of type ThemeLayer.

Maybe the solution for this issue is trivial and means I have to add a property to my ThemeLayer definition, but how would that look, and is this the right solution?

CodePudding user response:

The problem seems to be due to the fact that the AddServiceFromContext command is defined in the viewModel and not in the class used to define the LayersFiltered list

There can be 2 solutions:

  1. Define the command into the LayersFiltered class
  2. use relative context, something like

{Binding RelativeSource = {RelativeSource Mode = FindAncestor, AncestorType = {x: Type UserControl}}, Path = DataContext.AddServiceFromContext}

CodePudding user response:

The AddServiceFromContext command is defined in the view model that also exposes the LayersFiltered collection. However, a ListBoxItem inside a ListBox gets its DataContext set to the corresponding data item in the items source, which is LayersFiltered. That is what the error is telling you, the data context is an item of type ThemeLayer and it does not expose a property AddServiceFromContext.

What you have to do is access the data context of the ListBox (or any other parent that shares this data context), which is DockpanePublicDataViewModel and contains the AddServiceFromContext command. The problem is that a simple RelativeSource binding to find parent elements will not work, as the context menu is not part of the same visual tree as the ListBox.

There is a workaround that you can use in cases like this, where the data context is inaccessible. Create a binding proxy type that indirectly provides a data context as a resource. For more information how this works, you can refer to this blog post by Thomas Levesque.

public class BindingProxy : Freezable
{
   protected override Freezable CreateInstanceCore()
   {
      return new BindingProxy();
   }

   public object Data
   {
      get => GetValue(DataProperty);
      set => SetValue(DataProperty, value);
   }

   public static readonly DependencyProperty DataProperty = DependencyProperty.Register(
      nameof(Data), typeof(object), typeof(BindingProxy));
}

Create an instance of the binding proxy in the ListBox resources or anywhere else, where you can bind the target data context via the Data property.

<ListBox.Resources>
   <local:BindingProxy x:Key="DataLayerListBindingProxy" Data="{Binding}"/>
</ListBox.Resources>

Then you can bind the command using the binding proxy as Source.

<Setter Property="ContextMenu">
    <Setter.Value>
        <ContextMenu>
            <MenuItem Header="Add to current / new Map" 
                      Command="{Binding Data.AddServiceFromContext, Source={StaticResource DataLayerListBindingProxy}}">
                <MenuItem.Icon>
                    <Image Source="{DynamicResource AddContent16}" Width="16"
                           RenderOptions.BitmapScalingMode="HighQuality"/>
                </MenuItem.Icon>
            </MenuItem>
        </ContextMenu>
    </Setter.Value>
</Setter>
  • Related