TextSearch.cs source code in C# .NET

Source code for the .NET framework in C#

                        

Code:

/ Net / Net / 3.5.50727.3053 / DEVDIV / depot / DevDiv / releases / Orcas / SP / wpf / src / Framework / System / Windows / Controls / TextSearch.cs / 1 / TextSearch.cs

                            //---------------------------------------------------------------------------- 
//
// Copyright (C) Microsoft Corporation.  All rights reserved.
//
//--------------------------------------------------------------------------- 
using System;
using System.Diagnostics; 
using System.Windows; 
using System.Windows.Threading;
using System.Windows.Data; 
using System.ComponentModel;
using System.Windows.Input;

using System.Collections; 
using MS.Win32;
using System.Globalization; 
using System.Windows.Controls; 
using System.Windows.Controls.Primitives;
using System.Windows.Markup;    // for XmlLanguage 
using System.Windows.Media;
using System.Text;
using System.Collections.Generic;
using MS.Internal; 
using MS.Internal.Data;
 
namespace System.Windows.Controls 
{
    // 



 

 
 

    ///  
    ///     Text Search is a feature that allows the user to quickly access items in a set by typing prefixes of the strings.
    /// 
    public sealed class TextSearch : DependencyObject
 	{ 
        /// 
        ///     Make a new TextSearch instance attached to the given object. 
        ///     Create the instance in the same context as the given DO. 
        /// 
        ///  
        private TextSearch(ItemsControl itemsControl)
        {
            if (itemsControl == null)
            { 
                throw new ArgumentNullException("itemsControl");
            } 
 
            _attachedTo = itemsControl;
 
            ResetState();
        }

        ///  
        ///     Get the instance of TextSearch attached to the given ItemsControl or make one and attach it if it's not.
        ///  
        ///  
        /// 
        internal static TextSearch EnsureInstance(ItemsControl itemsControl) 
        {
            TextSearch instance = (TextSearch)itemsControl.GetValue(TextSearchInstanceProperty);

            if (instance == null) 
            {
                instance = new TextSearch(itemsControl); 
                itemsControl.SetValue(TextSearchInstancePropertyKey, instance); 
            }
 
            return instance;
        }

        #region Text and TextPath Properties 

        ///  
        ///     Attached property to indicate which property on the item in the items collection to use for the "primary" text, 
        ///     or the text against which to search.
        ///  
        public static readonly DependencyProperty TextPathProperty
            = DependencyProperty.RegisterAttached("TextPath", typeof(string), typeof(TextSearch),
                                                  new FrameworkPropertyMetadata(String.Empty /* default value */));
 
        /// 
        ///     Writes the attached property to the given element. 
        ///  
        /// 
        ///  
        public static void SetTextPath(DependencyObject element, string path)
        {
            if (element == null)
            { 
                throw new ArgumentNullException("element");
            } 
 
            element.SetValue(TextPathProperty, path);
        } 

        /// 
        ///     Reads the attached property from the given element.
        ///  
        /// 
        ///  
        [AttachedPropertyBrowsableForType(typeof(DependencyObject))] 
        public static string GetTextPath(DependencyObject element)
        { 
            if (element == null)
            {
                throw new ArgumentNullException("element");
            } 

            return (string)element.GetValue(TextPathProperty); 
        } 

