Reputation: 3585
I am trying to use an attached event to invoke an ICommand
.
I am using the Microsoft.Toolkit.Mvvm
NuGet package, along with the Microsoft.Xaml.Behaviors.Wpf
Nuget package.
I have had success starting a Storyboard using the <BeginStoryBoardAction />
within the <FOO.Triggers />
by defining an <EventTrigger />
and setting the RoutedEvent
equal to the name of the attached event in question.
However, to my knowledge, there exists no way to invoke an ICommand
using anything provided within the <EventTrigger />
. By which I mean, there is nothing that I can use within the body of the <EventTriggers.Actions />
block (similar to <Behaviors.InvokeCommandAction />
) which will result in the ICommand
being invoked.
In compliance with the Minimal, Complete and Verifiable example, to demonstrate what I am trying to achieve, you can reference the project on GitHub - https://github.com/AbbottWC/MCVE_AttachedEventFailure.
Or
Open Visual Studio. Create a WPF Application (Targeting .Net Core 3.1)
Tools -> NuGet Package Manager -> Manage Packages for this solution
Add the Microsoft.Toolkit.Mvvm (for the RelayCommand
class) and the Microsoft.Xaml.Behaviors.Wpf
packages.
In the App.Xaml.cs
private RelayCommand testCommand = new RelayCommand(( ) => MessageBox.Show("Event Captured!", "Success!"));
public RelayCommand TestCommand => testCommand;
In the MainWindow.xaml
Define the namespace xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
Rename local
to l
Add the following to the body of the XAML before the closing </Window>
tag:
<i:Interaction.Triggers>
<!--This will work, and is present only to prove the problem lies not with the Command or how it is being accessed.-->
<i:EventTrigger EventName="MouseEnter">
<i:EventTrigger.Actions>
<i:InvokeCommandAction Command="{Binding TestCommand, Source={x:Static l:App.Current}}" />
</i:EventTrigger.Actions>
</i:EventTrigger>
<!--This will not work, and is meant to provide an example of the problem.-->
<i:EventTrigger EventName="Mouse.MouseEnter">
<i:EventTrigger.Actions>
<i:InvokeCommandAction Command="{Binding TestCommand, Source={x:Static l:App.Current}}" />
</i:EventTrigger.Actions>
</i:EventTrigger>
</i:Interaction.Triggers>
So to restate my question, is what I am trying to achieve here possible? Is it just not possible to use an attached event in this way?
Thanks.
Upvotes: 3
Views: 3024
Reputation: 1505
Here's a RoutedEventTrigger
class that should get the job done.
using Microsoft.Xaml.Behaviors;
using System.Windows;
//..
public class RoutedEventTrigger : TriggerBase<DependencyObject>
{
public RoutedEvent TargetEvent
{
get { return (RoutedEvent)GetValue(TargetEventProperty); }
set { SetValue(TargetEventProperty, value); }
}
public static readonly DependencyProperty TargetEventProperty =
DependencyProperty.Register("TargetEvent", typeof(RoutedEvent),
typeof(RoutedEventTrigger), new PropertyMetadata(null, HandleEventChanged));
private RoutedEventHandler _handler;
protected override void OnAttached()
{
base.OnAttached();
RegisterEvent();
}
protected override void OnDetaching()
{
base.OnDetaching();
UnregisterEvent();
}
private void OnEventImpl(object sender, RoutedEventArgs eventArgs)
{
InvokeActions(eventArgs);
}
private static void HandleEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not RoutedEventTrigger trigger) return;
trigger.HandleEventChanged(e);
}
private void HandleEventChanged(DependencyPropertyChangedEventArgs e)
{
if (e.OldValue is RoutedEvent oev)
UnregisterEvent(oev);
if (e.NewValue is RoutedEvent nev)
RegisterEvent(nev);
}
private void RegisterEvent(RoutedEvent ev = null)
{
if (AssociatedObject is not UIElement el) return;
if (ev == null) ev = TargetEvent;
if (ev != null)
{
_handler = new RoutedEventHandler(OnEventImpl);
el.AddHandler(ev, _handler);
}
}
private void UnregisterEvent(RoutedEvent ev = null)
{
if (ev == null) ev = TargetEvent;
if (AssociatedObject is UIElement el && ev != null && _handler != null)
{
el.RemoveHandler(ev, _handler);
}
_handler = null;
}
}
Here's how you can use it in your XAML
.
<i:Interaction.Triggers>
<ui:RoutedEventTrigger TargetEvent="{x:Static Mouse.MouseEnterEvent}">
<prism:InvokeCommandAction Command="{Binding TestCommand}" />
</ui:RoutedEventTrigger>
</i:Interaction.Triggers>
Upvotes: 0
Reputation: 52365
From what I understand, the EventTrigger
can only listen to events that the source object "owns". So to answer your question, this is not possible using the EventTrigger
class.
If you look at the source, when you pass Mouse.MouseEnter
, it tries to get that event from the target type. Also, since you did not specify the target, it defaults to the AssociatedObject
, which is Window
in your case.
Type targetType = obj.GetType();
EventInfo eventInfo = targetType.GetEvent(eventName);
Now the problem I find, is if it cannot find the event, it only throws an exception when the source object is not null, and in your case you haven't specified one, so it silently fails. Not sure why the designers made it this way.
Now, I did find this blog post, which describes exactly how you can workaround this problem: https://sergecalderara.wordpress.com/2012/08/23/how-to-attached-an-mvvm-eventtocommand-to-an-attached-event/
Basically, you inherit from EventTriggerBase<T>
and in the OnAttached
method you need to call AddHandler
on your AssociatedObject
to add the event.
Upvotes: 2