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:
- Define the command into the
LayersFiltered
class - 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>