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
- BitmapEffectInput.cs
- DateTimeHelper.cs
- EdmType.cs
- Memoizer.cs
- InputProviderSite.cs
- UTF32Encoding.cs
- DynamicPropertyHolder.cs
- CharStorage.cs
- ServiceControllerDesigner.cs
- PersonalizationState.cs
- RepeaterItemCollection.cs
- XD.cs
- ClientSideQueueItem.cs
- IdentityModelStringsVersion1.cs
- LinqDataSourceInsertEventArgs.cs
- DocumentViewerBase.cs
- SafeHandles.cs
- DataControlFieldCollection.cs
- DataGridPagerStyle.cs
- ScaleTransform.cs
- MarginCollapsingState.cs
- SdlChannelSink.cs
- DesignerExtenders.cs
- GroupBox.cs
- EnvelopedPkcs7.cs
- SoapCodeExporter.cs
- SortDescriptionCollection.cs
- DataFieldConverter.cs
- HyperLinkColumn.cs
- StateChangeEvent.cs
- PieceNameHelper.cs
- _NestedSingleAsyncResult.cs
- SafeRightsManagementEnvironmentHandle.cs
- RightsManagementInformation.cs
- DirtyTextRange.cs
- ImageUrlEditor.cs
- TableColumnCollectionInternal.cs
- KeyFrames.cs
- AssemblyResourceLoader.cs
- PriorityQueue.cs
- StorageFunctionMapping.cs
- RichTextBoxContextMenu.cs
- MobileUserControlDesigner.cs
- XhtmlTextWriter.cs
- StaticContext.cs
- TaskExtensions.cs
- PolicyStatement.cs
- GridViewCommandEventArgs.cs
- AncestorChangedEventArgs.cs
- TextSchema.cs
- ServiceReference.cs
- AssertValidation.cs
- HostSecurityManager.cs
- CancellationHandler.cs
- EmptyStringExpandableObjectConverter.cs
- DeobfuscatingStream.cs
- MarshalByValueComponent.cs
- NamespaceEmitter.cs
- Convert.cs
- DetailsViewUpdatedEventArgs.cs
- PixelShader.cs
- WizardPanelChangingEventArgs.cs
- DetailsViewInsertedEventArgs.cs
- EntityType.cs
- Constants.cs
- DataSourceNameHandler.cs
- ConfigurationErrorsException.cs
- IteratorDescriptor.cs
- dtdvalidator.cs
- Resources.Designer.cs
- DataGridViewCellStyleContentChangedEventArgs.cs
- LogAppendAsyncResult.cs
- ModifierKeysConverter.cs
- GridViewSortEventArgs.cs
- ValidatingPropertiesEventArgs.cs
- ImageAnimator.cs
- RbTree.cs
- PrintDialogDesigner.cs
- ColorDialog.cs
- JournalEntry.cs
- ProfileService.cs
- ServiceBusyException.cs
- CodeIndexerExpression.cs
- TreeNodeCollectionEditorDialog.cs
- EditorPart.cs
- CodeCommentStatementCollection.cs
- X509CertificateEndpointIdentity.cs
- ModifyActivitiesPropertyDescriptor.cs
- HierarchicalDataTemplate.cs
- PackWebResponse.cs
- XmlDataSource.cs
- UMPAttributes.cs
- loginstatus.cs
- FormattedTextSymbols.cs
- RegexRunner.cs
- VScrollBar.cs
- TreeView.cs
- HwndSource.cs
- SafeHandles.cs
- DictionaryItemsCollection.cs