从 WPF 到 Avalonia 的迁移系列实战篇2:路由事件的异同点与迁移技巧
从 WPF 到 Avalonia 的迁移系列实战篇2:路由事件的异同点与迁移技巧
我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。
我的gitcode仓库是Avalonia学习项目。
文中主要示例代码均可在仓库中查看,涵盖核心功能实现与优化方案。
点击链接即可直接访问,建议结合代码注释逐步调试。
在 WPF 开发中,路由事件(Routed Event)是 UI 交互和控件通信的重要机制。而在 Avalonia 中,也提供了类似的事件机制,但实现方式和使用习惯与 WPF 有一定差异。本文将从概念、分类、注册、处理以及迁移技巧几个方面详细对比 WPF 和 Avalonia 的路由事件,帮助开发者顺利迁移项目。
一、路由事件的基本概念
WPF:
-
路由事件是
UIElement
或ContentElement
提供的一种事件传播机制。 -
支持三种路由策略:
- 冒泡事件(Bubbling):事件从源控件向父控件逐层传递。
- 隧道事件(Tunneling):事件从根控件向源控件逐层传递,通常以
Preview
开头,如PreviewMouseDown
。 - 直接事件(Direct):只在源控件触发,不向父控件传播。
Avalonia:
-
Avalonia 的路由事件机制与 WPF 类似,但没有
Preview
前缀,直接使用RoutingStrategies
来指定策略。 -
支持三种路由策略:
- Bubble(冒泡)
- Tunnel(隧道)
- 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 的过程中,有几个注意点:
-
Preview 事件需要替换
-
WPF 中
PreviewMouseDown
→ Avalonia 中PointerPressed
或自定义隧道事件。 -
例如:
AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
-
-
自定义事件注册
- WPF 的
EventManager.RegisterRoutedEvent
→ Avalonia 的RoutedEvent.Register
- 需要指定控件类型和事件参数类型。
- WPF 的
-
事件处理顺序
- Avalonia 冒泡和隧道事件顺序与 WPF 相同:Tunnel → 源控件 → Bubble。
- 如果依赖
Preview*
的事件拦截逻辑,需要明确使用RoutingStrategies.Tunnel
。
-
事件参数自定义
- Avalonia 推荐自定义事件参数时继承
RoutedEventArgs
并泛型化。
- Avalonia 推荐自定义事件参数时继承
-
绑定命令替代
- 在 WPF 中,有些路由事件用于触发
Command
,在 Avalonia 中可以使用ReactiveCommand
或Interaction
实现类似功能。
- 在 WPF 中,有些路由事件用于触发
五、示例: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 DependencyPropertyIsBlinkingProperty = 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();elsebtn.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));}
}
<WindowHeight="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:BlinkingButtonBlinkingStarted="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();elseStopBlinking();}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));}
}
<WindowHeight="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:BlinkingButtonBlinkingStarted="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学习项目。
文中主要示例代码均可在仓库中查看,涵盖核心功能实现与优化方案。
点击链接即可直接访问,建议结合代码注释逐步调试。