약한 이벤트 패턴(WPF .NET)

애플리케이션에서는 이벤트 원본에 연결된 처리기는 처리기를 원본에 연결한 수신기 개체와 조정하여 제거되지 않을 수 있습니다. 이 경우 메모리 누수로 이어질 수 있습니다. WPF(Windows Presentation Foundation)에는 이 문제를 해결하는 데 사용할 수 있는 디자인 패턴이 도입되었습니다. 디자인 패턴은 특정 이벤트에 대한 전용 관리자 클래스를 제공하고 해당 이벤트에 대한 수신기에 대한 인터페이스를 구현합니다. 이 디자인 패턴을 약한 이벤트 패턴이라고 합니다.

중요

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

필수 구성 요소

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

약한 이벤트 패턴을 구현하는 이유는 무엇인가요?

이벤트를 수신 대기하면 메모리 누수가 발생할 수 있습니다. 이벤트를 수신 대기하는 보통의 방법은 처리기를 원본의 이벤트에 연결하는 언어별 구문을 사용하는 것입니다. 예를 들어 C# 문 source.SomeEvent += new SomeEventHandler(MyEventHandler) 또는 VB 문 AddHandler source.SomeEvent, AddressOf MyEventHandler입니다. 그러나 이 기술은 이벤트 원본에서 이벤트 수신기에 대한 강력한 참조를 만듭니다. 이벤트 처리기가 명시적으로 등록 취소되지 않는 한 수신기의 개체 수명은 원본의 개체 수명에 영향을 받습니다. 특정 상황에서는 수신기의 개체 수명이 현재 애플리케이션의 시각적 트리에 속하는지 여부와 같은 다른 요인에 의해 제어되도록 할 수 있습니다. 원본 개체 수명이 수신기의 유용한 개체 수명을 초과할 때마다 수신기는 필요한 것보다 더 오래 활성 상태로 유지됩니다. 이 경우 할당되지 않은 메모리는 메모리 누수에 해당합니다.

약한 이벤트 패턴은 이 메모리 누수 문제를 해결하도록 설계되었습니다. 수신기가 이벤트에 등록해야 할 때마다 약한 이벤트 패턴을 사용할 수 있지만 수신기는 등록 취소 시점을 명시적으로 알지 못합니다. 소스의 개체 수명이 수신기의 유용한 개체 수명을 초과할 때마다 약한 이벤트 패턴을 사용할 수도 있습니다. 이 경우 유용성은 사용자가 결정합니다. 약한 이벤트 패턴을 사용하면 수신기가 어떤 방식으로든 수신기의 개체 수명 특성에 영향을 주지 않고 이벤트를 등록하고 받을 수 있습니다. 실제로 원본의 암시적 참조는 수신기가 가비지 수집에 적합한지 여부를 결정하지 않습니다. 참조는 약한 참조이므로 약한 이벤트 패턴 및 관련 API의 이름을 지정합니다. 수신기는 가비지 수집되거나 제거될 수 있으며, 원본은 이제 소멸된 개체에 대한 수집 불가능한 처리기 참조를 유지하지 않고도 계속할 수 있습니다.

약한 이벤트 패턴은 누가 구현해야 하나요?

약한 이벤트 패턴은 주로 컨트롤 작성자와 관련이 있습니다. 컨트롤 작성자는 컨트롤의 동작과 억제, 컨트롤이 삽입되는 애플리케이션에 미치는 영향에 대해 주로 책임을 집니다. 여기에는 컨트롤 개체 수명 동작, 특히 설명된 메모리 누수 문제의 처리가 포함됩니다.

특정 시나리오는 기본적으로 약한 이벤트 패턴의 적용에 적합합니다. 이러한 시나리오 중 하나는 데이터 바인딩입니다. 데이터 바인딩에서 원본 개체는 바인딩의 대상인 수신기 개체와 독립적이어야 합니다. WPF 데이터 바인딩의 많은 측면에는 이미 이벤트가 구현되는 방식에 약한 이벤트 패턴이 적용되어 있습니다.

약한 이벤트 패턴을 구현하는 방법

