从 WPF 到 Avalonia 的迁移系列实战篇2:路由事件的异同点与迁移技巧

发布于:2025-08-29 ⋅ 阅读:(16) ⋅ 点赞:(0)

从 WPF 到 Avalonia 的迁移系列实战篇2:路由事件的异同点与迁移技巧

我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。
我的gitcode仓库是Avalonia学习项目
文中主要示例代码均可在仓库中查看,涵盖核心功能实现与优化方案。
点击链接即可直接访问,建议结合代码注释逐步调试。

在 WPF 开发中,路由事件(Routed Event)是 UI 交互和控件通信的重要机制。而在 Avalonia 中,也提供了类似的事件机制,但实现方式和使用习惯与 WPF 有一定差异。本文将从概念、分类、注册、处理以及迁移技巧几个方面详细对比 WPF 和 Avalonia 的路由事件,帮助开发者顺利迁移项目。


一、路由事件的基本概念

WPF:

  • 路由事件是 UIElementContentElement 提供的一种事件传播机制。

  • 支持三种路由策略:

    1. 冒泡事件(Bubbling):事件从源控件向父控件逐层传递。
    2. 隧道事件(Tunneling):事件从根控件向源控件逐层传递,通常以 Preview 开头,如 PreviewMouseDown
    3. 直接事件(Direct):只在源控件触发,不向父控件传播。

Avalonia:

  • Avalonia 的路由事件机制与 WPF 类似,但没有 Preview 前缀,直接使用 RoutingStrategies 来指定策略。

  • 支持三种路由策略:

    1. Bubble(冒泡)
    2. Tunnel(隧道)
    3. Direct(直接)

⚠️ 区别:Avalonia 没有 WPF 的 PreviewXXX 命名约定,需要通过 RoutingStrategies.Tunnel 显式注册隧道事件。


二、路由事件的注册方式

1. WPF 注册路由事件

public static readonly RoutedEvent MyClickEvent =
    EventManager.RegisterRoutedEvent(
        "MyClick",
        RoutingStrategy.Bubble,
        typeof(RoutedEventHandler),
        typeof(MyButton));

public event RoutedEventHandler MyClick
{
    add { AddHandler(MyClickEvent, value); }
    remove { RemoveHandler(MyClickEvent, value); }
}

2. Avalonia 注册路由事件

public static readonly RoutedEvent<RoutedEventArgs> MyClickEvent =
    RoutedEvent.Register<MyButton, RoutedEventArgs>(
        "MyClick",
        RoutingStrategies.Bubble);

public event EventHandler<RoutedEventArgs> MyClick
{
    add { AddHandler(MyClickEvent, value); }
    remove { RemoveHandler(MyClickEvent, value); }
}

⚠️ 差异点:

  • Avalonia 的事件注册通过泛型指定控件类型和事件参数类型。
  • WPF 使用 EventManager.RegisterRoutedEvent,Avalonia 使用 RoutedEvent.Register
  • Avalonia 的路由策略枚举是 RoutingStrategies,而 WPF 是 RoutingStrategy

三、事件触发与处理

1. WPF 触发事件

RaiseEvent(new RoutedEventArgs(MyClickEvent));

2. Avalonia 触发事件

RaiseEvent(new RoutedEventArgs(MyClickEvent));

🔹 相同点:触发事件都使用 RaiseEvent,参数都是对应事件对象。
🔹 不同点:Avalonia 的 RoutedEventArgs 泛型更灵活,可携带自定义事件参数。

3. 事件处理方式

WPF:
myButton.AddHandler(MyButton.MyClickEvent, new RoutedEventHandler(OnMyClick));
Avalonia:
myButton.AddHandler(MyButton.MyClickEvent, OnMyClick);

Avalonia 的语法更简洁,但本质相同。


四、路由事件的迁移技巧

