// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993] for details.
// All other rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Windows.Automation.Peers;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Threading;
namespace System.Windows.Controls
{
///
/// Represents a control that provides a text box for user input and a
/// drop-down that contains possible matches based on the input in the text
/// box.
///
/// Stable
[TemplatePart(Name = AutoCompleteBox.ElementSelectionAdapter, Type = typeof(ISelectionAdapter))]
[TemplatePart(Name = AutoCompleteBox.ElementSelector, Type = typeof(Selector))]
[TemplatePart(Name = AutoCompleteBox.ElementTextBox, Type = typeof(TextBox))]
[TemplatePart(Name = AutoCompleteBox.ElementPopup, Type = typeof(Popup))]
[StyleTypedProperty(Property = AutoCompleteBox.ElementTextBoxStyle, StyleTargetType = typeof(TextBox))]
[StyleTypedProperty(Property = AutoCompleteBox.ElementItemContainerStyle, StyleTargetType = typeof(ListBox))]
[TemplateVisualState(Name = VisualStates.StateNormal, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StateMouseOver, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StatePressed, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StateDisabled, GroupName = VisualStates.GroupCommon)]
[TemplateVisualState(Name = VisualStates.StateFocused, GroupName = VisualStates.GroupFocus)]
[TemplateVisualState(Name = VisualStates.StateUnfocused, GroupName = VisualStates.GroupFocus)]
[TemplateVisualState(Name = VisualStates.StatePopupClosed, GroupName = VisualStates.GroupPopup)]
[TemplateVisualState(Name = VisualStates.StatePopupOpened, GroupName = VisualStates.GroupPopup)]
[TemplateVisualState(Name = VisualStates.StateValid, GroupName = VisualStates.GroupValidation)]
[TemplateVisualState(Name = VisualStates.StateInvalidFocused, GroupName = VisualStates.GroupValidation)]
[TemplateVisualState(Name = VisualStates.StateInvalidUnfocused, GroupName = VisualStates.GroupValidation)]
[SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Large implementation keeps the components contained.")]
[ContentProperty("ItemsSource")]
public partial class AutoCompleteBox : Control, IUpdateVisualState
{
#region Template part and style names
///
/// Specifies the name of the selection adapter TemplatePart.
///
private const string ElementSelectionAdapter = "SelectionAdapter";
///
/// Specifies the name of the Selector TemplatePart.
///
private const string ElementSelector = "Selector";
///
/// Specifies the name of the Popup TemplatePart.
///
private const string ElementPopup = "Popup";
///
/// The name for the text box part.
///
private const string ElementTextBox = "Text";
///
/// The name for the text box style.
///
private const string ElementTextBoxStyle = "TextBoxStyle";
///
/// The name for the adapter's item container style.
///
private const string ElementItemContainerStyle = "ItemContainerStyle";
#endregion
///
/// Gets or sets a local cached copy of the items data.
///
private List _items;
///
/// Gets or sets the observable collection that contains references to
/// all of the items in the generated view of data that is provided to
/// the selection-style control adapter.
///
private ObservableCollection _view;
///
/// Gets or sets a value to ignore a number of pending change handlers.
/// The value is decremented after each use. This is used to reset the
/// value of properties without performing any of the actions in their
/// change handlers.
///
/// The int is important as a value because the TextBox
/// TextChanged event does not immediately fire, and this will allow for
/// nested property changes to be ignored.
private int _ignoreTextPropertyChange;
///
/// Gets or sets a value indicating whether to ignore calling a pending
/// change handlers.
///
private bool _ignorePropertyChange;
///
/// Gets or sets a value indicating whether to ignore the selection
/// changed event.
///
private bool _ignoreTextSelectionChange;
///
/// Gets or sets a value indicating whether to skip the text update
/// processing when the selected item is updated.
///
private bool _skipSelectedItemTextUpdate;
///
/// Gets or sets the last observed text box selection start location.
///
private int _textSelectionStart;
///
/// Gets or sets a value indicating whether the user initiated the
/// current populate call.
///
private bool _userCalledPopulate;
///
/// A value indicating whether the popup has been opened at least once.
///
private bool _popupHasOpened;
///
/// Gets or sets the DispatcherTimer used for the MinimumPopulateDelay
/// condition for auto completion.
///
private DispatcherTimer _delayTimer;
///
/// Gets or sets a value indicating whether a read-only dependency
/// property change handler should allow the value to be set. This is
/// used to ensure that read-only properties cannot be changed via
/// SetValue, etc.
///
private bool _allowWrite;
///
/// Gets or sets the helper that provides all of the standard
/// interaction functionality. Making it internal for subclass access.
///
internal InteractionHelper Interaction { get; set; }
///
/// Gets or sets the BindingEvaluator, a framework element that can
/// provide updated string values from a single binding.
///
private BindingEvaluator _valueBindingEvaluator;
///
/// A weak event listener for the collection changed event.
///
private WeakEventListener _collectionChangedWeakEventListener;
#region public int MinimumPrefixLength
///
/// Gets or sets the minimum number of characters required to be entered
/// in the text box before the
/// displays
/// possible matches.
/// matches.
///
///
/// The minimum number of characters to be entered in the text box
/// before the
/// displays possible matches. The default is 1.
///
///
/// If you set MinimumPrefixLength to -1, the AutoCompleteBox will
/// not provide possible matches. There is no maximum value, but
/// setting MinimumPrefixLength to value that is too large will
/// prevent the AutoCompleteBox from providing possible matches as well.
///
public int MinimumPrefixLength
{
get { return (int)GetValue(MinimumPrefixLengthProperty); }
set { SetValue(MinimumPrefixLengthProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty MinimumPrefixLengthProperty =
DependencyProperty.Register(
"MinimumPrefixLength",
typeof(int),
typeof(AutoCompleteBox),
new PropertyMetadata(1, OnMinimumPrefixLengthPropertyChanged));
///
/// MinimumPrefixLengthProperty property changed handler.
///
/// AutoCompleteBox that changed its MinimumPrefixLength.
/// Event arguments.
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "MinimumPrefixLength is the name of the actual dependency property.")]
private static void OnMinimumPrefixLengthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
int newValue = (int)e.NewValue;
if (newValue < 0 && newValue != -1)
{
throw new ArgumentOutOfRangeException("MinimumPrefixLength");
}
}
#endregion public int MinimumPrefixLength
#region public int MinimumPopulateDelay
///
/// Gets or sets the minimum delay, in milliseconds, after text is typed
/// in the text box before the
/// control
/// populates the list of possible matches in the drop-down.
///
/// The minimum delay, in milliseconds, after text is typed in
/// the text box, but before the
/// populates
/// the list of possible matches in the drop-down. The default is 0.
/// The set value is less than 0.
public int MinimumPopulateDelay
{
get { return (int)GetValue(MinimumPopulateDelayProperty); }
set { SetValue(MinimumPopulateDelayProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty MinimumPopulateDelayProperty =
DependencyProperty.Register(
"MinimumPopulateDelay",
typeof(int),
typeof(AutoCompleteBox),
new PropertyMetadata(OnMinimumPopulateDelayPropertyChanged));
///
/// MinimumPopulateDelayProperty property changed handler. Any current
/// dispatcher timer will be stopped. The timer will not be restarted
/// until the next TextUpdate call by the user.
///
/// AutoCompleteTextBox that changed its
/// MinimumPopulateDelay.
/// Event arguments.
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The exception is most likely to be called through the CLR property setter.")]
private static void OnMinimumPopulateDelayPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
if (source._ignorePropertyChange)
{
source._ignorePropertyChange = false;
return;
}
int newValue = (int)e.NewValue;
if (newValue < 0)
{
source._ignorePropertyChange = true;
d.SetValue(e.Property, e.OldValue);
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Muchinfo.WPF.Controls.Properties.Resources.AutoComplete_OnMinimumPopulateDelayPropertyChanged_InvalidValue, newValue), "value");
}
// Stop any existing timer
if (source._delayTimer != null)
{
source._delayTimer.Stop();
if (newValue == 0)
{
source._delayTimer = null;
}
}
// Create or clear a dispatcher timer instance
if (newValue > 0 && source._delayTimer == null)
{
source._delayTimer = new DispatcherTimer();
source._delayTimer.Tick += source.PopulateDropDown;
}
// Set the new tick interval
if (newValue > 0 && source._delayTimer != null)
{
source._delayTimer.Interval = TimeSpan.FromMilliseconds(newValue);
}
}
#endregion public int MinimumPopulateDelay
#region public bool IsTextCompletionEnabled
///
/// Gets or sets a value indicating whether the first possible match
/// found during the filtering process will be displayed automatically
/// in the text box.
///
///
/// True if the first possible match found will be displayed
/// automatically in the text box; otherwise, false. The default is
/// false.
///
public bool IsTextCompletionEnabled
{
get { return (bool)GetValue(IsTextCompletionEnabledProperty); }
set { SetValue(IsTextCompletionEnabledProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty IsTextCompletionEnabledProperty =
DependencyProperty.Register(
"IsTextCompletionEnabled",
typeof(bool),
typeof(AutoCompleteBox),
new PropertyMetadata(false, null));
#endregion public bool IsTextCompletionEnabled
#region public DataTemplate ItemTemplate
///
/// Gets or sets the used
/// to display each item in the drop-down portion of the control.
///
/// The used to
/// display each item in the drop-down. The default is null.
///
/// You use the ItemTemplate property to specify the visualization
/// of the data objects in the drop-down portion of the AutoCompleteBox
/// control. If your AutoCompleteBox is bound to a collection and you
/// do not provide specific display instructions by using a
/// DataTemplate, the resulting UI of each item is a string
/// representation of each object in the underlying collection.
///
public DataTemplate ItemTemplate
{
get { return GetValue(ItemTemplateProperty) as DataTemplate; }
set { SetValue(ItemTemplateProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register(
"ItemTemplate",
typeof(DataTemplate),
typeof(AutoCompleteBox),
new PropertyMetadata(null));
#endregion public DataTemplate ItemTemplate
#region public Style ItemContainerStyle
///
/// Gets or sets the that is
/// applied to the selection adapter contained in the drop-down portion
/// of the
/// control.
///
/// The applied to the
/// selection adapter contained in the drop-down portion of the
/// control.
/// The default is null.
///
/// The default selection adapter contained in the drop-down is a
/// ListBox control.
///
public Style ItemContainerStyle
{
get { return GetValue(ItemContainerStyleProperty) as Style; }
set { SetValue(ItemContainerStyleProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty ItemContainerStyleProperty =
DependencyProperty.Register(
ElementItemContainerStyle,
typeof(Style),
typeof(AutoCompleteBox),
new PropertyMetadata(null, null));
#endregion public Style ItemContainerStyle
#region public Style TextBoxStyle
///
/// Gets or sets the applied to
/// the text box portion of the
/// control.
///
/// The applied to the text
/// box portion of the
/// control.
/// The default is null.
public Style TextBoxStyle
{
get { return GetValue(TextBoxStyleProperty) as Style; }
set { SetValue(TextBoxStyleProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty TextBoxStyleProperty =
DependencyProperty.Register(
ElementTextBoxStyle,
typeof(Style),
typeof(AutoCompleteBox),
new PropertyMetadata(null));
#endregion public Style TextBoxStyle
#region public double MaxDropDownHeight
///
/// Gets or sets the maximum height of the drop-down portion of the
/// control.
///
/// The maximum height of the drop-down portion of the
/// control.
/// The default is .
/// The specified value is less than 0.
public double MaxDropDownHeight
{
get { return (double)GetValue(MaxDropDownHeightProperty); }
set { SetValue(MaxDropDownHeightProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty MaxDropDownHeightProperty =
DependencyProperty.Register(
"MaxDropDownHeight",
typeof(double),
typeof(AutoCompleteBox),
new PropertyMetadata(double.PositiveInfinity, OnMaxDropDownHeightPropertyChanged));
///
/// MaxDropDownHeightProperty property changed handler.
///
/// AutoCompleteTextBox that changed its MaxDropDownHeight.
/// Event arguments.
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The exception will be called through a CLR setter in most cases.")]
private static void OnMaxDropDownHeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
if (source._ignorePropertyChange)
{
source._ignorePropertyChange = false;
return;
}
double newValue = (double)e.NewValue;
// Revert to the old value if invalid (negative)
if (newValue < 0)
{
source._ignorePropertyChange = true;
source.SetValue(e.Property, e.OldValue);
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Muchinfo.WPF.Controls.Properties.Resources.AutoComplete_OnMaxDropDownHeightPropertyChanged_InvalidValue, e.NewValue), "value");
}
source.OnMaxDropDownHeightChanged(newValue);
}
#endregion public double MaxDropDownHeight
#region public bool IsDropDownOpen
///
/// Gets or sets a value indicating whether the drop-down portion of
/// the control is open.
///
///
/// True if the drop-down is open; otherwise, false. The default is
/// false.
///
public bool IsDropDownOpen
{
get { return (bool)GetValue(IsDropDownOpenProperty); }
set { SetValue(IsDropDownOpenProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty IsDropDownOpenProperty =
DependencyProperty.Register(
"IsDropDownOpen",
typeof(bool),
typeof(AutoCompleteBox),
new PropertyMetadata(false, OnIsDropDownOpenPropertyChanged));
///
/// IsDropDownOpenProperty property changed handler.
///
/// AutoCompleteTextBox that changed its IsDropDownOpen.
/// Event arguments.
private static void OnIsDropDownOpenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
// Ignore the change if requested
if (source._ignorePropertyChange)
{
source._ignorePropertyChange = false;
return;
}
bool oldValue = (bool)e.OldValue;
bool newValue = (bool)e.NewValue;
if (newValue)
{
source.TextUpdated(source.Text, true);
}
else
{
source.ClosingDropDown(oldValue);
}
source.UpdateVisualState(true);
}
#endregion public bool IsDropDownOpen
#region public IEnumerable ItemsSource
///
/// Gets or sets a collection that is used to generate the items for the
/// drop-down portion of the
/// control.
///
/// The collection that is used to generate the items of the
/// drop-down portion of the
/// control.
public IEnumerable ItemsSource
{
get { return GetValue(ItemsSourceProperty) as IEnumerable; }
set { SetValue(ItemsSourceProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IEnumerable),
typeof(AutoCompleteBox),
new PropertyMetadata(OnItemsSourcePropertyChanged));
///
/// ItemsSourceProperty property changed handler.
///
/// AutoCompleteBox that changed its ItemsSource.
/// Event arguments.
private static void OnItemsSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox autoComplete = d as AutoCompleteBox;
autoComplete.OnItemsSourceChanged((IEnumerable)e.OldValue, (IEnumerable)e.NewValue);
}
#endregion public IEnumerable ItemsSource
#region public object SelectedItem
///
/// Gets or sets the selected item in the drop-down.
///
/// The selected item in the drop-down.
///
/// If the IsTextCompletionEnabled property is true and text typed by
/// the user matches an item in the ItemsSource collection, which is
/// then displayed in the text box, the SelectedItem property will be
/// a null reference.
///
public object SelectedItem
{
get { return GetValue(SelectedItemProperty) as object; }
set { SetValue(SelectedItemProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier the
///
/// dependency property.
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(
"SelectedItem",
typeof(object),
typeof(AutoCompleteBox),
new PropertyMetadata(OnSelectedItemPropertyChanged));
///
/// SelectedItemProperty property changed handler. Fires the
/// SelectionChanged event. The event data will contain any non-null
/// removed items and non-null additions.
///
/// AutoCompleteBox that changed its SelectedItem.
/// Event arguments.
private static void OnSelectedItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
if (source._ignorePropertyChange)
{
source._ignorePropertyChange = false;
return;
}
// Update the text display
if (source._skipSelectedItemTextUpdate)
{
source._skipSelectedItemTextUpdate = false;
}
else
{
source.OnSelectedItemChanged(e.NewValue);
}
// Fire the SelectionChanged event
List removed = new List();
if (e.OldValue != null)
{
removed.Add(e.OldValue);
}
List added = new List();
if (e.NewValue != null)
{
added.Add(e.NewValue);
}
source.OnSelectionChanged(new SelectionChangedEventArgs(
#if !SILVERLIGHT
SelectionChangedEvent,
#endif
removed,
added));
}
///
/// Called when the selected item is changed, updates the text value
/// that is displayed in the text box part.
///
/// The new item.
private void OnSelectedItemChanged(object newItem)
{
string text;
if (newItem == null)
{
text = SearchText;
}
else
{
text = FormatValue(newItem, true);
}
// Update the Text property and the TextBox values
UpdateTextValue(text);
// Move the caret to the end of the text box
if (TextBox != null && Text != null)
{
TextBox.SelectionStart = Text.Length;
}
}
#endregion public object SelectedItem
#region public string Text
///
/// Gets or sets the text in the text box portion of the
/// control.
///
/// The text in the text box portion of the
/// control.
public string Text
{
get { return GetValue(TextProperty) as string; }
set { SetValue(TextProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(
"Text",
typeof(string),
typeof(AutoCompleteBox),
new PropertyMetadata(string.Empty, OnTextPropertyChanged));
///
/// TextProperty property changed handler.
///
/// AutoCompleteBox that changed its Text.
/// Event arguments.
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
if (source != null) source.TextUpdated((string)e.NewValue, false);
}
#endregion public string Text
#region public string SearchText
///
/// Gets the text that is used to filter items in the
///
/// item collection.
///
/// The text that is used to filter items in the
///
/// item collection.
///
/// The SearchText value is typically the same as the
/// Text property, but is set after the TextChanged event occurs
/// and before the Populating event.
///
public string SearchText
{
get { return (string)GetValue(SearchTextProperty); }
private set
{
try
{
_allowWrite = true;
SetValue(SearchTextProperty, value);
}
finally
{
_allowWrite = false;
}
}
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty SearchTextProperty =
DependencyProperty.Register(
"SearchText",
typeof(string),
typeof(AutoCompleteBox),
new PropertyMetadata(string.Empty, OnSearchTextPropertyChanged));
///
/// OnSearchTextProperty property changed handler.
///
/// AutoCompleteBox that changed its SearchText.
/// Event arguments.
private static void OnSearchTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
if (source._ignorePropertyChange)
{
source._ignorePropertyChange = false;
return;
}
// Ensure the property is only written when expected
if (!source._allowWrite)
{
// Reset the old value before it was incorrectly written
source._ignorePropertyChange = true;
source.SetValue(e.Property, e.OldValue);
throw new InvalidOperationException(Muchinfo.WPF.Controls.Properties.Resources.AutoComplete_OnSearchTextPropertyChanged_InvalidWrite);
}
}
#endregion public string SearchText
#region public AutoCompleteFilterMode FilterMode
///
/// Gets or sets how the text in the text box is used to filter items
/// specified by the
///
/// property for display in the drop-down.
///
/// One of the
///
/// values The default is
/// .
/// The specified value is
/// not a valid
/// .
///
/// Use the FilterMode property to specify how possible matches are
/// filtered. For example, possible matches can be filtered in a
/// predefined or custom way. The search mode is automatically set to
/// Custom if you set the ItemFilter property.
///
public AutoCompleteFilterMode FilterMode
{
get { return (AutoCompleteFilterMode)GetValue(FilterModeProperty); }
set { SetValue(FilterModeProperty, value); }
}
///
/// Gets the identifier for the
///
/// dependency property.
///
public static readonly DependencyProperty FilterModeProperty =
DependencyProperty.Register(
"FilterMode",
typeof(AutoCompleteFilterMode),
typeof(AutoCompleteBox),
new PropertyMetadata(AutoCompleteFilterMode.StartsWith, OnFilterModePropertyChanged));
///
/// FilterModeProperty property changed handler.
///
/// AutoCompleteBox that changed its FilterMode.
/// Event arguments.
[SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "The exception will be thrown when the CLR setter is used in most situations.")]
private static void OnFilterModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue;
if (mode != AutoCompleteFilterMode.Contains &&
mode != AutoCompleteFilterMode.ContainsCaseSensitive &&
mode != AutoCompleteFilterMode.ContainsOrdinal &&
mode != AutoCompleteFilterMode.ContainsOrdinalCaseSensitive &&
mode != AutoCompleteFilterMode.Custom &&
mode != AutoCompleteFilterMode.Equals &&
mode != AutoCompleteFilterMode.EqualsCaseSensitive &&
mode != AutoCompleteFilterMode.EqualsOrdinal &&
mode != AutoCompleteFilterMode.EqualsOrdinalCaseSensitive &&
mode != AutoCompleteFilterMode.None &&
mode != AutoCompleteFilterMode.StartsWith &&
mode != AutoCompleteFilterMode.StartsWithCaseSensitive &&
mode != AutoCompleteFilterMode.StartsWithOrdinal &&
mode != AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive)
{
source.SetValue(e.Property, e.OldValue);
throw new ArgumentException(Muchinfo.WPF.Controls.Properties.Resources.AutoComplete_OnFilterModePropertyChanged_InvalidValue, "value");
}
// Sets the filter predicate for the new value
AutoCompleteFilterMode newValue = (AutoCompleteFilterMode)e.NewValue;
source.TextFilter = AutoCompleteSearch.GetFilter(newValue);
}
#endregion public AutoCompleteFilterMode FilterMode
#region public AutoCompleteFilterPredicate ItemFilter
///
/// Gets or sets the custom method that uses user-entered text to filter
/// the items specified by the
///
/// property for display in the drop-down.
///
/// The custom method that uses the user-entered text to filter
/// the items specified by the
///
/// property. The default is null.
///
/// The filter mode is automatically set to Custom if you set the
/// ItemFilter property.
///
public AutoCompleteFilterPredicate ItemFilter
{
get { return GetValue(ItemFilterProperty) as AutoCompleteFilterPredicate; }
set { SetValue(ItemFilterProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty ItemFilterProperty =
DependencyProperty.Register(
"ItemFilter",
typeof(AutoCompleteFilterPredicate),
typeof(AutoCompleteBox),
new PropertyMetadata(OnItemFilterPropertyChanged));
///
/// ItemFilterProperty property changed handler.
///
/// AutoCompleteBox that changed its ItemFilter.
/// Event arguments.
private static void OnItemFilterPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
AutoCompleteBox source = d as AutoCompleteBox;
AutoCompleteFilterPredicate value = e.NewValue as AutoCompleteFilterPredicate;
// If null, revert to the "None" predicate
if (value == null)
{
source.FilterMode = AutoCompleteFilterMode.None;
}
else
{
source.FilterMode = AutoCompleteFilterMode.Custom;
source.TextFilter = null;
}
}
#endregion public AutoCompleteFilterPredicate ItemFilter
#region public AutoCompleteStringFilterPredicate TextFilter
///
/// Gets or sets the custom method that uses the user-entered text to
/// filter items specified by the
///
/// property in a text-based way for display in the drop-down.
///
/// The custom method that uses the user-entered text to filter
/// items specified by the
///
/// property in a text-based way for display in the drop-down.
///
/// The search mode is automatically set to Custom if you set the
/// TextFilter property.
///
public AutoCompleteFilterPredicate TextFilter
{
get { return GetValue(TextFilterProperty) as AutoCompleteFilterPredicate; }
set { SetValue(TextFilterProperty, value); }
}
///
/// Identifies the
///
/// dependency property.
///
/// The identifier for the
///
/// dependency property.
public static readonly DependencyProperty TextFilterProperty =
DependencyProperty.Register(
"TextFilter",
typeof(AutoCompleteFilterPredicate),
typeof(AutoCompleteBox),
new PropertyMetadata(AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)));
#endregion public AutoCompleteStringFilterPredicate TextFilter
#region Template parts
///
/// Gets or sets the drop down popup control.
///
private PopupHelper DropDownPopup { get; set; }
///
/// The TextBox template part.
///
private TextBox _text;
///
/// The SelectionAdapter.
///
private ISelectionAdapter _adapter;
///
/// Gets or sets the Text template part.
///
internal TextBox TextBox
{
get { return _text; }
set
{
// Detach existing handlers
if (_text != null)
{
_text.SelectionChanged -= OnTextBoxSelectionChanged;
_text.TextChanged -= OnTextBoxTextChanged;
}
_text = value;
// Attach handlers
if (_text != null)
{
_text.SelectionChanged += OnTextBoxSelectionChanged;
_text.TextChanged += OnTextBoxTextChanged;
if (Text != null)
{
UpdateTextValue(Text);
}
}
}
}
///
/// Gets or sets the selection adapter used to populate the drop-down
/// with a list of selectable items.
///
/// The selection adapter used to populate the drop-down with a
/// list of selectable items.
///
/// You can use this property when you create an automation peer to
/// use with AutoCompleteBox or deriving from AutoCompleteBox to
/// create a custom control.
///
protected internal ISelectionAdapter SelectionAdapter
{
get { return _adapter; }
set
{
if (_adapter != null)
{
_adapter.SelectionChanged -= OnAdapterSelectionChanged;
_adapter.Commit -= OnAdapterSelectionComplete;
_adapter.Cancel -= OnAdapterSelectionCanceled;
_adapter.Cancel -= OnAdapterSelectionComplete;
_adapter.ItemsSource = null;
}
_adapter = value;
if (_adapter != null)
{
_adapter.SelectionChanged += OnAdapterSelectionChanged;
_adapter.Commit += OnAdapterSelectionComplete;
_adapter.Cancel += OnAdapterSelectionCanceled;
_adapter.Cancel += OnAdapterSelectionComplete;
_adapter.ItemsSource = _view;
}
}
}
#endregion
///
/// Occurs when the text in the text box portion of the
/// changes.
///
#if SILVERLIGHT
public event RoutedEventHandler TextChanged;
#else
public static readonly RoutedEvent TextChangedEvent = EventManager.RegisterRoutedEvent("TextChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the text in the text box portion of the
/// changes.
///
public event RoutedEventHandler TextChanged
{
add { AddHandler(TextChangedEvent, value); }
remove { RemoveHandler(TextChangedEvent, value); }
}
#endif
///
/// Occurs when the
/// is
/// populating the drop-down with possible matches based on the
///
/// property.
///
///
/// If the event is canceled, by setting the PopulatingEventArgs.Cancel
/// property to true, the AutoCompleteBox will not automatically
/// populate the selection adapter contained in the drop-down.
/// In this case, if you want possible matches to appear, you must
/// provide the logic for populating the selection adapter.
///
#if SILVERLIGHT
public event PopulatingEventHandler Populating;
#else
public static readonly RoutedEvent PopulatingEvent = EventManager.RegisterRoutedEvent("Populating", RoutingStrategy.Bubble, typeof(PopulatingEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the
/// is
/// populating the drop-down with possible matches based on the
///
/// property.
///
///
/// If the event is canceled, by setting the PopulatingEventArgs.Cancel
/// property to true, the AutoCompleteBox will not automatically
/// populate the selection adapter contained in the drop-down.
/// In this case, if you want possible matches to appear, you must
/// provide the logic for populating the selection adapter.
///
public event PopulatingEventHandler Populating
{
add { AddHandler(PopulatingEvent, value); }
remove { RemoveHandler(PopulatingEvent, value); }
}
#endif
///
/// Occurs when the
/// has
/// populated the drop-down with possible matches based on the
///
/// property.
///
#if SILVERLIGHT
public event PopulatedEventHandler Populated;
#else
public static readonly RoutedEvent PopulatedEvent = EventManager.RegisterRoutedEvent("Populated", RoutingStrategy.Bubble, typeof(PopulatedEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the
/// has
/// populated the drop-down with possible matches based on the
///
/// property.
///
public event PopulatedEventHandler Populated
{
add { AddHandler(PopulatedEvent, value); }
remove { RemoveHandler(PopulatedEvent, value); }
}
#endif
///
/// Occurs when the value of the
///
/// property is changing from false to true.
///
#if SILVERLIGHT
public event RoutedPropertyChangingEventHandler DropDownOpening;
#else
public static readonly RoutedEvent DropDownOpeningEvent = EventManager.RegisterRoutedEvent("DropDownOpening", RoutingStrategy.Bubble, typeof(RoutedPropertyChangingEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the value of the
///
/// property is changing from false to true.
///
public event RoutedPropertyChangingEventHandler DropDownOpening
{
add { AddHandler(PopulatedEvent, value); }
remove { RemoveHandler(PopulatedEvent, value); }
}
#endif
///
/// Occurs when the value of the
///
/// property has changed from false to true and the drop-down is open.
///
#if SILVERLIGHT
public event RoutedPropertyChangedEventHandler DropDownOpened;
#else
public static readonly RoutedEvent DropDownOpenedEvent = EventManager.RegisterRoutedEvent("DropDownOpened", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the value of the
///
/// property has changed from false to true and the drop-down is open.
///
public event RoutedPropertyChangedEventHandler DropDownOpened
{
add { AddHandler(DropDownOpenedEvent, value); }
remove { RemoveHandler(DropDownOpenedEvent, value); }
}
#endif
///
/// Occurs when the
///
/// property is changing from true to false.
///
#if SILVERLIGHT
public event RoutedPropertyChangingEventHandler DropDownClosing;
#else
public static readonly RoutedEvent DropDownClosingEvent = EventManager.RegisterRoutedEvent("DropDownClosing", RoutingStrategy.Bubble, typeof(RoutedPropertyChangingEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the
///
/// property is changing from true to false.
///
public event RoutedPropertyChangingEventHandler DropDownClosing
{
add { AddHandler(DropDownClosingEvent, value); }
remove { RemoveHandler(DropDownClosingEvent, value); }
}
#endif
///
/// Occurs when the
///
/// property was changed from true to false and the drop-down is open.
///
#if SILVERLIGHT
public event RoutedPropertyChangedEventHandler DropDownClosed;
#else
public static readonly RoutedEvent DropDownClosedEvent = EventManager.RegisterRoutedEvent("DropDownClosed", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the
///
/// property was changed from true to false and the drop-down is open.
///
public event RoutedPropertyChangedEventHandler DropDownClosed
{
add { AddHandler(DropDownClosedEvent, value); }
remove { RemoveHandler(DropDownClosedEvent, value); }
}
#endif
///
/// Occurs when the selected item in the drop-down portion of the
/// has
/// changed.
///
#if SILVERLIGHT
public event SelectionChangedEventHandler SelectionChanged;
#else
public static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent("SelectionChanged", RoutingStrategy.Bubble, typeof(SelectionChangedEventHandler), typeof(AutoCompleteBox));
///
/// Occurs when the selected item in the drop-down portion of the
/// has
/// changed.
///
public event SelectionChangedEventHandler SelectionChanged
{
add { AddHandler(SelectionChangedEvent, value); }
remove { RemoveHandler(SelectionChangedEvent, value); }
}
#endif
///
/// Gets or sets the that
/// is used to get the values for display in the text portion of
/// the
/// control.
///
/// The object used
/// when binding to a collection property.
public Binding ValueMemberBinding
{
get
{
return _valueBindingEvaluator != null ? _valueBindingEvaluator.ValueBinding : null;
}
set
{
_valueBindingEvaluator = new BindingEvaluator(value);
}
}
///
/// Gets or sets the property path that is used to get values for
/// display in the text portion of the
/// control.
///
/// The property path that is used to get values for display in
/// the text portion of the
/// control.
public string ValueMemberPath
{
get
{
return (ValueMemberBinding != null) ? ValueMemberBinding.Path.Path : null;
}
set
{
ValueMemberBinding = value == null ? null : new Binding(value);
}
}
#if !SILVERLIGHT
///
/// Initializes the static members of the
/// class.
///
static AutoCompleteBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoCompleteBox), new FrameworkPropertyMetadata(typeof(AutoCompleteBox)));
}
#endif
///
/// Initializes a new instance of the
/// class.
///
public AutoCompleteBox()
{
#if SILVERLIGHT
DefaultStyleKey = typeof(AutoCompleteBox);
Loaded += (sender, e) => ApplyTemplate();
#endif
IsEnabledChanged += ControlIsEnabledChanged;
Interaction = new InteractionHelper(this);
// Creating the view here ensures that View is always != null
ClearView();
}
///
/// Arranges and sizes the
///
/// control and its contents.
///
/// The size allowed for the
/// control.
/// The , unchanged.
protected override Size ArrangeOverride(Size finalSize)
{
Size r = base.ArrangeOverride(finalSize);
if (DropDownPopup != null)
{
DropDownPopup.Arrange();
}
return r;
}
///
/// Builds the visual tree for the
/// control
/// when a new template is applied.
///
public override void OnApplyTemplate()
{
#if !SILVERLIGHT
if (TextBox != null)
{
TextBox.PreviewKeyDown -= OnTextBoxPreviewKeyDown;
}
#endif
if (DropDownPopup != null)
{
DropDownPopup.Closed -= DropDownPopup_Closed;
DropDownPopup.FocusChanged -= OnDropDownFocusChanged;
DropDownPopup.UpdateVisualStates -= OnDropDownPopupUpdateVisualStates;
DropDownPopup.BeforeOnApplyTemplate();
DropDownPopup = null;
}
base.OnApplyTemplate();
// Set the template parts. Individual part setters remove and add
// any event handlers.
Popup popup = GetTemplateChild(ElementPopup) as Popup;
if (popup != null)
{
DropDownPopup = new PopupHelper(this, popup);
DropDownPopup.MaxDropDownHeight = MaxDropDownHeight;
DropDownPopup.AfterOnApplyTemplate();
DropDownPopup.Closed += DropDownPopup_Closed;
DropDownPopup.FocusChanged += OnDropDownFocusChanged;
DropDownPopup.UpdateVisualStates += OnDropDownPopupUpdateVisualStates;
}
SelectionAdapter = GetSelectionAdapterPart();
TextBox = GetTemplateChild(AutoCompleteBox.ElementTextBox) as TextBox;
#if !SILVERLIGHT
if (TextBox != null)
{
TextBox.PreviewKeyDown += OnTextBoxPreviewKeyDown;
}
#endif
Interaction.OnApplyTemplateBase();
// If the drop down property indicates that the popup is open,
// flip its value to invoke the changed handler.
if (IsDropDownOpen && DropDownPopup != null && !DropDownPopup.IsOpen)
{
OpeningDropDown(false);
}
}
///
/// Allows the popup wrapper to fire visual state change events.
///
/// The source object.
/// The event data.
private void OnDropDownPopupUpdateVisualStates(object sender, EventArgs e)
{
UpdateVisualState(true);
}
///
/// Allows the popup wrapper to fire the FocusChanged event.
///
/// The source object.
/// The event data.
private void OnDropDownFocusChanged(object sender, EventArgs e)
{
FocusChanged(HasFocus());
}
///
/// Begin closing the drop-down.
///
/// The original value.
private void ClosingDropDown(bool oldValue)
{
bool delayedClosingVisual = false;
if (DropDownPopup != null)
{
delayedClosingVisual = DropDownPopup.UsesClosingVisualState;
}
#if SILVERLIGHT
RoutedPropertyChangingEventArgs args = new RoutedPropertyChangingEventArgs(IsDropDownOpenProperty, oldValue, false, true);
#else
RoutedPropertyChangingEventArgs args = new RoutedPropertyChangingEventArgs(IsDropDownOpenProperty, oldValue, false, true, DropDownClosingEvent);
#endif
OnDropDownClosing(args);
if (_view == null || _view.Count == 0)
{
delayedClosingVisual = false;
}
if (args.Cancel)
{
_ignorePropertyChange = true;
SetValue(IsDropDownOpenProperty, oldValue);
}
else
{
// Immediately close the drop down window:
// When a popup closed visual state is present, the code path is
// slightly different and the actual call to CloseDropDown will
// be called only after the visual state's transition is done
RaiseExpandCollapseAutomationEvent(oldValue, false);
if (!delayedClosingVisual)
{
CloseDropDown(oldValue, false);
}
}
UpdateVisualState(true);
}
///
/// Begin opening the drop down by firing cancelable events, opening the
/// drop-down or reverting, depending on the event argument values.
///
/// The original value, if needed for a revert.
private void OpeningDropDown(bool oldValue)
{
#if SILVERLIGHT
RoutedPropertyChangingEventArgs args = new RoutedPropertyChangingEventArgs(IsDropDownOpenProperty, oldValue, true, true);
#else
RoutedPropertyChangingEventArgs args = new RoutedPropertyChangingEventArgs(IsDropDownOpenProperty, oldValue, true, true, DropDownOpeningEvent);
#endif
// Opening
OnDropDownOpening(args);
if (args.Cancel)
{
_ignorePropertyChange = true;
SetValue(IsDropDownOpenProperty, oldValue);
}
else
{
RaiseExpandCollapseAutomationEvent(oldValue, true);
OpenDropDown(oldValue, true);
}
UpdateVisualState(true);
}
///
/// Raise an expand/collapse event through the automation peer.
///
/// The old value.
/// The new value.
private void RaiseExpandCollapseAutomationEvent(bool oldValue, bool newValue)
{
AutoCompleteBoxAutomationPeer peer = FrameworkElementAutomationPeer.FromElement(this) as AutoCompleteBoxAutomationPeer;
if (peer != null)
{
peer.RaiseExpandCollapseAutomationEvent(oldValue, newValue);
}
}
#if !SILVERLIGHT
///
/// Handles the PreviewKeyDown event on the TextBox for WPF. This method
/// is not implemented for Silverlight.
///
/// The source object.
/// The event data.
private void OnTextBoxPreviewKeyDown(object sender, KeyEventArgs e)
{
OnKeyDown(e);
}
#endif
///
/// Connects to the DropDownPopup Closed event.
///
/// The source object.
/// The event data.
private void DropDownPopup_Closed(object sender, EventArgs e)
{
// Force the drop down dependency property to be false.
if (IsDropDownOpen)
{
IsDropDownOpen = false;
}
// Fire the DropDownClosed event
if (_popupHasOpened)
{
#if SILVERLIGHT
OnDropDownClosed(new RoutedPropertyChangedEventArgs(true, false));
#else
OnDropDownClosed(new RoutedPropertyChangedEventArgs(true, false, DropDownClosedEvent));
#endif
}
}
///
/// Creates an
///
///
/// A
///
/// for the
/// object.
protected override AutomationPeer OnCreateAutomationPeer()
{
return new AutoCompleteBoxAutomationPeer(this);
}
#region Focus
///
/// Handles the FocusChanged event.
///
/// A value indicating whether the control
/// currently has the focus.
private void FocusChanged(bool hasFocus)
{
// The OnGotFocus & OnLostFocus are asynchronously and cannot
// reliably tell you that have the focus. All they do is let you
// know that the focus changed sometime in the past. To determine
// if you currently have the focus you need to do consult the
// FocusManager (see HasFocus()).
if (hasFocus)
{
if (TextBox != null && TextBox.SelectionLength == 0)
{
TextBox.SelectAll();
}
}
else
{
IsDropDownOpen = false;
_userCalledPopulate = false;
if (TextBox != null)
{
TextBox.Select(TextBox.Text.Length, 0);
}
}
}
///
/// Determines whether the text box or drop-down portion of the
/// control has
/// focus.
///
/// true to indicate the
/// has focus;
/// otherwise, false.
protected bool HasFocus()
{
DependencyObject focused =
#if SILVERLIGHT
FocusManager.GetFocusedElement() as DependencyObject;
#else
// For WPF, check if the element that has focus is within the control, as
// FocusManager.GetFocusedElement(this) will return null in such a case.
this.IsKeyboardFocusWithin ? Keyboard.FocusedElement as DependencyObject : FocusManager.GetFocusedElement(this) as DependencyObject;
#endif
while (focused != null)
{
if (object.ReferenceEquals(focused, this))
{
return true;
}
// This helps deal with popups that may not be in the same
// visual tree
DependencyObject parent = VisualTreeHelper.GetParent(focused);
if (parent == null)
{
// Try the logical parent.
FrameworkElement element = focused as FrameworkElement;
if (element != null)
{
parent = element.Parent;
}
}
focused = parent;
}
return false;
}
///
/// Provides handling for the
/// event.
///
/// A
/// that contains the event data.
protected override void OnGotFocus(RoutedEventArgs e)
{
base.OnGotFocus(e);
FocusChanged(HasFocus());
}
#if !SILVERLIGHT
///
/// Handles change of keyboard focus, which is treated differently than control focus
///
///
protected override void OnIsKeyboardFocusWithinChanged(DependencyPropertyChangedEventArgs e)
{
base.OnIsKeyboardFocusWithinChanged(e);
FocusChanged((bool)e.NewValue);
}
#endif
///
/// Provides handling for the
/// event.
///
/// A
/// that contains the event data.
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
FocusChanged(HasFocus());
}
#endregion
///
/// Handle the change of the IsEnabled property.
///
/// The source object.
/// The event data.
private void ControlIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
bool isEnabled = (bool)e.NewValue;
if (!isEnabled)
{
IsDropDownOpen = false;
}
}
///
/// Returns the
/// part, if
/// possible.
///
///
/// A object,
/// if possible. Otherwise, null.
///
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Following the GetTemplateChild pattern for the method.")]
protected virtual ISelectionAdapter GetSelectionAdapterPart()
{
ISelectionAdapter adapter = null;
Selector selector = GetTemplateChild(ElementSelector) as Selector;
if (selector != null)
{
// Check if it is already an IItemsSelector
adapter = selector as ISelectionAdapter;
if (adapter == null)
{
// Built in support for wrapping a Selector control
adapter = new SelectorSelectionAdapter(selector);
}
}
if (adapter == null)
{
adapter = GetTemplateChild(ElementSelectionAdapter) as ISelectionAdapter;
}
return adapter;
}
///
/// Handles the timer tick when using a populate delay.
///
/// The source object.
/// The event arguments.
private void PopulateDropDown(object sender, EventArgs e)
{
if (_delayTimer != null)
{
_delayTimer.Stop();
}
// Update the prefix/search text.
SearchText = Text;
// The Populated event enables advanced, custom filtering. The
// client needs to directly update the ItemsSource collection or
// call the Populate method on the control to continue the
// display process if Cancel is set to true.
#if SILVERLIGHT
PopulatingEventArgs populating = new PopulatingEventArgs(SearchText);
#else
PopulatingEventArgs populating = new PopulatingEventArgs(SearchText, PopulatingEvent);
#endif
OnPopulating(populating);
if (!populating.Cancel)
{
PopulateComplete();
}
}
///
/// Raises the
///
/// event.
///
/// A
/// that
/// contains the event data.
protected virtual void OnPopulating(PopulatingEventArgs e)
{
#if SILVERLIGHT
PopulatingEventHandler handler = Populating;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Raises the
///
/// event.
///
/// A
///
/// that contains the event data.
protected virtual void OnPopulated(PopulatedEventArgs e)
{
#if SILVERLIGHT
PopulatedEventHandler handler = Populated;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Raises the
///
/// event.
///
/// A
///
/// that contains the event data.
protected virtual void OnSelectionChanged(SelectionChangedEventArgs e)
{
#if SILVERLIGHT
SelectionChangedEventHandler handler = SelectionChanged;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Raises the
///
/// event.
///
/// A
///
/// that contains the event data.
protected virtual void OnDropDownOpening(RoutedPropertyChangingEventArgs e)
{
#if SILVERLIGHT
RoutedPropertyChangingEventHandler handler = DropDownOpening;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Raises the
///
/// event.
///
/// A
///
/// that contains the event data.
protected virtual void OnDropDownOpened(RoutedPropertyChangedEventArgs e)
{
#if SILVERLIGHT
RoutedPropertyChangedEventHandler handler = DropDownOpened;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Raises the
///
/// event.
///
/// A
///
/// that contains the event data.
protected virtual void OnDropDownClosing(RoutedPropertyChangingEventArgs e)
{
#if SILVERLIGHT
RoutedPropertyChangingEventHandler handler = DropDownClosing;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Raises the
///
/// event.
///
/// A
///
/// which contains the event data.
protected virtual void OnDropDownClosed(RoutedPropertyChangedEventArgs e)
{
#if SILVERLIGHT
RoutedPropertyChangedEventHandler handler = DropDownClosed;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Formats an Item for text comparisons based on Converter
/// and ConverterCulture properties.
///
/// The object to format.
/// A value indicating whether to clear
/// the data context after the lookup is performed.
/// Formatted Value.
private string FormatValue(object value, bool clearDataContext)
{
string str = FormatValue(value);
if (clearDataContext && _valueBindingEvaluator != null)
{
_valueBindingEvaluator.ClearDataContext();
}
return str;
}
///
/// Converts the specified object to a string by using the
/// and
/// values
/// of the binding object specified by the
///
/// property.
///
/// The object to format as a string.
/// The string representation of the specified object.
///
/// Override this method to provide a custom string conversion.
///
protected virtual string FormatValue(object value)
{
if (_valueBindingEvaluator != null)
{
return _valueBindingEvaluator.GetDynamicValue(value) ?? string.Empty;
}
return value == null ? string.Empty : value.ToString();
}
///
/// Raises the
///
/// event.
///
/// A
/// that contains the event data.
protected virtual void OnTextChanged(RoutedEventArgs e)
{
#if SILVERLIGHT
RoutedEventHandler handler = TextChanged;
if (handler != null)
{
handler(this, e);
}
#else
RaiseEvent(e);
#endif
}
///
/// Handle the TextChanged event that is directly attached to the
/// TextBox part. This ensures that only user initiated actions will
/// result in an AutoCompleteBox suggestion and operation.
///
/// The source TextBox object.
/// The TextChanged event data.
private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
// Call the central updated text method as a user-initiated action
TextUpdated(_text.Text, true);
}
///
/// When selection changes, save the location of the selection start.
///
/// The source object.
/// The event data.
private void OnTextBoxSelectionChanged(object sender, RoutedEventArgs e)
{
// If ignoring updates. This happens after text is updated, and
// before the PopulateComplete method is called. Required for the
// IsTextCompletionEnabled feature.
if (_ignoreTextSelectionChange)
{
return;
}
_textSelectionStart = _text.SelectionStart;
}
///
/// Updates both the text box value and underlying text dependency
/// property value if and when they change. Automatically fires the
/// text changed events when there is a change.
///
/// The new string value.
private void UpdateTextValue(string value)
{
UpdateTextValue(value, null);
}
///
/// Updates both the text box value and underlying text dependency
/// property value if and when they change. Automatically fires the
/// text changed events when there is a change.
///
/// The new string value.
/// A nullable bool value indicating whether
/// the action was user initiated. In a user initiated mode, the
/// underlying text dependency property is updated. In a non-user
/// interaction, the text box value is updated. When user initiated is
/// null, all values are updated.
private void UpdateTextValue(string value, bool? userInitiated)
{
// Update the Text dependency property
if ((userInitiated == null || userInitiated == true) && Text != value)
{
_ignoreTextPropertyChange++;
Text = value;
#if SILVERLIGHT
OnTextChanged(new RoutedEventArgs());
#else
OnTextChanged(new RoutedEventArgs(TextChangedEvent));
#endif
}
// Update the TextBox's Text dependency property
if ((userInitiated == null || userInitiated == false) && TextBox != null && TextBox.Text != value)
{
_ignoreTextPropertyChange++;
TextBox.Text = value ?? string.Empty;
// Text dependency property value was set, fire event
if (Text == value || Text == null)
{
#if SILVERLIGHT
OnTextChanged(new RoutedEventArgs());
#else
OnTextChanged(new RoutedEventArgs(TextChangedEvent));
#endif
}
}
}
///
/// Handle the update of the text for the control from any source,
/// including the TextBox part and the Text dependency property.
///
/// The new text.
/// A value indicating whether the update
/// is a user-initiated action. This should be a True value when the
/// TextUpdated method is called from a TextBox event handler.
private void TextUpdated(string newText, bool userInitiated)
{
// Only process this event if it is coming from someone outside
// setting the Text dependency property directly.
if (_ignoreTextPropertyChange > 0)
{
_ignoreTextPropertyChange--;
return;
}
if (newText == null)
{
newText = string.Empty;
}
// The TextBox.TextChanged event was not firing immediately and
// was causing an immediate update, even with wrapping. If there is
// a selection currently, no update should happen.
if (IsTextCompletionEnabled && TextBox != null && TextBox.SelectionLength > 0 && TextBox.SelectionStart != TextBox.Text.Length)
{
return;
}
// Evaluate the conditions needed for completion.
// 1. Minimum prefix length
// 2. If a delay timer is in use, use it
bool populateReady = newText.Length >= MinimumPrefixLength && MinimumPrefixLength >= 0;
_userCalledPopulate = populateReady ? userInitiated : false;
// Update the interface and values only as necessary
UpdateTextValue(newText, userInitiated);
if (populateReady)
{
_ignoreTextSelectionChange = true;
if (_delayTimer != null)
{
_delayTimer.Start();
}
else
{
PopulateDropDown(this, EventArgs.Empty);
}
}
else
{
SearchText = string.Empty;
if (SelectedItem != null)
{
_skipSelectedItemTextUpdate = true;
}
SelectedItem = null;
if (IsDropDownOpen)
{
IsDropDownOpen = false;
}
}
}
///
/// Notifies the
/// that the
///
/// property has been set and the data can be filtered to provide
/// possible matches in the drop-down.
///
///
/// Call this method when you are providing custom population of
/// the drop-down portion of the AutoCompleteBox, to signal the control
/// that you are done with the population process.
/// Typically, you use PopulateComplete when the population process
/// is a long-running process and you want to cancel built-in filtering
/// of the ItemsSource items. In this case, you can handle the
/// Populated event and set PopulatingEventArgs.Cancel to true.
/// When the long-running process has completed you call
/// PopulateComplete to indicate the drop-down is populated.
///
public void PopulateComplete()
{
// Apply the search filter
RefreshView();
// Fire the Populated event containing the read-only view data.
#if SILVERLIGHT
PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view));
#else
PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view), PopulatedEvent);
#endif
OnPopulated(populated);
if (SelectionAdapter != null && SelectionAdapter.ItemsSource != _view)
{
SelectionAdapter.ItemsSource = _view;
}
bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0);
if (isDropDownOpen != IsDropDownOpen)
{
_ignorePropertyChange = true;
IsDropDownOpen = isDropDownOpen;
}
if (IsDropDownOpen)
{
OpeningDropDown(false);
if (DropDownPopup != null)
{
DropDownPopup.Arrange();
}
}
else
{
ClosingDropDown(true);
}
UpdateTextCompletion(_userCalledPopulate);
}
///
/// Performs text completion, if enabled, and a lookup on the underlying
/// item values for an exact match. Will update the SelectedItem value.
///
/// A value indicating whether the operation
/// was user initiated. Text completion will not be performed when not
/// directly initiated by the user.
private void UpdateTextCompletion(bool userInitiated)
{
// By default this method will clear the selected value
object newSelectedItem = null;
string text = Text;
// Text search is StartsWith explicit and only when enabled, in
// line with WPF's ComboBox lookup. When in use it will associate
// a Value with the Text if it is found in ItemsSource. This is
// only valid when there is data and the user initiated the action.
if (_view.Count > 0)
{
if (IsTextCompletionEnabled && TextBox != null && userInitiated)
{
int currentLength = TextBox.Text.Length;
int selectionStart = TextBox.SelectionStart;
if (selectionStart == text.Length && selectionStart > _textSelectionStart)
{
// When the FilterMode dependency property is set to
// either StartsWith or StartsWithCaseSensitive, the
// first item in the view is used. This will improve
// performance on the lookup. It assumes that the
// FilterMode the user has selected is an acceptable
// case sensitive matching function for their scenario.
object top = FilterMode == AutoCompleteFilterMode.StartsWith || FilterMode == AutoCompleteFilterMode.StartsWithCaseSensitive
? _view[0]
: TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith));
// If the search was successful, update SelectedItem
if (top != null)
{
newSelectedItem = top;
string topString = FormatValue(top, true);
// Only replace partially when the two words being the same
int minLength = Math.Min(topString.Length, Text.Length);
if (AutoCompleteSearch.Equals(Text.Substring(0, minLength), topString.Substring(0, minLength)))
{
// Update the text
UpdateTextValue(topString);
// Select the text past the user's caret
TextBox.SelectionStart = currentLength;
TextBox.SelectionLength = topString.Length - currentLength;
}
}
}
}
else
{
// Perform an exact string lookup for the text. This is a
// design change from the original Toolkit release when the
// IsTextCompletionEnabled property behaved just like the
// WPF ComboBox's IsTextSearchEnabled property.
//
// This change provides the behavior that most people expect
// to find: a lookup for the value is always performed.
newSelectedItem = TryGetMatch(text, _view, AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.EqualsCaseSensitive));
}
}
// Update the selected item property
if (SelectedItem != newSelectedItem)
{
_skipSelectedItemTextUpdate = true;
}
SelectedItem = newSelectedItem;
// Restore updates for TextSelection
if (_ignoreTextSelectionChange)
{
_ignoreTextSelectionChange = false;
if (TextBox != null)
{
_textSelectionStart = TextBox.SelectionStart;
}
}
}
///
/// Attempts to look through the view and locate the specific exact
/// text match.
///
/// The search text.
/// The view reference.
/// The predicate to use for the partial or
/// exact match.
/// Returns the object or null.
private object TryGetMatch(string searchText, ObservableCollection view, AutoCompleteFilterPredicate predicate)
{
if (view != null && view.Count > 0)
{
foreach (object o in view)
{
if (predicate(searchText, FormatValue(o)))
{
return o;
}
}
}
return null;
}
///
/// A simple helper method to clear the view and ensure that a view
/// object is always present and not null.
///
private void ClearView()
{
if (_view == null)
{
_view = new ObservableCollection();
}
else
{
_view.Clear();
}
}
///
/// Walks through the items enumeration. Performance is not going to be
/// perfect with the current implementation.
///
private void RefreshView()
{
if (_items == null)
{
ClearView();
return;
}
// Cache the current text value
string text = Text ?? string.Empty;
// Determine if any filtering mode is on
bool stringFiltering = TextFilter != null;
bool objectFiltering = FilterMode == AutoCompleteFilterMode.Custom && TextFilter == null;
int view_index = 0;
int view_count = _view.Count;
List items = _items;
foreach (object item in items)
{
bool inResults = !(stringFiltering || objectFiltering);
if (!inResults)
{
inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item);
}
if (view_count > view_index && inResults && _view[view_index] == item)
{
// Item is still in the view
view_index++;
}
else if (inResults)
{
// Insert the item
if (view_count > view_index && _view[view_index] != item)
{
// Replace item
// Unfortunately replacing via index throws a fatal
// exception: View[view_index] = item;
// Cost: O(n) vs O(1)
_view.RemoveAt(view_index);
_view.Insert(view_index, item);
view_index++;
}
else
{
// Add the item
if (view_index == view_count)
{
// Constant time is preferred (Add).
_view.Add(item);
}
else
{
_view.Insert(view_index, item);
}
view_index++;
view_count++;
}
}
else if (view_count > view_index && _view[view_index] == item)
{
// Remove the item
_view.RemoveAt(view_index);
view_count--;
}
}
// Clear the evaluator to discard a reference to the last item
if (_valueBindingEvaluator != null)
{
_valueBindingEvaluator.ClearDataContext();
}
}
///
/// Handle any change to the ItemsSource dependency property, update
/// the underlying ObservableCollection view, and set the selection
/// adapter's ItemsSource to the view if appropriate.
///
/// The old enumerable reference.
/// The new enumerable reference.
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "oldValue", Justification = "This makes it easy to add validation or other changes in the future.")]
private void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
// Remove handler for oldValue.CollectionChanged (if present)
INotifyCollectionChanged oldValueINotifyCollectionChanged = oldValue as INotifyCollectionChanged;
if (null != oldValueINotifyCollectionChanged && null != _collectionChangedWeakEventListener)
{
_collectionChangedWeakEventListener.Detach();
_collectionChangedWeakEventListener = null;
}
// Add handler for newValue.CollectionChanged (if possible)
INotifyCollectionChanged newValueINotifyCollectionChanged = newValue as INotifyCollectionChanged;
if (null != newValueINotifyCollectionChanged)
{
_collectionChangedWeakEventListener = new WeakEventListener(this);
_collectionChangedWeakEventListener.OnEventAction = (instance, source, eventArgs) => instance.ItemsSourceCollectionChanged(source, eventArgs);
_collectionChangedWeakEventListener.OnDetachAction = (weakEventListener) => newValueINotifyCollectionChanged.CollectionChanged -= weakEventListener.OnEvent;
newValueINotifyCollectionChanged.CollectionChanged += _collectionChangedWeakEventListener.OnEvent;
}
// Store a local cached copy of the data
_items = newValue == null ? null : new List(newValue.Cast().ToList());
// Clear and set the view on the selection adapter
ClearView();
if (SelectionAdapter != null && SelectionAdapter.ItemsSource != _view)
{
SelectionAdapter.ItemsSource = _view;
}
if (IsDropDownOpen)
{
RefreshView();
}
}
///
/// Method that handles the ObservableCollection.CollectionChanged event for the ItemsSource property.
///
/// The object that raised the event.
/// The event data.
private void ItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Update the cache
if (e.Action == NotifyCollectionChangedAction.Remove && e.OldItems != null)
{
for (int index = 0; index < e.OldItems.Count; index++)
{
_items.RemoveAt(e.OldStartingIndex);
}
}
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null && _items.Count >= e.NewStartingIndex)
{
for (int index = 0; index < e.NewItems.Count; index++)
{
_items.Insert(e.NewStartingIndex + index, e.NewItems[index]);
}
}
if (e.Action == NotifyCollectionChangedAction.Replace && e.NewItems != null && e.OldItems != null)
{
for (int index = 0; index < e.NewItems.Count; index++)
{
_items[e.NewStartingIndex] = e.NewItems[index];
}
}
// Update the view
if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Replace)
{
for (int index = 0; index < e.OldItems.Count; index++)
{
_view.Remove(e.OldItems[index]);
}
}
if (e.Action == NotifyCollectionChangedAction.Reset)
{
// Significant changes to the underlying data.
ClearView();
if (ItemsSource != null)
{
_items = new List(ItemsSource.Cast().ToList());
}
}
// Refresh the observable collection used in the selection adapter.
RefreshView();
}
#region Selection Adapter
///
/// Handles the SelectionChanged event of the selection adapter.
///
/// The source object.
/// The selection changed event data.
private void OnAdapterSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItem = _adapter.SelectedItem;
}
///
/// Handles the Commit event on the selection adapter.
///
/// The source object.
/// The event data.
private void OnAdapterSelectionComplete(object sender, RoutedEventArgs e)
{
IsDropDownOpen = false;
// Completion will update the selected value
UpdateTextCompletion(false);
// Text should not be selected
if (TextBox != null)
{
TextBox.Select(TextBox.Text.Length, 0);
}
#if SILVERLIGHT
Focus();
#else
// Focus is treated differently in SL and WPF.
// This forces the textbox to get keyboard focus, in the case where
// another part of the control may have temporarily received focus.
if (TextBox != null)
{
Keyboard.Focus(TextBox);
}
else
{
Focus();
}
#endif
}
///
/// Handles the Cancel event on the selection adapter.
///
/// The source object.
/// The event data.
private void OnAdapterSelectionCanceled(object sender, RoutedEventArgs e)
{
UpdateTextValue(SearchText);
// Completion will update the selected value
UpdateTextCompletion(false);
}
#endregion
#region Popup
///
/// Handles MaxDropDownHeightChanged by re-arranging and updating the
/// popup arrangement.
///
/// The new value.
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "newValue", Justification = "This makes it easy to add validation or other changes in the future.")]
private void OnMaxDropDownHeightChanged(double newValue)
{
if (DropDownPopup != null)
{
DropDownPopup.MaxDropDownHeight = newValue;
DropDownPopup.Arrange();
}
UpdateVisualState(true);
}
///
/// Private method that directly opens the popup, checks the expander
/// button, and then fires the Opened event.
///
/// The old value.
/// The new value.
private void OpenDropDown(bool oldValue, bool newValue)
{
if (DropDownPopup != null)
{
DropDownPopup.IsOpen = true;
}
_popupHasOpened = true;
#if SILVERLIGHT
OnDropDownOpened(new RoutedPropertyChangedEventArgs(oldValue, newValue));
#else
OnDropDownOpened(new RoutedPropertyChangedEventArgs(oldValue, newValue, DropDownOpenedEvent));
#endif
}
///
/// Private method that directly closes the popup, flips the Checked
/// value, and then fires the Closed event.
///
/// The old value.
/// The new value.
private void CloseDropDown(bool oldValue, bool newValue)
{
if (_popupHasOpened)
{
if (SelectionAdapter != null)
{
SelectionAdapter.SelectedItem = null;
}
if (DropDownPopup != null)
{
DropDownPopup.IsOpen = false;
}
#if SILVERLIGHT
OnDropDownClosed(new RoutedPropertyChangedEventArgs(oldValue, newValue));
#else
OnDropDownClosed(new RoutedPropertyChangedEventArgs(oldValue, newValue, DropDownClosedEvent));
#endif
}
}
#endregion
///
/// Provides handling for the
/// event.
///
/// A
/// that contains the event data.
protected override void OnKeyDown(KeyEventArgs e)
{
if (e == null)
{
throw new ArgumentNullException("e");
}
base.OnKeyDown(e);
if (e.Handled || !IsEnabled)
{
return;
}
// The drop down is open, pass along the key event arguments to the
// selection adapter. If it isn't handled by the adapter's logic,
// then we handle some simple navigation scenarios for controlling
// the drop down.
if (IsDropDownOpen)
{
if (SelectionAdapter != null)
{
SelectionAdapter.HandleKeyDown(e);
if (e.Handled)
{
return;
}
}
if (e.Key == Key.Escape)
{
OnAdapterSelectionCanceled(this, new RoutedEventArgs());
e.Handled = true;
}
}
else
{
// The drop down is not open, the Down key will toggle it open.
if (e.Key == Key.Down)
{
IsDropDownOpen = true;
e.Handled = true;
}
}
// Standard drop down navigation
switch (e.Key)
{
case Key.F4:
IsDropDownOpen = !IsDropDownOpen;
e.Handled = true;
break;
case Key.Enter:
OnAdapterSelectionComplete(this, new RoutedEventArgs());
e.Handled = true;
break;
default:
break;
}
}
///
/// Update the visual state of the control.
///
///
/// A value indicating whether to automatically generate transitions to
/// the new state, or instantly transition to the new state.
///
void IUpdateVisualState.UpdateVisualState(bool useTransitions)
{
UpdateVisualState(useTransitions);
}
///
/// Update the current visual state of the button.
///
///
/// True to use transitions when updating the visual state, false to
/// snap directly to the new visual state.
///
internal virtual void UpdateVisualState(bool useTransitions)
{
// Popup
VisualStateManager.GoToState(this, IsDropDownOpen ? VisualStates.StatePopupOpened : VisualStates.StatePopupClosed, useTransitions);
// Handle the Common and Focused states
Interaction.UpdateVisualStateBase(useTransitions);
}
}
}