【问题标题】:WPF ContextMenu loses datacontext if it is displayed using a left-click event如果使用左键单击事件显示 WPF ContextMenu 会丢失数据上下文
【发布时间】:2020-01-07 03:30:01
【问题描述】:

我有一个按钮,当用户左键单击它时,我想显示一个弹出菜单。我已将弹出窗口实现为按钮的ContextMenu。需要通过绑定到各种ViewModel 属性来控制上下文菜单中菜单项的可用性。如您所知,ContextMenus 存在于与父窗口不同的可视树上,因此我不得不做一些研究以弄清楚如何将ContextMenuDataContext 设置为与父控件(即按钮)。我是这样做的:

<Button Name="AddRangeBtn" Height="50" Width="50" Margin="5" 
    **Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}">**
    <Button.Content>
        <Image Source="{StaticResource DmxPlusIconImage}" 
               HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
               MaxHeight="50" MaxWidth="50" Margin="-10" />
    </Button.Content>
    <Button.ToolTip>
        <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                         Content="{x:Static Localization:StringResources.AddFrequencyRangeTooltip}"/>
    </Button.ToolTip>
    <Button.ContextMenu>
        **<ContextMenu Name="AddRngContextMenu" 
                     DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">**
            <MenuItem Style="{StaticResource localMenuItem}"
                      IsEnabled="{Binding CanAddLinearRange}">
                <MenuItem.ToolTip>
                    <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                             Content="{x:Static Localization:StringResources.ToolTip_AddLinearFrequencyRange}"/>

设置 datacontext 的技巧是将 contextmenu 的 datacontext 绑定到 contextmenu 中的 PlacementTarget.Tag 并在按钮中添加相应的 Tag 属性。有了这些,我就可以直接将MenuItems 中的属性(如IsEnabled="{Binding CanAddLinearRange}")绑定到ViewModel 的依赖属性。当我通过右键单击按钮访问上下文菜单时,这一切都很好,但我希望菜单通过正常的左键单击显示。所以,我在按钮上添加了以下EventTrigger

<Button.Triggers>
    <EventTrigger SourceName="AddRangeBtn" RoutedEvent="Button.Click">
        <BeginStoryboard>
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="AddRngContextMenu" Storyboard.TargetProperty="(ContextMenu.IsOpen)">
                    <DiscreteObjectKeyFrame KeyTime="0:0:0">
                        <DiscreteObjectKeyFrame.Value>
                            <system:Boolean>True</system:Boolean>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Button.Triggers>

这样,右键单击按钮会显示上下文菜单,但是该上下文菜单的实例似乎没有获得与右键单击显示的实例相同的数据上下文!!!在左键版本中,菜单中的所有项目都已启用,因此很明显,上下文菜单没有从视图模型中看到依赖项属性。我什至不知道这怎么可能有两种不同版本的上下文菜单,一种具有正确的数据上下文,另一种具有默认行为。

右键单击启动时的上下文菜单:

右键单击启动时的上下文菜单:

我正在使用 XAML 来创建左键单击行为,并且在一些示例中,我看到人们使用代码隐藏来实现左键单击上下文菜单,他们将上下文菜单的数据上下文设置为button.click 事件处理程序中的 button.datacontext。我猜这也是我需要做的,但我不知道如何在 xaml 中做到这一点。

任何想法都将受到高度赞赏。

【问题讨论】:

  • @vatbub - 我喜欢你对问题的编辑,但我认为你为第二张图片选择了错误的链接,直到我接受你的更改才意识到这一点。我不被允许嵌入图片,所以它们被添加为链接,但第二张图片应该显示所有项目都已启用。
  • 对不起,我现在修好了。不幸的是,我还必须在答案末尾添加一些 .... 以使 StackOverflow 接受编辑。不过,您应该能够将这些点编辑掉。再次抱歉????

标签: c# wpf xaml


【解决方案1】:

感谢@KeithStein 让我走上了正确的道路。我在项目中添加了以下附加行为类:

public class OpenContextMenuOnLeftClickBehavior : DependencyObject
{

    public static bool GetIsEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    /// <summary>
    /// The DependencyProperty that backs the IsEnabled property.
    /// </summary>
    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
        "IsEnabled", typeof(bool), typeof(OpenContextMenuOnLeftClickBehavior), new FrameworkPropertyMetadata(false, IsEnabledProperty_Changed));

    /// <summary>
    /// The property changed callback for the IsEnabled dependency property.
    /// </summary>
    /// <param name="dpobj">
    /// The dependency object whose property changed.
    /// </param>
    /// <param name="args">
    /// The event arguments.
    /// </param>
    private static void IsEnabledProperty_Changed(DependencyObject dpobj, DependencyPropertyChangedEventArgs args)
    {
        FrameworkElement f = dpobj as FrameworkElement; 
        if (f != null)
        {
            bool newValue = (bool)args.NewValue;

            if (newValue)
                f.PreviewMouseLeftButtonUp += Target_PreviewMouseLeftButtonUpEvent;
            else
                f.PreviewMouseLeftButtonUp -= Target_PreviewMouseLeftButtonUpEvent;
        }
    }

    protected static void Target_PreviewMouseLeftButtonUpEvent(object sender, RoutedEventArgs e)
    {
        FrameworkElement f = sender as FrameworkElement;

        if (f?.ContextMenu != null)
        {
            f.ContextMenu.PlacementTarget = f;
            f.ContextMenu.IsOpen = true;
        }

        e.Handled = true;
    }
}

然后我在 xaml 中为具有 ContextMenuButton 引用了新属性:

<Button Name="AddRangeBtn" Height="50" Width="50" Margin="5" 
                    Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}"
                    controls:OpenContextMenuOnLeftClickBehavior.IsEnabled="True">
                <Button.Content>
                    <Image Source="{StaticResource DmxPlusIconImage}" 
                           HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                           MaxHeight="50" MaxWidth="50" Margin="-10" />
                </Button.Content>
                <Button.ToolTip>
                    <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                             Content="{x:Static Localization:StringResources.AddFrequencyRangeTooltip}"/>
                </Button.ToolTip>
                <Button.ContextMenu>
                    <ContextMenu Name="AddRngContextMenu" 
                                 DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <MenuItem Style="{StaticResource localMenuItem}"
                                  IsEnabled="{Binding CanAddLinearRange}">
                            <MenuItem.ToolTip>
                                <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                                     Content="{x:Static Localization:StringResources.ToolTip_AddLinearFrequencyRange}"/>
                            </MenuItem.ToolTip>
                            <MenuItem.Header>
                                <Button>
                                    <Image Source="{StaticResource DMXAddLinFreqRngTypeIconImage}" MinHeight="37" MinWidth="37"/>
                                </Button>
                            </MenuItem.Header>
                        </MenuItem>

这使得左键单击“ContextMenu”的行为与右键单击版本一样。

【讨论】:

  • 这还不错,但实际上如果你这样做,你也可以直接分配 DataContext 而不是绑定它。
【解决方案2】:

我还没有找到只使用 XAML 的方法,但我使用了 attached behavior 来帮助保持干净。

问题在于,很明显,PlacementTarget 在您右键单击之前不会被设置。我假设 WPF 框架将其设置为上下文菜单的默认处理的一部分。但是当你手动设置IsOpen 时,PlacementTarget 仍然是null。当然,需要先设置PlacementTarget,然后才能进行绑定。

我使用的解决方案涉及一个带有附加依赖属性IsEnabledLeftClickContextMenuBehavior 类。当IsEnabled 设置为true 时,我将一个处理程序添加到目标的PreviewMouseLeftButtonUpEvent,如下所示:

    protected static void Target_PreviewMouseLeftButtonUpEvent(object sender, RoutedEventArgs e) {
        FrameworkElement f = sender;

        if (f.ContextMenu != null) {
            f.ContextMenu.PlacementTarget = f;
            f.ContextMenu.IsOpen = true;
        }

        e.Handled = true;
    }

【讨论】:

  • 您对 PlacementTarget 何时设置的见解非常有用,它解释了我所看到的行为 - 谢谢。虽然我希望有人可以提出一个 xaml 解决方案,但我将开始实施“附加行为”的想法。它比添加 button.click 事件处理程序要复杂一些,但它更加优雅和可重用,并且我们的应用程序中有其他上下文菜单可以使用它。我赞成你的回答,但由于我是一个没有名声的低级新用户,我的投票没有显示:-(
  • 在实施您建议的修复后,ContextMenu 拾取DataContext 的问题已解决,但不知何故,ContextMenu 内的MenuItems 不响应左-再点击。即使我在每个 MenuItem 后面的代码中放置一个 Click 事件处理程序,也不会。但是,它们会响应右键单击。知道这是否是附加行为的副作用吗?使用 SnoopWPF,我可以看到菜单项具有正确的数据上下文,但我无法弄清楚点击事件切换的原因!
  • 我不确定是什么导致了这个问题。我刚刚使用嵌套菜单项对我的进行了测试,一切都按预期工作。必须有其他东西干扰您的项目或与您的行为实现不同的东西(我敢打赌前者)。
  • 只是为了让任何阅读此线程的人关闭它,左键单击未注册的问题与我放入 ContextMenu's MenuItems 的内容有关。每个菜单项都是一个按钮,但我将命令或事件处理程序与菜单项相关联,而不是菜单项内的按钮。所以那个按钮会吞下点击事件,并使它看起来菜单项没有响应左键单击。将命令绑定移动到菜单项内的按钮,现在一切正常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-20
  • 1970-01-01
相关资源
最近更新 更多