天天看點

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線程上調用它。