라우트된 이벤트를 처리된 것으로 표시 및 클래스 처리(WPF .NET)

라우트된 이벤트를 처리된 것으로 표시하는 시기에 대한 절대적인 규칙은 없지만, 코드가 중요한 방식으로 이벤트에 응답하는 경우 이벤트를 처리된 것으로 표시하는 것이 좋습니다. 처리된 것으로 표시된 라우트된 이벤트는 계속해서 해당 경로를 따르지만, 처리된 이벤트에 응답하도록 구성된 처리기만 호출됩니다. 기본적으로 라우트된 이벤트를 처리된 것으로 표시하면 이벤트 경로를 따라 수신기에 대한 가시성이 제한됩니다.

라우트된 이벤트 처리기는 인스턴스 처리기 또는 클래스 처리기입니다. 인스턴스 처리기는 개체 또는 XAML 요소에서 라우트된 이벤트를 처리합니다. 클래스 처리기는 클래스 수준에서 라우트된 이벤트를 처리하며, 클래스의 인스턴스에서 동일한 이벤트에 응답하는 인스턴스 처리기보다 먼저 호출됩니다. 라우트된 이벤트가 처리된 것으로 표시되는 경우 클래스 처리기 내에서도 처리된 것으로 표시되는 경우가 자주 있습니다. 이 문서에서는 라우트된 이벤트를 처리된 것으로 표시할 때의 이점과 잠재적인 문제, 라우트된 이벤트 및 라우트된 이벤트 처리기의 다양한 유형, 복합 컨트롤에서 이벤트 표시 안 함에 대해 설명합니다.

중요

.NET 7 및 .NET 6에 관한 데스크톱 가이드 설명서는 제작 중입니다.

필수 구성 요소

이 문서에서는 독자들이 라우트된 이벤트에 대한 기본 지식을 갖고 있으며 라우트된 이벤트 개요를 읽었다고 가정합니다. XAML(Extensible Application Markup Language)에 익숙하고 WPF(Windows Presentation Foundation) 애플리케이션을 작성하는 방법을 알고 있으면 이 문서의 예제를 따라 하는 데 도움이 됩니다.

라우트된 이벤트를 처리된 것으로 표시하는 시기

일반적으로 하나의 처리기만 각각의 라우트된 이벤트에 대해 중요한 응답을 제공해야 합니다. 라우트된 이벤트 시스템을 사용하여 여러 처리기에서 중요한 응답을 제공하지 마세요. 중요한 응답에 대한 정의는 주관적이며 애플리케이션에 따라 달라집니다. 일반 지침:

  • 중요한 응답에는 포커스 설정, 공용 상태 수정, 시각적 표현에 영향을 주는 속성 설정, 새 이벤트 발생 및 이벤트 완전 처리가 포함됩니다.
  • 중요하지 않은 응답에는 시각적 또는 프로그래밍적 영향 없이 비공개 상태를 수정하고, 이벤트 로깅을 수행하고, 이벤트에 응답하지 않고 이벤트 데이터를 검사하는 것이 포함됩니다.

일부 WPF 컨트롤은 추가 처리가 필요 없는 구성 요소 수준 이벤트를 처리된 것으로 표시하여 이벤트를 표시하지 않습니다. 컨트롤에서 처리된 것으로 표시한 이벤트를 처리하려면 컨트롤에서 억제하는 이벤트 문제 해결을 참조하세요.

이벤트를 처리됨으로 표시하려면 이벤트 데이터의 Handled 속성 값을 true로 설정합니다. 이 값을 false로 되돌릴 수 있지만, 그렇게 해야 하는 경우는 드뭅니다.

미리 보기 및 버블링 라우트된 이벤트 쌍

