In a software that is highly interactive, the user can do drag and drop operations on a collection of UserControl
s. Upon drop, they should be presented with a ContextMenu
offering some choices on how to perform the action, e.g., copy the item, or swap positions if there is another item at the drop location.
Using the Prism framework, the ideal way of implementing this would be by means of the InteractionRequestTrigger
, for instance:
<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>
This raises a doubt on whether to implement the InteractionRequestTrigger
in the XAML of the ItemsControl
containing the drag-and-droppable UserControl
, or if it should go into the UserControl
itself. In case of the latter, how would the various instances of that particular UserControl
“know” which one is to react on the interaction request?
Second, the child element of an InteractionRequestTrigger
must be a System.Windows.Interactivity.TriggerAction
. It seems that this is not widely used for anything other than opening popup windows. The documentation on TriggerAction
is quite sparse, and I don’t know how to go about implementing its Invoke
method. Any pointer to documentation would be much appreciated!
Using an InteractionRequestTrigger
definitely is the way to go here, but since the ContextMenu
control doesn’t reside in the same visual/logical tree as the control that defines it, one has to walk through some dark alleys.
Before coming to the actual code, I’d also highlight the reason I didn’t go for @Haukinger’s suggestion to use a popup window instead of a ContextMenu
: while providing the advantage of making direct use of the properties I define for my custom Notification
(plus the callback mechanism) by means of IInteractionRequestAware
, I’d have had to implement some magic to make the popup window appear at the mouse cursor location. Plus, in my particular case, I’m manipulating the data model as a result of the context menu click, meaning that I’d have had to use dependency injection with the popup window in order to access the correct instance of my data model, which I frankly don’t know how to do, either.
Anyway, I got it to work smoothly with a ContextMenu
. Here’s what I did. (I won’t post the obvious boilerplate code; just keep in mind that I’m using Prism with the GongSolutions Drag and Drop Library.
A) Drop Handler
The drop handler class must be augmented with an event that we can call upon drop. This event will later be consumed by the view model belonging to the view that’s hosting the drag and drop action.
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
}
The DragDropContextMenuEventArgs
is straightforward; refer to the Prism manual if you need assistance.
B) Interaction Request
In my case, I’ve got a custom UserControl
that’s hosting the elements I want to drag and drop. Its view model needs an InteractionRequest
as well as an object that gathers the arguments to pass along with a click command on the ContextMenu
. This is because the ContextMenu
doesn’t implement IInteractionRequestAware
, which means we’ll have to use the standard way of invoking command actions. I’ve simply used the DragDropContextMenuEventArgs
defined above, since it’s an object that already hosts all the required properties.
B.1) View Model
This makes use of a custom notification request with a corresponding interface, the implementation of which is straightforward. I’ll skip the code here to keep this entry more manageable. There’s a lot on StackExchange on the topic; see, for instance, the link @Haukinger provided as a comment to my original question.
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
As a sibling to the design elements of MyContainerControl
, we define the InteractionTrigger
for the notification request.
<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) Trigger Action and Other Magic
This is where things get tricky. First of all, we need to define a custom TriggerAction
that invokes our ContextMenu
.
C.1) Custom Trigger Action
The ContextMenuContent
dependency property makes sure that we can define a ContextMenu
as content of our custom TriggerAction
. In the Invoke
method, after a couple of safety checks, we can make the context menu pop up. (Mouse location and destroying the context menu after the user clicked an option is handled by 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) Binding Proxy
You’ll note that there’s a second dependency property called ContextMenuDataContext
. This is the solution to a problem that arises from the fact that a ContextMenu
doesn’t live inside the same visual/logical tree as the rest of the view. Figuring out this solution took me almost as long as all the rest of this combined, and I wouldn’t have gotten there if it wasn’t for @Cameron-McFarland’s answer to Cannot find source for binding with reference 'RelativeSource FindAncestor' as well as the WPF Tutorial on Context Menus.
In fact, I’ll refer to those resources for the code. Suffice it to say that we need to use a binding proxy to set the ContextMenu
’s DataContext
. I resolved doing this programmatically via a dependency property in my custom TriggerAction
, since the DataContext
property of the ContextMenu
needs the PlacementTarget
mechanism to work properly, which isn’t possible in this case, since the TriggerAction
(as element containing the ContextMenu
) doesn’t have its own data context.
D) Wrapping everything up
In retrospective, it wasn’t that hard to implement. With the above in place, it’s child’s play to hook up some commands defined in the view model of the view that hosts the MyContainerControl
and pass those via the usual binding mechanism and dependency properties. This allows for manipulation of the data at its very root.
I’m happy about this solution; what I don’t like that much is that communication is doubled when the custom interaction request notification is raised. But this can’t be helped, since the information gathered in the drop handler must somehow reach the place where we react upon the different choices the user can make on the context menu.