Использование InteractionRequestTrigger
определенно - это способ пойти сюда, но поскольку элемент управления ContextMenu
не находится в том же визуальном / логическом дереве, что и элемент управления, который его определяет, необходимопройтись по темным переулкам.
Прежде чем перейти к реальному коду, я бы также выделил причину, по которой я не согласился с предложением @ Haukinger использовать всплывающее окно вместо ContextMenu
: при предоставленииПреимущество прямого использования свойств, которые я определяю для своего пользовательского Notification
(плюс механизм обратного вызова) с помощью IInteractionRequestAware
, мне пришлось бы реализовать некоторую магию, чтобы всплывающее окно появлялось в месте расположения курсора мыши.Кроме того, в моем конкретном случае я манипулирую моделью данных в результате щелчка контекстного меню, что означает, что мне пришлось бы использовать внедрение зависимостей во всплывающее окно, чтобы получить доступ к правильному экземпляру моей модели данных,что я, честно говоря, тоже не знаю, как это сделать.
Во всяком случае, я заставил его работать гладко с ContextMenu
.Вот что я сделал.(Я не буду публиковать очевидный шаблонный код; просто имейте в виду, что я использую Prism с библиотекой GongSolutions Drag and Drop .
A) DropОбработчик
Класс обработчика удаления должен быть дополнен событием, которое мы можем вызвать при удалении.Это событие позже будет использовано моделью представления, принадлежащей представлению, в котором размещено действие перетаскивания.
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
, в котором находятся элементы, которые я хочу перетащить.Его модели представления требуется InteractionRequest
, а также объект, который собирает аргументы для передачи вместе с командой click для ContextMenu
.Это потому, что ContextMenu
не реализует IInteractionRequestAware
, что означает, что мы должны будем использовать стандартный способ вызова командных действий.Я просто использовал DragDropContextMenuEventArgs
, определенный выше, поскольку это объект, который уже содержит все необходимые свойства.
B.1) View Model
При этом используется пользовательский запрос уведомленияс соответствующим интерфейсом, реализация которого проста.Я пропущу здесь код, чтобы сделать эту запись более управляемой.В 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) Binding Proxy
Вы заметите, что существует вторая зависимостьсвойство под названием ContextMenuDataContext
.Это решение проблемы, которая возникает из-за того, что ContextMenu
не находится внутри того же визуального / логического дерева, что и остальная часть представления.Изучение этого решения заняло у меня почти столько же времени, сколько и все остальное, вместе взятое, и я бы этого не добился, если бы не ответ @ Cameron-McFarland на Не удается найти источник для привязки со ссылкой 'RelativeSource FindAncestor, а также Учебное пособие WPF по контекстным меню .
Фактически, я буду ссылаться на эти ресурсы длякод.Достаточно сказать, что нам нужно использовать связующий прокси-сервер для установки ContextMenu
DataContext
.Я решил сделать это программно через свойство зависимостей в моем пользовательском TriggerAction
, так как для свойства DataContext
ContextMenu
требуется механизм PlacementTarget
для правильной работы, что в данном случае невозможно, так как TriggerAction
(поскольку элемент, содержащий ContextMenu
) не имеет собственного контекста данных.
D) Завершение всего
В ретроспективе это было не так сложно реализовать.Имея вышесказанное, мы можем подключить некоторые команды, определенные в модели представления, в которой размещен MyContainerControl
, и передать их через обычный механизм привязки и свойства зависимостей.Это позволяет манипулировать данными в самом их корне.
Я рад этому решению;что мне не очень нравится, так это то, что связь увеличивается вдвое, когда выдается уведомление о пользовательском запросе взаимодействияНо с этим ничего не поделаешь, поскольку информация, собранная в обработчике отбрасывания, должна каким-то образом достигать места, где мы реагируем на различные варианты выбора, которые пользователь может сделать в контекстном меню.