// (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); } } }