WPF ContextMenu bound to 3 Listboxes on right-clic

2019-09-06 19:30发布

问题:

I have three tabs and each has a listbox with different types of files.

When I right-click on an item in the listbox, I want a ContextMenu with "New, Edit and Delete" as Item headers.

I guess I could have a ContextMenu for each listbox, and then have a seperate method for each header, such as:

               <ListBox.ContextMenu>
                    <ContextMenu x:Name="NewEditDeleteAdvCalcFileContextMenu">
                        <MenuItem Name="NewAdv" Header="New" Click="NewAdv_Click" />
                        <MenuItem Name="EditAdv" Header="Edit" Click="EditAdv_Click"/>
                        <MenuItem Name="DeleteAdv" Header="Delete" Click="DeleteAdv_Click"/>
                    </ContextMenu>
                </ListBox.ContextMenu>

But really, I hope there is a better way.

I saw this post which shows the ContextMenu as Static Resource

and this seems to be something I would like to do. In the same thread it is suggested to use commands: ContextMenu with Commands

and with that I'm hoping I can get the type of the ListBoxItem that was clicked, because I need that. A new file type B must be handled differently than a new file type C, but I don't want a gazillion contextmenus and New/Edit/Delete methods.

So, currently I have this higher up in my xaml file:

<UserControl.Resources>
    <ContextMenu x:Key="NewEditDeleteContextMenu">
        <MenuItem Header="New" 
                  Command="{Binding Path=NewFileCommand}"  
                  CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"/>
        <MenuItem Header="Edit" 
                  Command="{Binding Path=EditFileCommand}"  
                  CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"/>
        <MenuItem Header="Delete" 
                  Command="{Binding Path=DeleteFileCommand}"  
                  CommandTarget="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}"/>
    </ContextMenu>
</UserControl.Resources>

And then a listbox in the tabItem:

<ListBox Name="CalcFilesListBox" 
                     Margin="20" ItemsSource="{Binding CalcFilesList}" 
                     PreviewMouseRightButtonUp="ListBox_PreviewMouseRightButtonUp" 
                     ContextMenu="{StaticResource NewEditDeleteContextMenu}">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding Path=Name}" />
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
                <ListBox.ItemContainerStyle>
                    <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
                        <EventSetter Event="MouseDoubleClick" Handler="CalcFileListBox_MouseDoubleClick"/>
                    </Style>
                </ListBox.ItemContainerStyle>
            </ListBox>

Question #1

How do I get the rightclick of a ListBoxItem to show the ContextMenu, which is now a static resource? Because in my xaml.cs I had this:

private void ListBox_PreviewMouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        // SelectItemOnRightClick(e);
        NewEditDeleteContextMenu.PlacementTarget = sender as UIElement;
        NewEditDeleteContextMenu.IsOpen = true;

    }

But now I have an error saying:

The name 'NewEditDeleteContextMenu' does not exist in the current context.

because originally I had the contextmenu as part of the ListBox such as:

<ListBox.ContextMenu>
...

But as far as I could see that would mean a separate ContextMenu for each ListBox.

Question #2

Is the correct way to use a command, let's say NewFileCommand for the New item header in the ContextMenu (shown in the UserControl.Resources block of code) to do the following:

In my ViewModel:

 public RelayCommand<string> NewFileCommand { get; private set; }

and then in the ViewModel's constructor:

 public CalcViewModel()
    {
        NewFileCommand = new RelayCommand<object>(NewFile);
    }

 public void NewFile(object sender)
    {
         //Determine the type of file, based on the ListBoxItem's DataContext. 
That is, supposing the ListBoxItem is the object being passed as the sender.
    } 

Basically, I want one ContextMenu bound to the different ListBox components, and this should pop up on a rightclick, and when for instance the New item is chosen on the ContextMenu, I want to determine the type of the file that has been bound to the ListBox. E.g.: ListBox 1 is bound to a collection of file type B. ListBox 2 is bound to a collection of file type C. When I rightclick on an item in ListBox 2, and choose New, I need to make a new file of type C.

