关于c#:WPF ContextMenu如果使用左键事件显示会丢失datacontext

WPF ContextMenu loses datacontext if it is displayed using a left-click event

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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属性。有了这些,我就可以直接将 MenuItem 中的属性(如 IsEnabled="{Binding CanAddLinearRange}")绑定到 ViewModel\\ 的依赖属性。当我通过右键单击按钮访问上下文菜单时,这一切都很好,但我希望菜单通过正常的左键单击显示。因此,我将以下 EventTrigger 添加到按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>

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

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

ContextMenu

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

ContextMenu

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

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 中为具有 ContextMenu:

Button 引用了新属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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>

这使得左键单击"上下文菜单"的行为与右键单击版本一样。


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

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

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

1
2
3
4
5
6
7
8
9
10
    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;
    }