Home > Back-end >  Bind Command To ContextMenu Item
Bind Command To ContextMenu Item

Time:05-18

I am getting this error/warning:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=DrivesListView'

When I press 'Refresh', the command does not fire. I am guessing since it is a ContextMenu, I need to somehow access the parent control in the binding path and then I can use MouseDownCommand.

MouseDownCommand is located in my tab viewmodel, TabItemViewModel. My MainWindowViewModel contains a list of TabItemViewModels, and that is the source of the TabControl's items.

What I've tried:

1)

I've tried setting the ContextMenu Opened event to set the DataContext manually like this to see if it would fix the DataContext somehow:

private void ContextMenu_Opened(object sender, RoutedEventArgs e)
{
    ContextMenu menu = sender as ContextMenu;
    ListView listView = menu.PlacementTarget as ListView;
    Grid grid = listView.Parent as Grid;
    TabControl tabControl = grid.Parent as TabControl;
    menu.DataContext = (TabItemViewModel)tabControl.SelectedItem;
}

The problem with this is the fact that I cannot seem to get the tabControl from the grid. Doing .Parent just returns null for some unknown reason.

2)

I also tried setting the Tag of the control, which did not work either:

<ListView Grid.Row="1" Name="DrivesListView" ItemsSource="{Binding Drives}"
                                  Tag="{Binding DataContext,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}">>
    <ListView.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Refresh">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="MouseDown">
                        <i:InvokeCommandAction 
                                                    Command="{Binding Path=PlacementTarget.Tag.MouseDownCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}" 
                                                       CommandParameter="{Binding ElementName=DrivesListView}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </MenuItem>
        </ContextMenu>
    </ListView.ContextMenu>
    <ListView.Style>
        <Style TargetType="{x:Type ListView}" BasedOn="{StaticResource MahApps.Styles.ListView}">
            <Style.Triggers>
                <DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="true">
                    <Setter Property="Visibility" Value="Visible" />
                </DataTrigger>
                <DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="false">
                    <Setter Property="Visibility" Value="Hidden" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ListView.Style>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseDoubleClick">
            <i:InvokeCommandAction Command="{Binding Path=MouseDoubleClickCommand}" 
                                                       CommandParameter="{Binding ElementName=DrivesListView}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <ListView.View>
        <GridView>
            <GridViewColumn x:Name="NameHeader" Header="Name" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn x:Name="TypeHeader" Header="Type" DisplayMemberBinding="{Binding Type}"/>
            <GridViewColumn x:Name="TotalSizeHeader" Header="Total Size" DisplayMemberBinding="{Binding TotalSize}"/>
            <GridViewColumn x:Name="FreeSpaceHeader" Header="Free Space" DisplayMemberBinding="{Binding FreeSpace}"/>
        </GridView>
    </ListView.View>
</ListView>

Here is my XAML

