File: Microsoft\Windows\Controls\TextSearchInternal.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\System.Windows.Controls.Ribbon\System.Windows.Controls.Ribbon_dxtfdo3u_wpftmp.csproj (System.Windows.Controls.Ribbon)
// 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.Data;
using System.Globalization;
using System.Windows.Markup;
using System.Collections.Generic;
using System.Windows.Threading;
using System.Diagnostics;
using System.Xml;
#if RIBBON_IN_FRAMEWORK
using System.Windows.Controls.Ribbon;
#else
using Microsoft.Windows.Controls.Ribbon;
#endif
 
#if RIBBON_IN_FRAMEWORK
namespace System.Windows.Controls
#else
namespace Microsoft.Windows.Controls
#endif
{
    /// <summary>
    ///     Almost same as TextSearch, except for the public properties (i.e. TextPath), which are used from TextSearch.
    ///     Text Search is a feature that allows the user to quickly access items in a set by typing prefixes of the strings.
    /// </summary>
    internal sealed class TextSearchInternal : DependencyObject
	{
        /// <summary>
        ///     Make a new TextSearchInternal instance attached to the given object.
        ///     Create the instance in the same context as the given DO.
        /// </summary>
        /// <param name="itemsControl"></param>
        private TextSearchInternal(ItemsControl itemsControl)
        {
            ArgumentNullException.ThrowIfNull(itemsControl);
 
            _attachedTo = itemsControl;
 
            ResetState();
        }
 
        /// <summary>
        ///     Get the instance of TextSearchInternal attached to the given ItemsControl or make one and attach it if it's not.
        /// </summary>
        /// <param name="itemsControl"></param>
        /// <returns></returns>
        internal static TextSearchInternal EnsureInstance(ItemsControl itemsControl)
        {
            TextSearchInternal instance = (TextSearchInternal)itemsControl.GetValue(TextSearchInternalInstanceProperty);
 
            if (instance == null)
            {
                instance = new TextSearchInternal(itemsControl);
                itemsControl.SetValue(TextSearchInternalInstancePropertyKey, instance);
            }
 
            return instance;
        }
 
#if DEBUG
 
        #region Properties
 
        /// <summary>
        ///     Prefix that is currently being used in the algorithm.
        /// </summary>
        private static readonly DependencyProperty CurrentPrefixProperty =
            DependencyProperty.RegisterAttached("CurrentPrefix", typeof(string), typeof(TextSearchInternal),
                                                new FrameworkPropertyMetadata((string)null));
 
        /// <summary>
        ///     If TextSearchInternal is currently active.
        /// </summary>
        private static readonly DependencyProperty IsActiveProperty =
            DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(TextSearchInternal),
                                                new FrameworkPropertyMetadata(false));
 
        #endregion
 
#endif
 
        #region Private Properties
 
        /// <summary>
        ///     The key needed to set a read-only property.
        /// </summary>
        private static readonly DependencyPropertyKey TextSearchInternalInstancePropertyKey =
            DependencyProperty.RegisterAttachedReadOnly("TextSearchInternalInstance", typeof(TextSearchInternal), typeof(TextSearchInternal),
                                                new FrameworkPropertyMetadata((object)null /* default value */));
 
        /// <summary>
        ///     Instance of TextSearchInternal -- attached property so that the instance can be stored on the element
        ///     which wants the service.
        /// </summary>
        private static readonly DependencyProperty TextSearchInternalInstanceProperty =
            TextSearchInternalInstancePropertyKey.DependencyProperty;
 
        #endregion
 
        #region Private Methods
 
        /// <summary>
        /// TextSearchInternal in an ItemsControl containing ItemsControls as children (i.e. RibbonGallery)
        /// performs TextSearchInternal on the second level Items.
        /// </summary>
        /// <param name="nextChar"></param>
        /// <returns></returns>
        internal bool DoHierarchicalSearch(string nextChar)
        {
            bool repeatedChar = false;
 
            // If they pressed the same character as last time, we will do the fallback search.
            //     Fallback search is if they type "bob" and then press "b"
            //     we'll look for "bobb" and when we don't find it we should
            //     find the next item starting with "bob".
            if (_charsEntered.Count > 0 && string.Compare(_charsEntered[_charsEntered.Count - 1], nextChar, true, GetCulture(_attachedTo)) == 0)
            {
                repeatedChar = true;
            }
 
            bool wasNewCharUsed = false;
 
            int matchedItemIndex = -1;
 
            int startItemsControlIndex = 0;
 
            // If TextSearchInternal is not active, then we should start
            // the search from the beginning.  If it is active, we should
            // start the search from the ItemsControl containing the currently-matched item.
            if (IsActive)
            {
                startItemsControlIndex = MatchedItemsControlIndex;
            }
 
            int itemCount = _attachedTo.Items.Count;
            for (int currentIndex = startItemsControlIndex; currentIndex < itemCount; )
            {
                // Skip over filtered categories
                ItemsControl childItemsControl = _attachedTo.ItemContainerGenerator.ContainerFromIndex(currentIndex) as ItemsControl;
                if (childItemsControl != null && childItemsControl.Visibility == Visibility.Visible)
                {
                    int startItemIndex = 0;
 
                    // If TextSearchInternal is not active, then we should start
                    // the search from the beginning.  If it is active, we should
                    // start the search from the currently-matched item.
                    if (IsActive)
                    {
                        startItemIndex = MatchedItemIndex;
                    }
 
                    ItemCollection itemCollection = childItemsControl.Items as ItemCollection;
 
                    // Get the primary TextPath from the child ItemsControl
                    string primaryTextPath = GetPrimaryTextPath(childItemsControl, true);
 
                    matchedItemIndex = FindMatchingPrefix(childItemsControl, primaryTextPath, Prefix, nextChar, startItemIndex, repeatedChar, ref wasNewCharUsed, 
                        /* hierarchicalSearch */ true, 
                        /* fallbackFirstItem */ currentIndex != startItemsControlIndex);
 
                    // If there was an item that matched, move to that item in the collection
                    if (matchedItemIndex != -1)
                    {
                        // Don't have to move currency if it didn't actually move.
                        // startItemIndex is the index of the current item only if IsActive is true,
                        // So, we have to move currency when IsActive is false.
                        if (!IsActive || matchedItemIndex != startItemIndex || currentIndex != startItemsControlIndex)
                        {
                            object matchedItem = itemCollection[matchedItemIndex];
                            
                            // Let the control decide what to do with matched-item
                            RibbonHelper.NavigateToItem(childItemsControl, matchedItemIndex , null);
 
                            // Store current match
                            MatchedItemIndex = matchedItemIndex;
                            MatchedItemsControlIndex = currentIndex;
                        }
 
                        // Update the prefix if it changed
                        if (wasNewCharUsed)
                        {
                            AddCharToPrefix(nextChar);
                        }
 
                        // User has started typing (successfully), so we're active now.
                        if (!IsActive)
                        {
                            IsActive = true;
                        }
                    }
 
                    // Reset the timeout and remember this character, but only if we're
                    // active -- this is because if we got called but the match failed
                    // we don't need to set up a timeout -- no state needs to be reset.
                    if (IsActive)
                    {
                        ResetTimeout();
                    }
 
                    // Found a match so exit the loop.
                    if (matchedItemIndex != -1)
                    {
                        return true;
                    }
 
                    // Reset matchedItemIndex because we are moving to the next childItemsControl
                    MatchedItemIndex = 0;
                }
 
                // Move next and wrap-around if we pass the end of the container.
                currentIndex++;
                if (currentIndex >= itemCount)
                {
                    currentIndex = 0;
                }
 
                // Stop where we started but only after the first pass
                // through the loop -- we should process the startItem.
                if (currentIndex == startItemsControlIndex)
                {
                    break;
                }
            }
 
            return false;
        }
 
        /// <summary>
        ///     Called by consumers of TextSearchInternal when a TextInput event is received
        ///     to kick off the algorithm.
        /// </summary>
        /// <param name="nextChar"></param>
        /// <returns></returns>
        internal bool DoSearch(string nextChar)
        {
            bool repeatedChar = false;
 
            int startItemIndex = 0;
 
            ItemCollection itemCollection = _attachedTo.Items as ItemCollection;
 
            // If TextSearchInternal is not active, then we should start
            // the search from the beginning.  If it is active, we should
            // start the search from the currently-matched item.
            if (IsActive)
            {
                // ISSUE: This falls victim to duplicate elements being in the view.
                //        To mitigate this, we could remember ItemUI ourselves.
 
                startItemIndex = MatchedItemIndex;
            }
 
            // If they pressed the same character as last time, we will do the fallback search.
            //     Fallback search is if they type "bob" and then press "b"
            //     we'll look for "bobb" and when we don't find it we should
            //     find the next item starting with "bob".
            if (_charsEntered.Count > 0 && string.Compare(_charsEntered[_charsEntered.Count - 1], nextChar, true, GetCulture(_attachedTo)) == 0)
            {
                repeatedChar = true;
            }
 
            // Get the primary TextPath from the ItemsControl to which we are attached.
            string primaryTextPath = GetPrimaryTextPath(_attachedTo, false);
 
            bool wasNewCharUsed = false;
 
            int matchedItemIndex = FindMatchingPrefix(_attachedTo, primaryTextPath, Prefix,nextChar, startItemIndex, repeatedChar, ref wasNewCharUsed);
 
            // If there was an item that matched, move to that item in the collection
            if (matchedItemIndex != -1)
            {
                // Don't have to move currency if it didn't actually move.
                // startItemIndex is the index of the current item only if IsActive is true,
                // So, we have to move currency when IsActive is false.
                if (!IsActive || matchedItemIndex != startItemIndex)
                {
                    object matchedItem = itemCollection[matchedItemIndex];
                    // Let the control decide what to do with matched-item
 
                    RibbonHelper.NavigateToItem(_attachedTo, matchedItemIndex , null);
                    
                    // Store current match
                    MatchedItemIndex = matchedItemIndex;
                }
 
                // Update the prefix if it changed
                if (wasNewCharUsed)
                {
                    AddCharToPrefix(nextChar);
                }
 
                // User has started typing (successfully), so we're active now.
                if (!IsActive)
                {
                    IsActive = true;
                }
            }
 
            // Reset the timeout and remember this character, but only if we're
            // active -- this is because if we got called but the match failed
            // we don't need to set up a timeout -- no state needs to be reset.
            if (IsActive)
            {
                ResetTimeout();
            }
 
            return (matchedItemIndex != -1);
        }
 
        /// <summary>
        ///     Called when the user presses backspace.
        /// </summary>
        /// <returns></returns>
        internal bool DeleteLastCharacter()
        {
            if (IsActive)
            {
                // Remove the last character from the prefix string.
                // Get the last character entered and then remove a string of
                // that length off the prefix string.
                if (_charsEntered.Count > 0)
                {
                    string lastChar = _charsEntered[_charsEntered.Count - 1];
                    string prefix = Prefix;
 
                    _charsEntered.RemoveAt(_charsEntered.Count - 1);
                    Prefix = prefix.Substring(0, prefix.Length - lastChar.Length);
 
                    ResetTimeout();
 
                    return true;
                }
            }
 
            return false;
        }
 
        private static int FindMatchingPrefix(ItemsControl itemsControl, string primaryTextPath, 
                                               string prefix, string newChar, int startItemIndex, bool lookForFallbackMatchToo, ref bool wasNewCharUsed)
        {
            return FindMatchingPrefix(itemsControl, primaryTextPath, prefix, newChar, startItemIndex, lookForFallbackMatchToo, ref wasNewCharUsed,
                false, false);
        }
 
        /// <summary>
        ///     Searches through the given itemCollection for the first item matching the given prefix.
        /// </summary>
        /// <remarks>
        ///     --------------------------------------------------------------------------
        ///     Incremental Type Search algorithm
        ///     --------------------------------------------------------------------------
        ///
        ///     Given a prefix and new character, we loop through all items in the collection
        ///     and look for an item that starts with the new prefix.  If we find such an item,
        ///     select it.  If the new character is repeated, we look for the next item after
        ///     the current one that begins with the old prefix**.  We can optimize by
        ///     performing both of these searches in parallel.
        ///
        ///     **NOTE: Win32 will only do this if the old prefix is of length 1 - in other
        ///             words, first-character-only matching.  The algorithm described here
        ///             is an extension of ITS as implemented in Win32.  This variant was
        ///             described to me by JeffBog as what was done in AFC - but I have yet
        ///             to find a listbox which behaves this way.
        ///
        ///     --------------------------------------------------------------------------
        /// </remarks>
        /// <returns>Item that matches the given prefix</returns>
        private static int FindMatchingPrefix(ItemsControl itemsControl, string primaryTextPath, 
                                               string prefix, string newChar, int startItemIndex, bool lookForFallbackMatchToo, ref bool wasNewCharUsed, bool inHierarchicalSearch, bool fallbackFirstItemToo)
        {
            ItemCollection itemCollection = itemsControl.Items;
 
            // Using indices b/c this is a better way to uniquely
            // identify an element in the collection.
            int matchedItemIndex = -1;
            int fallbackMatchIndex = -1;
 
            int count = itemCollection.Count;
 
            // Return immediately with no match if there were no items in the view.
            if (count == 0)
            {
                return -1;
            }
 
            string newPrefix = prefix + newChar;
 
            // With an empty prefix, we'd match anything
            if (String.IsNullOrEmpty(newPrefix))
            {
                return -1;
            }
 
            bool firstItem = true;
 
            wasNewCharUsed = false;
 
            CultureInfo cultureInfo = GetCulture(itemsControl);
 
            // ISSUE: what about changing the collection while this is running?
            for (int currentIndex = startItemIndex; currentIndex < count; )
            {
                object item = itemCollection[currentIndex];
 
                if (item != null)
                {
                    string itemString = GetPrimaryText(item, primaryTextPath);
 
                    // See if the current item matches the newPrefix, if so we can
                    // stop searching and accept this item as the match.
                    if (itemString != null && itemString.StartsWith(newPrefix, /* ignoreCase */ true, cultureInfo))
                    {
                        // Accept the new prefix as the current prefix.
                        wasNewCharUsed = true;
                        matchedItemIndex = currentIndex;
                        break;
                    }
 
                    // Find the next string that matches the last prefix.  This
                    // string will be used in the case that the new prefix isn't
                    // matched. This enables pressing the last character multiple
                    // times and cylcing through the set of items that match that
                    // prefix.
                    //
                    // Unlike the above search, this search must start *after*
                    // the currently selected item.  This search also shouldn't
                    // happen if there was no previous prefix to match against
                    //
                    // In hierarchical Search the first item should also be considered 
                    // for fallbackSearch. 
                    if (lookForFallbackMatchToo)
                    {
                        if ((!firstItem || fallbackFirstItemToo) && !string.IsNullOrEmpty(prefix))
                        {
                            if (itemString != null)
                            {
                                if (fallbackMatchIndex == -1 && itemString.StartsWith(prefix, /* ignoreCase */ true, cultureInfo))
                                {
                                    fallbackMatchIndex = currentIndex;
                                }
                            }
                        }
                        else
                        {
                            firstItem = false;
                        }
                    }
                }
 
                // Move next and wrap-around if we pass the end of the container.
                // In a hierachical TextSearchInternal we dont want to wrap around Items in a childItemsControl
                // Instead we move to the next childItemsControl.
                currentIndex++;
                if (!inHierarchicalSearch && currentIndex >= count)
                {
                    currentIndex = 0;
                }
 
                // Stop where we started but only after the first pass
                // through the loop -- we should process the startItem.
                if (currentIndex == startItemIndex)
                {
                    break;
                }
            }
 
            // In the case that the new prefix didn't match anything and
            // there was a fallback match that matched the old prefix, move
            // to that one.
            if (matchedItemIndex == -1 && fallbackMatchIndex != -1)
            {
                matchedItemIndex = fallbackMatchIndex;
            }
 
            return matchedItemIndex;
        }
 
        /// <summary>
        ///     Helper function called by Editable ComboBox to search through items.
        /// </summary>
        internal static int FindMatchingPrefix(ItemsControl itemsControl, string prefix, bool doHierarchicalSearch)
        {
            bool wasNewCharUsed = false;
 
            return FindMatchingPrefix(itemsControl, GetPrimaryTextPath(itemsControl, doHierarchicalSearch), prefix, String.Empty, 0, false, ref wasNewCharUsed);
        }
 
        /// <summary>
        ///     Helper function called by Editable ComboBox to search through items.
        /// </summary>
        internal static object FindMatchingPrefix(ItemsControl itemsControl, string prefix, bool doHierarchicalSearch, out ItemsControl matchedChildItemsControl)
        {
            matchedChildItemsControl = null;
 
            if (doHierarchicalSearch)
            {
                foreach (object item in itemsControl.Items)
                {
                    // Skip over filtered categories
                    ItemsControl childItemsControl = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as ItemsControl;
                    if (childItemsControl != null && childItemsControl.Visibility == Visibility.Visible)
                    {
                        int matchedIndex = FindMatchingPrefix(childItemsControl, prefix, doHierarchicalSearch);
                        if (matchedIndex != -1)
                        {
                            matchedChildItemsControl = childItemsControl;
                            return childItemsControl.Items[matchedIndex];
                        }
                    }
                }
            }
            else
            {
                int matchedIndex = FindMatchingPrefix(itemsControl, prefix, doHierarchicalSearch);
                if (matchedIndex != -1)
                {
                    return itemsControl.Items[matchedIndex];
                }
            }
 
            return null;
        }
 
        private void ResetTimeout()
        {
            // Called when we get some input. Start or reset the timer.
            // Queue an inactive priority work item and set its deadline.
            if (_timeoutTimer == null)
            {
                _timeoutTimer = new DispatcherTimer(DispatcherPriority.Normal);
                _timeoutTimer.Tick += new EventHandler(OnTimeout);
            }
            else
            {
                _timeoutTimer.Stop();
            }
 
            // Schedule this operation to happen a certain number of milliseconds from now.
            _timeoutTimer.Interval = TimeOut;
            _timeoutTimer.Start();
        }
 
        private void AddCharToPrefix(string newChar)
        {
            Prefix += newChar;
            _charsEntered.Add(newChar);
        }
 
        private static string GetPrimaryTextPath(ItemsControl itemsControl, bool doHierarchicalSearch)
        {
            string primaryTextPath = (string)itemsControl.GetValue(TextSearch.TextPathProperty);
 
            if (String.IsNullOrEmpty(primaryTextPath))
            {
                primaryTextPath = itemsControl.DisplayMemberPath;
            }
 
            if (String.IsNullOrEmpty(primaryTextPath) && doHierarchicalSearch)
            {
                // Fallback on ParentItemsControl
                ItemsControl parentItemsControl = ItemsControl.ItemsControlFromItemContainer(itemsControl);
                if (parentItemsControl != null)
                {
                    primaryTextPath = (string)parentItemsControl.GetValue(TextSearch.TextPathProperty);
                }
 
                if (String.IsNullOrEmpty(primaryTextPath) && doHierarchicalSearch)
                {
                    // Fallback on GrandParentItemsControl
                    ItemsControl grandParentItemsControl = ItemsControl.ItemsControlFromItemContainer(parentItemsControl);
                    if (grandParentItemsControl != null)
                    {
                        primaryTextPath = (string)grandParentItemsControl.GetValue(TextSearch.TextPathProperty);
                    }
                }
            }
 
            return primaryTextPath;
        }
 
        private static string GetPrimaryText(object item, string primaryTextPath)
        {
            // Order of precedence for getting Primary Text is as follows:
            //
            // 1) PrimaryText
            // 2) PrimaryTextPath (TextSearch.TextPath or ItemsControl.DisplayMemberPath)
            // 3) GetPlainText()
            // 4) ToString()
 
            DependencyObject itemDO = item as DependencyObject;
 
            if (itemDO != null)
            {
                string primaryText = (string)itemDO.GetValue(TextSearch.TextProperty);
 
                if (!String.IsNullOrEmpty(primaryText))
                {
                    return primaryText;
                }
            }
 
            // Here hopefully they've supplied a path into their object which we can use.
            if (!string.IsNullOrEmpty(primaryTextPath))
            {
                // Take the binding and apply it to a dummy ContentControl with Source as current item.  
                // Then read the value of the Content property,
                // Try to convert the resulting object to a string.
 
                DummyElement.DataContext = item;
                DummyElement.BindingPath = primaryTextPath;
                object primaryText = DummyElement.Content;
                
                return ConvertToPlainText(primaryText);
            }
 
            return ConvertToPlainText(item);
        }
 
        private static string ConvertToPlainText(object o)
        {
            // Alernative to FE.GetPlainText internal method 
            if (o is FrameworkElement)
            {
                HeaderedContentControl hcc;
                ContentControl cc;
                HeaderedItemsControl hic;
                TextBlock textBlock;
                TextBox textBox;
 
                if ((hcc = o as HeaderedContentControl) != null)
                {
                    return ConvertToPlainText(hcc.Header);
                }
                else if ((cc = o as ContentControl) != null)
                {
                    return ConvertToPlainText(cc.Content);
                }
                else if ((hic = o as HeaderedItemsControl) != null)
                {
                    return ConvertToPlainText(hic.Header);
                }
                else if ((textBlock = o as TextBlock) != null)
                {
                    return textBlock.Text;
                }
                else if ((textBox = o as TextBox) != null)
                {
                    return textBox.Text;
                }
            }
 
            // Try to convert the item to a string
            return (o != null) ? o.ToString() : String.Empty;
        }
 
        /// <summary>
        ///     Internal helper method that uses the same primary text lookup steps but doesn't require
        ///     the user passing in all of the bindings that we need.
        /// </summary>
        /// <param name="itemsControl"></param>
        /// <param name="item"></param>
        /// <returns></returns>
        internal static string GetPrimaryTextFromItem(ItemsControl itemsControl, object item, bool doHierarchicalSearch)
        {
            if (item == null)
                return String.Empty;
 
            return GetPrimaryText(item, GetPrimaryTextPath(itemsControl, doHierarchicalSearch));
        }
 
        private static Binding CreateBinding(object item, string primaryTextPath)
        {
            Binding binding = new Binding();
            binding.Mode = BindingMode.OneWay;
 
            // Use xpath for xmlnodes (See Selector.PrepareItemValueBinding)
            if (AssemblyHelper.IsXmlNode(item))
            {
                binding.XPath = primaryTextPath;
                binding.Path = new PropertyPath("/InnerText");
            }
            else
            {
                binding.Path = new PropertyPath(primaryTextPath);
            }
 
            return binding;
        }
 
        private void OnTimeout(object sender, EventArgs e)
        {
            ResetState();
        }
 
        private void ResetState()
        {
            // Reset the prefix string back to empty.
            IsActive = false;
            Prefix = String.Empty;
            MatchedItemIndex = -1;
            MatchedItemsControlIndex = -1;
            if (_charsEntered == null)
            {
                _charsEntered = new List<string>(10);
            }
            else
            {
                _charsEntered.Clear();
            }
 
            if(_timeoutTimer != null)
            {
                _timeoutTimer.Stop();
            }
            _timeoutTimer = null;
 
        }
 
        /// <summary>
        ///     Time until the search engine resets.
        /// </summary>
        private static TimeSpan TimeOut
        {
            get
            {
                // NOTE: NtUser does the following (file: windows/ntuser/kernel/sysmet.c)
                //     gpsi->dtLBSearch = dtTime * 4;            // dtLBSearch   =  4  * gdtDblClk
                //     gpsi->dtScroll = gpsi->dtLBSearch / 5;  // dtScroll     = 4/5 * gdtDblClk
                //
                // 4 * DoubleClickSpeed seems too slow for the search
                // So for now we'll do 2 * DoubleClickSpeed
 
                return TimeSpan.FromMilliseconds(NativeMethods.GetDoubleClickTime() * 2);
            }
        }
 
        #endregion
 
        #region Testing API
 
        // Being that this is a time-sensitive operation, it's difficult
        // to get the timing right in a DRT.  I'll leave input testing up to BVTs here
        // but this internal API is for the DRT to do basic coverage.
        private static TextSearchInternal GetInstance(DependencyObject d)
        {
            return EnsureInstance(d as ItemsControl);
        }
 
        private void TypeAKey(string c)
        {
            DoSearch(c);
        }
 
        private void CauseTimeOut()
        {
            if (_timeoutTimer != null)
            {
                _timeoutTimer.Stop();
                OnTimeout(_timeoutTimer, EventArgs.Empty);
            }
        }
 
        internal string GetCurrentPrefix()
        {
            return Prefix;
        }
 
        #endregion
 
 
        #region Internal Accessibility API
 
        internal static string GetPrimaryText(FrameworkElement element)
        {
            ArgumentNullException.ThrowIfNull(element);
 
            string text = (string)element.GetValue(TextSearch.TextProperty);
 
            if (!string.IsNullOrEmpty(text))
            {
                return text;
            }
 
            return element.ToString();
        }
 
        #endregion
 
        #region Private Fields
 
        private string Prefix
        {
            get { return _prefix; }
            set
            {
                _prefix = value;
 
#if DEBUG
                // Also need to invalidate the property CurrentPrefixProperty on the instance to which we are attached.
                Debug.Assert(_attachedTo != null);
 
                _attachedTo.SetValue(CurrentPrefixProperty, _prefix);
#endif
            }
        }
 
        private bool IsActive
        {
            get { return _isActive; }
            set
            {
                _isActive = value;
 
#if DEBUG
                Debug.Assert(_attachedTo != null);
 
                _attachedTo.SetValue(IsActiveProperty, _isActive);
#endif
            }
        }
 
        private int MatchedItemIndex
        {
            get { return _matchedItemIndex; }
            set
            {
                _matchedItemIndex = value;
            }
        }
 
        /// <summary>
        /// For a hierachical TextSearchInternal store the index of the last matched child ItemsControl of _attachedTo.
        /// </summary>
        private int MatchedItemsControlIndex
        {
            get { return _matchedItemsControlIndex; }
            set { _matchedItemsControlIndex = value; }
        }
 
        private static DummyObject DummyElement
        {
            get
            {
                if (_dummyElement == null)
                {
                    _dummyElement = new DummyObject();
                }
                return _dummyElement;
            }
        }
 
        private static CultureInfo GetCulture(DependencyObject element)
        {
            object o = element.GetValue(FrameworkElement.LanguageProperty);
            CultureInfo culture = null;
 
            if (o != null)
            {
                XmlLanguage language = (XmlLanguage) o;
                try
                {
                    culture = language.GetSpecificCulture();
                }
                catch (InvalidOperationException)
                {
                }
            }
 
            return culture;
        }
 
        // Element to which this TextSearchInternal instance is attached.
        private ItemsControl _attachedTo;
 
        // String of characters matched so far.
        private string _prefix;
 
        private List<string> _charsEntered;
 
        private bool _isActive;
 
        private int _matchedItemIndex, _matchedItemsControlIndex;
 
        private DispatcherTimer _timeoutTimer;
 
        [ThreadStatic]
        private static DummyObject _dummyElement = new DummyObject();
 
        #endregion
 
        #region DummyObject
 
        private class DummyObject : FrameworkElement
        {
            private static DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(DummyObject));
 
            public object Content
            {
                get { return GetValue(ContentProperty); }
                set { SetValue(ContentProperty, value); }
            }
 
            public string BindingPath
            {
                get { return _bindingPath; }
                set
                {
                    if (!string.Equals(_bindingPath, value))
                    {
                        _bindingPath = value;
                        Binding binding = new Binding();
                        if (DataContext != null && AssemblyHelper.IsXmlNode(DataContext))
                        {
                            // Use xpath for xmlnodes (See Selector.PrepareItemValueBinding)
                            binding.XPath = _bindingPath;
                            binding.Path = new PropertyPath("/InnerText");
                        }
                        else
                        {
                            binding.Path = new PropertyPath(_bindingPath);
                        }
 
                        binding.Mode = BindingMode.OneWay;
                        SetBinding(ContentProperty, binding);
                    }
                }
            }
 
            private string _bindingPath; 
        }
 
        #endregion
    }
 
    internal static class AssemblyHelper
    {
        // return true if the item is an XmlNode
        [System.Runtime.CompilerServices.MethodImplAttribute(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        public static bool IsXmlNode(object item)
        {
            return item is XmlNode;
        }
    }
}