File: system\runtime\interopservices\windowsruntime\eventregistrationtokentable.cs
Project: ndp\clr\src\bcl\mscorlib.csproj (mscorlib)
// ==++==
// 
//   Copyright (c) Microsoft Corporation.  All rights reserved.
// 
// ==--==
//
// <OWNER>Microsoft</OWNER>
// <OWNER>Microsoft</OWNER>
// <OWNER>Microsoft</OWNER>
 
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Threading;
 
namespace System.Runtime.InteropServices.WindowsRuntime
{
    // An event registration token table stores mappings from delegates to event tokens, in order to support
    // sourcing WinRT style events from managed code.
    public sealed class EventRegistrationTokenTable<T> where T : class
    {
        // Note this dictionary is also used as the synchronization object for this table
        private Dictionary<EventRegistrationToken, T> m_tokens = new Dictionary<EventRegistrationToken, T>();
 
        // Cached multicast delegate which will invoke all of the currently registered delegates.  This
        // will be accessed frequently in common coding paterns, so we don't want to calculate it repeatedly.
        private volatile T m_invokeList;
 
        public EventRegistrationTokenTable()
        {
            // T must be a delegate type, but we cannot constrain on being a delegate.  Therefore, we'll do a
            // static check at construction time
            if (!typeof(Delegate).IsAssignableFrom(typeof(T)))
            {
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EventTokenTableRequiresDelegate", typeof(T)));
            }
        }
 
        // The InvocationList property provides access to a delegate which will invoke every registered event handler
        // in this table.  If the property is set, the new value will replace any existing token registrations.
        public T InvocationList
        {
            get
            {
                return m_invokeList;
            }
 
            set
            {
                lock (m_tokens)
                {
                    // The value being set replaces any of the existing values
                    m_tokens.Clear();
                    m_invokeList = null;
 
                    if (value != null)
                    {
                        AddEventHandlerNoLock(value);
                    }
                }
            }
        }
 
        public EventRegistrationToken AddEventHandler(T handler)
        {
            // Windows Runtime allows null handlers.  Assign those a token value of 0 for easy identity
            if (handler == null)
            {
                return new EventRegistrationToken(0);
            }
 
            lock (m_tokens)
            {
                return AddEventHandlerNoLock(handler);
            }
        }
 
        private EventRegistrationToken AddEventHandlerNoLock(T handler)
        {
            Contract.Requires(handler != null);
 
            // Get a registration token, making sure that we haven't already used the value.  This should be quite
            // rare, but in the case it does happen, just keep trying until we find one that's unused.
            EventRegistrationToken token = GetPreferredToken(handler);
            while (m_tokens.ContainsKey(token))
            {
                token = new EventRegistrationToken(token.Value + 1);
            }
            m_tokens[token] = handler;
 
            // Update the current invocation list to include the newly added delegate
            Delegate invokeList = (Delegate)(object)m_invokeList;
            invokeList = MulticastDelegate.Combine(invokeList, (Delegate)(object)handler);
            m_invokeList = (T)(object)invokeList;
 
            return token;
        }
 
        // Get the delegate associated with an event registration token if it exists.  Additionally,
        // remove the registration from the table at the same time.  If the token is not registered,
        // Extract returns null and does not modify the table.
        [System.Runtime.CompilerServices.FriendAccessAllowed]
        internal T ExtractHandler(EventRegistrationToken token)
        {
            T handler = null;
            lock (m_tokens)
            {
                if (m_tokens.TryGetValue(token, out handler))
                {
                    RemoveEventHandlerNoLock(token);
                }
            }
 
            return handler;
        }
 
