0

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)
{

}
Meme Machine
  • 949
  • 3
  • 14
  • 28
  • So `DataContext` of the ListView is bound to `TabItemViewModel` and `Drives` is one of its properties? Then you want to set that ListView into CommandParameter? – emoacht May 17 '22 at 08:28
  • https://stackoverflow.com/help/minimal-reproducible-example – Orace May 17 '22 at 08:39
  • The trigger on the `SelectionChanged` event is a mess, just use a binding: `SelectedItem="{Binding SelectedTab, Mode=OneWayToSource}"` ; then in the VM, add a `SelectedTab` property of the type of the objects in the `Tabs` collection. Then move the code of `TabChangedCommand` to the setter of the `SelectedTab` property. – Orace May 17 '22 at 09:34
  • The `ListView.Style` is also a mess, just use a [BooleanToVisibilityConverter](https://stackoverflow.com/questions/3128023): `Visibility="{Binding DrivesListViewEnabled, Converter={StaticResource BooleanToVisibilityConverter}}"` – Orace May 17 '22 at 09:43
  • I'll look into those two changes you suggested. The last one however requires the addition of a custom class if I'm not mistaken, and that is rather annoying and what I was trying to avoid. – Meme Machine May 17 '22 at 10:43

2 Answers2

1

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>
emoacht
  • 2,764
  • 1
  • 13
  • 24
  • 1
    Why do you bother to use a trigger when a `Command` property is available ? – Orace May 17 '22 at 09:07
  • You are right. In the case of "Click" event, it is not necessary to use `InvokeCommandAction`. – emoacht May 17 '22 at 09:12
  • Can you explain how your CommandParameter works? I used a mixture of both of your answers since @Orace forgot to add the CommandParameter to his, and the one from your answer works great. I wish I could select both of yours as the right answer. Thanks! – Meme Machine May 17 '22 at 10:57
  • The binding for CommandParameter refers to `ContextMenu.PlacementTarget` property and this property provides the element to which the ContextMenu is attached. In this case, it is the ListView. That's it. – emoacht May 17 '22 at 11:08
  • @MemeMachine why do you need a command parameter? To pass a `ListView` ? so you pass a view object to the view model ?! What will you do with that? – Orace May 17 '22 at 11:52
  • @emoacht I modified your code today to try and make it so a particular MenuItem will only be available when a ListView item is selected. I am getting "Propery 'Path' is set multiple times". Any idea why? Thank you so much for your help https://pastebin.com/RVGk8UwF – Meme Machine May 19 '22 at 16:46
  • @MemeMachine You set path two times by `PlacementTarget` and `Path=SelectedItem`. You can change it to `Path=PlacementTarget.SelectedItem`. I don't know whether it works in your particular case though. – emoacht May 20 '22 at 00:12
  • @emoacht Hm, doesn't seem to work - https://pastebin.com/GE4mE852 - does not appear to check if no item is selected (i.e if i select an empty space in the ListView, 'Rename' menu option still appears. If you don't see anything wrong, I will open another question – Meme Machine May 20 '22 at 01:13
1

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.

Orace
  • 7,822
  • 30
  • 45
  • If you really need to run the command on the `MouseDown` event and not just when the menu item is clicked / selected (via keyboard), then the answer is [here](https://stackoverflow.com/questions/17835205). But this is not a standard behavior and I don't see why one may want to do it this way. – Orace May 17 '22 at 09:43
  • I fixed it by changing it to Click. MouseDown appears to not function at all for ContextMenu. Not sure why it's even available as an event. – Meme Machine May 17 '22 at 10:52
  • @MemeMachine `I fixed it by changing it to Click` to handle a Click event, you don't need a `Interaction.Triggers\EventTrigger\InvokeCommandAction` subtree, just use the `Command` and `CommandParameter` attribute of the menu item. And again, I don't believe you need to pass a parameter, so `CommandParameter` is useless in your case. – Orace May 17 '22 at 12:27