在 MVVM 中,拖动完成后打开上下文菜单

In MVVM, open context menu upon drag completion

在交互性很强的软件中,用户可以对UserControl的集合进行拖放操作。放下时,他们应该看到 ContextMenu 提供一些关于如何执行操作的选择,例如,复制项目,或者如果在放置位置有另一个项目则交换位置。

使用 Prism 框架,理想的实现方式是通过 InteractionRequestTrigger,例如:

<i:Interaction.Triggers>
    <prism:InteractionRequestTrigger SourceObject="{Binding SomeCustomNotificationRequest, Mode=OneWay}" >
        <!-- some subclass of TriggerAction-->
            <ContextMenu>
                <MenuItem Header="Copy" />
                <MenuItem Header="Swap" />
            </ContextMenu>
        <!-- end some subclass of TriggerAction-->
    </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

这引发了一个疑问,即是否在包含可拖放 UserControlItemsControl 的 XAML 中实施 InteractionRequestTrigger,或者是否应该实施 InteractionRequestTrigger进入 UserControl 本身。在后者的情况下,该特定 UserControl 的各种实例如何“知道”哪个实例将对交互请求做出反应?

其次,InteractionRequestTrigger 的子元素必须是 System.Windows.Interactivity.TriggerAction。似乎除了打开弹出窗口 windows 之外,这并没有被广泛用于任何其他用途。关于 TriggerAction 的文档非常少,我不知道如何去实现它的 Invoke 方法。非常感谢任何指向文档的指针!

使用 InteractionRequestTrigger 绝对 的方法,但是由于 ContextMenu 控件不在同一个 visual/logical 树作为定义它的控件,必须要穿过一些黑暗的小巷。

在进入实际代码之前,我还要强调我没有采纳@Haukinger 建议使用弹出窗口 window 而不是 ContextMenu 的原因:同时提供优势通过 IInteractionRequestAware 直接使用我为自定义 Notification(加上回调机制)定义的属性,我不得不实施一些魔法来制作弹出窗口 window出现在鼠标光标位置。另外,在我的特殊情况下,我通过单击上下文菜单来操作数据模型,这意味着我必须使用弹出窗口 window 的依赖注入才能访问正确的实例我的数据模型,坦率地说,我也不知道该怎么做。

无论如何,我让它在 ContextMenu 上顺利运行。这就是我所做的。 (我不会 post 明显的样板代码;请记住我使用的是 Prism with the GongSolutions Drag and Drop Library

A) 丢弃处理程序

丢弃处理程序 class 必须增加一个我们可以在丢弃时调用的事件。此事件稍后将由属于承载拖放操作的视图的视图模型使用。

public class MyCustomDropHandler : IDropTarget {
  public event EventHandler<DragDropContextMenuEventArgs> DragDropContextMenuEvent;

  public void Drop(IDropInfo dropInfo) {
    // do more things if you like to

    DragDropContextMenuEvent?.Invoke(this, new DragDropContextMenuEventArgs() {
      // set all the properties you need to
    });
  }

  // don't forget about the other methods of IDropTarget
}

DragDropContextMenuEventArgs简单明了;如果您需要帮助,请参阅 Prism 手册。

B) 交互请求

在我的例子中,我有一个自定义的 UserControl 来托管我想要拖放的元素。它的视图模型需要一个 InteractionRequest 以及一个对象,该对象收集要传递的参数以及 ContextMenu 上的单击命令。这是因为 ContextMenu 没有实现 IInteractionRequestAware,这意味着我们必须使用调用命令操作的标准方式。我只是使用上面定义的 DragDropContextMenuEventArgs,因为它是一个已经包含所有必需属性的对象。

B.1) 查看模型

这利用了具有相应接口的自定义通知请求,其实现很简单。我将跳过此处的代码以使该条目更易于管理。 StackExchange 上有很多关于该主题的内容;例如,请参阅 link @Haukinger 作为对我的原始问题的评论提供的。

public InteractionRequest<IDragDropContextMenuNotification> DragDropContextMenuNotificationRequest { get; set; }

public DragDropContextMenuEventArgs DragDropActionElements { get; set; }

public MyContainerControlConstructor() {
  DragDropContextMenuNotificationRequest = new InteractionRequest<IDragDropContextMenuNotification>();
  MyCustomDropHandler.DragDropContextMenuEvent += OnDragDropContextMenuShown;
}

private void OnDragDropContextMenuShown(object sender, DragDropContextMenuEventArgs e) {
  DragDropActionElements = e;
  DragDropContextMenuNotificationRequest.Raise(new DragDropContextMenuNotification {
    // you can set your properties here, but it won’t matter much
    // since the ContextMenu can’t consume these
  });
}

