天天看点

WPF程序中的弱事件模式

原文: WPF程序中的弱事件模式

在C#中,得益于强大的GC机制,使得我们开发程序变得非常简单,很多时候我们只需要管使用,而并不需要关心什么时候释放资源。但是,GC有的时并不是按照我们所期望的方式工作。

例如,我想实现一个在窗口的标题栏中实时显示当前的时间,一个比较常规的做法如下:

    var timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };

    timer.Tick += (_s, _e) => this.Title = DateTime.Now.ToString();

    timer.Start();

这种做法看起来非常简单而直接,它也确实能老老实实按照我们所设计的那样在窗口中实时显示并更新时间。但是,有经验的程序员们就知道,这里存在一个隐患:这个窗口永远不会释放。比较简单的验证方式是:手动关闭窗口,调用GC.Collect()函数,发现析构函数是不会调用的。

可能有的人会问了:不是有万能的GC嘛,为什么这个窗口不会释放?究其原因也非常简单,DispatchTimer的Tick事件中包含了对Window的引用,当窗口关闭时,DispatchTimer仍然在执行,因此Window就得不到释放。

知道了原因后,要解决也不难:在Window的关闭事件中,停止Timer的调用即可。这种方式确实行之有效,但显得不大优雅,感觉回到了要手动控制申请和释放的C语言年代,没有了GC自动管理下的"管杀不管埋"的便捷感觉。 那么,有没有一种我们只管使用,而不管释放的方案呢,答案就是

弱事件模式

在弱事件模式下,事件委托只保留对象的弱引用,这样GC仍然能将该对象给回收掉。例如,对于上述代码,可以修改如下:

    var timer = new

DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };

    WeakEventManager<DispatcherTimer, EventArgs>.AddHandler(timer, "Tick", (_s, _e) => this.Title = DateTime.Now.ToString());

由于Timer没有保存Window的强引用,当Windows关闭后,是会被GC回收掉的。

现在看起来没有什么问题了,不过,敏感的程序员们会发现,这里还存在一个隐患:DispatchTimer没有释放。虽然我们没有保存Timer的引用,但为了避免其被GC回收,内部仍然会维持其引用,必须显式停止。这里我们仍然可以利用弱事件模式,在感知到回调对象被释放时,手动停止Timer。要实现这个方法,必须我们实现自己的弱事件管理器: 

WPF程序中的弱事件模式
WPF程序中的弱事件模式

public class DispatcherTimerManager : WeakEventManager
    {
        public static void Create(TimeSpan interval, EventHandler<EventArgs> handler)
        {
            var dispatcherTimer = new DispatcherTimer() { Interval = interval };
            DispatcherTimerManager.AddHandler(dispatcherTimer, handler);
            dispatcherTimer.Start();
        }

        public static void AddHandler(DispatcherTimer source, EventHandler<EventArgs> handler)
        {
            current.ProtectedAddHandler(source, handler);
        }

        public static void RemoveHandler(DispatcherTimer source, EventHandler<EventArgs> handler)
        {
            current.ProtectedRemoveHandler(source, handler);
        }

        static DispatcherTimerManager current;
        static DispatcherTimerManager()
        {
            current = new DispatcherTimerManager();
            SetCurrentManager(typeof(DispatcherTimerManager), current);
        }

        protected override ListenerList NewListenerList()
        {
            return new ListenerList<EventArgs>();
        }

        protected override void StartListening(object source)
        {
            var timer = (DispatcherTimer)source;
            timer.Tick += OnSomeEvent;
        }

        protected override void StopListening(object source)
        {
            var timer = (DispatcherTimer)source;
            timer.Tick -= OnSomeEvent;
            timer.Stop();
        }

        void OnSomeEvent(object sender, EventArgs e)
        {
            DeliverEvent(sender, e);
        }
    }      

View Code

代码比较简单:当感知到回调对象被释放时,会执行StopListening函数我们只需要重写改函数,加入停止Timer操作即可。同样,我们也可以基于弱事件模式实现一个IObservable的自动管理类:

WPF程序中的弱事件模式
WPF程序中的弱事件模式
1     public static class ObservableDispatcher
 2     {
 3         public static void AddHandler<T>(IObservable<T> source, EventHandler<DataEventArgs<T>> handler)
 4         {
 5             if ( Application.Current.Dispatcher != Dispatcher.CurrentDispatcher)
 6                 throw new InvalidOperationException("需要在主线程上调用");
 7 
 8             AnymousDispatcher<T>.AddHandler(source, handler);
 9         }
10 
11         public static void RemoveHandler<T>(IObservable<T> source, EventHandler<DataEventArgs<T>> handler)
12         {
13             AnymousDispatcher<T>.RemoveHandler(source, handler);
14         }
15 
16 
17         class AnymousDispatcher<T> : WeakEventManager
18         {
19             public static void AddHandler(IObservable<T> source, EventHandler<DataEventArgs<T>> handler)
20             {
21                 var wrapper = new ObservableEventWrapper<T>(source);
22                 current.ProtectedAddHandler(wrapper, handler);
23             }
24 
25             public static void RemoveHandler(IObservable<T> source, EventHandler<DataEventArgs<T>> handler)
26             {
27                 var wrapper = new ObservableEventWrapper<T>(source);
28                 current.ProtectedRemoveHandler(wrapper, handler);
29             }
30 
31             static AnymousDispatcher<T> current;
32             static AnymousDispatcher()
33             {
34                 current = new AnymousDispatcher<T>();
35                 SetCurrentManager(typeof(AnymousDispatcher<T>), current);
36             }
37 
38             protected override ListenerList NewListenerList()
39             {
40                 return new ListenerList<DataEventArgs<T>>();
41             }
42 
43             protected override void StartListening(object source)
44             {
45                 var wrapper = source as ObservableEventWrapper<T>;
46                 wrapper.OnData += wrapper_OnData;
47             }
48 
49             void wrapper_OnData(object sender, DataEventArgs<T> e)
50             {
51                 DeliverEvent(sender, e);
52             }
53 
54             protected override void StopListening(object source)
55             {
56                 var wrapper = source as ObservableEventWrapper<T>;
57                 wrapper.OnData -= wrapper_OnData;
58                 wrapper.Dispose();
59             }
60         }
61 
62         class ObservableEventWrapper<T> : IDisposable
63         {
64             IDisposable disposeHandler;
65             public ObservableEventWrapper(IObservable<T> dataSource)
66             {
67                 disposeHandler = dataSource.Subscribe(onData);
68             }
69 
70             void onData(T data)
71             {
72                 OnData(this, new DataEventArgs<T>(data));
73             }
74 
75             public event EventHandler<DataEventArgs<T>> OnData;
76 
77             public void Dispose()
78             {
79                 disposeHandler.Dispose();
80             }
81         }
82     }      

限制:

弱事件模式非常有用,但不知道为什么微软将其限制在了WPF框架中了,从其实现上来看,应该是在UI线程上调用,但在MSDN上也没有找到其限制的说明。我试过在非UI线程上调用它,也是弱事件,但是不能触发StopListening函数。不知道这样有没有什么影响,但最好还是在UI线程上调用它。