<UserControl x:Class="FileExplorerModuleServer.Views.FileBrowserTabView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!--ItemsSource is bound to the 'Tabs' property on the view-
                        model, while DisplayMemeberPath tells TabControl 
                        which property on each tab has the tab's name -->
        <TabControl Name="SubTabControl" Grid.Row="1" 
                    ItemsSource="{Binding Tabs}" 
                    DisplayMemberPath="Header" SelectedIndex="{Binding SelectedIndex}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="SelectionChanged">
                    <i:InvokeCommandAction Command="{Binding Path=TabChangedCommand}" 
                                                       CommandParameter="{Binding ElementName=SubTabControl}"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>

                        <TextBox Grid.Row="0" HorizontalAlignment="Left" Width="300" Text="{Binding Path}">

                        </TextBox>

                        <mah:ProgressRing Grid.Row="1" Height="1">
                            <mah:ProgressRing.Style>
                                <Style TargetType="{x:Type mah:ProgressRing}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding IsLoading}" Value="true">
                                            <Setter Property="Visibility" Value="Visible" />
                                        </DataTrigger>
                                        <DataTrigger Binding="{Binding IsLoading}" Value="false">
                                            <Setter Property="Visibility" Value="Hidden" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </mah:ProgressRing.Style>
                        </mah:ProgressRing>

                        <ListView Grid.Row="1" Name="DrivesListView" ItemsSource="{Binding Drives}">
                            <ListView.ContextMenu>
                                <ContextMenu Opened="ContextMenu_Opened">
                                    <MenuItem Header="Refresh">
                                        <i:Interaction.Triggers>
                                            <i:EventTrigger EventName="MouseDown">
                                                <i:InvokeCommandAction 
                                                    Command="{Binding Path=MouseDownCommand}" 
                                                       CommandParameter="{Binding ElementName=DrivesListView}"/>
                                            </i:EventTrigger>
                                        </i:Interaction.Triggers>
                                    </MenuItem>
                                </ContextMenu>
                            </ListView.ContextMenu>
                            <ListView.Style>
                                <Style TargetType="{x:Type ListView}" BasedOn="{StaticResource MahApps.Styles.ListView}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="true">
                                            <Setter Property="Visibility" Value="Visible" />
                                        </DataTrigger>
                                        <DataTrigger Binding="{Binding DrivesListViewEnabled}" Value="false">
                                            <Setter Property="Visibility" Value="Hidden" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </ListView.Style>
                            <i:Interaction.Triggers>
                                <i:EventTrigger EventName="MouseDoubleClick">
                                    <i:InvokeCommandAction Command="{Binding Path=MouseDoubleClickCommand}" 
                                                       CommandParameter="{Binding ElementName=DrivesListView}"/>
                                </i:EventTrigger>
                            </i:Interaction.Triggers>
                            <ListView.View>
                                <GridView>
                                    <GridViewColumn x:Name="NameHeader" Header="Name" DisplayMemberBinding="{Binding Name}"/>
                                    <GridViewColumn x:Name="TypeHeader" Header="Type" DisplayMemberBinding="{Binding Type}"/>
                                    <GridViewColumn x:Name="TotalSizeHeader" Header="Total Size" DisplayMemberBinding="{Binding TotalSize}"/>
                                    <GridViewColumn x:Name="FreeSpaceHeader" Header="Free Space" DisplayMemberBinding="{Binding FreeSpace}"/>
                                </GridView>
                            </ListView.View>
                        </ListView>

Here is the ViewModel.cs

public ICommand MouseDownCommand
    => new RelayCommand<object>(e => MouseDown(e));

private void MouseDown(object commandParameter)
{

}

CodePudding user response:

As stated in the documentation, [the] menu [...] is specific to the context of the control.
In other words, the ContextMenu has the same data context as it's parent control (here the ListView).
To follow the MVVM pattern, you then only need to add a ICommand in the data context of the ListView and bind it to the MenuItem.Command property.

The View:

<ListView ItemsSource="{Binding Drives}">
    <ListView.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Refresh" Command="{Binding RefreshCommand}" />
        </ContextMenu>
    </ListView.ContextMenu>
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
            <GridViewColumn Header="Type" DisplayMemberBinding="{Binding Type}"/>
            <GridViewColumn Header="Total Size" DisplayMemberBinding="{Binding TotalSize}"/>
            <GridViewColumn Header="Free Space" DisplayMemberBinding="{Binding FreeSpace, StringFormat='{}{0:0.00}'}"/>
        </GridView>
    </ListView.View>
</ListView>

The ViewModel:

public class ViewModel
{
    public ViewModel()
    {
        RefreshCommand = new ActionCommand(Refresh);
    }

    public IReadOnlyList<DriveViewModel> Drives { get; }

    public ICommand RefreshCommand { get; }

    private void Refresh()
    {
        // ...
    }
}

Working demo available here.

CodePudding user response:

First of all, MouseDown event doesn't seem to work well in the case of MenuItem. So, you need to use PreviewMouseDown or probaby Click event instread. Also, you cannot use ElementName for referring elements outside of ContextMenu.

Then, assuming DataContext of the ListView is implicitly bound to TabItemViewModel and you want to specify that ListView as CommandParameter, it could be modified as follows:

<MenuItem Header="Refresh">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <i:InvokeCommandAction
                Command="{Binding PlacementTarget.DataContext.MouseDownCommand, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"
                CommandParameter="{Binding PlacementTarget, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</MenuItem>
  • Related