Avalonia UI 라우팅 이벤트 심화: 터널링과 버블링 전략 구현

Avalonia UI는 WPF와 유사한 라우팅 이벤트(Routed Events) 시스템을 제공합니다. 라우팅 이벤트는 시각적 트리(Visual Tree)를 따라 전파되며, 이벤트가 발생하는 방향에 따라 크게 터널링(Tunnelling)버블링(Bubbling) 전략으로 나뉩니다. 이 글에서는 AddHandler 메서드를 사용하여 두 가지 라우팅 전략을 구현하고 제어하는 방법을 살펴봅니다.

1. UI 레이아웃 구성 (XAML)

터널링과 버블링 이벤트를 테스트하기 위해 StackPanel 내에 두 개의 BorderButton을 배치합니다. 각 버튼은 서로 다른 라우팅 전략을 검증하는 데 사용됩니다.

<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 컨트롤을 정확하게 식별합니다.

태그: AvaloniaUI csharp XAML RoutedEvents Tunnelling

7월 21일 19:35에 게시됨