        ///  
        ///     Attached property to indicate the value to use for the "primary" text of an element.
        /// 
        public static readonly DependencyProperty TextProperty
            = DependencyProperty.RegisterAttached("Text", typeof(string), typeof(TextSearch), 
                                                  new FrameworkPropertyMetadata((string)String.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
 
        ///  
        ///     Writes the attached property to the given element.
        ///  
        /// 
        /// 
        public static void SetText(DependencyObject element, string text)
        { 
            if (element == null)
            { 
                throw new ArgumentNullException("element"); 
            }
 
            element.SetValue(TextProperty, text);
        }

        ///  
        ///     Reads the attached property from the given element.
        ///  
        ///  
        /// 
        [AttachedPropertyBrowsableForType(typeof(DependencyObject))] 
        public static string GetText(DependencyObject element)
        {
            if (element == null)
            { 
                throw new ArgumentNullException("element");
            } 
 
            return (string)element.GetValue(TextProperty);
        } 

        #endregion

        #region Properties 

        ///  
        ///     Prefix that is currently being used in the algorithm. 
        /// 
        private static readonly DependencyProperty CurrentPrefixProperty = 
            DependencyProperty.RegisterAttached("CurrentPrefix", typeof(string), typeof(TextSearch),
                                                new FrameworkPropertyMetadata((string)null));

        ///  
        ///     If TextSearch is currently active.
        ///  
        private static readonly DependencyProperty IsActiveProperty = 
            DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(TextSearch),
                                                new FrameworkPropertyMetadata(false)); 

        #endregion

        #region Private Properties 

        ///  
        ///     The key needed set a read-only property. 
        /// 
        private static readonly DependencyPropertyKey TextSearchInstancePropertyKey = 
            DependencyProperty.RegisterAttachedReadOnly("TextSearchInstance", typeof(TextSearch), typeof(TextSearch),
                                                new FrameworkPropertyMetadata((object)null /* default value */));

        ///  
        ///     Instance of TextSearch -- attached property so that the instance can be stored on the element
        ///     which wants the service. 
        ///  
        private static readonly DependencyProperty TextSearchInstanceProperty =
            TextSearchInstancePropertyKey.DependencyProperty; 


        // used to retrieve the value of an item, according to the TextPath
        private static readonly BindingExpressionUncommonField TextValueBindingExpression = new BindingExpressionUncommonField(); 

        #endregion 
 
        #region Private Methods
 
        /// 
        ///     Called by consumers of TextSearch when a TextInput event is received
        ///     to kick off the algorithm.
        ///  
        /// 
        ///  
        internal bool DoSearch(string nextChar) 
        {
            bool repeatedChar = false; 

            int startItemIndex = 0;

            ItemCollection itemCollection = _attachedTo.Items as ItemCollection; 

            // If TextSearch 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);

            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
                    _attachedTo.NavigateToItem(matchedItem, matchedItemIndex, new ItemsControl.ItemNavigateArgs(Keyboard.PrimaryDevice, ModifierKeys.None)); 
                    // 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);
        } 

        /// 
        ///     Called when the user presses backspace.
        ///  
        /// 
        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; 
        }

        /// 
        ///     Searches through the given itemCollection for the first item matching the given prefix. 
        /// 
        ///  
        ///     ------------------------------------------------------------------------- 
        ///     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.
        /// 
        ///     --------------------------------------------------------------------------
        ///  
        /// Item that matches the given prefix 
        private static int FindMatchingPrefix(ItemsControl itemsControl, string primaryTextPath, string prefix,
                                               string newChar, int startItemIndex, bool lookForFallbackMatchToo, ref bool wasNewCharUsed) 
        {
            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; 
            }
 
            // Hook up the binding we will apply to each object.  Get the 
            // PrimaryTextPath off of the attached instance and then make
            // a binding with that path. 

            BindingExpression primaryTextBinding = null;

            object item0 = itemsControl.Items[0]; 
            bool useXml = XmlHelper.IsXmlNode(item0);
 
            if (useXml || !String.IsNullOrEmpty(primaryTextPath)) 
            {
                primaryTextBinding = CreateBindingExpression(itemsControl, item0, primaryTextPath); 
                TextValueBindingExpression.SetValue(itemsControl, primaryTextBinding);
            }
            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, primaryTextBinding, itemsControl); 