약한 이벤트 패턴을 구현하는 방법에는 네 가지가 있으며 각 방법은 다른 이벤트 관리자를 사용합니다. 시나리오에 가장 적합한 이벤트 관리자를 선택합니다.

  • 기존 약한 이벤트 관리자:

    구독하려는 이벤트에 해당 WeakEventManager이(가) 있는 경우 기존의 약한 이벤트 관리자 클래스를 사용합니다. WPF에 포함된 약한 이벤트 관리자 목록은 WeakEventManager 클래스의 상속 계층 구조를 참조하세요. 포함된 약한 이벤트 관리자가 제한되어 있으므로 다른 방법 중 하나를 선택해야 할 수 있습니다.

  • 일반 약한 이벤트 관리자:

    기존 WeakEventManager을(를) 사용할 수 없고 약한 이벤트를 구현하는 가장 쉬운 방법을 찾고 있는 경우 제네릭 WeakEventManager<TEventSource,TEventArgs>을(를) 사용합니다. 그러나 제네릭 WeakEventManager<TEventSource,TEventArgs>은(는) 리플렉션을 사용하여 이름에서 이벤트를 검색하기 때문에 기존 또는 사용자 지정 약한 이벤트 관리자보다 효율성이 떨어집니다. 또한 제네릭 WeakEventManager<TEventSource,TEventArgs>을(를) 사용하여 이벤트를 등록해야 하는 코드는 기존 또는 사용자 지정 WeakEventManager을(를) 사용하는 것보다 더 자세한 정보입니다.

  • 사용자 지정 약한 이벤트 관리자:

    기존 WeakEventManager을(를) 사용할 수 없고 효율성이 중요한 경우 사용자 지정 WeakEventManager을(를) 만듭니다. 제네릭 WeakEventManager보다 효율적이지만 사용자 지정 WeakEventManager을(를) 사용하려면 더 많은 선행 코드를 작성해야 합니다.

  • 타사 약한 이벤트 관리자:

    다른 접근 방식에서 제공하지 않는 기능이 필요한 경우 타사 약한 이벤트 관리자를 사용합니다. NuGet에는 일부 약한 이벤트 관리자가 있습니다. 많은 WPF 프레임워크도 패턴을 지원합니다.

다음 섹션에서는 다양한 이벤트 관리자 유형을 사용하여 약한 이벤트 패턴을 구현하는 방법을 설명합니다. 제네릭 및 사용자 지정 약한 이벤트 관리자 예제의 경우 구독할 이벤트에는 다음과 같은 특성이 있습니다.

  • 이벤트 이름은 SomeEvent입니다.
  • 이벤트는 SomeEventSource 클래스에서 발생합니다.
  • 이벤트 처리기에는 EventHandler<SomeEventArgs> 형식이 있습니다.
  • 이벤트는 SomeEventArgs 형식의 매개 변수를 이벤트 처리기에 전달합니다.

기존 약한 이벤트 관리자 클래스 사용

  1. 기존 약한 이벤트 관리자를 찾습니다. WPF에 포함된 약한 이벤트 관리자 목록은 WeakEventManager 클래스의 상속 계층 구조를 참조하세요.

  2. 일반 이벤트 연결 대신 새 약한 이벤트 관리자를 사용합니다.

    예를 들어 코드에서 다음 패턴을 사용하여 이벤트를 구독하는 경우:

    source.LostFocus += new RoutedEventHandler(Source_LostFocus);
    
    AddHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

    다음 패턴으로 변경합니다.

    LostFocusEventManager.AddHandler(source, Source_LostFocus);
    
    LostFocusEventManager.AddHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

    마찬가지로 코드에서 다음 패턴을 사용하여 이벤트에서 구독을 취소하는 경우:

    source.LostFocus -= new RoutedEventHandler(Source_LostFocus);
    
    RemoveHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

    다음 패턴으로 변경합니다.

    LostFocusEventManager.RemoveHandler(source, Source_LostFocus);
    
    LostFocusEventManager.RemoveHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

제네릭 약한 이벤트 관리자 클래스 사용

일반 이벤트 연결 대신 제네릭 WeakEventManager<TEventSource,TEventArgs> 클래스를 사용합니다.

WeakEventManager<TEventSource,TEventArgs>을(를) 사용하여 이벤트 수신기를 등록하는 경우 이벤트 원본 및 EventArgs 형식을 클래스에 형식 매개 변수로 제공합니다. 다음 코드와 같이 AddHandler을(를) 설정합니다.

WeakEventManager<SomeEventSource, SomeEventArgs>.AddHandler(source, "SomeEvent", Source_SomeEvent);
WeakEventManager(Of SomeEventSource, SomeEventArgs).AddHandler(
    source, "SomeEvent", New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))

