简体   繁体   中英

WPF Databinding ContextMenu of Button inside a DataTemplate inside an ItemsControl

I am trying to figure out how I can bind the ContextMenu of the Button that is being added in the ItemsControl I have. Basically, I'm wanting to be able to right click on a button and remove it from the observable collection that sits on my viewmodel. I understand that ContextMenu's are not part of the VisualTree, so using RelativeSource to walk up the tree to find my DataContext hasn't been useful to me.

The end goal of what I want to do is Bind the Command on the MenuItem to the RemoveCommand on my ViewModel and then pass in the Content property of the Button that you right click on so that I can remove it from the observable collection.

Any help on this would be greatly appreciated.

Model:

public class Preset
{
    public string Name { get; set; }
}

ViewModel:

public class SettingsWindowViewModel
{
    public ObservableCollection<Preset> MyPresets { get; } = new ObservableCollection<Preset>();

    private ICommand _plusCommand;
    public ICommand PlusCommand => _plusCommand ?? (_plusCommand = new DelegateCommand(AddPreset));

    private ICommand _removeCommand;
    public ICommand RemoveCommand => _removeCommand ?? (_removeCommand = new DelegateCommand<string>(RemovePreset));

    private void AddPreset()
    {
        var count = MyPresets.Count;
        MyPresets.Add(new Preset {Name = $"Preset{count+1}"});
    }

    private void RemovePreset(string name)
    {
        var preset = MyPresets.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.CurrentCultureIgnoreCase));
        if (preset!= null)
        {
            MyPresets.Remove(preset);
        }
    }
}

XAML:

<Window x:Class="WpfTesting.Esper.Views.SettingsWindow"
        x:Name="MainSettingsWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:WpfTesting.Esper.ViewModels"
        mc:Ignorable="d"
        Title="SettingsWindow" Height="470" Width="612">
    <Window.DataContext>
        <viewModels:SettingsWindowViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <Style BasedOn="{StaticResource {x:Type MenuItem}}" TargetType="{x:Type MenuItem}" x:Key="PopupMenuItem">
            <Setter Property="OverridesDefaultStyle" Value="True"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type MenuItem}">
                        <Border>
                            <ContentPresenter ContentSource="Header"/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="1*"/>
            <RowDefinition Height="35"/>
        </Grid.RowDefinitions>
        <StackPanel Grid.Row="2" Orientation="Horizontal">
            <Button Width="70" Content="Load"/>
            <Button Width="70"  Content="Save As"/>
            <ItemsControl ItemsSource="{Binding MyPresets}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Button Width="70" Content="{Binding Name}">
                            <Button.ContextMenu>
                                <ContextMenu>
                                    <MenuItem Style="{StaticResource PopupMenuItem}" Header="Remove">
                                        <!--
                                        I need to set up binding a Command to a method on the DataContext of the Window, and I need to pass in the Content of the Button that is the parent of the ContextMenu
                                        -->
                                    </MenuItem>
                                </ContextMenu>
                            </Button.ContextMenu>
                        </Button>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
            <Button Width="20" Background="Transparent" BorderBrush="Transparent" Content="+" FontSize="21.333" HorizontalAlignment="Center" VerticalAlignment="Center" Command="{Binding PlusCommand}"/>
        </StackPanel>
    </Grid>
</Window>

I had basically a similar problem, and the solution I found was to use the Messenger class some MVVM frameworks like Devexpress or Mvvm Light have.

Basically you can register in a viewModel to listen for incoming messages. The class itself, at least in the Devexpress implementation works with weak references, so you may not even unregister message handlers and it will not cause memory leaks.

I had used this method for removing on right click tabs from a ObservableCollection, so it was similar to your scenario.

You can have a look here :

https://community.devexpress.com/blogs/wpf/archive/2013/12/13/devexpress-mvvm-framework-interaction-of-viewmodels-messenger.aspx

and here :

https://msdn.microsoft.com/en-us/magazine/jj694937.aspx

Using WPF: Binding a ContextMenu to an MVVM Command as an introduction to what Tags can do, I figured out how to do what I was looking for by using multiple Tags to save the Context of what I was looking for.

I first made sure to give my window ax:Name

<Window x:Name="MainSettingsWindow"

Next, on the Button inside my DataTemplate of my ItemsControl, I set a Tag and set it to my Window

<ItemsControl ItemsSource="{Binding MyPresets}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Width="70" Content="{Binding Name}" Tag="{Binding ElementName=MainSettingsWindow}">

Next, in the ContextMenu, I seth the DataContext of the ContextMenu to the Tag I set on the Button, I also needed to create a Tag on the ContextMenu and point it back to the Content Property of the Button so that I can pass that into the CommandParameter

<ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Mode=Self}}" Tag="{Binding PlacementTarget.Content, RelativeSource={RelativeSource Mode=Self}}">

At this point, I can now bind my MenuItem correctly using the Command from my ViewModel and the Content Property from the Button

This is the final XAML for my ItemsControl:

<ItemsControl ItemsSource="{Binding MyPresets}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Width="70" Content="{Binding Name}" Tag="{Binding ElementName=MainSettingsWindow}">
                <Button.ContextMenu>
                    <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Mode=Self}}" Tag="{Binding PlacementTarget.Content, RelativeSource={RelativeSource Mode=Self}}">
                        <MenuItem Header="Remove" 
                                  Style="{StaticResource PopupMenuItem}"
                                  Command="{Binding Path=DataContext.RemoveCommand}"
                                  CommandParameter="{Binding Path=Tag, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"/>
                    </ContextMenu>
                </Button.ContextMenu>
            </Button>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

One thing to note is that I had to change the CommandParameter on my ViewModel to take an Object instead of a String. The reason I did this was because I was getting an exception on the CanExecute method in my DelegateCommand

This is the exception I was getting:

Unable to cast object of type 'MS.Internal.NamedObject' to type 'System.String'.

I'm not sure exactly what's causing that exception to throw, but changing it to Object works ok for me.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM