File: MS\Internal\Automation\EventMap.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// 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.
 
using System.Windows;
using System.Windows.Automation;
using System.Windows.Automation.Peers;
using System.Windows.Threading;
 
namespace MS.Internal.Automation
{
    // Manages the event map that is used to determine if there are Automation
    // clients interested in specific events.
    internal static class EventMap
    {
        private class EventInfo
        {
            internal EventInfo()
            {
                NumberOfListeners = 1;
            }
 
            internal int NumberOfListeners;
        }
 
        // Never inline, as we don't want to unnecessarily link the automation DLL.
        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static bool IsKnownLegacyEvent(int id)
        {
            if (    id == AutomationElementIdentifiers.ToolTipOpenedEvent.Id
                ||  id == AutomationElementIdentifiers.ToolTipClosedEvent.Id
                ||  id == AutomationElementIdentifiers.MenuOpenedEvent.Id
                ||  id == AutomationElementIdentifiers.MenuClosedEvent.Id
                ||  id == AutomationElementIdentifiers.AutomationFocusChangedEvent.Id
                ||  id == InvokePatternIdentifiers.InvokedEvent.Id
                ||  id == SelectionItemPatternIdentifiers.ElementAddedToSelectionEvent.Id
                ||  id == SelectionItemPatternIdentifiers.ElementRemovedFromSelectionEvent.Id
                ||  id == SelectionItemPatternIdentifiers.ElementSelectedEvent.Id
                ||  id == SelectionPatternIdentifiers.InvalidatedEvent.Id
                ||  id == TextPatternIdentifiers.TextSelectionChangedEvent.Id
                ||  id == TextPatternIdentifiers.TextChangedEvent.Id
                ||  id == AutomationElementIdentifiers.AsyncContentLoadedEvent.Id
                ||  id == AutomationElementIdentifiers.AutomationPropertyChangedEvent.Id
                ||  id == AutomationElementIdentifiers.StructureChangedEvent.Id
                ||  id == SynchronizedInputPatternIdentifiers.InputReachedTargetEvent?.Id
                ||  id == SynchronizedInputPatternIdentifiers.InputReachedOtherElementEvent?.Id
                ||  id == SynchronizedInputPatternIdentifiers.InputDiscardedEvent?.Id)
            {
                return true;
            }
 
            return false;
        }
 
        // Never inline, as we don't want to unnecessarily link the automation DLL.
        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static bool IsKnownNewEvent(int id)
        {
            if (   id == AutomationElementIdentifiers.LiveRegionChangedEvent?.Id
                || id == AutomationElementIdentifiers.NotificationEvent?.Id
                || id == AutomationElementIdentifiers.ActiveTextPositionChangedEvent?.Id)
            {
                return true;
            }
            return false;
        }
 
        // Never inline, as we don't want to unnecessarily link the automation DLL.
        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static bool IsKnownEvent(int id)
        {
            if (IsKnownLegacyEvent(id) ||
                (!AccessibilitySwitches.UseNetFx47CompatibleAccessibilityFeatures && IsKnownNewEvent(id)))
            {
                return true;
            }
            return false;
        }
 