사용자 지정 약한 이벤트 관리자 클래스 만들기

  1. 다음 클래스 템플릿을 프로젝트에 복사합니다. 다음 클래스는 WeakEventManager 클래스에서 상속됩니다.

    class SomeEventWeakEventManager : WeakEventManager
    {
        private SomeEventWeakEventManager()
        {
        }
    
        /// <summary>
        /// Add a handler for the given source's event.
        /// </summary>
        public static void AddHandler(SomeEventSource source,
                                      EventHandler<SomeEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));
    
            CurrentManager.ProtectedAddHandler(source, handler);
        }
    
        /// <summary>
        /// Remove a handler for the given source's event.
        /// </summary>
        public static void RemoveHandler(SomeEventSource source,
                                         EventHandler<SomeEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));
    
            CurrentManager.ProtectedRemoveHandler(source, handler);
        }
    
        /// <summary>
        /// Get the event manager for the current thread.
        /// </summary>
        private static SomeEventWeakEventManager CurrentManager
        {
            get
            {
                Type managerType = typeof(SomeEventWeakEventManager);
                SomeEventWeakEventManager manager =
                    (SomeEventWeakEventManager)GetCurrentManager(managerType);
    
                // at first use, create and register a new manager
                if (manager == null)
                {
                    manager = new SomeEventWeakEventManager();
                    SetCurrentManager(managerType, manager);
                }
    
                return manager;
            }
        }
    
        /// <summary>
        /// Return a new list to hold listeners to the event.
        /// </summary>
        protected override ListenerList NewListenerList()
        {
            return new ListenerList<SomeEventArgs>();
        }
    
        /// <summary>
        /// Listen to the given source for the event.
        /// </summary>
        protected override void StartListening(object source)
        {
            SomeEventSource typedSource = (SomeEventSource)source;
            typedSource.SomeEvent += new EventHandler<SomeEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Stop listening to the given source for the event.
        /// </summary>
        protected override void StopListening(object source)
        {
            SomeEventSource typedSource = (SomeEventSource)source;
            typedSource.SomeEvent -= new EventHandler<SomeEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Event handler for the SomeEvent event.
        /// </summary>
        void OnSomeEvent(object sender, SomeEventArgs e)
        {
            DeliverEvent(sender, e);
        }
    }
    
    Class SomeEventWeakEventManager
        Inherits WeakEventManager
    
        Private Sub New()
        End Sub
    
        ''' <summary>
        ''' Add a handler for the given source's event.
        ''' </summary>
        Public Shared Sub [AddHandler](source As SomeEventSource,
                                       handler As EventHandler(Of SomeEventArgs))
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            If handler Is Nothing Then Throw New ArgumentNullException(NameOf(handler))
            CurrentManager.ProtectedAddHandler(source, handler)
        End Sub
    
        ''' <summary>
        ''' Remove a handler for the given source's event.
        ''' </summary>
        Public Shared Sub [RemoveHandler](source As SomeEventSource,
                                          handler As EventHandler(Of SomeEventArgs))
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            If handler Is Nothing Then Throw New ArgumentNullException(NameOf(handler))
            CurrentManager.ProtectedRemoveHandler(source, handler)
        End Sub
    
        ''' <summary>
        ''' Get the event manager for the current thread.
        ''' </summary>
        Private Shared ReadOnly Property CurrentManager As SomeEventWeakEventManager
            Get
                Dim managerType As Type = GetType(SomeEventWeakEventManager)
                Dim manager As SomeEventWeakEventManager =
                    CType(GetCurrentManager(managerType), SomeEventWeakEventManager)
    
                If manager Is Nothing Then
                    manager = New SomeEventWeakEventManager()
                    SetCurrentManager(managerType, manager)
                End If
    
                Return manager
            End Get
        End Property
    
        ''' <summary>
        ''' Return a new list to hold listeners to the event.
        ''' </summary>
        Protected Overrides Function NewListenerList() As ListenerList
            Return New ListenerList(Of SomeEventArgs)()
        End Function
    
        ''' <summary>
        ''' Listen to the given source for the event.
        ''' </summary>
        Protected Overrides Sub StartListening(source As Object)
            Dim typedSource As SomeEventSource = CType(source, SomeEventSource)
            AddHandler typedSource.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf OnSomeEvent)
        End Sub
    
        ''' <summary>
        ''' Stop listening to the given source for the event.
        ''' </summary>
        Protected Overrides Sub StopListening(source As Object)
            Dim typedSource As SomeEventSource = CType(source, SomeEventSource)
            AddHandler typedSource.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf OnSomeEvent)
        End Sub
    
        ''' <summary>
        ''' Event handler for the SomeEvent event.
        ''' </summary>
        Private Sub OnSomeEvent(sender As Object, e As SomeEventArgs)
            DeliverEvent(sender, e)
        End Sub
    End Class
    
  2. 이벤트 이름과 일치하도록 SomeEventWeakEventManager, SomeEvent, SomeEventSourceSomeEventArgs의 이름을 바꿉니다.

  3. 약한 이벤트 관리자 클래스에 대한 액세스 한정자를 관리되는 이벤트의 접근성과 일치하도록 설정합니다.

  4. 일반 이벤트 연결 대신 새 약한 이벤트 관리자를 사용합니다.

    예를 들어 코드에서 다음 패턴을 사용하여 이벤트를 구독하는 경우:

    source.SomeEvent += new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    AddHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

    다음 패턴으로 변경합니다.

    SomeEventWeakEventManager.AddHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.AddHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

    마찬가지로 코드에서 다음 패턴을 사용하여 이벤트 구독을 취소하는 경우:

    source.SomeEvent -= new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    RemoveHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

    다음 패턴으로 변경합니다.

    SomeEventWeakEventManager.RemoveHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.RemoveHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

참고 항목