The correct way to do a tunneled event

感情迁移 提交于 2019-12-03 16:43:24
Groo

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);
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!