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

时间:2019-01-29 06:53:08

标签: wpf xaml mvvm drag-and-drop prism

在高度交互的软件中,用户可以对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>

这使人们怀疑是在包含可拖放InteractionRequestTrigger的{​​{1}}的XAML中实现ItemsControl,还是应该将其放入{{1 }}本身。对于后者,该特定UserControl的各个实例如何“知道”哪个将对交互请求做出反应?

第二,UserControl的子元素必须是UserControl。似乎除了打开弹出窗口外,它还没有广泛使用。 InteractionRequestTrigger上的文档非常稀疏,我不知道如何实现其System.Windows.Interactivity.TriggerAction方法。任何指向文档的指针将不胜感激!

1 个答案:

答案 0 :(得分:1)

使用InteractionRequestTrigger肯定是 的方法,但是由于ContextMenu控件与定义该控件的控件不在同一视觉/逻辑树中它,必须走过一些黑暗的小巷。

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

无论如何,我可以使它与ContextMenu一起正常工作。这就是我所做的。 (我不会发布明显的样板代码;请记住,我在Prism中使用GongSolutions Drag and Drop Library

A)删除处理程序

在放置处理程序类中,必须添加一个我们可以在放置时调用的事件。稍后,该事件将由属于托管拖放操作的视图的视图模型使用。

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很简单;如果需要帮助,请参阅棱镜手册。

B)互动请求

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

B.1)视图模型

这利用了带有相应接口的自定义通知请求,该接口的实现非常简单。为了使该条目更易于管理,我将在此处跳过代码。关于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设计元素的同级元素,我们为通知请求定义了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与视图其余部分不在同一个视觉/逻辑树中而引起的问题的解决方案。弄清楚这个解决方案花了我几乎所有其余的时间,而如果不是@ Cameron-McFarland对Cannot find source for binding with reference 'RelativeSource FindAncestor'和{{3}的回答,我就不会到达那儿。 }。

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

D)包好一切

回顾起来,实施起来并不难。完成上述操作后,您就可以挂起在托管TriggerAction的视图的视图模型中定义的一些命令,并通过通常的绑定机制和依赖项属性来传递这些命令的工作。这样一来,就可以从根本上对数据进行操作。

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