        // Generate a token that may be used for a particular event handler.  We will frequently be called
        // upon to look up a token value given only a delegate to start from.  Therefore, we want to make
        // an initial token value that is easily determined using only the delegate instance itself.  Although
        // in the common case this token value will be used to uniquely identify the handler, it is not
        // the only possible token that can represent the handler.
        //
        // This means that both:
        //  * if there is a handler assigned to the generated initial token value, it is not necessarily
        //    this handler.
        //  * if there is no handler assigned to the generated initial token value, the handler may still
        //    be registered under a different token
        //
        // Effectively the only reasonable thing to do with this value is either to:
        //  1. Use it as a good starting point for generating a token for handler
        //  2. Use it as a guess to quickly see if the handler was really assigned this token value
        private static EventRegistrationToken GetPreferredToken(T handler)
        {
            Contract.Requires(handler != null);
 
            // We want to generate a token value that has the following properties:
            //  1. is quickly obtained from the handler instance
            //  2. uses bits in the upper 32 bits of the 64 bit value, in order to avoid bugs where code
            //     may assume the value is realy just 32 bits
            //  3. uses bits in the bottom 32 bits of the 64 bit value, in order to ensure that code doesn't
            //     take a dependency on them always being 0.
            //
            // The simple algorithm chosen here is to simply assign the upper 32 bits the metadata token of the
            // event handler type, and the lower 32 bits the hash code of the handler instance itself. Using the
            // metadata token for the upper 32 bits gives us at least a small chance of being able to identify a
            // totally corrupted token if we ever come across one in a minidump or other scenario.
            //
            // The hash code of a unicast delegate is not tied to the method being invoked, so in the case
            // of a unicast delegate, the hash code of the target method is used instead of the full delegate
            // hash code.
            //
            // While calculating this initial value will be somewhat more expensive than just using a counter
            // for events that have few registrations, it will also gives us a shot at preventing unregistration
            // from becoming an O(N) operation.
            //
            // We should feel free to change this algorithm as other requirements / optimizations become
            // available.  This implementation is sufficiently random that code cannot simply guess the value to
            // take a dependency upon it.  (Simply applying the hash-value algorithm directly won't work in the
            // case of collisions, where we'll use a different token value).
 
            uint handlerHashCode = 0;
            Delegate[] invocationList = ((Delegate)(object)handler).GetInvocationList();
            if (invocationList.Length == 1)
            {
                handlerHashCode = (uint)invocationList[0].Method.GetHashCode();
            }
            else
            {
                handlerHashCode = (uint)handler.GetHashCode();
            }
 
            ulong tokenValue = ((ulong)(uint)typeof(T).MetadataToken << 32) | handlerHashCode;
            return new EventRegistrationToken(tokenValue);
        }
 
        public void RemoveEventHandler(EventRegistrationToken token)
        {
            // The 0 token is assigned to null handlers, so there's nothing to do
            if (token.Value == 0)
            {
                return;
            }
 
            lock (m_tokens)
            {
                RemoveEventHandlerNoLock(token);
            }
        }
 
        public void RemoveEventHandler(T handler)
        {
            // To match the Windows Runtime behaivor when adding a null handler, removing one is a no-op
            if (handler == null)
            {
                return;
            }
 
            lock (m_tokens)
            {
                // Fast path - if the delegate is stored with its preferred token, then there's no need to do
                // a full search of the table for it.  Note that even if we find something stored using the
                // preferred token value, it's possible we have a collision and another delegate was using that
                // value.  Therefore we need to make sure we really have the handler we want before taking the
                // fast path.
                EventRegistrationToken preferredToken = GetPreferredToken(handler);
                T registeredHandler;
                if (m_tokens.TryGetValue(preferredToken, out registeredHandler))
                {
                    if (registeredHandler == handler)
                    {
                        RemoveEventHandlerNoLock(preferredToken);
                        return;
                    }
                }
 
                // Slow path - we didn't find the delegate with its preferred token, so we need to fall
                // back to a search of the table
                foreach (KeyValuePair<EventRegistrationToken, T> registration in m_tokens)
                {
                    if (registration.Value == (T)(object)handler)
                    {
                        RemoveEventHandlerNoLock(registration.Key);
 
                        // If a delegate has been added multiple times to handle an event, then it
                        // needs to be removed the same number of times to stop handling the event.
                        // Stop after the first one we find.
                        return;
                    }
                }
 
                // Note that falling off the end of the loop is not an error, as removing a registration
                // for a handler that is not currently registered is simply a no-op
            }
        }
 
        private void RemoveEventHandlerNoLock(EventRegistrationToken token)
        {
            T handler;
            if (m_tokens.TryGetValue(token, out handler))
            {
                m_tokens.Remove(token);
 
                // Update the current invocation list to remove the delegate
                Delegate invokeList = (Delegate)(object)m_invokeList;
                invokeList = MulticastDelegate.Remove(invokeList, (Delegate)(object)handler);
                m_invokeList = (T)(object)invokeList;
            }
        }
 
        public static EventRegistrationTokenTable<T> GetOrCreateEventRegistrationTokenTable(ref EventRegistrationTokenTable<T> refEventTable)
        {
            if (refEventTable == null)
            {
                Interlocked.CompareExchange(ref refEventTable, new EventRegistrationTokenTable<T>(), null);
            }
            return refEventTable;
        }
    }
}