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.