在从 WPF 迁移到 Avalonia 的过程中,有几个注意点:

  1. Preview 事件需要替换

    • WPF 中 PreviewMouseDown → Avalonia 中 PointerPressed 或自定义隧道事件。

    • 例如:

      AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
      
  2. 自定义事件注册

    • WPF 的 EventManager.RegisterRoutedEvent → Avalonia 的 RoutedEvent.Register
    • 需要指定控件类型和事件参数类型。
  3. 事件处理顺序

    • Avalonia 冒泡和隧道事件顺序与 WPF 相同:Tunnel → 源控件 → Bubble。
    • 如果依赖 Preview* 的事件拦截逻辑,需要明确使用 RoutingStrategies.Tunnel
  4. 事件参数自定义

    • Avalonia 推荐自定义事件参数时继承 RoutedEventArgs 并泛型化。
  5. 绑定命令替代

    • 在 WPF 中,有些路由事件用于触发 Command,在 Avalonia 中可以使用 ReactiveCommandInteraction 实现类似功能。

五、示例:WPF 与 Avalonia 对比

WPF

<Button Content="Click Me" PreviewMouseDown="Button_PreviewMouseDown"/>
private void Button_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
    MessageBox.Show("WPF PreviewMouseDown");
}

Avalonia

<Button Content="Click Me" />
myButton.AddHandler(PointerPressedEvent, (s, e) =>
{
    Console.WriteLine("Avalonia PointerPressed (Tunnel equivalent)");
}, RoutingStrategies.Tunnel);

六、基于 BlinkingButton 控件的详细使用示例

在实例中,通过点击BlinkingButton控件,控制它的IsBlinking属性,触发定义的BlinkingStartedEvent 和BlinkingStoppedEvent 事件,控制闪烁,并且在Title上显示不同的文字,直观感受路由事件的用法。

WPF

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;

namespace WpfDemo.control;

public class BlinkingButton : Button
{
    public static readonly DependencyProperty
        IsBlinkingProperty = DependencyProperty.Register(
            nameof(IsBlinking), typeof(bool), typeof(BlinkingButton), new PropertyMetadata(false, OnIsBlinkingChanged));


    // ================== 路由事件定义 ==================
    public static readonly RoutedEvent BlinkingStartedEvent =
        EventManager.RegisterRoutedEvent(
            nameof(BlinkingStarted),
            RoutingStrategy.Bubble, // 事件冒泡
            typeof(RoutedEventHandler),
            typeof(BlinkingButton));

    public static readonly RoutedEvent BlinkingStoppedEvent =
        EventManager.RegisterRoutedEvent(
            nameof(BlinkingStopped),
            RoutingStrategy.Bubble,
            typeof(RoutedEventHandler),
            typeof(BlinkingButton));

    private Storyboard? _blinkStoryboard;

    static BlinkingButton()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(BlinkingButton),
            new FrameworkPropertyMetadata(typeof(BlinkingButton)));
    }

    public bool IsBlinking
    {
        get => (bool)GetValue(IsBlinkingProperty);
        set => SetValue(IsBlinkingProperty, value);
    }

    // CLR 封装
    public event RoutedEventHandler BlinkingStarted
    {
        add => AddHandler(BlinkingStartedEvent, value);
        remove => RemoveHandler(BlinkingStartedEvent, value);
    }

    public event RoutedEventHandler BlinkingStopped
    {
        add => AddHandler(BlinkingStoppedEvent, value);
        remove => RemoveHandler(BlinkingStoppedEvent, value);
    }

    private static void OnIsBlinkingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var btn = (BlinkingButton)d;
        if ((bool)e.NewValue)
            btn.StartBlinking();
        else
            btn.StopBlinking();
    }

    private void StartBlinking()
    {
        if (_blinkStoryboard == null)
        {
            var animation = new DoubleAnimation
            {
                From = 1.0,
                To = 0.3,
                Duration = new Duration(TimeSpan.FromSeconds(0.6)),
                AutoReverse = true,
                RepeatBehavior = RepeatBehavior.Forever
            };
            _blinkStoryboard = new Storyboard();
            _blinkStoryboard.Children.Add(animation);
            Storyboard.SetTarget(animation, this);
            Storyboard.SetTargetProperty(animation, new PropertyPath("Opacity"));
        }

        _blinkStoryboard.Begin();
        RaiseEvent(new RoutedEventArgs(BlinkingStartedEvent, this));
    }

    private void StopBlinking()
    {
        _blinkStoryboard?.Stop();
        Opacity = 1.0;
        // 触发路由事件
        RaiseEvent(new RoutedEventArgs(BlinkingStoppedEvent, this));
    }
}
<Window
    Height="300"
    Title="MainWindow"
    Width="600"
    mc:Ignorable="d"
    x:Class="WpfDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:control="clr-namespace:WpfDemo.control"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <control:BlinkingButton
            BlinkingStarted="BlinkingButton_OnBlinkingStarted"
            BlinkingStopped="BlinkingButton_OnBlinkingStopped"
            Click="ButtonBase_OnClick"
            Content="点击我"
            FontSize="20"
            Grid.Row="0"
            Height="80"
            HorizontalAlignment="Center"
            IsBlinking="True"
            VerticalAlignment="Center"
            Width="300"
            x:Name="MyBlinkButton" />
    </Grid>