Question #3

This isn't a very intricate View. I haven't used a MVVM framework because so far I haven't thought that the time it would take me to learn one would be worth it, but considering this scenario, and a simpler case for a double-click on the ListBoxItems that can be seen in one of the blocks of code, would you recommend the use of a framework?

回答1:

You're going in the right direction, you code just needs a bit of updating. First, don't need any right-click handlers -- if a control has a ContextMenu set, right-clicking will invoke that ContextMenu. Having a ContextMenu as a StaticResource and attaching it to multiple controls creates a bit of a problem because of a bug in .NET where a ContextMenu doesn't update its DataContext after initially setting it. That means if you first invoke the menu on listbox #2, you'll get the selected item in that listbox... but if you then invoke it on listbox #3, you'll still get the selected item in listbox #2. But there's a way around this.

First, let's look at the context menu and how it's bound to a list box:

<ContextMenu x:Key="contextMenu" DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
    <MenuItem Header="New" Command="{Binding DataContext.NewFileCommand}" CommandParameter="{Binding}"/>
    <MenuItem Header="Delete" Command="{Binding DataContext.DeleteFileCommand}" CommandParameter="{Binding SelectedItem}"/>
</ContextMenu>

...

<ListBox Margin="10" ItemsSource="{Binding Files1}" ContextMenu="{StaticResource contextMenu}"/>

PlacementTarget is the control the ContextMenu is attached to. Explicitly binding the menu's data context to PlacementTarget ensures it's pointing to the correct ListBox every time it's invoked. Commands like "Edit" and "Delete" that deal with list items are then easy: Just bind the CommandParameter (not the CommandTarget as you did) to the ListBox's SelectedItem. The item you want to edit or delete will then be given as a parameter to the command.

Since you used RelayCommand I'm assuming you used GalaSoft's MVVM framework. In that case here's how your "Delete" command might look:

public RelayCommand<object> DeleteFileCommand { get; } = new RelayCommand<object>( DeleteFile_Executed, DeleteFile_CanExecute );

private static bool DeleteFile_CanExecute( object file )
{
    return file != null;
}

private static void DeleteFile_Executed( object file )
{
    var filetype = file.GetType();
    System.Diagnostics.Debug.WriteLine( string.Format( "Deleting file {0} of type {1}", file, file.GetType() ) );

    // if( filetype == typeof( FileTypeA ) ) DeleteFileTypeA( file as FileTypeA );
    // else if( filetype == typeof( FileTypeB ) ) DeleteFileTypeB( file as FileTypeB );
    // etc...
}

The "New" command will be a bit tricker because you want to be able to create a new item whether an item is selected or not. So we'll bind the CommandParameter to the ListBox itself. Unfortunately there's not a good way to get the type of item the ListBox contains. It could contain multiple types of items, or no items at all. You could give it an x:Name then look at the name in your command handler, but what I choose to do is put the type of item this ListBox handles as the Tag parameter of the ListBox. Tag is a bit of extra data you can use for whatever purpose you like:

<ListBox Margin="10" ItemsSource="{Binding Files1}" ContextMenu="{StaticResource contextMenu}" Tag="{x:Type local:FileTypeA}"/>

Now we can define our "New" command handlers like this:

private static bool NewFile_CanExecute( ListBox listbox ) { return true; }

private static void NewFile_Executed( ListBox listbox )
{
    var filetype = listbox.Tag as Type;

    System.Diagnostics.Debug.WriteLine( string.Format( "Creating new file of type {0}", filetype ) );

    // if( filetype == typeof( FileTypeA ) ) CreateNewFileTypeA();
    // else if( filetype == typeof( FileTypeB ) ) CreateNewFileTypeB();
    // etc...
}

As for whether this scenario warrants an MVVM or not, you can certainly put your three file lists in a ViewModel, along with code that actually creates, edits, and deletes the files, and have your commands in the Window invoke the code in the ViewModel. I usually don't, though, until the scenario becomes more complicated.