미리 보기 및 버블링 라우트된 이벤트 쌍은 입력 이벤트와 관련이 있습니다. 여러 입력 이벤트는 터널링버블링 라우트된 이벤트 쌍을 구현합니다(예: PreviewKeyDownKeyDown). Preview 접두사는 미리 보기 이벤트가 완료되면 버블링 이벤트가 시작됨을 나타냅니다. 각 미리 보기 및 버블링 이벤트 쌍은 동일한 이벤트 데이터 인스턴스를 공유합니다.

라우트된 이벤트 처리기는 다음과 같이 이벤트의 라우팅 전략과 일치하는 순서로 호출됩니다.

  1. 미리 보기 이벤트가 애플리케이션 루트 요소에서 라우트된 이벤트를 발생시킨 요소로 이동합니다. 애플리케이션 루트 요소에 연결된 미리 보기 이벤트 처리기가 먼저 호출된 다음, 연속 중첩된 요소에 연결된 처리기가 호출됩니다.
  2. 미리 보기 이벤트가 완료되면 쌍을 이루는 버블링 이벤트가 라우트된 이벤트를 발생시킨 요소에서 애플리케이션 루트 요소로 이동합니다. 라우트된 이벤트를 발생시킨 동일한 요소에 연결된 버블링 이벤트 처리기가 먼저 호출된 다음, 연속 부모 요소에 연결된 처리기가 호출됩니다.

쌍을 이루는 미리 보기 및 버블링 이벤트는 자체적인 라우트된 이벤트를 선언하고 발생시키는 여러 WPF 클래스의 내부 구현에 포함됩니다. 해당 클래스 수준 내부 구현이 없으면 라우트된 이벤트 미리 보기 및 버블링 이벤트가 완전히 분리되며 이벤트 이름 지정에 관계없이 이벤트 데이터를 공유하지 않습니다. 사용자 지정 클래스에서 버블링 또는 터널링 입력 라우트된 이벤트를 구현하는 방법에 대한 자세한 내용은 사용자 지정 라우트된 이벤트 만들기를 참조하세요.

각 미리 보기 및 버블링 이벤트 쌍은 동일한 이벤트 데이터 인스턴스를 공유하므로, 미리 보기 라우트된 이벤트가 처리된 것으로 표시되면 쌍을 이루는 버블링 이벤트도 처리된 것으로 표시됩니다. 버블링 라우트된 이벤트가 처리된 것으로 표시되어도 쌍을 이루는 미리 보기 이벤트에 영향을 주지 않습니다. 미리 보기 이벤트가 완료되었기 때문입니다. 미리 보기 및 버블링 입력 이벤트 쌍을 처리된 것으로 표시할 때는 주의해야 합니다. 처리된 미리 보기 입력 이벤트는 터널링 경로의 나머지 부분에 대해 정상적으로 등록된 이벤트 처리기를 호출하지 않으며, 쌍을 이루는 버블링 이벤트가 발생하지 않습니다. 처리된 버블링 입력 이벤트는 버블링 경로의 나머지 부분에 대해 정상적으로 등록된 이벤트 처리기를 호출하지 않습니다.

인스턴스 및 클래스 라우트된 이벤트 처리기

라우트된 이벤트 처리기는 인스턴스 처리기 또는 클래스 처리기입니다. 지정된 클래스에 대한 클래스 처리기는 해당 클래스의 인스턴스에서 동일한 이벤트에 응답하는 인스턴스 처리기보다 먼저 호출됩니다. 이 동작 때문에, 라우트된 이벤트가 처리된 것으로 표시되는 경우 클래스 처리기 내에서도 처리된 것으로 표시되는 경우가 자주 있습니다. 클래스 처리기는 다음 두 가지 유형이 있습니다.

인스턴스 이벤트 처리기