</Window>

using System.Windows;

namespace WpfDemo;

/// <summary>
///     Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }


    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        MyBlinkButton.IsBlinking = !MyBlinkButton.IsBlinking;
    }

    private void BlinkingButton_OnBlinkingStarted(object sender, RoutedEventArgs e)
    {
        Title = "警告:按钮正在闪烁!";
    }

    private void BlinkingButton_OnBlinkingStopped(object sender, RoutedEventArgs e)
    {
        Title = "路由事件 Demo";
    }
}

Avalonia

using System;
using System.Threading;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Styling;

namespace AvaloniaDemo.Controls;

public class BlinkingButton : Button
{
    public static readonly StyledProperty<bool> IsBlinkingProperty =
        AvaloniaProperty.Register<BlinkingButton, bool>(nameof(IsBlinking));

    // 路由事件定义
    public static readonly RoutedEvent<RoutedEventArgs> BlinkingStartedEvent =
        RoutedEvent.Register<BlinkingButton, RoutedEventArgs>(
            nameof(BlinkingStarted),
            RoutingStrategies.Bubble);

    public static readonly RoutedEvent<RoutedEventArgs> BlinkingStoppedEvent =
        RoutedEvent.Register<BlinkingButton, RoutedEventArgs>(
            nameof(BlinkingStopped),
            RoutingStrategies.Bubble);

    private Animation? _blinkAnimation;
    private CancellationTokenSource? _cts;

    public BlinkingButton()
    {
        // 监听属性变化
        this.GetObservable(IsBlinkingProperty).Subscribe(OnIsBlinkingChanged);
    }

    public bool IsBlinking
    {
        get => GetValue(IsBlinkingProperty);
        set => SetValue(IsBlinkingProperty, value);
    }

    // CLR 包装
    public event EventHandler<RoutedEventArgs>? BlinkingStarted
    {
        add => AddHandler(BlinkingStartedEvent, value);
        remove => RemoveHandler(BlinkingStartedEvent, value);
    }

    public event EventHandler<RoutedEventArgs>? BlinkingStopped
    {
        add => AddHandler(BlinkingStoppedEvent, value);
        remove => RemoveHandler(BlinkingStoppedEvent, value);
    }

    private void OnIsBlinkingChanged(bool isBlinking)
    {
        if (isBlinking)
            StartBlinking();
        else
            StopBlinking();
    }

    private void StartBlinking()
    {
        _blinkAnimation ??= new Animation
        {
            Duration = TimeSpan.FromSeconds(1.2),
            IterationCount = IterationCount.Infinite,
            Children =
            {
                new KeyFrame
                {
                    Cue = new Cue(0d),
                    Setters = { new Setter(OpacityProperty, 1.0) }
                },
                new KeyFrame
                {
                    Cue = new Cue(0.5d),
                    Setters = { new Setter(OpacityProperty, 0.3) }
                },
                new KeyFrame
                {
                    Cue = new Cue(1d),
                    Setters = { new Setter(OpacityProperty, 1.0) }
                }
            }
        };
        // 取消上一次动画
        _cts?.Cancel();

        _cts = new CancellationTokenSource();
        _blinkAnimation.RunAsync(this, _cts.Token);
        // 触发路由事件
        RaiseEvent(new RoutedEventArgs(BlinkingStartedEvent));
    }