                    // 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, 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 
                    if (lookForFallbackMatchToo)
                    {
                        if (!firstItem && prefix != String.Empty)
                        { 
                            if (itemString != null)
                            { 
                                if (fallbackMatchIndex == -1 && itemString.StartsWith(prefix, true, cultureInfo)) 
                                {
                                    fallbackMatchIndex = currentIndex; 
                                }
                            }
                        }
                        else 
                        {
                            firstItem = false; 
                        } 
                    }
                } 

                // Move next and wrap-around if we pass the end of the container.
                currentIndex++;
                if (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; 
                }
            } 
 
            if (primaryTextBinding != null)
            { 
                // Clean up the binding for the primary text path.
                TextValueBindingExpression.ClearValue(itemsControl);
            }
 
            // 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; 
        }
 
        ///  
        ///     Helper function called by Editable ComboBox to search through items.
        ///  
        internal static int FindMatchingPrefix(ItemsControl itemsControl, string prefix)
        {
            bool wasNewCharUsed = false;
 
            return FindMatchingPrefix(itemsControl, GetPrimaryTextPath(itemsControl),
                                      prefix, String.Empty, 0, false, ref wasNewCharUsed); 
        } 

        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) 
        {
            string primaryTextPath = (string)itemsControl.GetValue(TextPathProperty); 

            if (String.IsNullOrEmpty(primaryTextPath))
            {
                primaryTextPath = itemsControl.DisplayMemberPath; 
            }
            return primaryTextPath; 
        } 

        private static string GetPrimaryText(object item, BindingExpression primaryTextBinding, DependencyObject primaryTextBindingHome) 
        {
            // 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(TextProperty); 

                if (!String.IsNullOrEmpty(primaryText)) 
                { 
                    return primaryText;
                } 
            }

            // Here hopefully they've supplied a path into their object which we can use.
            if (primaryTextBinding != null && primaryTextBindingHome != null) 
            {
                // Take the binding that we hooked up at the beginning of the search 
                // and apply it to the current item.  Then, read the value of the 
                // ItemPrimaryText property (where the binding actually lives).
                // Try to convert the resulting object to a string. 
                primaryTextBinding.Activate(item);

                object primaryText = primaryTextBinding.Value;
 
                return ConvertToPlainText(primaryText);
            } 
 
            return ConvertToPlainText(item);
        } 

        private static string ConvertToPlainText(object o)
        {
            FrameworkElement fe = o as FrameworkElement; 

            // Try to return FrameworkElement.GetPlainText() 
            if (fe != null) 
            {
                string text = fe.GetPlainText(); 

                if (text != null)
                {
                    return text; 
                }
            } 
 
            // Try to convert the item to a string
            return (o != null) ? o.ToString() : String.Empty; 
        }

        /// 
        ///     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.
        ///  
        ///  
        /// 
        ///  
        internal static string GetPrimaryTextFromItem(ItemsControl itemsControl, object item)
        {
            if (item == null)
                return String.Empty; 

            BindingExpression primaryTextBinding = CreateBindingExpression(itemsControl, item, GetPrimaryTextPath(itemsControl)); 
            TextValueBindingExpression.SetValue(itemsControl, primaryTextBinding); 

            string primaryText = GetPrimaryText(item, primaryTextBinding, itemsControl); 

            TextValueBindingExpression.ClearValue(itemsControl);

            return primaryText; 
        }
 
        private static BindingExpression CreateBindingExpression(ItemsControl itemsControl, object item, string primaryTextPath) 
        {
            Binding binding = new Binding(); 

            // Use xpath for xmlnodes (See Selector.PrepareItemValueBinding)
            if (XmlHelper.IsXmlNode(item))
            { 
                binding.XPath = primaryTextPath;
                binding.Path = new PropertyPath("/InnerText"); 
            } 
            else
            { 
                binding.Path = new PropertyPath(primaryTextPath);
            }

            binding.Mode = BindingMode.OneWay; 
            binding.Source = null;
            return (BindingExpression)BindingExpression.CreateUntargetedBindingExpression(itemsControl, 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;
            if (_charsEntered == null)
            {
                _charsEntered = new List(10); 
            }
            else 
            { 
                _charsEntered.Clear();
            } 

            if(_timeoutTimer != null)
            {
                _timeoutTimer.Stop(); 
            }
            _timeoutTimer = null; 
 
        }
 
        /// 
        ///     Time until the search engine resets.
        /// 
        private 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(SafeNativeMethods.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 TextSearch 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)
        {
            if (element == null)
            { 
                throw new ArgumentNullException("element");
            } 
 
            string text = (string)element.GetValue(TextProperty);
 
            if (text != null && text != String.Empty)
            {
                return text;
            } 

            return element.GetPlainText(); 
        } 

        #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; 
            }
        } 
 
        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 TextSearch instance is attached.
        private ItemsControl _attachedTo;

        // String of characters matched so far. 
        private string _prefix;
 
        private List _charsEntered; 

        private bool _isActive; 

        private int _matchedItemIndex;

        private DispatcherTimer _timeoutTimer; 

        #endregion 
    } 
}

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
//---------------------------------------------------------------------------- 
//
// Copyright (C) Microsoft Corporation.  All rights reserved.
//
//--------------------------------------------------------------------------- 
using System;
using System.Diagnostics; 
using System.Windows; 
using System.Windows.Threading;
using System.Windows.Data; 
using System.ComponentModel;
using System.Windows.Input;