AddHandler 메서드를 직접 호출하여 개체 또는 XAML 요소에 인스턴스 처리기를 연결할 수 있습니다. WPF 라우트된 이벤트는 AddHandler 메서드를 사용하여 이벤트 처리기를 연결하는 CLR(공용 언어 런타임) 이벤트 래퍼를 구현합니다. 이벤트 처리기를 연결하기 위한 XAML 특성 구문은 CLR 이벤트 래퍼를 호출하므로 XAML에서 처리기를 연결해도 AddHandler 호출로 확인됩니다. 처리된 이벤트의 경우:

  • XAML 특성 구문 또는 AddHandler의 공통 서명을 사용하여 연결된 처리기는 호출되지 않습니다.
  • handledEventsToo 매개 변수가 true로 설정된 AddHandler(RoutedEvent, Delegate, Boolean) 오버로드를 사용하여 연결된 처리기가 호출됩니다. 이 오버로드는 처리된 이벤트에 응답해야 하는 아주 가끔 있는 경우에 사용할 수 있습니다. 예를 들어 요소 트리의 일부 요소는 이벤트를 처리된 것으로 표시했지만 이벤트 경로를 따라 더 멀리 있는 다른 요소는 처리된 이벤트에 응답해야 합니다.

다음 XAML 샘플은 componentTextBox라는 TextBoxouterStackPanel이라는 StackPanel에 추가하는 componentWrapper라는 사용자 지정 컨트롤을 추가합니다. PreviewKeyDown 이벤트에 대한 인스턴스 이벤트 처리기는 XAML 특성 구문을 사용하여 componentWrapper에 연결됩니다. 결과적으로 인스턴스 처리기는 componentTextBox에서 발생한 처리되지 않은 PreviewKeyDown 터널링 이벤트에만 응답합니다.

<StackPanel Name="outerStackPanel" VerticalAlignment="Center">
    <custom:ComponentWrapper
        x:Name="componentWrapper"
        TextBox.PreviewKeyDown="HandlerInstanceEventInfo"
        HorizontalAlignment="Center">
        <TextBox Name="componentTextBox" Width="200" />
    </custom:ComponentWrapper>
</StackPanel>

MainWindow 생성자는 handledEventsToo 매개 변수가 true로 설정된 UIElement.AddHandler(RoutedEvent, Delegate, Boolean) 오버로드를 사용하여 KeyDown 버블링 이벤트에 대한 인스턴스 처리기를 componentWrapper에 연결합니다. 결과적으로 인스턴스 이벤트 처리기는 처리되지 않은 이벤트와 처리된 이벤트 모두에 응답합니다.

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

        // Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.AddHandler(KeyDownEvent, new RoutedEventHandler(Handler.InstanceEventInfo),
            handledEventsToo: true);
    }

    // The handler attached to componentWrapper in XAML.
    public void HandlerInstanceEventInfo(object sender, KeyEventArgs e) => 
        Handler.InstanceEventInfo(sender, e);
}
Partial Public Class MainWindow
    Inherits Window

    Public Sub New()
        InitializeComponent()

        ' Attach an instance handler on componentWrapper that will be invoked by handled KeyDown events.
        componentWrapper.[AddHandler](KeyDownEvent, New RoutedEventHandler(AddressOf InstanceEventInfo),
                                      handledEventsToo:=True)
    End Sub

    ' The handler attached to componentWrapper in XAML.
    Public Sub HandlerInstanceEventInfo(sender As Object, e As KeyEventArgs)
        InstanceEventInfo(sender, e)
    End Sub

End Class

ComponentWrapper의 코드 숨김 구현은 다음 섹션에 나와 있습니다.

정적 클래스 이벤트 처리기

클래스의 정적 생성자에서 RegisterClassHandler 메서드를 호출하여 정적 클래스 이벤트 처리기를 연결할 수 있습니다. 클래스 계층의 각 클래스는 각각의 라우트된 이벤트에 대한 자체 정적 클래스 처리기를 등록할 수 있습니다. 따라서 이벤트 경로의 특정 노드에서 동일한 이벤트에 대해 여러 정적 클래스 처리기가 호출될 수 있습니다. 이벤트에 대한 이벤트 경로가 생성되면 각 노드에 대한 모든 정적 클래스 처리기가 이벤트 경로에 추가됩니다. 노드에서 정적 클래스 처리기를 호출하는 순서는 가장 많이 파생된 정적 클래스 처리기부터 시작하여 각 연속 기본 클래스의 정적 클래스 처리기로 이어집니다.

