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

标签 wpf xaml mvvm drag-and-drop prism

在一个交互性很强的软件中,用户可以对UserControl的集合进行拖放操作。 s。掉落时,它们应该显示 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>

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

二、InteractionRequestTrigger的子元素必须是 System.Windows.Interactivity.TriggerAction .除了打开弹出窗口之外,这似乎并没有被广泛用于任何其他事情。 TriggerAction 上的文档很稀疏,不知道怎么去实现它的Invoke方法。任何指向文档的指针将不胜感激!

最佳答案

使用 InteractionRequestTrigger绝对是去这里的方式,但由于ContextMenu控件与定义它的控件不在同一个视觉/逻辑树中,你必须穿过一些黑暗的小巷。

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

不管怎样,我用 ContextMenu 让它顺利工作.这就是我所做的。 (我不会发布明显的样板代码;请记住,我将 PrismGongSolutions Drag and Drop Library 一起使用。

A) 丢弃处理程序

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

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那是托管我想要拖放的元素。它的 View 模型需要一个 InteractionRequest以及收集参数的对象以及 ContextMenu 上的单击命令一起传递.这是因为 ContextMenu没有实现 IInteractionRequestAware ,这意味着我们必须使用调用命令操作的标准方式。我只是使用了 DragDropContextMenuEventArgs上面定义的,因为它是一个已经拥有所有必需属性的对象。

B.1) 查看模型

这利用了带有相应接口(interface)的自定义通知请求,其实现很简单。我将跳过此处的代码以使此条目更易于管理。 StackExchange 上有很多关于这个主题的内容;例如,请参阅@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 设计元素的 sibling ,我们定义了 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) 触发 Action 和其他魔法

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

C.1) 自定义触发 Action
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) 绑定(bind)代理

您会注意到还有一个名为 ContextMenuDataContext 的依赖属性。 .这是由 ContextMenu 引起的问题的解决方案。与 View 的其余部分不在同一个视觉/逻辑树中。找出这个解决方案所花的时间几乎与其他所有解决方案的总和一样长,如果不是@Cameron-McFarland 对 Cannot find source for binding with reference 'RelativeSource FindAncestor' 的回答,我就不会到达那里。以及 WPF Tutorial on Context Menus .

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

D)把所有东西都包起来

回想起来,实现起来并不难。有了上面的内容,连接一些在承载 MyContainerControl 的 View 的 View 模型中定义的命令就很简单了。并通过通常的绑定(bind)机制和依赖属性传递它们。这允许在其根部操作数据。

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

关于wpf - 在 MVVM 中,拖动完成后打开上下文菜单,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54415381/

相关文章:

c# - 我的属性(property)是否更新了我的领域,或者我是否错误地使用了 MVVM

wpf - XAML 组合样式超越 BasedOn?

wpf - DataTrigger 在条件为假时设置默认值

c# - 如何在每个wpf/MVVM组合框行上显示2个字符串?

silverlight - 将 ListBoxItem.ItemSelected 绑定(bind)到 Silverlight 中的绑定(bind)项

wpf - 提供程序 : System. Data.SqlServerCe.3.5 未安装

wpf - 我可以将数据绑定(bind)到 DataGridRow.DetailsVisibility 吗?

wpf - 如何使 XmlnsDefinition 在本地程序集上工作?

c# - 在 Xamarin 中使用 SkiaSharp 动态绘制线条

c# - 使用 mvvm 填充 Listview