B.2) XAML

作为 MyContainerControl 设计元素的兄弟,我们为通知请求定义 InteractionTrigger

<i:Interaction.Triggers>
  <prism:InteractionRequestTrigger SourceObject="{Binding DragDropContextMenuNotificationRequest, ElementName=MyContainerControlRoot, Mode=OneWay}">
    <local:ContextMenuAction ContextMenuDataContext="{Binding Data, Source={StaticResource Proxy}}">
      <local:ContextMenuAction.ContextMenuContent>
        <ContextMenu>
          <MenuItem Header="Move">
            <i:Interaction.Triggers>
              <i:EventTrigger EventName="Click">
                <prism:InvokeCommandAction Command="{Binding MoveCommand}"
                                           CommandParameter="{Binding DragDropActionElements}" />
              </i:EventTrigger>
            </i:Interaction.Triggers>
          </MenuItem>
          <MenuItem Header="Copy">
            <i:Interaction.Triggers>
              <i:EventTrigger EventName="Click">
                <prism:InvokeCommandAction Command="{Binding CopyCommand}"
                                           CommandParameter="{Binding DragDropActionElements}" />
              </i:EventTrigger>
            </i:Interaction.Triggers>
          </MenuItem>
        </ContextMenu>
      </local:ContextMenuAction.ContextMenuContent>
    </local:ContextMenuAction>
  </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>

C) 触发动作和其他魔法

这就是事情变得棘手的地方。首先,我们需要定义一个自定义 TriggerAction 来调用我们的 ContextMenu.

C.1) 自定义触发动作

ContextMenuContent 依赖项 属性 确保我们可以将 ContextMenu 定义为自定义 TriggerAction 的内容。在 Invoke 方法中,经过几次安全检查后,我们可以弹出上下文菜单。 (鼠标定位和在用户单击选项后销毁上下文菜单由 WPF 处理。)

public class ContextMenuAction : TriggerAction<FrameworkElement> {
  public static readonly DependencyProperty ContextMenuContentProperty =
    DependencyProperty.Register("ContextMenuContent",
                                typeof(FrameworkElement),
                                typeof(ContextMenuAction));

  public FrameworkElement ContextMenuContent {
    get { return (FrameworkElement)GetValue(ContextMenuContentProperty); }
    set { SetValue(ContextMenuContentProperty, value); }
  }

  public static readonly DependencyProperty ContextMenuDataContextProperty =
    DependencyProperty.Register("ContextMenuDataContext",
                                typeof(FrameworkElement),
                                typeof(ContextMenuAction));

  public FrameworkElement ContextMenuDataContext {
    get { return (FrameworkElement)GetValue(ContextMenuDataContextProperty); }
    set { SetValue(ContextMenuDataContextProperty, value); }
  }

  protected override void Invoke(object parameter) {
    if (!(parameter is InteractionRequestedEventArgs args)) {
      return;
    }

    if (!(ContextMenuContent is ContextMenu contextMenu)) {
      return;
    }

    contextMenu.DataContext = ContextMenuDataContext;
    contextMenu.IsOpen = true;
  }
}

C.2) 绑定代理

您会注意到还有第二个依赖项 属性,称为 ContextMenuDataContext。这是由于 ContextMenu 与视图的其余部分不在同一个 visual/logical 树中这一事实引起的问题的解决方案。弄清楚这个解决方案花了我几乎所有其他所有时间的总和,如果不是@Cameron-McFarland 对 Cannot find source for binding with reference 'RelativeSource FindAncestor' as well as the WPF Tutorial on Context Menus.

的回答,我就不会到达那里。

事实上,我会参考那些资源来获取代码。只要说我们需要使用绑定代理来设置 ContextMenuDataContext 就够了。我决定通过自定义 TriggerAction 中的依赖项 属性 以编程方式执行此操作,因为 ContextMenuDataContext 属性 需要 PlacementTarget 机制正常工作,这在这种情况下是不可能的,因为 TriggerAction(作为包含 ContextMenu 的元素)没有自己的数据上下文。

D) 把所有东西都包起来

回想起来,实施起来并不难。有了上面的内容,就可以轻而易举地连接承载 MyContainerControl 的视图的视图模型中定义的一些命令,并通过通常的绑定机制和依赖属性传递这些命令。这允许从根本上操纵数据。

我很满意这个解决方案;我不太喜欢的是,当自定义交互请求通知被提出时,通信会增加一倍。但这无济于事,因为在放置处理程序中收集的信息必须以某种方式到达我们对用户可以在上下文菜单上做出的不同选择做出反应的地方。