Home > database >  wpf Interaction EventTrigger with multiple sources
wpf Interaction EventTrigger with multiple sources

Time:11-03

I am trying to catch a RountedEvent fired from multiple instances of a UserControl. This works in the non-MVVM world easily by catching the routed event at the Window level (local:MyControl.MyEvent="Window_CodeBehindHandler"). But I need to forward that event on to the View Model, and I am struggling to get an Interaction.Triggers setup to behave the same. The EventTrigger behaves as expected if I pass in a specific SourceName or SourceObject or place the Interaction.Triggers inside of the UserControl in the XAML, but that doesn't work the same.

I have tried various versions of setting SourceName or SourceObject to the control type or name without luck.

I've put together an MRE here: WpfInteractionEventTriggerMRE. The "real" implementation has an ItemsView bound to a collection created at runtime of these control objects, with a lot more going on than a single "Select" event, so "just use an {x} control with Click already implemented and restyle it" suggestions aren't helpful.

<Window x:Class="WpfApp5.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp5"
        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        
        Title="MainWindow" Height="450" Width="800"
        
        local:ElipseControl.Select="Window_Select"> <!-- This works as expected -->

    <b:Interaction.Triggers> <!-- This does not -->
        <b:EventTrigger EventName="Select">
            <b:InvokeCommandAction Command="{Binding ElipseSelectedCommand}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <local:ElipseControl Grid.Row="0" ElipseName="One" />
        <local:ElipseControl Grid.Row="1" ElipseName="Two" />
    </Grid>
</Window>

I am hoping there is something I am just overlooking here.

CodePudding user response:

The EventTrigger that you are using doesn't support attached events. You will either have to implement your own custom event trigger or you could simply invoke the command from the Window_Select event handler in the window:

var viewModel = DataContext as YourViewModel;
if (viewModel != null)
    viewModel.ElipseSelectedCommand.Execute(null);

This doesn't break the MVVM pattern in any way since you are invoking the very same command from the very same view compared to if you have used an interaction trigger. MVVM isn't about eliminating code from the view - it's about separation of concerns.

CodePudding user response:

The custom event trigger worked. The MRE in the original question is updated. Here is the relevant updated code.

<Window x:Class="WpfApp5.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp5"
        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        
        Title="MainWindow" Height="450" Width="800">
    
    <b:Interaction.Triggers>
        <local:RoutedEventTrigger RoutedEvent="local:ElipseControl.Select" >
            <b:InvokeCommandAction Command="{Binding ElipseSelectedCommand}" PassEventArgsToCommand="True" />
        </local:RoutedEventTrigger>
    </b:Interaction.Triggers>
    
    <Grid>
        <ItemsControl ItemsSource="{Binding Elipses}" />
    </Grid>
</Window>

As per custom event trigger, a slightly updated version:

public class RoutedEventTrigger : EventTriggerBase<DependencyObject>
    {
        private RoutedEventHandler _eventHandler;

        public RoutedEvent RoutedEvent { get; set; }

        public RoutedEventTrigger()
        {

        }

        protected override void OnAttached()
        {
            var associatedElement = GetAssociatedElement();

            if (associatedElement != null && RoutedEvent != null)
            {
                _eventHandler = new RoutedEventHandler(OnRoutedEvent);

                associatedElement.AddHandler(RoutedEvent, _eventHandler);
            }
        }

        protected override void OnDetaching()
        {
            var associatedElement = GetAssociatedElement();

            if (associatedElement != null && RoutedEvent != null && _eventHandler != null)
            {
                associatedElement.RemoveHandler(RoutedEvent, _eventHandler);
            }

            base.OnDetaching();
        }

        private FrameworkElement GetAssociatedElement()
        {
            FrameworkElement associatedElement = null;

            if (AssociatedObject is Behavior behavior)
            {
                associatedElement = ((IAttachedObject)behavior).AssociatedObject as FrameworkElement;
            }
            else if (AssociatedObject is FrameworkElement frameworkElement)
            {
                associatedElement = frameworkElement;
            }

            if (associatedElement == null)
            {
                throw new ArgumentException("Routed Event Trigger can only be associated to framework elements");
            }

            return associatedElement;
        }

        private void OnRoutedEvent(object sender, RoutedEventArgs e) => base.OnEvent(e);

        protected override string GetEventName() => RoutedEvent?.Name ?? string.Empty;
    }
  • Related