Will
Will

Reputation: 3585

Is it possible to invoke a command using an Microsoft.Xaml.Behaviors.EventTrigger with attached events?

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

<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

Answers (2)

Prince Owen
Prince Owen

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

Jesse Good
Jesse Good

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

Related Questions