        // Never inline, as we don't want to unnecessarily link the automation DLL.
        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static AutomationEvent GetRegisteredEventObjectHelper(AutomationEvents eventId)
        {
            AutomationEvent eventObject = null;
 
            switch(eventId)
            {
                case AutomationEvents.ToolTipOpened:                                        eventObject = AutomationElementIdentifiers.ToolTipOpenedEvent; break;
                case AutomationEvents.ToolTipClosed:                                        eventObject = AutomationElementIdentifiers.ToolTipClosedEvent; break;
                case AutomationEvents.MenuOpened:                                           eventObject = AutomationElementIdentifiers.MenuOpenedEvent; break;
                case AutomationEvents.MenuClosed:                                           eventObject = AutomationElementIdentifiers.MenuClosedEvent; break;
                case AutomationEvents.AutomationFocusChanged:                               eventObject = AutomationElementIdentifiers.AutomationFocusChangedEvent; break;
                case AutomationEvents.InvokePatternOnInvoked:                               eventObject = InvokePatternIdentifiers.InvokedEvent; break;
                case AutomationEvents.SelectionItemPatternOnElementAddedToSelection:        eventObject = SelectionItemPatternIdentifiers.ElementAddedToSelectionEvent; break;
                case AutomationEvents.SelectionItemPatternOnElementRemovedFromSelection:    eventObject = SelectionItemPatternIdentifiers.ElementRemovedFromSelectionEvent; break;
                case AutomationEvents.SelectionItemPatternOnElementSelected:                eventObject = SelectionItemPatternIdentifiers.ElementSelectedEvent; break;
                case AutomationEvents.SelectionPatternOnInvalidated:                        eventObject = SelectionPatternIdentifiers.InvalidatedEvent; break;
                case AutomationEvents.TextPatternOnTextSelectionChanged:                    eventObject = TextPatternIdentifiers.TextSelectionChangedEvent; break;
                case AutomationEvents.TextPatternOnTextChanged:                             eventObject = TextPatternIdentifiers.TextChangedEvent; break;
                case AutomationEvents.AsyncContentLoaded:                                   eventObject = AutomationElementIdentifiers.AsyncContentLoadedEvent; break;
                case AutomationEvents.PropertyChanged:                                      eventObject = AutomationElementIdentifiers.AutomationPropertyChangedEvent; break;
                case AutomationEvents.StructureChanged:                                     eventObject = AutomationElementIdentifiers.StructureChangedEvent; break;
                case AutomationEvents.InputReachedTarget:                                   eventObject = SynchronizedInputPatternIdentifiers.InputReachedTargetEvent; break;
                case AutomationEvents.InputReachedOtherElement:                             eventObject = SynchronizedInputPatternIdentifiers.InputReachedOtherElementEvent; break;
                case AutomationEvents.InputDiscarded:                                       eventObject = SynchronizedInputPatternIdentifiers.InputDiscardedEvent; break;
                case AutomationEvents.LiveRegionChanged:                                    eventObject = AutomationElementIdentifiers.LiveRegionChangedEvent; break;
                case AutomationEvents.Notification:                                         eventObject = AutomationElementIdentifiers.NotificationEvent; break;
                case AutomationEvents.ActiveTextPositionChanged:                            eventObject = AutomationElementIdentifiers.ActiveTextPositionChangedEvent; break;
 
                default:
                    throw new ArgumentException(SR.Automation_InvalidEventId, "eventId");
            }
 
            if ((eventObject != null) && (!_eventsTable.ContainsKey(eventObject.Id)))
            {
                eventObject = null;
            }
 
            return (eventObject);
        }
 
        internal static void AddEvent(int idEvent)
        {
            //  to avoid unbound memory allocations,
            //  register only events that we recognize
            if (IsKnownEvent(idEvent))
            {
                bool firstEvent = false;
                lock (_lock)
                {
                    if (_eventsTable == null)
                    {
                        _eventsTable = new Dictionary<int, EventInfo>(20);
                        firstEvent = true;
                    }
 
                    if (_eventsTable.TryGetValue(idEvent, out EventInfo info))
                    {
                        info.NumberOfListeners++;
                    }
                    else
                    {
                        _eventsTable[idEvent] = new EventInfo();
                    }
                }
 
                // notify PresentationSources (outside the lock) when the number
                // of listeners becomes non-zero
                if (firstEvent)
                {
                    NotifySources();
                }
            }
        }
 
        internal static void RemoveEvent(int idEvent)
        {
            lock (_lock)
            {
                if (_eventsTable != null)
                {
                    // Decrement the count of listeners for this event
                    if (_eventsTable.TryGetValue(idEvent, out EventInfo info))
                    {
                        // Update or remove the entry based on remaining listeners
                        info.NumberOfListeners--;
                        if (info.NumberOfListeners <= 0)
                        {
                            _eventsTable.Remove(idEvent);
 
                            // If no more entries exist kill the table
                            if (_eventsTable.Count == 0)
                            {
                                _eventsTable = null;
                            }
                        }
                    }
                }
            }
        }
 
