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 theContextMenu
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 customNotification
(plus the callback mechanism) by means ofIInteractionRequestAware
, 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.
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 anInteractionRequest
as well as an object that gathers the arguments to pass along with a click command on theContextMenu
. This is because theContextMenu
doesn’t implementIInteractionRequestAware
, which means we’ll have to use the standard way of invoking command actions. I’ve simply used theDragDropContextMenuEventArgs
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.
B.2) XAML
As a sibling to the design elements of
MyContainerControl
, we define theInteractionTrigger
for the notification request.C) Trigger Action and Other Magic
This is where things get tricky. First of all, we need to define a custom
TriggerAction
that invokes ourContextMenu
.C.1) Custom Trigger Action
The
ContextMenuContent
dependency property makes sure that we can define aContextMenu
as content of our customTriggerAction
. In theInvoke
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.)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 aContextMenu
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
’sDataContext
. I resolved doing this programmatically via a dependency property in my customTriggerAction
, since theDataContext
property of theContextMenu
needs thePlacementTarget
mechanism to work properly, which isn’t possible in this case, since theTriggerAction
(as element containing theContextMenu
) 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.