using System.Collections; 
using MS.Win32;
using System.Globalization; 
using System.Windows.Controls; 
using System.Windows.Controls.Primitives;
using System.Windows.Markup;    // for XmlLanguage 
using System.Windows.Media;
using System.Text;
using System.Collections.Generic;
using MS.Internal; 
using MS.Internal.Data;
 
namespace System.Windows.Controls 
{
    // 



 

 
 

    ///  
    ///     Text Search is a feature that allows the user to quickly access items in a set by typing prefixes of the strings.
    /// 
    public sealed class TextSearch : DependencyObject
 	{ 
        /// 
        ///     Make a new TextSearch instance attached to the given object. 
        ///     Create the instance in the same context as the given DO. 
        /// 
        ///  
        private TextSearch(ItemsControl itemsControl)
        {
            if (itemsControl == null)
            { 
                throw new ArgumentNullException("itemsControl");
            } 
 
            _attachedTo = itemsControl;
 
            ResetState();
        }

        ///  
        ///     Get the instance of TextSearch attached to the given ItemsControl or make one and attach it if it's not.
        ///  
        ///  
        /// 
        internal static TextSearch EnsureInstance(ItemsControl itemsControl) 
        {
            TextSearch instance = (TextSearch)itemsControl.GetValue(TextSearchInstanceProperty);

            if (instance == null) 
            {
                instance = new TextSearch(itemsControl); 
                itemsControl.SetValue(TextSearchInstancePropertyKey, instance); 
            }
 
            return instance;
        }

        #region Text and TextPath Properties 

        ///  
        ///     Attached property to indicate which property on the item in the items collection to use for the "primary" text, 
        ///     or the text against which to search.
        ///  
        public static readonly DependencyProperty TextPathProperty
            = DependencyProperty.RegisterAttached("TextPath", typeof(string), typeof(TextSearch),
                                                  new FrameworkPropertyMetadata(String.Empty /* default value */));
 
        /// 
        ///     Writes the attached property to the given element. 
        ///  
        /// 
        ///  
        public static void SetTextPath(DependencyObject element, string path)
        {
            if (element == null)
            { 
                throw new ArgumentNullException("element");
            } 
 
            element.SetValue(TextPathProperty, path);
        } 

        /// 
        ///     Reads the attached property from the given element.
        ///  
        /// 
        ///  
        [AttachedPropertyBrowsableForType(typeof(DependencyObject))] 
        public static string GetTextPath(DependencyObject element)
        { 
            if (element == null)
            {
                throw new ArgumentNullException("element");
            } 

            return (string)element.GetValue(TextPathProperty); 
        } 

