Avalonia UI는 WPF와 유사한 라우팅 이벤트(Routed Events) 시스템을 제공합니다. 라우팅 이벤트는 시각적 트리(Visual Tree)를 따라 전파되며, 이벤트가 발생하는 방향에 따라 크게 터널링(Tunnelling)과 버블링(Bubbling) 전략으로 나뉩니다. 이 글에서는 AddHandler 메서드를 사용하여 두 가지 라우팅 전략을 구현하고 제어하는 방법을 살펴봅니다.
1. UI 레이아웃 구성 (XAML)
터널링과 버블링 이벤트를 테스트하기 위해 StackPanel 내에 두 개의 Border와 Button을 배치합니다. 각 버튼은 서로 다른 라우팅 전략을 검증하는 데 사용됩니다.
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="400" d:DesignHeight="300"
x:Class="EventRoutingDemo.Views.MainWindow"
Title="Avalonia Routing Events">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="24">
<Border Name="TunnelContainer" Background="#E0F7FA" CornerRadius="12" Padding="24">
<Button Name="BtnTunnel" Content="Tunnelling 테스트" />
</Border>
<Border Name="BubbleContainer" Background="#FFF3E0" CornerRadius="12" Padding="24">
<Button Name="BtnBubble" Content="Bubbling 테스트" />
</Border>
</StackPanel>
</Window>
2. 이벤트 핸들러 및 라우팅 전략 구현 (C#)
코드 비하인드(Code-Behind)에서 PointerPressedEvent를 구독합니다. 터널링은 루트 요소에서 타겟 요소로 이벤트가 하향 전파되며, 버블링은 타겟 요소에서 루트 요소로 상향 전파됩니다. 특히 버블링 단계에서는 handledEventsToo 매개변수를 true로 설정하여, 하위 요소에서 이미 처리된(Handled = true) 이벤트도 강제로 캡처할 수 있도록 구성합니다.
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using System.Threading.Tasks;
namespace EventRoutingDemo.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// 1. 터널링(Tunnelling) 이벤트 등록: 외부(루트)에서 내부(타겟)로 전파
this.AddHandler(
InputElement.PointerPressedEvent,
OnTunnelingPointerPressed,
RoutingStrategies.Tunnel);
// 2. 버블링(Bubbling) 이벤트 등록: 내부(타겟)에서 외부(루트)로 전파
this.AddHandler(
InputElement.PointerPressedEvent,
OnBubblingPointerPressed,
RoutingStrategies.Bubble,
handledEventsToo: true); // 하위 컨트롤이 이벤트를 소비해도 강제로 수신
}
private async void OnTunnelingPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.Source is Visual sourceVisual)
{
// 이벤트 소스에서 가장 가까운 Button 조상 찾기
var targetButton = sourceVisual.FindAncestorOfType<Button>(includeSelf: true);
if (targetButton?.Name == "BtnTunnel")
{
await ShowCustomDialog(
"터널링 단계 감지",
"이벤트가 Button에 도달하기 전에 외부 Border(루트)에서 먼저 감지되었습니다.");
}
}
}
private async void OnBubblingPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.Source is Visual sourceVisual)
{
var targetButton = sourceVisual.FindAncestorOfType<Button>(includeSelf: true);
if (targetButton?.Name == "BtnBubble")
{
await ShowCustomDialog(
"버블링 단계 감지",
"Button 클릭 후 이벤트가 외부 Border(루트)로 성공적으로 전파되었습니다.");
}
}
}
private async Task ShowCustomDialog(string title, string message)
{
var dialog = new Window
{
Title = title,
Width = 320,
Height = 160,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = new TextBlock
{
Text = message,
Margin = new Avalonia.Thickness(20),
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
TextWrapping = Avalonia.Media.TextWrapping.Wrap
}
};
await dialog.ShowDialog(this);
}
}
}
3. 동작 원리 분석
- 터널링 (Tunnelling):
RoutingStrategies.Tunnel로 등록하면 포인터 입력이 발생했을 때Window레벨에서 이벤트가 먼저 트리거된 후Border를 거쳐 최종적으로Button에 도달합니다. 이를 통해 하위 컨트롤이 이벤트를 처리하기 전에 상위 컨테이너에서 입력을 가로채거나 사전 검증을 수행할 수 있습니다. - 버블링 (Bubbling):
RoutingStrategies.Bubble로 등록하면Button에서 발생한 이벤트가Border를 거쳐Window까지 올라갑니다. 기본적으로 Avalonia의Button은 클릭 이벤트를 내부적으로 처리(e.Handled = true)하여 버블링을 중단시키지만,handledEventsToo: true옵션을 사용하면 이미 소비된 이벤트도 상위 요소에서 안정적으로 수신할 수 있습니다. - 시각적 트리 탐색:
e.Source는 실제 클릭된 가장 안쪽 요소(예:TextBlock또는AccessText)일 수 있습니다. 따라서FindAncestorOfType<T>확장 메서드를 사용하여 시각적 트리를 거슬러 올라가며 의도한Button컨트롤을 정확하게 식별합니다.