进行隧道事件的正确方法

Sco*_*ain 4 c# wpf event-handling routed-events

编辑:我想我问了一些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>
Run Code Online (Sandbox Code Playgroud)
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); }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)
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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当我运行程序时,父窗口会像我期望的那样做出反应,但是子UserControl永远不会运行我传入的委托AddHandler.

将子控件更改为

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

也没有解决问题.我搜索了很多,发现了如何做一个冒泡事件从一个孩子到家长会的例子很多,但我无法找到一个完整的例子展示了如何从父做隧道事件一个孩子.

Gro*_*roo 6

如果您更仔细地查看有关WPF中的路由事件MSDN文章(已存档),您会看到它说:

Bubble是最常见的,意味着事件将从源元素冒泡(传播)到可视树中,直到它被处理或到达根元素.这允许您在源元素的元素层次结构上进一步处理对象上的事件.

Tunnel事件从另一个方向开始,从根元素开始并遍历元素树,直到它们被处理或到达事件的源元素.这允许上游元素拦截事件并在事件到达源元素之前处理它.隧道事件的名称以"按预期预览"为前缀(例如PreviewMouseDown).

这确实是反直觉的,但是隧道事件向源元素传播.在你的情况下,元素是MainWindow,但元素实际上是ChildControl.当你在里面引发事件时MainWindow,恰好是.

Source元素是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));
    }
}
Run Code Online (Sandbox Code Playgroud)

并在MainWindow:

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

如果您使用现有的隧道事件,事情会更简单,但请注意它们仍然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")));
    }
}
Run Code Online (Sandbox Code Playgroud)

这也会将以下内容输出到控制台(鼠标向上):

Parent handler
Child handler
Run Code Online (Sandbox Code Playgroud)

当然,如果将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")));
    }
}
Run Code Online (Sandbox Code Playgroud)

根据您的实际用例,您几乎看起来希望父窗口在没有实际隧道的情况下通知子控件.在那种情况下,我不确定你是否还需要活动?就是这个有什么问题:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 对于任何来到这里寻找如何从不知道孩子或其在树中的位置的情况下从父母那里引发事件的人:这基本上是一个“广播”事件,这不是路由事件的用途,请参阅 Ben Carter 的回复[MSDN 上的这个问题](https://social.msdn.microsoft.com/Forums/vstudio/en-US/1f7cfccd-12e2-4478-b96b-b2e36f76ed01/eventtrigger-not-firing-on-custom-routed-event ?论坛=wpf)。在这种情况下,一个可能的解决方案是使用事件聚合器。 (3认同)
  • 如果您更新了显示如何解决我的真实问题的问题,请保留原来的答案.您目前的答案是我在互联网上看到的解释隧道事件的最佳答案. (2认同)