        ///  
        ///     Attached property to indicate the value to use for the "primary" text of an element.
        /// 
        public static readonly DependencyProperty TextProperty
            = DependencyProperty.RegisterAttached("Text", typeof(string), typeof(TextSearch), 
                                                  new FrameworkPropertyMetadata((string)String.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
 
        ///  
        ///     Writes the attached property to the given element.
        ///  
        /// 
        /// 
        public static void SetText(DependencyObject element, string text)
        { 
            if (element == null)
            { 
                throw new ArgumentNullException("element"); 
            }
 
            element.SetValue(TextProperty, text);
        }

        ///  
        ///     Reads the attached property from the given element.
        ///  
        ///  
        /// 
        [AttachedPropertyBrowsableForType(typeof(DependencyObject))] 
        public static string GetText(DependencyObject element)
        {
            if (element == null)
            { 
                throw new ArgumentNullException("element");
            } 
 
            return (string)element.GetValue(TextProperty);
        } 

        #endregion

        #region Properties 

        ///  
        ///     Prefix that is currently being used in the algorithm. 
        /// 
        private static readonly DependencyProperty CurrentPrefixProperty = 
            DependencyProperty.RegisterAttached("CurrentPrefix", typeof(string), typeof(TextSearch),
                                                new FrameworkPropertyMetadata((string)null));

        ///  
        ///     If TextSearch is currently active.
        ///  
        private static readonly DependencyProperty IsActiveProperty = 
            DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(TextSearch),
                                                new FrameworkPropertyMetadata(false)); 

        #endregion

        #region Private Properties 

        ///  
        ///     The key needed set a read-only property. 
        /// 
        private static readonly DependencyPropertyKey TextSearchInstancePropertyKey = 
            DependencyProperty.RegisterAttachedReadOnly("TextSearchInstance", typeof(TextSearch), typeof(TextSearch),
                                                new FrameworkPropertyMetadata((object)null /* default value */));

        ///  
        ///     Instance of TextSearch -- attached property so that the instance can be stored on the element
        ///     which wants the service. 
        ///  
        private static readonly DependencyProperty TextSearchInstanceProperty =
            TextSearchInstancePropertyKey.DependencyProperty; 


        // used to retrieve the value of an item, according to the TextPath
        private static readonly BindingExpressionUncommonField TextValueBindingExpression = new BindingExpressionUncommonField(); 

        #endregion 
 
        #region Private Methods
 
        /// 
        ///     Called by consumers of TextSearch when a TextInput event is received
        ///     to kick off the algorithm.
        ///  
        /// 
        ///  
        internal bool DoSearch(string nextChar) 
        {
            bool repeatedChar = false; 

            int startItemIndex = 0;

            ItemCollection itemCollection = _attachedTo.Items as ItemCollection; 

            // If TextSearch 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);

            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
                    _attachedTo.NavigateToItem(matchedItem, matchedItemIndex, new ItemsControl.ItemNavigateArgs(Keyboard.PrimaryDevice, ModifierKeys.None)); 
                    // 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);
        } 

        /// 
        ///     Called when the user presses backspace.
        ///  
        /// 
        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; 
        }

        /// 
        ///     Searches through the given itemCollection for the first item matching the given prefix. 
        /// 
        ///  
        ///     ------------------------------------------------------------------------- 
        ///     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.
        /// 
        ///     --------------------------------------------------------------------------
        ///  
        /// Item that matches the given prefix 
        private static int FindMatchingPrefix(ItemsControl itemsControl, string primaryTextPath, string prefix,
                                               string newChar, int startItemIndex, bool lookForFallbackMatchToo, ref bool wasNewCharUsed) 
        {
            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; 
            }
 
            // Hook up the binding we will apply to each object.  Get the 
            // PrimaryTextPath off of the attached instance and then make
            // a binding with that path. 

            BindingExpression primaryTextBinding = null;

            object item0 = itemsControl.Items[0]; 
            bool useXml = XmlHelper.IsXmlNode(item0);
 
            if (useXml || !String.IsNullOrEmpty(primaryTextPath)) 
            {
                primaryTextBinding = CreateBindingExpression(itemsControl, item0, primaryTextPath); 
                TextValueBindingExpression.SetValue(itemsControl, primaryTextBinding);
            }
            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, primaryTextBinding, itemsControl); 

