|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//
// Description: Manager for static property-changed events 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; // EventInfo
using System.Windows; // WeakEventManager
namespace MS.Internal.Data
{
/// <summary>
/// Manager for the INotifyPropertyChanged.PropertyChanged event.
/// </summary>
internal class StaticPropertyChangedEventManager : WeakEventManager
{
#region Constructors
//
// Constructors
//
private StaticPropertyChangedEventManager()
{
}
#endregion Constructors
#region Public Methods
//
// Public Methods
//
/// <summary>
/// Add a handler for the given source's event.
/// </summary>
public static void AddHandler(Type type, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(handler);
CurrentManager.PrivateAddHandler(type, handler, propertyName);
}
/// <summary>
/// Remove a handler for the given source's event.
/// </summary>
public static void RemoveHandler(Type type, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
ArgumentNullException.ThrowIfNull(type);
ArgumentNullException.ThrowIfNull(handler);
CurrentManager.PrivateRemoveHandler(type, 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)
{
Debug.Assert(false, "Should never get here");
}
/// <summary>
/// Stop listening to the given source for the event.
/// </summary>
protected override void StopListening(object source)
{
Debug.Assert(false, "Should never get here");
}
/// <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)
{
TypeRecord typeRecord = (TypeRecord)data;
bool foundDirt = typeRecord.Purge(purgeAll);
if (!purgeAll && typeRecord.IsEmpty)
{
Remove(typeRecord.Type);
}
return foundDirt;
}
#endregion Protected Methods
#region Private Properties
//
// Private Properties
//
// get the event manager for the current thread
private static StaticPropertyChangedEventManager CurrentManager
{
get
{
Type managerType = typeof(StaticPropertyChangedEventManager);
StaticPropertyChangedEventManager manager = (StaticPropertyChangedEventManager)GetCurrentManager(managerType);
// at first use, create and register a new manager
if (manager == null)
{
manager = new StaticPropertyChangedEventManager();
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 PrivateAddHandler(Type type, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
Debug.Assert(handler != null && type != null && propertyName != null,
"Handler, type, and propertyName of event cannot be null");
using (WriteLock)
{
TypeRecord tr = (TypeRecord)this[type];
if (tr == null)
{
// no entry in the hashtable - add a new one
tr = new TypeRecord(type, this);
this[type] = tr;
// listen for the desired events
tr.StartListening();
}
tr.AddHandler(handler, propertyName);
}
}
// Remove a handler to the named property (empty means "any property")
private void PrivateRemoveHandler(Type type, EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
Debug.Assert(handler != null && type != null && propertyName != null,
"Handler, type, and propertyName of event cannot be null");
using (WriteLock)
{
TypeRecord tr = (TypeRecord)this[type];
if (tr != null)
{
tr.RemoveHandler(handler, propertyName);
if (tr.IsEmpty)
{
tr.StopListening();
Remove(tr.Type);
}
}
}
}
// event handler for PropertyChanged event
private void OnStaticPropertyChanged(TypeRecord typeRecord, PropertyChangedEventArgs args)
{
ListenerList list;
// get the list of listeners
using (ReadLock)
{
list = typeRecord.GetListenerList(args.PropertyName);
// 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(null, 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 (list == typeRecord.ProposedAllListenersList)
{
using (WriteLock)
{
typeRecord.StoreAllListenersList((ListenerList<PropertyChangedEventArgs>)list);
}
}
}
#if WeakEventTelemetry
void LogAllocationRelay(Type type, int count, int bytes)
{
LogAllocation(type, count, bytes);
}
#endif
#endregion Private Methods
static readonly string AllListenersKey = "<All Listeners>"; // not a legal property name
static readonly string StaticPropertyChanged = "StaticPropertyChanged";
#region TypeRecord
class TypeRecord
{
public TypeRecord(Type type, StaticPropertyChangedEventManager manager)
{
_type = type;
_manager = manager;
_dict = new HybridDictionary(true);
}
public Type Type { get { return _type; } }
public bool IsEmpty { get { return (_dict.Count == 0); } }
public ListenerList ProposedAllListenersList { get { return _proposedAllListenersList; } }
static MethodInfo OnStaticPropertyChangedMethodInfo
{
get
{
return typeof(TypeRecord).GetMethod("OnStaticPropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
}
}
public void StartListening()
{
EventInfo spcEvent = _type.GetEvent(StaticPropertyChanged, BindingFlags.Public | BindingFlags.Static);
if (spcEvent != null)
{
Delegate d = Delegate.CreateDelegate(spcEvent.EventHandlerType, this, OnStaticPropertyChangedMethodInfo);
spcEvent.AddEventHandler(null, d);
}
}
public void StopListening()
{
EventInfo spcEvent = _type.GetEvent(StaticPropertyChanged, BindingFlags.Public | BindingFlags.Static);
if (spcEvent != null)
{
Delegate d = Delegate.CreateDelegate(spcEvent.EventHandlerType, this, OnStaticPropertyChangedMethodInfo);
spcEvent.RemoveEventHandler(null, d);
}
}
void OnStaticPropertyChanged(object sender, PropertyChangedEventArgs e)
{
HandleStaticPropertyChanged(e);
}
public void HandleStaticPropertyChanged(PropertyChangedEventArgs e)
{
_manager.OnStaticPropertyChanged(this, e);
}
public void AddHandler(EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
PropertyRecord pr = (PropertyRecord)_dict[propertyName];
if (pr == null)
{
// no entry in the dictionary - add a new one
pr = new PropertyRecord(propertyName, this);
_dict[propertyName] = pr;
pr.StartListening(_type);
}
pr.AddHandler(handler);
// invalidate list of all listeners
_dict.Remove(AllListenersKey);
_proposedAllListenersList = null;
// schedule a cleanup pass
_manager.ScheduleCleanup();
}
public void RemoveHandler(EventHandler<PropertyChangedEventArgs> handler, string propertyName)
{
PropertyRecord pr = (PropertyRecord)_dict[propertyName];
if (pr != null)
{
pr.RemoveHandler(handler);
if (pr.IsEmpty)
{
_dict.Remove(propertyName);
}
// invalidate list of all listeners
_dict.Remove(AllListenersKey);
_proposedAllListenersList = null;
}
}
public ListenerList GetListenerList(string propertyName)
{
ListenerList list;
if (!String.IsNullOrEmpty(propertyName))
{
// source has changed a particular property. Notify targets
// who are listening either for this property or for all properties.
PropertyRecord pr = (PropertyRecord)_dict[propertyName];
ListenerList<PropertyChangedEventArgs> listeners = (pr == null) ? null : pr.List;
PropertyRecord genericRecord = (PropertyRecord)_dict[String.Empty];
ListenerList<PropertyChangedEventArgs> genericListeners = (genericRecord == null) ? null : genericRecord.List;
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[i]);
for (int i = 0, n = genericListeners.Count; i < n; ++i)
list.Add(genericListeners[i]);
}
else
{
list = genericListeners; // only generic listeners
}
}
}
else
{
// source has changed all properties. Notify all targets.
// Use previously calculated combined list, if available.
PropertyRecord pr = (PropertyRecord)_dict[AllListenersKey];
ListenerList<PropertyChangedEventArgs> pcList = (pr == null) ? null : pr.List;
if (pcList == 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 += ((PropertyRecord)de.Value).List.Count;
}
// create the combined list
pcList = new ListenerList<PropertyChangedEventArgs>(size);
// fill in the combined list
foreach (DictionaryEntry de in _dict)
{
ListenerList listeners = ((PropertyRecord)de.Value).List;
for (int i = 0, n = listeners.Count; i < n; ++i)
{
pcList.Add(listeners.GetListener(i));
}
}
// save the result for future use (see below)
_proposedAllListenersList = pcList;
}
list = pcList;
}
return list;
}
public void StoreAllListenersList(ListenerList<PropertyChangedEventArgs> list)
{
// test again, in case another thread changed _proposedAllListersList.
if (_proposedAllListenersList == list)
{
_dict[AllListenersKey] = new PropertyRecord(AllListenersKey, this, 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.
}
public bool Purge(bool purgeAll)
{
bool foundDirt = false;
if (!purgeAll)
{
if (!MS.Internal.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)
continue; // ignore the special entry for now
// for each key, remove dead entries in its list
PropertyRecord pr = (PropertyRecord)_dict[keys[i]];
if (pr.Purge())
{
foundDirt = true;
}
// if there are no more entries, remove the key
if (pr.IsEmpty)
{
pr.StopListening(_type);
_dict.Remove(keys[i]);
}
}
#if WeakEventTelemetry
_manager.LogAllocationRelay(ic.GetType(), 1, 12); // dict.Keys - Hashtable+KeyCollection
_manager.LogAllocationRelay(typeof(String[]), 1, 12+ic.Count*4); // keys
#endif
}
else
{
Debug.Assert(_toRemove.Count == 0, "to-remove list should be empty");
// 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)
continue; // ignore the special entry for now
// for each key, remove dead entries in its list
PropertyRecord pr = (PropertyRecord)ide.Value;
if (pr.Purge())
{
foundDirt = true;
}
// if there are no more entries, remove the key
if (pr.IsEmpty)
{
pr.StopListening(_type);
_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();
}
#if WeakEventTelemetry
Type enumeratorType = ide.GetType();
if (enumeratorType.Name.IndexOf("NodeEnumerator") >= 0)
{
_manager.LogAllocationRelay(enumeratorType, 1, 24); // ListDictionary+NodeEnumerator
}
else
{
_manager.LogAllocationRelay(enumeratorType, 1, 36); // Hashtable+HashtableEnumerator
}
#endif
}
if (foundDirt)
{
// if any entries were purged, invalidate the special entry
_dict.Remove(AllListenersKey);
_proposedAllListenersList = null;
}
if (IsEmpty)
{
StopListening();
}
}
else
{
// stop listening. List cleanup is handled by Purge()
foundDirt = true;
StopListening();
foreach (DictionaryEntry de in _dict)
{
PropertyRecord pr = (PropertyRecord)de.Value;
pr.StopListening(_type);
}
}
return foundDirt;
}
Type _type; // the type whose static property-changes we're listening to
HybridDictionary _dict; // Property-name -> PropertyRecord
StaticPropertyChangedEventManager _manager; // owner
ListenerList<PropertyChangedEventArgs> _proposedAllListenersList;
List<String> _toRemove = new List<String>();
}
#endregion TypeRecord
#region PropertyRecord
class PropertyRecord
{
public PropertyRecord(string propertyName, TypeRecord owner)
: this(propertyName, owner, new ListenerList<PropertyChangedEventArgs>())
{
}
public PropertyRecord(string propertyName, TypeRecord owner, ListenerList<PropertyChangedEventArgs> list)
{
_propertyName = propertyName;
_typeRecord = owner;
_list = list;
}
public bool IsEmpty { get { return _list.IsEmpty; } }
public ListenerList<PropertyChangedEventArgs> List { get { return _list; } }
static MethodInfo OnStaticPropertyChangedMethodInfo
{
get
{
return typeof(PropertyRecord).GetMethod("OnStaticPropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
}
}
public void StartListening(Type type)
{
string eventName = $"{_propertyName}Changed";
EventInfo eventInfo = type.GetEvent(eventName, BindingFlags.Public | BindingFlags.Static);
if (eventInfo != null)
{
Delegate d = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, OnStaticPropertyChangedMethodInfo);
eventInfo.AddEventHandler(null, d);
}
}
public void StopListening(Type type)
{
string eventName = $"{_propertyName}Changed";
EventInfo eventInfo = type.GetEvent(eventName, BindingFlags.Public | BindingFlags.Static);
if (eventInfo != null)
{
Delegate d = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, OnStaticPropertyChangedMethodInfo);
eventInfo.RemoveEventHandler(null, d);
}
}
void OnStaticPropertyChanged(object sender, EventArgs e)
{
_typeRecord.HandleStaticPropertyChanged(new PropertyChangedEventArgs(_propertyName));
}
public void AddHandler(EventHandler<PropertyChangedEventArgs> handler)
{
// make sure list is ready for writing
ListenerList list = _list;
if (ListenerList.PrepareForWriting(ref list))
_list = (ListenerList<PropertyChangedEventArgs>)list;
// add a listener to the list
_list.AddHandler(handler);
}
public void RemoveHandler(EventHandler<PropertyChangedEventArgs> handler)
{
// make sure list is ready for writing
ListenerList list = _list;
if (ListenerList.PrepareForWriting(ref list))
_list = (ListenerList<PropertyChangedEventArgs>)list;
// remove a listener from the list
_list.RemoveHandler(handler);
}
public bool Purge()
{
ListenerList list = _list;
if (ListenerList.PrepareForWriting(ref list))
_list = (ListenerList<PropertyChangedEventArgs>)list;
return _list.Purge();
}
string _propertyName;
ListenerList<PropertyChangedEventArgs> _list;
TypeRecord _typeRecord;
}
#endregion PropertyRecord
}
}
|