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

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

我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。

我的gitcode仓库是Avalonia学习项目。

文中主要示例代码均可在仓库中查看,涵盖核心功能实现与优化方案。

点击链接即可直接访问,建议结合代码注释逐步调试。

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


一、路由事件的基本概念

WPF:

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

  • 支持三种路由策略:

    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 注册路由事件

csharp 复制代码
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 注册路由事件

csharp 复制代码
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 触发事件

csharp 复制代码
RaiseEvent(new RoutedEventArgs(MyClickEvent));

2. Avalonia 触发事件

csharp 复制代码
RaiseEvent(new RoutedEventArgs(MyClickEvent));

🔹 相同点:触发事件都使用 RaiseEvent,参数都是对应事件对象。

🔹 不同点:Avalonia 的 RoutedEventArgs 泛型更灵活,可携带自定义事件参数。

3. 事件处理方式

WPF:
csharp 复制代码
myButton.AddHandler(MyButton.MyClickEvent, new RoutedEventHandler(OnMyClick));
Avalonia:
csharp 复制代码
myButton.AddHandler(MyButton.MyClickEvent, OnMyClick);

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


四、路由事件的迁移技巧

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

  1. Preview 事件需要替换

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

    • 例如:

      csharp 复制代码
      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 中可以使用 ReactiveCommand 或 Interaction 实现类似功能。

五、示例:WPF 与 Avalonia 对比

WPF

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

Avalonia

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

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

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

WPF

csharp 复制代码
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));
    }
}
xml 复制代码
<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>
csharp 复制代码
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

csharp 复制代码
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));
    }
}
xml 复制代码
<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>
csharp 复制代码
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学习项目。

文中主要示例代码均可在仓库中查看,涵盖核心功能实现与优化方案。

点击链接即可直接访问,建议结合代码注释逐步调试。

相关推荐
光泽雨1 天前
WPF中的依赖属性
wpf
海马1 天前
Spring Cloud 开发知识整理
spring·spring cloud·wpf
闻哥1 天前
分布式任务调度框架:XXL-Job / ElasticJob / DolphinScheduler / SchedulerX 架构对比与选型
分布式·架构·wpf
预立科技2 天前
CDN(内容分发网络)知识总结
网络·wpf·cdn
FuckPatience3 天前
WPF DataGrid 行根据某个属性循环变色动画
wpf
一条闲鱼_mytube5 天前
高并发短链接系统设计
wpf
一个天蝎座 白勺 程序猿6 天前
IoTDB集群扩容:从慌乱到从容的经验分享
数据库·wpf·时序数据库·iotdb
samble6 天前
从零搭建灌装监控系统(十三):报警系统设计,生命周期与通知
c#·wpf·mvvm·modbus·工业控制
samble7 天前
从零搭建灌装监控系统(十二):配置系统,原子写入与容错
c#·wpf·mvvm·modbus·工业控制
蜗牛互联网10 天前
OLMo-core 3的token gerrymandering提醒:MoE路由要按时间窗验收
java·人工智能·wpf