C# 弱事件模式

传统事件侦听模式潜在内存泄漏问题

普通事件使用 source.event+=listener.delegate,这时event所在的委托会持有listener的强引用;如果listener不再监听event时,需要使用source.event-=listener.delegate来解除引用关系。
若忘记解决引用,则listener的生命周期会受到event的影响,从而产生内存泄漏

⚠⚠⚠人生建议:不需要订阅事件的时候,请取消事件订阅。

传统事件模式

弱事件模式主要针对的场景:

  • 当listener不知道何时注销事件监听
  • 当事件源生存周期比监听者生存周期长
  • 当事件源和事件监听者的生命周期并不明确
    就可以使用弱事件模式。

弱事件模式的宗旨就是,通过引入事件源和监听者的中间人"WeakEventManager",当事件源的事件触发时,由它负责向事件监听者传递事件。由于WeakEventManager对事件监听者的引用是弱引用,所以,并不会影响监听者被垃圾回收:
弱事件模式

弱事件模式的三种实现方式

  1. 使用WeakEventManager<TEventSource,TEventArgs> ;
  2. 使用自定义的WeakEventManager;
  3. 使用现有的WeakEventManager,如:PropertyChangedEventManager等等。

下面以ColorChange事件为Demo,进行对上面介绍的几种实现方式演示:
在演示前,我们先将事件、事件源、事件参数等进行定义

using System;

namespace WeakEventDemo
{
    public class ColorChangeEventArgs : EventArgs
    {
        public int Color { get; set; }
    }

    public delegate void ColorChangeHanlder(object sender, ColorChangeEventArgs e);

    public class ColorController
    {
        private static ColorController instance;

        public static ColorController Instance
        {
            get
            {
                if (instance == null)
                {
                    instance = new ColorController();
                }
                return instance;
            }
        }

        public event ColorChangeHanlder ColorChanged;

        public void RaiseColorChangeEvent(int color)
        {
            var e = new ColorChangeEventArgs();
            e.Color = color;
            this.ColorChanged?.Invoke(this, e);
        }
    }
}

使用+=,但是忘记使用-=

  • 在UserControl1中订阅ColorController的ColroChanged事件,当ColroChanged事件发生,会弹出消息框以表示响应了ColroChanged事件;
  • 按钮MainWindow中第一个按钮,打开子窗口,其中子窗口的内容是UserControl1对象;
  • 点击MainWIndow中第二个按钮,引发ColorChanged事件,由于UserControl1订阅了ColroChanged事件,所以会有消息框弹出;
  • 关闭子窗口,再次点击MainWindow中第二个按钮,观察是否有消息框弹出。
  • 可以看到,还有消息框弹出!为什么!!!难道子窗口复活了?

MainWindow逻辑

<Window
    x:Class="WeakEventDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WeakEventDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="525"
    Height="350"
    WindowStartupLocation="CenterScreen"
    mc:Ignorable="d">
    <Grid>
        <StackPanel>
            <Button
                Padding="0,8"
                Click="ShowBtn_OnClick"
                Content="open a window with content is listenning color change" />
            <Rectangle Height="8" />
            <Button
                Padding="0,8"
                Click="RaiseBtn_OnClick"
                Content="raise color change event" />
        </StackPanel>
    </Grid>
</Window>
using System.ComponentModel;
using System.Threading;
using System.Windows;

namespace WeakEventDemo
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ShowBtn_OnClick(object sender, RoutedEventArgs e)
        {
            var newWindow = new Window();
            newWindow.Width = 200;
            newWindow.Height = 200;
            newWindow.Owner = this;
            newWindow.WindowStartupLocation = WindowStartupLocation.CenterOwner;
            newWindow.Content = new UserControl1();
            newWindow.Show();
        }


        private void RaiseBtn_OnClick(object sender, RoutedEventArgs e)
        {
            ColorController.Instance.RaiseColorChangeEvent(22);
        }
    }
}

UserControl1逻辑

<UserControl
    x:Class="WeakEventDemo.UserControl1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WeakEventDemo"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">
    <Grid>
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Text="listen to color change event" />
    </Grid>
</UserControl>

using System.Windows;
using System.Windows.Controls;

namespace WeakEventDemo
{
    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
            ColorController.Instance.ColorChanged += Instance_ColorChanged;
        }

        private void Instance_ColorChanged(object sender, ColorChangeEventArgs e)
        {
            MessageBox.Show($"color change to {e.Color.ToString()}");
        }
    }
}

运行效果如下:
使用传统事件订阅模式窗口未显示取消订阅事件导致内存泄漏
内存泄漏

通过查看内存中快照可知UserControl1被ColorChanged持有了强引用,所以当子窗口关闭后,UserControl1不能被正常释放。

使用WeakEventManager<TEventSource,TEventArgs> 方式

使用弱事件管理器,解决上面内存泄漏问题,代码很简单,只需对UserControl1进行改造:

using System.Windows;
using System.Windows.Controls;

namespace WeakEventDemo
{
    /// <summary>
    /// UserControl1.xaml 的交互逻辑
    /// </summary>
    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
            //ColorController.Instance.ColorChanged += Instance_ColorChanged;
            WeakEventManager<ColorController, ColorChangeEventArgs>.AddHandler(ColorController.Instance, nameof(ColorController.ColorChanged), Instance_ColorChanged);
        }

        private void Instance_ColorChanged(object sender, ColorChangeEventArgs e)
        {
            MessageBox.Show($"color change to {e.Color.ToString()}");
        }
    }
}

下面是运行效果:
使用弱事件模式解决内存泄漏
可以看到在等待一段时间后,触发ColorChange事件,不会再得到任何响应。

