Home > Net >  WPF firing a view model Command from a list view item
WPF firing a view model Command from a list view item

Time:04-19

There is a WPF MVVM app. On the main view I have a list of elements, which are defined with ListView.ItemTemplate, in that I want to have a context menu with Delete action.

The Command for that is separated from the view and is kept in ViewModel DreamListingViewModel.

The problem is that on clicking on Delete I can't get it to execute the command on ViewModelk as context there is that of the item, not the items container.

I can make it work somehow by moving the context menu definition outside of the list view elements, but then when I open the context menu, it flickers, as if it's being called "20" times (which what I think does happen, as many times as I have elements in collection), anyways, I need a clean solution for that and I am very bad with XAML.

Here is how my View looks:

  <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid Margin="0 5 0 5" Background="Transparent" Width="auto">

                        <Grid.ContextMenu>
                            <ContextMenu>
                                <MenuItem Header="Delete"
                                          Command="{Binding DeleteSelectedDream}" 
                                          CommandParameter="{Binding DeleteSelectedDream, 
                                                                     RelativeSource={RelativeSource 
                                                                     Mode=FindAncestor,
                                                                     AncestorType={x:Type viewmodels:DreamListingViewModel}}}"
                                    />
                            </ContextMenu>
                        </Grid.ContextMenu>
...

It's the main window and initialized in a generic host in App.cs:

 public partial class App : Application
    {
        private readonly IHost _host;

        public App()
        {
            ...

            _host = Host.CreateDefaultBuilder().ConfigureServices(services =>
            {
                ...
                services.AddTransient<DreamListingViewModel>();
                services.AddSingleton((s) => new DreamListingView()
                {
                    DataContext = s.GetRequiredService<DreamListingViewModel>()
                });
                ...
            }).Build();

The Command and CommandParameter values are what I've been experimenting with, but it doesn't work

Here is how my ViewModel looks:

 internal class DreamListingViewModel : ViewModelBase
    {
        public ICommand DeleteSelectedDream{ get; }
 ...

Finally, when the command is fired, I need to pass the current element on which the menu has been shown.

So, here is what I want:

  1. User clicks on a list item with mouse right button - OK
  2. Sees a menu with Delete entry - OK
  3. On Delete click, Command DeleteSelectedDream is fired with current dream (item in the list) as a parameter - ERR

CodePudding user response:

Your example is somewhat lacking necessary information, but I'll try to help.

First you need to verify that you are actually bound to your view model. Are you using Prism or just standard WPF ? In the constructor of your code-behind of your view, set up the DataContext to an instance of your VM.

InitializeComponent(); 
this.DataContext = new DreamListingViewModel(); 

Now, you bind to a relative source via Mode 'FindAncestor' and the AncestorType is set to the type of a view model. That usually won't work, as the view model is not naturally a part of the visual tree of your WPF view. Maybe your ItemTemplate somehow wires it up. In a large WPF app of mine I use Telerik UI for WPF and a similar approach to you, however, I set up the DataContext of the Context menu to a RelativeSource set to Self combined with Path set to PlacementTarget.DataContext.

You do not have to use all the XAML in my example, just observe how I do it. Exchange 'RadContextMenu' with 'ContextMenu', Ignore the Norwegian words - here and only use what you need :

<telerik:RadContextMenu x:Key="CanceledOperationsViewContextMenu" DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext, UpdateSourceTrigger=PropertyChanged}">
                <MenuItem Header="{Binding PatientName}" IsEnabled="False" Style="{StaticResource ContextMenuHeading}" />
                <MenuItem Header="Gå til aktuell SomeAcme-liste" IsEnabled="{Binding IsValid}" Command="{Binding NavigateToListCommand}" />
                <MenuItem Header="Åpne protokoll..." Command="{Binding CommonFirstProtocolCommand, Mode=OneWay}" CommandParameter="{Binding}" />
                <MenuItem Header="Åpne Opr.spl.rapport...." Command="{Binding CommonFirstNurseReportCommand, Mode=OneWay}" CommandParameter="{Binding}" />
            </telerik:RadContextMenu>

In your example it will be :

<ContextMenu x:Key="SomeContextMenu" DataContext="{Binding RelativeSource={RelativeSource Mode=Self}, Path=PlacementTarget.DataContext, UpdateSourceTrigger=PropertyChanged}">
                <MenuItem Header="Delete" />
                Command="{Binding DeleteSelectedDream}" 
                                      CommandParameter="{Binding DeleteSelectedDream, 
                                                                 RelativeSource={RelativeSource 
                                                                 Mode=FindAncestor,
                                                                 AncestorType={x:Type ListViewItem}}}"
                                />
            </telerik:RadContextMenu>

Now I here consider you are using the class ListViewItem https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.listviewitem?view=netframework-4.8 It might be that you need to specify DataContext.DeleteSelectedDream here to be sure you bind up to the DataContext where your implementation of ICommand is.

CodePudding user response:

Accidentally found this answer, that's basically what I needed, just added to it a CommandParameter to send the item and it works like magic!

<ListView Name="lvDreams" ItemsSource="{Binding Dreams}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <Grid Margin="0 5 0 5" Background="Transparent" Width="auto">

                <Grid.ContextMenu>
                    <ContextMenu>
                        <MenuItem 
                            Header="Delete"
                            Command="{Binding DataContext.DeleteSelectedDream, Source={x:Reference lvDreams}}"
                            CommandParameter="{Binding}"
                        />
                    </ContextMenu>
                </Grid.ContextMenu>

                ...

        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>
  • Related