    private void StopBlinking()
    {
        if (_blinkAnimation != null)
        {
            _cts?.Cancel(); // 立即停止动画
            _cts = null;
            Opacity = 1.0;
        }

        // 触发路由事件
        RaiseEvent(new RoutedEventArgs(BlinkingStoppedEvent));
    }
}
<Window
    Height="300"
    Icon="/Assets/avalonia-logo.ico"
    Title="AvaloniaDemo"
    Width="600"
    d:DesignHeight="300"
    d:DesignWidth="600"
    mc:Ignorable="d"
    x:Class="AvaloniaDemo.Views.MainWindow"
    x:DataType="vm:MainWindowViewModel"
    xmlns="https://github.com/avaloniaui"
    xmlns:controls="clr-namespace:AvaloniaDemo.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="using:AvaloniaDemo.ViewModels"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Design.DataContext>
        <!--
            This only sets the DataContext for the previewer in an IDE,
            to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs)
        -->
        <vm:MainWindowViewModel />
    </Design.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <controls:BlinkingButton
            BlinkingStarted="BlinkingButton_OnBlinkingStarted"
            BlinkingStopped="BlinkingButton_OnBlinkingStopped"
            Click="Button_OnClick"
            Content="点击我"
            Grid.Row="0"
            Height="80"
            HorizontalAlignment="Center"
            IsBlinking="False"
            VerticalAlignment="Center"
            Width="300"
            x:Name="BlinkBtn" />
    </Grid>
</Window>
using Avalonia.Controls;
using Avalonia.Interactivity;

namespace AvaloniaDemo.Views;

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

    private void Button_OnClick(object? sender, RoutedEventArgs e)
    {
        BlinkBtn.IsBlinking = !BlinkBtn.IsBlinking;
    }

    private void BlinkingButton_OnBlinkingStarted(object? sender, RoutedEventArgs e)
    {
        Title = "⚠ 警告:按钮正在闪烁!";
    }

    private void BlinkingButton_OnBlinkingStopped(object? sender, RoutedEventArgs e)
    {
        Title = "路由事件 Demo";
    }
}

七、小结

特性 WPF Avalonia 迁移技巧
冒泡事件 RoutingStrategy.Bubble RoutingStrategies.Bubble 保持原有逻辑即可
隧道事件 Preview* / RoutingStrategy.Tunnel RoutingStrategies.Tunnel 用 Tunnel 代替 Preview* 前缀
直接事件 RoutingStrategy.Direct RoutingStrategies.Direct 基本一致
自定义事件注册 EventManager.RegisterRoutedEvent RoutedEvent.Register<T, Args> 泛型指定控件类型和事件参数
添加处理器 AddHandler AddHandler 语法稍有不同,Avalonia 可省略委托类型
事件参数 RoutedEventArgs / MouseEventArgs RoutedEventArgs / PointerEventArgs 可自定义泛型事件参数

迁移过程中核心是理解 Avalonia 没有 Preview 前缀,而是通过 RoutingStrategies 指定策略,其他大部分逻辑与 WPF 类似,代码调整量不会太大。


通过掌握上述技巧,WPF 路由事件的迁移到 Avalonia 将变得顺畅,也为后续控件交互、命令绑定和自定义控件开发打下基础。


我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。
我的gitcode仓库是Avalonia学习项目
文中主要示例代码均可在仓库中查看,涵盖核心功能实现与优化方案。
点击链接即可直接访问,建议结合代码注释逐步调试。


网站公告

今日签到

点亮在社区的每一天
去签到