使用自定义的WeakEventManager方式

不使用泛型版本的弱事件管理器的话,可以通过实现WeakEventManager抽象类,来管理事件和事件监听者。不过会多写一些代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace WeaEventDemo
{
    public class ColorChangeWeakEventManager : WeakEventManager
    {
        protected override void StartListening(object source)
        {
            var eventSource = source as ColorController;
            if (eventSource != null)
            {
                eventSource.ColorChangedEvent += EventSource_ColorChangedEvent;
            }
        }

        private void EventSource_ColorChangedEvent(object sender, ColorChangeEventArgs e)
        {
            this.DeliverEvent(sender, e);
        }

        protected override void StopListening(object source)
        {
            var eventSource = source as ColorController;
            if (eventSource != null)
            {
                eventSource.ColorChangedEvent -= EventSource_ColorChangedEvent;
            }
        }

        protected override ListenerList NewListenerList()
        {
            return new ListenerList();
        }

        public void AddListener(object source, IWeakEventListener listener)
        {
            base.ProtectedAddListener(source, listener);
        }

        public void RemoveListener(object source, IWeakEventListener listener)
        {
            base.ProtectedRemoveListener(source, listener);
        }
    }
}

事件监听者需要实现IWeakEventListener接口

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WeaEventDemo
{
    public partial class UserControl1 : UserControl, IWeakEventListener
    {
        public UserControl1()
        {
            InitializeComponent();
            // 将事件监听者加入事件管理器中
            var colorWeakEventManager = new ColorChangeWeakEventManager();
            colorWeakEventManager.AddListener(ColorController.Instance, this);
        }

        public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            if (managerType == typeof(ColorChangeWeakEventManager))
            {
                var colorChangeArgs = e as ColorChangeEventArgs;
                MessageBox.Show($"color changed to {colorChangeArgs?.Color.ToString(CultureInfo.InvariantCulture)}");
                return true;
            }
            return false;
        }
    }
}

为特殊场景定制WeakEventManager

如果要处理的事件是一类相关性很高的事件,那么我们可以为这类事件写一套专用的事件处理程序。比如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WeaEventDemo
{
    public delegate void ColorChangeHandlerNew(int color);

    public static class ColorControllerNew
    {
        public static event ColorChangeHandlerNew ColorChangedEvent;

        public static void RaiseColorChangedEvent(int color)
        {
            ColorChangedEvent?.Invoke(color);
        }
    }
}

上面的事件属于和WPF常见的事件不太一样,它没有使用EventArgs来传递事件信息。如何改造事件管理器和事件监听者呢?
事件管理器对外提供添加、移除监听者的静态方法;在事件管理器内部处理事件,然后发送给监听者。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace WeaEventDemo
{
    public class SpecificSeriesWeakEventManager : WeakEventManager
    {
        protected override void StartListening(object source)
        {
            ColorControllerNew.ColorChangedEvent += EventSource_ColorChangedEvent;
            //handle more events here,such as fontsize change,language change ect...
        }

        private void EventSource_ColorChangedEvent(int color)
        {
            ColorChangeEventArgs args = new ColorChangeEventArgs();
            args.Color = color;
            this.DeliverEvent(null, args);
        }

        protected override void StopListening(object source)
        {
            ColorControllerNew.ColorChangedEvent -= EventSource_ColorChangedEvent;
        }

        protected override ListenerList NewListenerList()
        {
            return new ListenerList();
        }

        private static SpecificSeriesWeakEventManager CurrentManager
        {
            get
            {
                var cachedManager = WeakEventManager.GetCurrentManager(typeof(SpecificSeriesWeakEventManager)) as SpecificSeriesWeakEventManager;
                if (cachedManager == null)
                {
                    cachedManager = new SpecificSeriesWeakEventManager();
                    WeakEventManager.SetCurrentManager(typeof(SpecificSeriesWeakEventManager), cachedManager);
                }
                return cachedManager;
            }
        }

        public static void AddListener(object source, IWeakEventListener listener)
        {
            CurrentManager.ProtectedAddListener(source, listener);
        }

        public static void RemoveListener(object source, IWeakEventListener listener)
        {
            CurrentManager.ProtectedRemoveListener(source, listener);
        }
    }
}

实现一个专门用于处理这类事件的监听者基类

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WeaEventDemo
{
    public class WeakEventControl : Border, IWeakEventListener
    {
        public WeakEventControl()
        {
            SpecificSeriesWeakEventManager.AddListener(null, this);
        }

        public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
            if (managerType == typeof(SpecificSeriesWeakEventManager))
            {
                if (e != null && e is ColorChangeEventArgs)
                {
                    OnColorChanged((e as ColorChangeEventArgs).Color);
                }
                //handle more events here,such as FontsizechangeEventArgs,LanguageChangeEventArgs ect...
                return true;
            }
            return false;
        }

        protected virtual void OnColorChanged(double color) { }
    }
}

具体的监听者

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WeaEventDemo
{
    public partial class UserControl1 : WeakEventControl
    {
        public UserControl1()
        {
            InitializeComponent();
        }

        protected override void OnColorChanged(double color)
        {
            MessageBox.Show($"color changed to {color}");
        }
    }
}

上面的写法的好处是:对于已知类型的一类特定事件,在监听侧只需接收管理器发送过来的消息;在事件触发侧只需将事件发送出去。事件管理器内部处理订阅、取消订阅事件、通知监听者等工作,较好地实现了解耦。
改造:

参考:

WPF: 深入理解 Weak Event 模型
弱事件模式

posted @ 2025-06-07 21:34  BigBosscyb  阅读(115)  评论(0)    收藏  举报