File: Base\System\ComponentModel\PropertyChangedEventManager.cs
Project: wpf\src\WindowsBase.csproj (WindowsBase)
//---------------------------------------------------------------------------
//
// <copyright file="PropertyChangedEventManager.cs" company="Microsoft">
//    Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// Description: Manager for the PropertyChanged event in the "weak event listener"
//              pattern.  See WeakEventTable.cs for an overview.
//
//---------------------------------------------------------------------------
 
using System;
using System.Collections;       // ICollection
using System.Collections.Generic; // List<T>
using System.Collections.Specialized;   // HybridDictionary
using System.ComponentModel;    // INotifyPropertyChanged
using System.Diagnostics;       // Debug
using System.Reflection;        // MethodInfo
using System.Windows;           // WeakEventManager
using MS.Internal;              // BaseAppContextSwitches
using MS.Internal.WindowsBase;  // SR
 
namespace System.ComponentModel
{
    /// <summary>
    /// Manager for the INotifyPropertyChanged.PropertyChanged event.
    /// </summary>
    public class PropertyChangedEventManager : WeakEventManager
    {
        #region Constructors
 
        //
        //  Constructors
        //
 
        private PropertyChangedEventManager()
        {
        }
 
        #endregion Constructors
 
        #region Public Methods
 
        //
        //  Public Methods
        //
 
        /// <summary>
        /// Add a listener to the given source's event.
        /// </summary>
        public static void AddListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (listener == null)
                throw new ArgumentNullException("listener");
 
            CurrentManager.PrivateAddListener(source, listener, propertyName);
        }
 