        //  Unlike GetRegisteredEvent below,
        //  HasRegisteredEvent does NOT cause automation DLLs loading
        internal static bool HasRegisteredEvent(AutomationEvents eventId)
        {
            lock (_lock)
            {
                if (_eventsTable != null && _eventsTable.Count != 0)
                {
                    return (GetRegisteredEventObjectHelper(eventId) != null);
                }
            }
            return (false);
        }
 
        internal static AutomationEvent GetRegisteredEvent(AutomationEvents eventId)
        {
            lock (_lock)
            {
                if (_eventsTable != null && _eventsTable.Count != 0)
                {
                    return (GetRegisteredEventObjectHelper(eventId));
                }
            }
 
            return (null);
        }
 
        internal static bool HasListeners
        {
            get { return (_eventsTable != null); }
        }
 
        // Most automation clients send WM_GETOBJECT messages to our hwnd(s).
        // We rely on these to add the top-level peers to the LayoutManager's
        // AutomationEvents list, so that the automation tree stays in sync with
        // the visual tree.  But some "clients" merely add WinEvent hooks to
        // intercept automation messages, and don't send WM_GETOBJECT messages.
        // This can lead to crashes (with System.Windows.Automation.ElementNotAvailableException) as follows:
        //  1. external process installs a WinEvent hook
        //  2. WPF app opens a popup.  PopupSecurityHelper.ForceMsaaToUiaBridge
        //      detects the hook, creates a peer, informs uiacore.
        //  3. uiacore registers for automation events, calling EventMap.AddEvent
        //  4. elements throughout the app (on any thread) create peers, thinking
        //      that there is interest in the relevant automation events
        //      (EventMap.HasRegisteredEvent returns true).
        //  5. after visual tree changes, the automation tree is not fixed up fully
        //  6. some automation code uses outdated information and throws
        //      an exception or makes bad decisions that lead to problems later
        // [In some cases, TreeViewItems get recycled to display different data,
        // but the corresponding TreeViewItemAutomationPeers don't always get
        // fixed up to refer to a different EventSource (TreeViewDataItemAutomationPeer).
        // A subsequent UpdatePeer (induced by changing IsEnabled) walks down
        // the wrong path, finds a stale data item peer, and throws ElementNotFoundException.]
        //
        // To mitigate this, ensure that all top-level peers get added to the
        // AutomationEvents list whenever there are any event listeners.  This
        // has two parts:
        //  a. new top-level elements (HwndSource.RootVisual) check for listeners
        //      and add themselves to the list
        //  b. when the listener count becomes non-zero, add existing top-level
        //      elements to the list
        // This strategy is much cheaper than checking something each time an
        // element creates a peer (as in (4) above), but it will create a full
        // automation tree for all windows, even those that don't have any
        // elements that check for events.  However, that's just what would
        // happen in the presence of a full automation client that sends
        // WM_GETOBJECT, so it's a cost we're already paying in the "normal" case.
        //
        // The following methods implement (b).   See <see cref="HwndSource.RootVisual"/> for (a).
 
        private static void NotifySources()
        {
            foreach (PresentationSource source in PresentationSource.CriticalCurrentSources)
            {
                if (!source.IsDisposed)
                {
                    source.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (DispatcherOperationCallback)(state =>
                    {
                        PresentationSource source = (PresentationSource)state;
                        if (source != null && !source.IsDisposed)
                        {
                            // setting the RootVisual to itself triggers the logic to
                            // add to the AutomationEvents list
                            source.RootVisual = source.RootVisual;
                        }
                        return null;
                    }), source);
                }
            }
        }
 
        private static Dictionary<int, EventInfo> _eventsTable;        // key=event id, data=listener count
        private readonly static object _lock = new object();
    }
}