WPF数据绑定ContextMenu在ItemsControl内的DataTemplate内的Button [英] WPF Databinding ContextMenu of Button inside a DataTemplate inside an ItemsControl

查看:65
本文介绍了WPF数据绑定ContextMenu在ItemsControl内的DataTemplate内的Button的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图弄清楚如何绑定到我拥有的ItemsControl中的Button的ContextMenu.基本上,我希望能够右键单击一个按钮并将其从位于我的视图模型上的可观察集合中删除.我知道ContextMenu并不是VisualTree的一部分,因此使用RelativeSource沿树查找我的DataContext对我没有用.

我要做的最终目标是将MenuItem上的命令绑定到ViewModel上的RemoveCommand,然后传入您右键单击的Button的Content属性,以便可以将其从可观察的集合中删除

在此方面的任何帮助将不胜感激.

型号:

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>

解决方案

使用

接下来,在ItemsControl的DataTemplate内的按钮上,设置一个标签并将其设置为我的窗口

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

接下来,在ContextMenu中,我将ContextMenu的DataContext设置为我在Button上设置的标签,我还需要在ContextMenu上创建一个标签,并将其指向Button的Content属性,这样我就可以将其传递给CommandParameter

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

现在,我现在可以使用ViewModel中的Command和Button中的Content属性正确绑定MenuItem了.

这是我的ItemsControl的最终XAML:

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

要注意的一件事是,我必须更改ViewModel上的CommandParameter以采用对象而不是字符串.我这样做的原因是因为我在DelegateCommand中的CanExecute方法上遇到了异常

这是我得到的例外:

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

我不确定到底是什么引发了该异常,但是将其更改为Object可以正常工作.

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>

解决方案

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 a x: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.

这篇关于WPF数据绑定ContextMenu在ItemsControl内的DataTemplate内的Button的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