        /// <summary>
        /// Remove a listener to the given source's event.
        /// </summary>
        public static void RemoveListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName)
        {
            /* for app-compat, allow RemoveListener(null, x) - it's a no-op (see Dev10 796788)
            if (source == null)
                throw new ArgumentNullException("source");
            */
            if (listener == null)
                throw new ArgumentNullException("listener");
 
            CurrentManager.PrivateRemoveListener(source, listener, propertyName);
        }
 
        /// <summary>
        /// Add a handler for the given source's event.
        /// </summary>
        public static void AddHandler(INotifyPropertyChanged source, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
        {
            if (handler == null)
                throw new ArgumentNullException("handler");
 
            CurrentManager.PrivateAddHandler(source, handler, propertyName);
        }
 
        /// <summary>
        /// Remove a handler for the given source's event.
        /// </summary>
        public static void RemoveHandler(INotifyPropertyChanged source, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
        {
            if (handler == null)
                throw new ArgumentNullException("handler");
 
            CurrentManager.PrivateRemoveHandler(source, handler, propertyName);
        }
 
        #endregion Public Methods
 
        #region Protected Methods
 
        //
        //  Protected Methods
        //
 
        /// <summary>
        /// Return a new list to hold listeners to the event.
        /// </summary>
        protected override ListenerList NewListenerList()
        {
            return new ListenerList<PropertyChangedEventArgs>();
        }
 
        /// <summary>
        /// Listen to the given source for the event.
        /// </summary>
        protected override void StartListening(object source)
        {
            INotifyPropertyChanged typedSource = (INotifyPropertyChanged)source;
            typedSource.PropertyChanged += new PropertyChangedEventHandler(OnPropertyChanged);
        }
 
        /// <summary>
        /// Stop listening to the given source for the event.
        /// </summary>
        protected override void StopListening(object source)
        {
            INotifyPropertyChanged typedSource = (INotifyPropertyChanged)source;
            typedSource.PropertyChanged -= new PropertyChangedEventHandler(OnPropertyChanged);
        }
 
        /// <summary>
        /// Remove dead entries from the data for the given source.   Returns true if
        /// some entries were actually removed.
        /// </summary>
        protected override bool Purge(object source, object data, bool purgeAll)
        {
            bool foundDirt = false;
 
            if (!purgeAll)
            {
                HybridDictionary dict = (HybridDictionary)data;
                int ignoredKeys = 0;
 
                if (!BaseAppContextSwitches.EnableWeakEventMemoryImprovements)
                {
                    // copy the keys into a separate array, so that later on
                    // we can change the dictionary while iterating over the keys
                    ICollection ic = dict.Keys;
                    String[] keys = new String[ic.Count];
                    ic.CopyTo(keys, 0);
 
                    for (int i=keys.Length-1; i>=0; --i)
                    {
                        if (keys[i] == AllListenersKey)
                        {
                            ++ignoredKeys;
                            continue;       // ignore the special entry for now
                        }
 
                        // for each key, remove dead entries in its list
                        bool removeList = purgeAll || source == null;
 
                        if (!removeList)
                        {
                            ListenerList list = (ListenerList)dict[keys[i]];
 
                            if (ListenerList.PrepareForWriting(ref list))
                                dict[keys[i]] = list;
 
                            if (list.Purge())
                                foundDirt = true;
 
                            removeList = (list.IsEmpty);
                        }
 
                        // if there are no more entries, remove the key
                        if (removeList)
                        {
                            dict.Remove(keys[i]);
                        }
                    }
#if WeakEventTelemetry
                    LogAllocation(ic.GetType(), 1, 12);                     // dict.Keys - Hashtable+KeyCollection
                    LogAllocation(typeof(String[]), 1, 12+ic.Count*4);      // keys
#endif
                }
                else
                {
                    Debug.Assert(_toRemove.Count == 0, "to-remove list should be empty");
 
                    // If an "in-use" list is changed, we will re-install its clone
                    // back into the dictionary.  Doing this inside the loop
                    // causes an exception "collection was modified after the enumerator
                    // was instantiated" (DDVSO 812614), so instead just record
                    // what to do and do the actual work after the loop.
                    // This is a rare case - it only arises if a PropertyChanged event
                    // handler calls (indirectly) into the cleanup code - so allocate
                    // the temporary memory lazily on the stack.
                    // [In the bug, the indirect call comes about because the app's
                    // event handler calls ShowDialog(), which pushes a dispatcher
                    // frame to run a nested message pump. Then a pending cleanup task
                    // reaches the front of the dispatcher queue before the dialog
                    // is dismissed, effectively calling this method while the
                    // PropertyChanged event delivery is in progress.]
                    HybridDictionary toInstall = null;
 
                    // enumerate the dictionary using IDE explicitly rather than
                    // foreach, to avoid allocating temporary DictionaryEntry objects
                    IDictionaryEnumerator ide = dict.GetEnumerator() as IDictionaryEnumerator;
                    while (ide.MoveNext())
                    {
                        String key = (String)ide.Key;
                        if (key == AllListenersKey)
                        {
                            ++ignoredKeys;
                            continue;       // ignore the special entry for now
                        }
 
                        // for each key, remove dead entries in its list
                        bool removeList = purgeAll || source == null;
 
                        if (!removeList)
                        {
                            ListenerList list = (ListenerList)ide.Value;
 
                            bool inUse = ListenerList.PrepareForWriting(ref list);
                            bool isChanged = false;
 
                            if (list.Purge())
                            {
                                isChanged = true;
                                foundDirt = true;
                            }
 
                            removeList = (list.IsEmpty);
 
                            // if a cloned list changed, remember the details
                            // so that the clone can be installed back into the
                            // dictionary outside the iteration (DDVSO 812614)
                            if (!removeList && inUse && isChanged)
                            {
                                if (toInstall == null)
                                {
                                    // lazy allocation
                                    toInstall = new HybridDictionary();
                                }
 
                                toInstall[key] = list;
                            }
                        }
 
                        // if there are no more entries, remove the key
                        if (removeList)
                        {
                            _toRemove.Add(key);
                        }
                    }
 
                    // do the actual removal (outside the dictionary iteration)
                    if (_toRemove.Count > 0)
                    {
                        foreach (String key in _toRemove)
                        {
                            dict.Remove(key);
                        }
                        _toRemove.Clear();
                        _toRemove.TrimExcess();
                    }
 
                    // do the actual re-install of "in-use" lists that changed
                    if (toInstall != null)
                    {
                        IDictionaryEnumerator installDE = toInstall.GetEnumerator() as IDictionaryEnumerator;
                        while (installDE.MoveNext())
                        {
                            String key = (String)installDE.Key;
                            ListenerList list = (ListenerList)installDE.Value;
                            dict[key] = list;
                        }
                    }
 
#if WeakEventTelemetry
                    Type enumeratorType = ide.GetType();
                    if (enumeratorType.Name.IndexOf("NodeEnumerator") >= 0)
                    {
                        LogAllocation(enumeratorType, 1, 24);                    // ListDictionary+NodeEnumerator
                    }
                    else
                    {
                        LogAllocation(enumeratorType, 1, 36);                    // Hashtable+HashtableEnumerator
                    }
#endif
                }
 
                if (dict.Count == ignoredKeys)
                {
                    // if there are no more listeners at all, remove the entry from
                    // the main table, and prepare to stop listening
                    purgeAll = true;
                    if (source != null)     // source may have been GC'd
                    {
                        this.Remove(source);
                    }
                }
                else if (foundDirt)
                {
                    // if any entries were purged, invalidate the special entry
                    dict.Remove(AllListenersKey);
                    _proposedAllListenersList = null;
                }
            }
 
            if (purgeAll)
            {
                // stop listening.  List cleanup is handled by Purge()
                if (source != null) // source may have been GC'd
                {
                    StopListening(source);
                }
                foundDirt = true;
            }
 
            return foundDirt;
        }
 
        #endregion Protected Methods
 
        #region Private Properties
 
        //
        //  Private Properties
        //
 
        // get the event manager for the current thread
        private static PropertyChangedEventManager CurrentManager
        {
            get
            {
                Type managerType = typeof(PropertyChangedEventManager);
                PropertyChangedEventManager manager = (PropertyChangedEventManager)GetCurrentManager(managerType);
 
                // at first use, create and register a new manager
                if (manager == null)
                {
                    manager = new PropertyChangedEventManager();
                    SetCurrentManager(managerType, manager);
                }
 
                return manager;
            }
        }
 
        #endregion Private Properties
 
        #region Private Methods
 
        //
        //  Private Methods
        //
 
        // PropertyChanged is a special case - we superimpose per-property granularity
        // on top of this event, by keeping separate lists of listeners for
        // each property.
 
        // Add a listener to the named property (empty means "any property")
        private void PrivateAddListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName)
        {
            Debug.Assert(listener != null && source != null && propertyName != null,
                "Listener, source, and propertyName of event cannot be null");
 
            AddListener(source, propertyName, listener, null);
        }
 
        // Remove a listener to the named property (empty means "any property")
        private void PrivateRemoveListener(INotifyPropertyChanged source, IWeakEventListener listener, string propertyName)
        {
            Debug.Assert(listener != null && source != null && propertyName != null,
                "Listener, source, and propertyName of event cannot be null");
 
            RemoveListener(source, propertyName, listener, null);
        }
 
        // Add a handler for the named property (empty means "any property")
        private void PrivateAddHandler(INotifyPropertyChanged source, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
        {
            AddListener(source, propertyName, null, handler);
        }
 
        // Remove a handler for the named property (empty means "any property")
        private void PrivateRemoveHandler(INotifyPropertyChanged source, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
        {
            RemoveListener(source, propertyName, null, handler);
        }
 
        private void AddListener(INotifyPropertyChanged source, string propertyName, IWeakEventListener listener, EventHandler<PropertyChangedEventArgs> handler)
        {
            using (WriteLock)
            {
                HybridDictionary dict = (HybridDictionary)this[source];
 
                if (dict == null)
                {
                    // no entry in the hashtable - add a new one
                    dict = new HybridDictionary(true /* case insensitive */);
 
                    this[source] = dict;
 
                    // listen for the desired events
                    StartListening(source);
                }
 
                ListenerList list = (ListenerList)dict[propertyName];
 
                if (list == null)
                {
                    // no entry in the dictionary - add a new one
                    list = new ListenerList<PropertyChangedEventArgs>();
 
                    dict[propertyName] = list;
                }
 
                // make sure list is ready for writing
                if (ListenerList.PrepareForWriting(ref list))
                {
                    dict[propertyName] = list;
                }
 
                // add a listener to the list
                if (handler != null)
                {
                    ListenerList<PropertyChangedEventArgs> hlist = (ListenerList<PropertyChangedEventArgs>)list;
                    hlist.AddHandler(handler);
                }
                else
                {
                    list.Add(listener);
                }
 
                dict.Remove(AllListenersKey);   // invalidate list of all listeners
                _proposedAllListenersList = null;
 
                // schedule a cleanup pass
                ScheduleCleanup();
            }
        }
 
        private void RemoveListener(INotifyPropertyChanged source, string propertyName, IWeakEventListener listener, EventHandler<PropertyChangedEventArgs> handler)
        {
            using (WriteLock)
            {
                HybridDictionary dict = (HybridDictionary)this[source];
 
                if (dict != null)
                {
                    ListenerList list = (ListenerList)dict[propertyName];
 
                    if (list != null)
                    {
                        // make sure list is ready for writing
                        if (ListenerList.PrepareForWriting(ref list))
                        {
                            dict[propertyName] = list;
                        }
 
                        // remove a listener from the list
                        if (handler != null)
                        {
                            ListenerList<PropertyChangedEventArgs> hlist = (ListenerList<PropertyChangedEventArgs>)list;
                            hlist.RemoveHandler(handler);
                        }
                        else
                        {
                            list.Remove(listener);
                        }
 
                        // when the last listener goes away, remove the list
                        if (list.IsEmpty)
                        {
                            dict.Remove(propertyName);
                        }
                    }
 
                    if (dict.Count == 0)
                    {
                        StopListening(source);
 
                        Remove(source);
                    }
 
                    dict.Remove(AllListenersKey);   // invalidate list of all listeners
                    _proposedAllListenersList = null;
                }
            }
        }
 
        // event handler for PropertyChanged event
        private void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            ListenerList list;
            string propertyName = args.PropertyName;
 
            // get the list of listeners
            using (ReadLock)
            {
                // look up the list of listeners
                HybridDictionary dict = (HybridDictionary)this[sender];
 
                if (dict == null)
                {
                    // this can happen when the last listener stops listening, but the
                    // source raises the event on another thread after the dictionary
                    // has been removed (bug 1235351)
                    list = ListenerList.Empty;
                }
                else if (!String.IsNullOrEmpty(propertyName))
                {
                    // source has changed a particular property.  Notify targets
                    // who are listening either for this property or for all properties.
                    ListenerList<PropertyChangedEventArgs> listeners = (ListenerList<PropertyChangedEventArgs>)dict[propertyName];
                    ListenerList<PropertyChangedEventArgs> genericListeners = (ListenerList<PropertyChangedEventArgs>)dict[String.Empty];
 
                    if (genericListeners == null)
                    {
                        if (listeners != null)
                        {
                            list = listeners;           // only specific listeners
                        }
                        else
                        {
                            list = ListenerList.Empty;  // no listeners at all
                        }
                    }
                    else
                    {
                        if (listeners != null)
                        {
                            // there are both specific and generic listeners -
                            // combine the two lists.
                            list = new ListenerList<PropertyChangedEventArgs>(listeners.Count + genericListeners.Count);
                            for (int i=0, n=listeners.Count; i<n; ++i)
                                list.Add(listeners.GetListener(i));
                            for (int i=0, n=genericListeners.Count; i<n; ++i)
                                list.Add(genericListeners.GetListener(i));
                        }
                        else
                        {
                            list = genericListeners;    // only generic listeners
                        }
                    }
                }
                else
                {
                    // source has changed all properties.  Notify all targets.
                    // Use previously calculated combined list, if available.
                    list = (ListenerList)dict[AllListenersKey];
 
                    if (list == null)
                    {
                        // make one pass to compute the size of the combined list.
                        // This avoids expensive reallocations.
                        int size = 0;
                        foreach (DictionaryEntry de in dict)
                        {
                            Debug.Assert((String)de.Key != AllListenersKey, "special key should not appear");
                            size += ((ListenerList)de.Value).Count;
                        }
 
                        // create the combined list
                        list = new ListenerList<PropertyChangedEventArgs>(size);
 
                        // fill in the combined list
                        foreach (DictionaryEntry de in dict)
                        {
                            ListenerList listeners = ((ListenerList)de.Value);
                            for (int i=0, n=listeners.Count;  i<n;  ++i)
                            {
                                list.Add(listeners.GetListener(i));
                            }
                        }
 
                        // save the result for future use (see below)
                        _proposedAllListenersList = list;
                    }
                }
 
                // mark the list "in use", even outside the read lock,
                // so that any writers will know not to modify it (they'll
                // modify a clone intead).
                list.BeginUse();
            }
 
            // deliver the event, being sure to undo the effect of BeginUse().
            try
            {
                DeliverEventToList(sender, args, list);
            }
            finally
            {
                list.EndUse();
            }
 
            // if we calculated an AllListeners list, we should now try to store
            // it in the dictionary so it can be used in the future.  This must be
            // done under a WriteLock - which is why we didn't do it immediately.
            if (_proposedAllListenersList == list)
            {
                using (WriteLock)
                {
                    // test again, in case another thread changed _proposedAllListersList.
                    if (_proposedAllListenersList == list)
                    {
                        HybridDictionary dict = (HybridDictionary)this[sender];
                        if (dict != null)
                        {
                            dict[AllListenersKey] = list;
                        }
 
                        _proposedAllListenersList = null;
                    }
 
                    // Another thread could have changed _proposedAllListersList
                    // since we set it (earlier in this method), either
                    // because it calculated a new one while handling a PropertyChanged(""),
                    // or because it added/removed/purged a listener.
                    // In that case, we will simply abandon our proposed list and we'll
                    // have to compute it again the next time.  But that only happens
                    // if there's thread contention.  It's not worth doing something
                    // more complicated just for that case.
                }
            }
        }
 
        #endregion Private Methods
 
        ListenerList _proposedAllListenersList;
        List<String> _toRemove = new List<String>();
        static readonly string AllListenersKey = "<All Listeners>"; // not a legal property name
    }
}