The correct way to do a tunneled event

2019-03-31 01:22发布

EDIT: I guess I asked a bit of a XY Problem. I don't really care about getting tunneled events working, what I care about is getting a event raised from the code behind of the parent window to be picked up and reacted to by a control that is a child of that window without explicitly needing to tell the child who its parent is and manually subscribing to the event.


I am trying to raise a event in a parent control and having the child controls listen for that event and react to it. From my research I thought I just needed to do a RoutedEvent but I am doing something incorrect.

Here is a MCVE showing what I have tried, it is a simple program with a window and a UserControl inside of it.

<Window x:Class="RoutedEventsTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:RoutedEventsTest"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Button Name="button" Click="ButtonBase_OnClick" HorizontalAlignment="Left" 
                VerticalAlignment="Top">Unhandled in parent</Button>
        <local:ChildControl Grid.Row="1"/>
    </Grid>
</Window>
using System.Windows;

namespace RoutedEventsTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            TestEventHandler += MainWindow_TestEventHandler;
        }

        void MainWindow_TestEventHandler(object sender, RoutedEventArgs e)
        {
            button.Content = "Handeled in parent";
            e.Handled = false;
        }

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            RaiseEvent(new RoutedEventArgs(TestEvent));
        }

        public static readonly RoutedEvent TestEvent = EventManager.RegisterRoutedEvent("TestEvent", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(MainWindow));

        public event RoutedEventHandler TestEventHandler
        {
            add { AddHandler(TestEvent, value); }
            remove { RemoveHandler(TestEvent, value); }
        }
    }
}
<UserControl x:Class="RoutedEventsTest.ChildControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
          <TextBlock Name="textBlock">Unhandeled in child</TextBlock>  
    </Grid>
</UserControl>
using System.Windows;
using System.Windows.Controls;

namespace RoutedEventsTest
{
    public partial class ChildControl : UserControl
    {
        public ChildControl()
        {
            InitializeComponent();
            AddHandler(MainWindow.TestEvent, new RoutedEventHandler(TestEventHandler));
        }

        private void TestEventHandler(object sender, RoutedEventArgs routedEventArgs)
        {
            textBlock.Text = "Handled in child";
            routedEventArgs.Handled = false;
        }
    }
}

When I run the program the parent window reacts like I expect, but the child UserControl never runs its delegate that I passed in to AddHandler.

Changing the child control to be

public partial class ChildControl : UserControl
{
    public ChildControl()
    {
        InitializeComponent();
        AddHandler(TestEvent, new RoutedEventHandler(TestEventHandler));
    }

    public static readonly RoutedEvent TestEvent = EventManager.RegisterRoutedEvent("TestEvent", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(ChildControl));

    private void TestEventHandler(object sender, RoutedEventArgs routedEventArgs)
    {
        textBlock.Text = "Handled in child";
        routedEventArgs.Handled = false;
    }
}

did not fix the issue either. I searched a lot and found many examples of how to do a bubbled event going from a child to the parent, but I could not find a single full example showing how to do a tunneled event from a parent to a child.

1条回答
地球回转人心会变
2楼-- · 2019-03-31 01:53

If you check out the MSDN article on routed events in WPF (archived) more closely, you will see that it says:

Bubble is the most common and means that an event will bubble (propagate) up the visual tree from the source element until either it has been handled or it reaches the root element. This allows you to handle an event on an object further up the element hierarchy from the source element.

Tunnel events go in the other direction, starting at the root element and traversing down the element tree until they are handled or reach the source element for the event. This allows upstream elements to intercept the event and handle it before the event reaches the source element. Tunnel events have their names prefixed with Preview by convention (such as PreviewMouseDown).

It's indeed counter intuitive, but a tunneled event propagates towards the source element. In your case, root element is MainWindow, but the source element is actually the ChildControl. When you raised the event inside the MainWindow, that happened to be both the source and the root.

Source element is the element on which the RaiseEvent method is invoked, even if the RoutedEvent is not a member of that element. Also, since RaiseEvent is a public method, other elements can make another element become the source element for a tunneled event.

In other words, you would need something like (added the Preview prefix cause that's the convention for tunneled events):

// ChildControl is the event source
public partial class ChildControl : UserControl
{
    public readonly static RoutedEvent PreviewEvent = 
        EventManager.RegisterRoutedEvent(
            "PreviewEvent",
            RoutingStrategy.Tunnel,
            typeof(RoutedEventHandler),
            typeof(ChildControl));

    public ChildControl()
    {
        InitializeComponent();
        AddHandler(PreviewEvent, 
          new RoutedEventHandler((s, e) => Console.WriteLine("Child handler")));
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // make this control the source element for tunneling
        this.RaiseEvent(new RoutedEventArgs(PreviewEvent));
    }
}

And in the MainWindow:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        AddHandler(ChildControl.PreviewEvent, 
          new RoutedEventHandler((s, e) => Console.WriteLine("Parent handler")));
    }
}

Things are simpler if you use existing tunneled events, but note that they are still defined on the Button as the source, not the root element:

// this uses the existing Button.PreviewMouseUpEvent tunneled event
public partial class ChildControl : UserControl
{
    public ChildControl()
    {
        InitializeComponent();
        AddHandler(Button.PreviewMouseUpEvent, 
          new RoutedEventHandler((s, e) => Console.WriteLine("Child handler")));
    }
}

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        AddHandler(Button.PreviewMouseUpEvent, 
          new RoutedEventHandler((s, e) => Console.WriteLine("Parent handler")));
    }
}

This would also output the following to the console (on mouse up):

Parent handler
Child handler

And of course, if you set the Handled property to true inside the parent handler, child handler will not be invoked.

[Update]

If you want to raise the event from the parent control, yet make the child control the source of the event, you can simply invoke the child control's public RaiseEvent method from outside:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        AddHandler(ChildControl.PreviewEvent,
          new RoutedEventHandler((s, e) => Console.WriteLine("Parent handler")));
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        // raise the child event from the main window
        childCtrl.RaiseEvent(new RoutedEventArgs(ChildControl.PreviewEvent));
    }
}

// child control handles its routed event, but doesn't know who triggered it
public partial class ChildControl : UserControl
{
    public readonly static RoutedEvent PreviewEvent = 
        EventManager.RegisterRoutedEvent(
            "PreviewEvent",
            RoutingStrategy.Tunnel,
            typeof(RoutedEventHandler),
            typeof(ChildControl));

    public ChildControl()
    {
        InitializeComponent();
        AddHandler(PreviewEvent, 
          new RoutedEventHandler((s, e) => Console.WriteLine("Child handler")));
    }
}

Depending on your actual use case, it almost looks like you want the parent window to notify the child control without actual tunneling. In that case, I am not sure if you even need events? I.e. what's wrong with simply this:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        childCtrl.DoSomething(this, "MainWindow just sent you an event");
    }
}

public partial class ChildControl : UserControl
{
    public ChildControl()
    {
        InitializeComponent();
    }

    public void DoSomething(UIElement sender, string message)
    {
        Console.WriteLine(sender.ToString() + ": " + message);
    }
}
查看更多
登录 后发表回答