handledEventsToo 매개 변수가 true로 설정된 RegisterClassHandler(Type, RoutedEvent, Delegate, Boolean) 오버로드를 사용하여 등록된 정적 클래스 이벤트 처리기는 처리되지 않은 이벤트와 처리된 라우트된 이벤트 모두에 응답합니다.

정적 클래스 처리기는 일반적으로 처리되지 않은 이벤트에만 응답하도록 등록됩니다. 이 경우 노드의 파생 클래스 처리기가 이벤트를 처리된 것으로 표시하면 해당 이벤트에 대한 기본 클래스 처리기가 호출되지 않습니다. 이 시나리오에서 기본 클래스 처리기는 파생 클래스 처리기로 효과적으로 대체됩니다. 기본 클래스 처리기는 시각적 모양, 상태 논리, 입력 처리 및 명령 처리와 같은 영역에서 컨트롤 디자인에 영향을 주는 경우가 많으므로 주의해서 바꿔야 합니다. 이벤트를 처리된 것으로 표시하지 않는 파생 클래스 처리기는 기본 클래스 처리기를 바꾸지 않고 보완합니다.

다음 코드 샘플은 앞의 XAML에서 참조된 ComponentWrapper 사용자 지정 컨트롤의 클래스 계층 구조를 보여줍니다. ComponentWrapper 클래스는 ComponentWrapperBase 클래스에서 파생되며 이 클래스는 StackPanel 클래스에서 파생됩니다. ComponentWrapperComponentWrapperBase 클래스의 정적 생성자에 사용되는 RegisterClassHandler 메서드는 각 클래스에 대한 정적 클래스 이벤트 처리기를 등록합니다. WPF 이벤트 시스템은 ComponentWrapperBase 정적 클래스 처리기보다 ComponentWrapper 정적 클래스 처리기를 먼저 호출합니다.

public class ComponentWrapper : ComponentWrapperBase
{
    static ComponentWrapper()
    {
        // Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(typeof(ComponentWrapper), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfo_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfo_Override(this, e);

        // Call the base OnKeyDown implementation on ComponentWrapperBase.
        base.OnKeyDown(e);
    }
}

public class ComponentWrapperBase : StackPanel
{
    // Class event handler implemented in the static constructor.
    static ComponentWrapperBase()
    {
        EventManager.RegisterClassHandler(typeof(ComponentWrapperBase), KeyDownEvent, 
            new RoutedEventHandler(Handler.ClassEventInfoBase_Static));
    }

    // Class event handler that overrides a base class virtual method.
    protected override void OnKeyDown(KeyEventArgs e)
    {
        Handler.ClassEventInfoBase_Override(this, e);

        e.Handled = true;
        Debug.WriteLine("The KeyDown routed event is marked as handled.");

        // Call the base OnKeyDown implementation on StackPanel.
        base.OnKeyDown(e);
    }
}
Public Class ComponentWrapper
    Inherits ComponentWrapperBase

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapper), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfo_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfo_Override(Me, e)

        ' Call the base OnKeyDown implementation on ComponentWrapperBase.
        MyBase.OnKeyDown(e)
    End Sub

End Class

Public Class ComponentWrapperBase
    Inherits StackPanel

    Shared Sub New()
        ' Class event handler implemented in the static constructor.
        EventManager.RegisterClassHandler(GetType(ComponentWrapperBase), KeyDownEvent,
                                          New RoutedEventHandler(AddressOf ClassEventInfoBase_Static))
    End Sub

    ' Class event handler that overrides a base class virtual method.
    Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
        ClassEventInfoBase_Override(Me, e)

        e.Handled = True
        Debug.WriteLine("The KeyDown event is marked as handled.")

        ' Call the base OnKeyDown implementation on StackPanel.
        MyBase.OnKeyDown(e)
    End Sub