                    // 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, 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 
                    if (lookForFallbackMatchToo)
                    {
                        if (!firstItem && prefix != String.Empty)
                        { 
                            if (itemString != null)
                            { 
                                if (fallbackMatchIndex == -1 && itemString.StartsWith(prefix, true, cultureInfo)) 
                                {
                                    fallbackMatchIndex = currentIndex; 
                                }
                            }
                        }
                        else 
                        {
                            firstItem = false; 
                        } 
                    }
                } 

                // Move next and wrap-around if we pass the end of the container.
                currentIndex++;
                if (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; 
                }
            } 
 
            if (primaryTextBinding != null)
            { 
                // Clean up the binding for the primary text path.
                TextValueBindingExpression.ClearValue(itemsControl);
            }
 
            // 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; 
        }
 
        ///  
        ///     Helper function called by Editable ComboBox to search through items.
        ///  
        internal static int FindMatchingPrefix(ItemsControl itemsControl, string prefix)
        {
            bool wasNewCharUsed = false;
 
            return FindMatchingPrefix(itemsControl, GetPrimaryTextPath(itemsControl),
                                      prefix, String.Empty, 0, false, ref wasNewCharUsed); 
        } 

        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) 
        {
            string primaryTextPath = (string)itemsControl.GetValue(TextPathProperty); 

            if (String.IsNullOrEmpty(primaryTextPath))
            {
                primaryTextPath = itemsControl.DisplayMemberPath; 
            }
            return primaryTextPath; 
        } 

        private static string GetPrimaryText(object item, BindingExpression primaryTextBinding, DependencyObject primaryTextBindingHome) 
        {
            // 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(TextProperty); 

                if (!String.IsNullOrEmpty(primaryText)) 
                { 
                    return primaryText;
                } 
            }

            // Here hopefully they've supplied a path into their object which we can use.
            if (primaryTextBinding != null && primaryTextBindingHome != null) 
            {
                // Take the binding that we hooked up at the beginning of the search 
                // and apply it to the current item.  Then, read the value of the 
                // ItemPrimaryText property (where the binding actually lives).
                // Try to convert the resulting object to a string. 
                primaryTextBinding.Activate(item);

                object primaryText = primaryTextBinding.Value;
 
                return ConvertToPlainText(primaryText);
            } 
 
            return ConvertToPlainText(item);
        } 

        private static string ConvertToPlainText(object o)
        {
            FrameworkElement fe = o as FrameworkElement; 

            // Try to return FrameworkElement.GetPlainText() 
            if (fe != null) 
            {
                string text = fe.GetPlainText(); 

                if (text != null)
                {
                    return text; 
                }
            } 
 
            // Try to convert the item to a string
            return (o != null) ? o.ToString() : String.Empty; 
        }

        /// 
        ///     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.
        ///  
        ///  
        /// 
        ///  
        internal static string GetPrimaryTextFromItem(ItemsControl itemsControl, object item)
        {
            if (item == null)
                return String.Empty; 

            BindingExpression primaryTextBinding = CreateBindingExpression(itemsControl, item, GetPrimaryTextPath(itemsControl)); 
            TextValueBindingExpression.SetValue(itemsControl, primaryTextBinding); 

            string primaryText = GetPrimaryText(item, primaryTextBinding, itemsControl); 

            TextValueBindingExpression.ClearValue(itemsControl);

            return primaryText; 
        }
 
        private static BindingExpression CreateBindingExpression(ItemsControl itemsControl, object item, string primaryTextPath) 
        {
            Binding binding = new Binding(); 

            // Use xpath for xmlnodes (See Selector.PrepareItemValueBinding)
            if (XmlHelper.IsXmlNode(item))
            { 
                binding.XPath = primaryTextPath;
                binding.Path = new PropertyPath("/InnerText"); 
            } 
            else
            { 
                binding.Path = new PropertyPath(primaryTextPath);
            }

            binding.Mode = BindingMode.OneWay; 
            binding.Source = null;
            return (BindingExpression)BindingExpression.CreateUntargetedBindingExpression(itemsControl, 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;
            if (_charsEntered == null)
            {
                _charsEntered = new List(10); 
            }
            else 
            { 
                _charsEntered.Clear();
            } 

            if(_timeoutTimer != null)
            {
                _timeoutTimer.Stop(); 
            }
            _timeoutTimer = null; 
 
        }
 
        /// 
        ///     Time until the search engine resets.
        /// 
        private 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(SafeNativeMethods.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 TextSearch 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)
        {
            if (element == null)
            { 
                throw new ArgumentNullException("element");
            } 
 
            string text = (string)element.GetValue(TextProperty);
 
            if (text != null && text != String.Empty)
            {
                return text;
            } 

            return element.GetPlainText(); 
        } 

        #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; 
            }
        } 
 
        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 TextSearch instance is attached.
        private ItemsControl _attachedTo;

        // String of characters matched so far. 
        private string _prefix;
 
        private List _charsEntered; 

        private bool _isActive; 

        private int _matchedItemIndex;

        private DispatcherTimer _timeoutTimer; 

        #endregion 
    } 
}

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
                        

Link Menu

Network programming in C#, Network Programming in VB.NET, Network Programming in .NET
This book is available now!
Buy at Amazon US or
Buy at Amazon UK