Code:
/ Dotnetfx_Vista_SP2 / Dotnetfx_Vista_SP2 / 8.0.50727.4016 / DEVDIV / depot / DevDiv / releases / Orcas / QFE / 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

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- StandardMenuStripVerb.cs
- PolygonHotSpot.cs
- CounterNameConverter.cs
- GradientStop.cs
- PermissionSetTriple.cs
- OracleLob.cs
- XmlRawWriter.cs
- TextElementCollection.cs
- StringPropertyBuilder.cs
- BufferedMessageWriter.cs
- PageHandlerFactory.cs
- BinaryFormatter.cs
- UpdatableWrapper.cs
- BigInt.cs
- TextBoxAutomationPeer.cs
- EntityCommandDefinition.cs
- PointConverter.cs
- PageSetupDialog.cs
- ThicknessAnimationUsingKeyFrames.cs
- ClientSettingsSection.cs
- BrushMappingModeValidation.cs
- HtmlTableRowCollection.cs
- DataGridViewCell.cs
- HMACSHA512.cs
- _LoggingObject.cs
- Atom10FormatterFactory.cs
- PerformanceCounterCategory.cs
- FreezableDefaultValueFactory.cs
- MenuAdapter.cs
- ReflectEventDescriptor.cs
- XhtmlBasicTextBoxAdapter.cs
- DocumentCollection.cs
- CodeValidator.cs
- EntityRecordInfo.cs
- TypeForwardedToAttribute.cs
- DetailsViewModeEventArgs.cs
- DesignerForm.cs
- ParseElementCollection.cs
- TdsRecordBufferSetter.cs
- OuterGlowBitmapEffect.cs
- TableLayoutStyle.cs
- SuppressMessageAttribute.cs
- XmlNamespaceDeclarationsAttribute.cs
- ArglessEventHandlerProxy.cs
- FixedSOMContainer.cs
- ExpressionBindingCollection.cs
- StandardOleMarshalObject.cs
- DataKey.cs
- DocumentViewerBase.cs
- SettingsPropertyValue.cs
- AssemblyAssociatedContentFileAttribute.cs
- BrowsableAttribute.cs
- ExternalException.cs
- ScalarType.cs
- HttpHandlerAction.cs
- SQLMoneyStorage.cs
- PropertyTabChangedEvent.cs
- HierarchicalDataTemplate.cs
- FigureHelper.cs
- RegistrySecurity.cs
- HttpEncoderUtility.cs
- safelink.cs
- DefaultBinder.cs
- StateInitializationDesigner.cs
- Form.cs
- Hash.cs
- Panel.cs
- SqlBulkCopyColumnMappingCollection.cs
- PatternMatcher.cs
- Paragraph.cs
- control.ime.cs
- OrderedDictionary.cs
- DataGridTablesFactory.cs
- WinInetCache.cs
- NGCPageContentSerializerAsync.cs
- DES.cs
- AuthenticationServiceManager.cs
- InvokeProviderWrapper.cs
- ImageField.cs
- PlatformCulture.cs
- metadatamappinghashervisitor.cs
- XPathChildIterator.cs
- PasswordTextContainer.cs
- BindingBase.cs
- Constants.cs
- bidPrivateBase.cs
- StyleHelper.cs
- EventPropertyMap.cs
- ReadContentAsBinaryHelper.cs
- ListViewSortEventArgs.cs
- CannotUnloadAppDomainException.cs
- ConnectionPointGlyph.cs
- EventLogStatus.cs
- AssemblyCollection.cs
- FixedSOMPage.cs
- ThousandthOfEmRealDoubles.cs
- GridItemPatternIdentifiers.cs
- SqlConnectionString.cs
- EpmTargetPathSegment.cs
- ToolTip.cs