End Class

이 코드 샘플에서 재정의 클래스 이벤트 처리기의 코드 숨김 구현은 다음 섹션에서 설명합니다.

클래스 이벤트 처리기 재정의

일부 시각적 요소 기본 클래스는 각 공용 라우트된 입력 이벤트에 대해 빈 On<이벤트 이름>OnPreview<이벤트 이름> 가상 메서드를 노출합니다. 예를 들어 UIElementOnKeyDownOnPreviewKeyDown 가상 이벤트 처리기와 기타 여러 이벤트를 구현합니다. 기본 클래스 가상 이벤트 처리기를 재정의하여 파생 클래스에 대한 재정의 클래스 이벤트 처리기를 구현할 수 있습니다. 예를 들어 OnDragEnter 가상 메서드를 재정의하여 UIElement 파생 클래스에서 DragEnter 이벤트에 대한 재정의 클래스 처리기를 추가할 수 있습니다. 기본 클래스 가상 메서드를 재정의하는 것은 정적 생성자에 클래스 처리기를 등록하는 것보다 더 간단하게 클래스 처리기를 구현하는 방법입니다. 재정의 내에서 이벤트를 발생시키거나, 클래스별 논리를 시작하여 인스턴스의 요소 속성을 변경하거나, 이벤트를 처리된 것으로 표시하거나, 다른 이벤트 처리 논리를 수행할 수 있습니다.

정적 클래스 이벤트 처리기와 달리 WPF 이벤트 시스템은 클래스 계층에서 가장 많이 파생된 클래스에 대한 재정의 클래스 이벤트 처리기만 호출합니다. 그러면 클래스 계층 구조에서 가장 많이 파생된 클래스는 기본 키워드를 사용하여 가상 메서드의 기본 구현을 호출할 수 있습니다. 대부분의 경우 이벤트를 처리된 것으로 표시하는지 여부에 관계없이 기본 구현을 호출해야 합니다. 기본 구현 논리(있는 경우)를 바꿔야 하는 클래스 요구 사항이 있는 경우에만 기본 구현 호출을 생략해야 합니다. 코드를 재정의하기 전에 기본 구현을 호출할 것인지 아니면 재정의한 후에 호출할 것인지는 구현 특성에 따라 결정됩니다.

앞의 코드 샘플에서 기본 클래스 OnKeyDown 가상 메서드는 ComponentWrapper 클래스와 ComponentWrapperBase 클래스 모두에서 재정의됩니다. WPF 이벤트 시스템은 ComponentWrapper.OnKeyDown 재정의 클래스 이벤트 처리기만 호출하므로 해당 처리기는 base.OnKeyDown(e)을 사용하여 ComponentWrapperBase.OnKeyDown 재정의 클래스 이벤트 처리기를 호출합니다. 그러면 호출된 재정의 클래스 이벤트 처리기는 base.OnKeyDown(e)을 사용하여 StackPanel.OnKeyDown 가상 메서드를 호출합니다. 이전 코드 샘플의 이벤트 순서는 다음과 같습니다.

  1. componentWrapper에 연결된 인스턴스 처리기는 라우트된 이벤트 PreviewKeyDown에 의해 트리거됩니다.
  2. componentWrapper에 연결된 정적 클래스 처리기는 라우트된 이벤트 KeyDown에 의해 트리거됩니다.
  3. componentWrapperBase에 연결된 정적 클래스 처리기는 라우트된 이벤트 KeyDown에 의해 트리거됩니다.
  4. componentWrapper에 연결된 재정의 클래스 처리기는 라우트된 이벤트 KeyDown에 의해 트리거됩니다.
  5. componentWrapperBase에 연결된 재정의 클래스 처리기는 라우트된 이벤트 KeyDown에 의해 트리거됩니다.
  6. 라우트된 이벤트 KeyDown은 처리된 것으로 표시됩니다.
  7. componentWrapper에 연결된 인스턴스 처리기는 라우트된 이벤트 KeyDown에 의해 트리거됩니다. 처리기는 handledEventsToo 매개 변수를 true로 설정하여 등록되었습니다.

