正确的隧道事件操作方法

7

编辑: 我想我提出了一个XY问题。我并不在意获取隧道事件的工作原理,我关心的是从父窗口的后台代码中引发事件,并且让该窗口的子控件能够捕获并对其做出反应,而不需要显式地告诉子控件其父控件是什么并手动订阅该事件。


我试图在父控件中引发事件,并使子控件侦听该事件并对其做出反应。根据我的研究,我认为只需要使用 RoutedEvent,但是我做错了些什么。

这里是一个MCVE,展示了我的尝试,它是一个带有窗口和其中的UserControl的简单程序。

<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;
        }
    }
}

当我运行程序时,父窗口的反应与我的期望相符,但是我传递给AddHandler的委托从未运行其子UserControl。

将子控件更改为
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;
    }
}

也没有解决问题。我搜索了很多,并找到了许多关于如何从子级到父级执行冒泡事件的示例,但我无法找到一个完整的示例,演示如何从父级到子级执行隧道事件。

1个回答

9
如果您仔细查看WPF中路由事件的MSDN文章(存档),您将会看到它说:
"Bubble"是最常见的,意味着事件将从源元素向上冒泡(传播),直到它被处理或达到根元素。这允许您在来自源元素的元素层次结构上方的对象上处理事件。
"Tunnel"事件则相反,从根元素开始沿着元素树向下遍历,直到被处理或到达事件的源元素。这允许上游元素在事件到达源元素之前拦截并处理它。按照惯例,隧道事件的名称以Preview为前缀(例如PreviewMouseDown)。
这确实与直觉相反,但隧道事件向源元素传播。在您的情况下,根元素是MainWindow,但源元素实际上是ChildControl。当您在MainWindow内引发事件时,它既是源也是根。
源元素是调用RaiseEvent方法的元素,即使RoutedEvent不是该元素的成员。此外,由于RaiseEvent是公共方法,其他元素可以使另一个元素成为隧道事件的源元素。
换句话说,您需要像这样的内容(添加了Preview前缀,因为这是隧道事件的约定):
// 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));
    }
}

并且在 MainWindow 中:

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

如果您使用现有的隧道事件,事情会更简单,但请注意它们仍然在源头Button上定义,而不是根元素:

// 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")));
    }
}

这也会在鼠标抬起时将以下内容输出到控制台:
Parent handler
Child handler

当然,如果您在父处理程序中将Handled属性设置为true,则不会调用子处理程序。 [更新] 如果您想从父控件触发事件,但让子控件成为事件源,则可以在外部简单地调用子控件的公共RaiseEvent方法:
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")));
    }
}

根据您实际的使用情况,看起来您希望父窗口通知子控件而无需实际隧道。在这种情况下,我不确定您是否需要事件?也就是说,直接使用以下方法有什么问题:

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);
    }
}

好的,所以我在错误地使用隧道事件。我真的不关心如何正确地进行隧道事件。我希望在父窗口上引发的事件可以在子控件上被捕获,而无需明确告诉子控件它的父级是谁。在WPF中是否可能实现这一点?我已经更新了我的问题,虽然你的答案非常好,并解释了为什么我的示例不起作用,但它并没有解决我试图解决的问题。 - Scott Chamberlain
2
如果您更新了您的问题并展示了如何解决我的实际问题,请同时保留您的原始答案。您目前的答案是我在互联网上看到的最好的解释隧道事件的答案。 - Scott Chamberlain
3
如果有人来这里寻求如何在不知道子元素或其在树中位置的情况下从父级引发事件:这基本上是一个“广播”事件,这并不是路由事件的用途,请参见Ben Carter在MSDN上的回复此问题。在这种情况下可能的解决方案是使用事件聚合器。 - Paul
1
在我的经验中,如果严格通过xaml创建自定义控件,则使用事件聚合器可能会很困难(最终我使用依赖属性来传输事件聚合器的非静态实例到自定义控件中)。 - Alan Wayne
@Alan Wayne,你的意思是什么?你能再清楚一些吗?有代码片段吗? - Özgür

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接