복합 컨트롤에서 입력 이벤트 표시 안 함

일부 복합 컨트롤은 입력 이벤트를 더 많은 정보를 전달하거나 보다 구체적인 동작을 의미하는 사용자 지정 상위 수준 이벤트로 바꾸기 위해 구성 요소 수준에서 입력 이벤트를 표시하지 않습니다. 복합 컨트롤은 그 이름처럼 여러 개의 실용적인 컨트롤 또는 컨트롤 기본 클래스로 구성됩니다. 클래식 예제는 다양한 마우스 이벤트를 Click 라우트된 이벤트로 변환하는 Button 컨트롤입니다. Button 기본 클래스는 UIElement에서 간접적으로 파생되는 ButtonBase 클래스입니다. 컨트롤 입력 처리에 필요한 대부분의 이벤트 인프라는 UIElement 수준에서 사용할 수 있습니다. UIElementMouseLeftButtonDownMouseRightButtonDown과 같은 여러 Mouse 이벤트를 노출합니다. UIElement는 빈 가상 메서드 OnMouseLeftButtonDownOnMouseRightButtonDown을 미리 등록된 클래스 처리기로 구현합니다. ButtonBase는 이러한 클래스 처리기를 재정의하고, 재정의 처리기 내에서 Handled 속성을 true로 설정하고 Click 이벤트를 발생시킵니다. 대부분의 수신기에서 최종 결과는 MouseLeftButtonDownMouseRightButtonDown 이벤트가 숨겨지고 상위 수준 Click 이벤트가 표시되는 것입니다.

입력 이벤트 표시 안 함 해결

개별 컨트롤 내에서 이벤트를 표시하지 않으면 애플리케이션의 이벤트 처리 논리에 방해가 되는 경우가 있습니다. 예를 들어 애플리케이션에서 XAML 특성 구문을 사용하여 XAML 루트 요소의 MouseLeftButtonDown 이벤트에 대한 처리기를 연결한 경우 Button 컨트롤이 MouseLeftButtonDown 이벤트를 처리된 것으로 표시하기 때문에 해당 처리기가 호출되지 않습니다. 애플리케이션의 루트를 향하는 요소가 처리된 라우트된 이벤트에 대해 호출되도록 하려면 다음 방법 중 하나를 사용하면 됩니다.

  • handledEventsToo 매개 변수가 true로 설정된 UIElement.AddHandler(RoutedEvent, Delegate, Boolean) 메서드를 호출하여 처리기를 연결합니다. 이 방법을 사용하려면 연결할 요소에 대한 개체 참조를 가져온 후 이벤트 처리기를 코드 숨김으로 연결해야 합니다.

  • 처리된 것으로 표시된 이벤트가 버블링 입력 이벤트인 경우 쌍을 이루는 미리 보기 이벤트에 대한 처리기(사용 가능한 경우)를 연결합니다. 예를 들어 컨트롤이 MouseLeftButtonDown 이벤트를 표시하지 않는 경우 PreviewMouseLeftButtonDown 이벤트에 대한 처리기를 대신 연결할 수 있습니다. 이 방법은 이벤트 데이터를 공유하는 미리 보기 및 버블링 입력 이벤트 쌍에서만 작동합니다. PreviewMouseLeftButtonDown을 처리된 것으로 표시하지 않도록 주의하세요. 처리된 것으로 표시하면 Click 이벤트가 전혀 표시되지 않습니다.

입력 이벤트 표시 안함 문제를 해결하는 방법의 예는 컨트롤에서 억제하는 이벤트 문제 해결을 참조하세요.

참고 항목