// (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.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls { /// /// Provides data for the /// /// event. /// public class PopulatedEventArgs : EventArgs { /// /// Gets the list of possible matches added to the drop-down portion of /// the /// control. /// /// The list of possible matches added to the /// . public IEnumerable Data { get; private set; } /// /// Initializes a new instance of the /// . /// /// The list of possible matches added to the /// drop-down portion of the /// control. public PopulatedEventArgs(IEnumerable data) { Data = data; } } /// /// Provides data for the /// /// event. /// public class PopulatingEventArgs : CancelEventArgs { /// /// Gets the text that is used to determine which items to display in /// the /// control. /// /// The text that is used to determine which items to display in /// the . public string Parameter { get; private set; } /// /// Initializes a new instance of the /// . /// /// The value of the /// /// property, which is used to filter items for the /// control. public PopulatingEventArgs(string parameter) { Parameter = parameter; } } /// /// Represents the filter used by the /// control to /// determine whether an item is a possible match for the specified text. /// /// true to indicate is a possible match /// for ; otherwise false. /// The string used as the basis for filtering. /// The item that is compared with the /// parameter. /// The type used for filtering the /// . This type can /// be either a string or an object. public delegate bool AutoCompleteFilterPredicate(string search, T item); /// /// Specifies how text in the text box portion of the /// control is used /// to filter items specified by the /// /// property for display in the drop-down. /// public enum AutoCompleteFilterMode { /// /// Specifies that no filter is used. All items are returned. /// None = 0, /// /// Specifies a culture-sensitive, case-insensitive filter where the /// returned items start with the specified text. The filter uses the /// /// method, specifying /// as /// the string comparison criteria. /// StartsWith = 1, /// /// Specifies a culture-sensitive, case-sensitive filter where the /// returned items start with the specified text. The filter uses the /// /// method, specifying /// as the string /// comparison criteria. /// StartsWithCaseSensitive = 2, /// /// Specifies an ordinal, case-insensitive filter where the returned /// items start with the specified text. The filter uses the /// /// method, specifying /// as the /// string comparison criteria. /// StartsWithOrdinal = 3, /// /// Specifies an ordinal, case-sensitive filter where the returned items /// start with the specified text. The filter uses the /// /// method, specifying as /// the string comparison criteria. /// StartsWithOrdinalCaseSensitive = 4, /// /// Specifies a culture-sensitive, case-insensitive filter where the /// returned items contain the specified text. /// Contains = 5, /// /// Specifies a culture-sensitive, case-sensitive filter where the /// returned items contain the specified text. /// ContainsCaseSensitive = 6, /// /// Specifies an ordinal, case-insensitive filter where the returned /// items contain the specified text. /// ContainsOrdinal = 7, /// /// Specifies an ordinal, case-sensitive filter where the returned items /// contain the specified text. /// ContainsOrdinalCaseSensitive = 8, /// /// Specifies a culture-sensitive, case-insensitive filter where the /// returned items equal the specified text. The filter uses the /// /// method, specifying /// as /// the search comparison criteria. /// Equals = 9, /// /// Specifies a culture-sensitive, case-sensitive filter where the /// returned items equal the specified text. The filter uses the /// /// method, specifying /// as the string /// comparison criteria. /// EqualsCaseSensitive = 10, /// /// Specifies an ordinal, case-insensitive filter where the returned /// items equal the specified text. The filter uses the /// /// method, specifying /// as the /// string comparison criteria. /// EqualsOrdinal = 11, /// /// Specifies an ordinal, case-sensitive filter where the returned items /// equal the specified text. The filter uses the /// /// method, specifying as /// the string comparison criteria. /// EqualsOrdinalCaseSensitive = 12, /// /// Specifies that a custom filter is used. This mode is used when the /// /// or /// /// properties are set. /// Custom = 13, } /// /// 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. /// public class AutoCompleteBox : TemplatedControl { /// /// Specifies the name of the selection adapter TemplatePart. /// private const string ElementSelectionAdapter = "PART_SelectionAdapter"; /// /// Specifies the name of the Selector TemplatePart. /// private const string ElementSelector = "PART_SelectingItemsControl"; /// /// Specifies the name of the Popup TemplatePart. /// private const string ElementPopup = "PART_Popup"; /// /// The name for the text box part. /// private const string ElementTextBox = "PART_TextBox"; private IEnumerable _itemsEnumerable; /// /// 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 AvaloniaList _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; /// /// The TextBox template part. /// private TextBox _textBox; private IDisposable _textBoxSubscriptions; /// /// The SelectionAdapter. /// private ISelectionAdapter _adapter; /// /// A control that can provide updated string values from a binding. /// private BindingEvaluator _valueBindingEvaluator; /// /// A weak subscription for the collection changed event. /// private IDisposable _collectionChangeSubscription; private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; private bool _itemTemplateIsFromValueMemberBinding = true; private bool _settingItemTemplateFromValueMemberBinding; private object _selectedItem; private bool _isDropDownOpen; private bool _isFocused = false; private string _text = string.Empty; private string _searchText = string.Empty; private AutoCompleteFilterPredicate _itemFilter; private AutoCompleteFilterPredicate _textFilter = AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith); public static readonly RoutedEvent SelectionChangedEvent = RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Bubble, typeof(AutoCompleteBox)); public static readonly StyledProperty WatermarkProperty = TextBox.WatermarkProperty.AddOwner(); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly StyledProperty MinimumPrefixLengthProperty = AvaloniaProperty.Register( nameof(MinimumPrefixLength), 1, validate: IsValidMinimumPrefixLength); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly StyledProperty MinimumPopulateDelayProperty = AvaloniaProperty.Register( nameof(MinimumPopulateDelay), TimeSpan.Zero, validate: IsValidMinimumPopulateDelay); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly StyledProperty MaxDropDownHeightProperty = AvaloniaProperty.Register( nameof(MaxDropDownHeight), double.PositiveInfinity, validate: IsValidMaxDropDownHeight); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly StyledProperty IsTextCompletionEnabledProperty = AvaloniaProperty.Register(nameof(IsTextCompletionEnabled)); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly DirectProperty IsDropDownOpenProperty = AvaloniaProperty.RegisterDirect( nameof(IsDropDownOpen), o => o.IsDropDownOpen, (o, v) => o.IsDropDownOpen = v); /// /// Identifies the /// /// dependency property. /// /// The identifier the /// /// dependency property. public static readonly DirectProperty SelectedItemProperty = AvaloniaProperty.RegisterDirect( nameof(SelectedItem), o => o.SelectedItem, (o, v) => o.SelectedItem = v); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly DirectProperty TextProperty = AvaloniaProperty.RegisterDirect( nameof(Text), o => o.Text, (o, v) => o.Text = v); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly DirectProperty SearchTextProperty = AvaloniaProperty.RegisterDirect( nameof(SearchText), o => o.SearchText, unsetValue: string.Empty); /// /// Gets the identifier for the /// /// dependency property. /// public static readonly StyledProperty FilterModeProperty = AvaloniaProperty.Register( nameof(FilterMode), defaultValue: AutoCompleteFilterMode.StartsWith, validate: IsValidFilterMode); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly DirectProperty> ItemFilterProperty = AvaloniaProperty.RegisterDirect>( nameof(ItemFilter), o => o.ItemFilter, (o, v) => o.ItemFilter = v); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly DirectProperty> TextFilterProperty = AvaloniaProperty.RegisterDirect>( nameof(TextFilter), o => o.TextFilter, (o, v) => o.TextFilter = v, unsetValue: AutoCompleteSearch.GetFilter(AutoCompleteFilterMode.StartsWith)); /// /// Identifies the /// /// dependency property. /// /// The identifier for the /// /// dependency property. public static readonly DirectProperty ItemsProperty = AvaloniaProperty.RegisterDirect( nameof(Items), o => o.Items, (o, v) => o.Items = v); public static readonly DirectProperty>>> AsyncPopulatorProperty = AvaloniaProperty.RegisterDirect>>>( nameof(AsyncPopulator), o => o.AsyncPopulator, (o, v) => o.AsyncPopulator = v); private static bool IsValidMinimumPrefixLength(int value) => value >= -1; private static bool IsValidMinimumPopulateDelay(TimeSpan value) => value.TotalMilliseconds >= 0.0; private static bool IsValidMaxDropDownHeight(double value) => value >= 0.0; private static bool IsValidFilterMode(AutoCompleteFilterMode mode) { switch (mode) { case AutoCompleteFilterMode.None: case AutoCompleteFilterMode.StartsWith: case AutoCompleteFilterMode.StartsWithCaseSensitive: case AutoCompleteFilterMode.StartsWithOrdinal: case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive: case AutoCompleteFilterMode.Contains: case AutoCompleteFilterMode.ContainsCaseSensitive: case AutoCompleteFilterMode.ContainsOrdinal: case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive: case AutoCompleteFilterMode.Equals: case AutoCompleteFilterMode.EqualsCaseSensitive: case AutoCompleteFilterMode.EqualsOrdinal: case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive: case AutoCompleteFilterMode.Custom: return true; default: return false; } } /// /// Handle the change of the IsEnabled property. /// /// The event data. private void OnControlIsEnabledChanged(AvaloniaPropertyChangedEventArgs e) { bool isEnabled = (bool)e.NewValue; if (!isEnabled) { IsDropDownOpen = false; } } /// /// 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. /// /// Event arguments. private void OnMinimumPopulateDelayChanged(AvaloniaPropertyChangedEventArgs e) { var newValue = (TimeSpan)e.NewValue; // Stop any existing timer if (_delayTimer != null) { _delayTimer.Stop(); if (newValue == TimeSpan.Zero) { _delayTimer = null; } } if (newValue > TimeSpan.Zero) { // Create or clear a dispatcher timer instance if (_delayTimer == null) { _delayTimer = new DispatcherTimer(); _delayTimer.Tick += PopulateDropDown; } // Set the new tick interval _delayTimer.Interval = newValue; } } /// /// IsDropDownOpenProperty property changed handler. /// /// Event arguments. private void OnIsDropDownOpenChanged(AvaloniaPropertyChangedEventArgs e) { // Ignore the change if requested if (_ignorePropertyChange) { _ignorePropertyChange = false; return; } bool oldValue = (bool)e.OldValue; bool newValue = (bool)e.NewValue; if (newValue) { TextUpdated(Text, true); } else { ClosingDropDown(oldValue); } UpdatePseudoClasses(); } private void OnSelectedItemPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (_ignorePropertyChange) { _ignorePropertyChange = false; return; } // Update the text display if (_skipSelectedItemTextUpdate) { _skipSelectedItemTextUpdate = false; } else { 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); } OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added)); } /// /// TextProperty property changed handler. /// /// Event arguments. private void OnTextPropertyChanged(AvaloniaPropertyChangedEventArgs e) { TextUpdated((string)e.NewValue, false); } private void OnSearchTextPropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (_ignorePropertyChange) { _ignorePropertyChange = false; return; } // Ensure the property is only written when expected if (!_allowWrite) { // Reset the old value before it was incorrectly written _ignorePropertyChange = true; SetValue(e.Property, e.OldValue); throw new InvalidOperationException("Cannot set read-only property SearchText."); } } /// /// FilterModeProperty property changed handler. /// /// Event arguments. private void OnFilterModePropertyChanged(AvaloniaPropertyChangedEventArgs e) { AutoCompleteFilterMode mode = (AutoCompleteFilterMode)e.NewValue; // Sets the filter predicate for the new value TextFilter = AutoCompleteSearch.GetFilter(mode); } /// /// ItemFilterProperty property changed handler. /// /// Event arguments. private void OnItemFilterPropertyChanged(AvaloniaPropertyChangedEventArgs e) { AutoCompleteFilterPredicate value = e.NewValue as AutoCompleteFilterPredicate; // If null, revert to the "None" predicate if (value == null) { FilterMode = AutoCompleteFilterMode.None; } else { FilterMode = AutoCompleteFilterMode.Custom; TextFilter = null; } } /// /// ItemsSourceProperty property changed handler. /// /// Event arguments. private void OnItemsPropertyChanged(AvaloniaPropertyChangedEventArgs e) { OnItemsChanged((IEnumerable)e.NewValue); } private void OnItemTemplatePropertyChanged(AvaloniaPropertyChangedEventArgs e) { if (!_settingItemTemplateFromValueMemberBinding) _itemTemplateIsFromValueMemberBinding = false; } private void OnValueMemberBindingChanged(IBinding value) { if(_itemTemplateIsFromValueMemberBinding) { var template = new FuncDataTemplate( typeof(object), (o, _) => { var control = new ContentControl(); control.Bind(ContentControl.ContentProperty, value); return control; }); _settingItemTemplateFromValueMemberBinding = true; ItemTemplate = template; _settingItemTemplateFromValueMemberBinding = false; } } static AutoCompleteBox() { FocusableProperty.OverrideDefaultValue(true); MinimumPopulateDelayProperty.Changed.AddClassHandler((x,e) => x.OnMinimumPopulateDelayChanged(e)); IsDropDownOpenProperty.Changed.AddClassHandler((x,e) => x.OnIsDropDownOpenChanged(e)); SelectedItemProperty.Changed.AddClassHandler((x,e) => x.OnSelectedItemPropertyChanged(e)); TextProperty.Changed.AddClassHandler((x,e) => x.OnTextPropertyChanged(e)); SearchTextProperty.Changed.AddClassHandler((x,e) => x.OnSearchTextPropertyChanged(e)); FilterModeProperty.Changed.AddClassHandler((x,e) => x.OnFilterModePropertyChanged(e)); ItemFilterProperty.Changed.AddClassHandler((x,e) => x.OnItemFilterPropertyChanged(e)); ItemsProperty.Changed.AddClassHandler((x,e) => x.OnItemsPropertyChanged(e)); IsEnabledProperty.Changed.AddClassHandler((x,e) => x.OnControlIsEnabledChanged(e)); } /// /// Initializes a new instance of the /// class. /// public AutoCompleteBox() { ClearView(); } /// /// 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 GetValue(MinimumPrefixLengthProperty); } set { SetValue(MinimumPrefixLengthProperty, value); } } /// /// 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 GetValue(IsTextCompletionEnabledProperty); } set { SetValue(IsTextCompletionEnabledProperty, value); } } /// /// 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 IDataTemplate ItemTemplate { get { return GetValue(ItemTemplateProperty); } set { SetValue(ItemTemplateProperty, value); } } /// /// Gets or sets the minimum delay, after text is typed /// in the text box before the /// control /// populates the list of possible matches in the drop-down. /// /// The minimum delay, 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. public TimeSpan MinimumPopulateDelay { get { return GetValue(MinimumPopulateDelayProperty); } set { SetValue(MinimumPopulateDelayProperty, value); } } /// /// 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 GetValue(MaxDropDownHeightProperty); } set { SetValue(MaxDropDownHeightProperty, value); } } /// /// 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 _isDropDownOpen; } set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } } /// /// 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. [AssignBinding] public IBinding ValueMemberBinding { get { return _valueBindingEvaluator?.ValueBinding; } set { if (ValueMemberBinding != value) { _valueBindingEvaluator = new BindingEvaluator(value); OnValueMemberBindingChanged(value); } } } /// /// 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 _selectedItem; } set { SetAndRaise(SelectedItemProperty, ref _selectedItem, value); } } /// /// 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 _text; } set { SetAndRaise(TextProperty, ref _text, value); } } /// /// 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 _searchText; } private set { try { _allowWrite = true; SetAndRaise(SearchTextProperty, ref _searchText, value); } finally { _allowWrite = false; } } } /// /// 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 GetValue(FilterModeProperty); } set { SetValue(FilterModeProperty, value); } } public string Watermark { get { return GetValue(WatermarkProperty); } set { SetValue(WatermarkProperty, value); } } /// /// 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 _itemFilter; } set { SetAndRaise(ItemFilterProperty, ref _itemFilter, value); } } /// /// 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 _textFilter; } set { SetAndRaise(TextFilterProperty, ref _textFilter, value); } } public Func>> AsyncPopulator { get { return _asyncPopulator; } set { SetAndRaise(AsyncPopulatorProperty, ref _asyncPopulator, value); } } /// /// 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 Items { get { return _itemsEnumerable; } set { SetAndRaise(ItemsProperty, ref _itemsEnumerable, value); } } /// /// Gets or sets the drop down popup control. /// private Popup DropDownPopup { get; set; } /// /// Gets or sets the Text template part. /// private TextBox TextBox { get { return _textBox; } set { _textBoxSubscriptions?.Dispose(); _textBox = value; // Attach handlers if (_textBox != null) { _textBoxSubscriptions = _textBox.GetObservable(TextBox.TextProperty) .Subscribe(_ => OnTextBoxTextChanged()); if (Text != null) { UpdateTextValue(Text); } } } } private int TextBoxSelectionStart { get { if (TextBox != null) { return Math.Min(TextBox.SelectionStart, TextBox.SelectionEnd); } else { return 0; } } } private int TextBoxSelectionLength { get { if (TextBox != null) { return Math.Abs(TextBox.SelectionEnd - TextBox.SelectionStart); } else { return 0; } } } /// /// 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 ISelectionAdapter SelectionAdapter { get { return _adapter; } set { if (_adapter != null) { _adapter.SelectionChanged -= OnAdapterSelectionChanged; _adapter.Commit -= OnAdapterSelectionComplete; _adapter.Cancel -= OnAdapterSelectionCanceled; _adapter.Cancel -= OnAdapterSelectionComplete; _adapter.Items = null; } _adapter = value; if (_adapter != null) { _adapter.SelectionChanged += OnAdapterSelectionChanged; _adapter.Commit += OnAdapterSelectionComplete; _adapter.Cancel += OnAdapterSelectionCanceled; _adapter.Cancel += OnAdapterSelectionComplete; _adapter.Items = _view; } } } /// /// Returns the /// part, if /// possible. /// /// /// A object, /// if possible. Otherwise, null. /// protected virtual ISelectionAdapter GetSelectionAdapterPart(INameScope nameScope) { ISelectionAdapter adapter = null; SelectingItemsControl selector = nameScope.Find(ElementSelector); 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 SelectingItemsControlSelectionAdapter(selector); } } if (adapter == null) { adapter = nameScope.Find(ElementSelectionAdapter); } return adapter; } /// /// Builds the visual tree for the /// control /// when a new template is applied. /// protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { if (DropDownPopup != null) { DropDownPopup.Closed -= DropDownPopup_Closed; DropDownPopup = null; } // Set the template parts. Individual part setters remove and add // any event handlers. Popup popup = e.NameScope.Find(ElementPopup); if (popup != null) { DropDownPopup = popup; DropDownPopup.Closed += DropDownPopup_Closed; } SelectionAdapter = GetSelectionAdapterPart(e.NameScope); TextBox = e.NameScope.Find(ElementTextBox); // 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); } base.OnTemplateApplied(e); } /// /// Provides handling for the /// event. /// /// A /// that contains the event data. protected override void OnKeyDown(KeyEventArgs e) { Contract.Requires(e != null); 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; } } /// /// Provides handling for the /// event. /// /// A /// that contains the event data. protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); FocusChanged(HasFocus()); } /// /// Provides handling for the /// event. /// /// A /// that contains the event data. protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); FocusChanged(HasFocus()); } /// /// 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() { IVisual focused = FocusManager.Instance.Current; while (focused != null) { if (object.ReferenceEquals(focused, this)) { return true; } // This helps deal with popups that may not be in the same // visual tree IVisual parent = focused.GetVisualParent(); if (parent == null) { // Try the logical parent. IControl element = focused as IControl; if (element != null) { parent = element.Parent; } } focused = parent; } return false; } /// /// 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()). bool wasFocused = _isFocused; _isFocused = hasFocus; if (hasFocus) { if (!wasFocused && TextBox != null && TextBoxSelectionLength <= 0) { TextBox.Focus(); TextBox.SelectionStart = 0; TextBox.SelectionEnd = TextBox.Text?.Length ?? 0; } } else { IsDropDownOpen = false; _userCalledPopulate = false; ClearTextBoxSelection(); } _isFocused = hasFocus; } /// /// Occurs when the text in the text box portion of the /// changes. /// public event EventHandler TextChanged; /// /// 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 EventHandler Populating; /// /// Occurs when the /// has /// populated the drop-down with possible matches based on the /// /// property. /// public event EventHandler Populated; /// /// Occurs when the value of the /// /// property is changing from false to true. /// public event EventHandler DropDownOpening; /// /// Occurs when the value of the /// /// property has changed from false to true and the drop-down is open. /// public event EventHandler DropDownOpened; /// /// Occurs when the /// /// property is changing from true to false. /// public event EventHandler DropDownClosing; /// /// Occurs when the /// /// property was changed from true to false and the drop-down is open. /// public event EventHandler DropDownClosed; /// /// Occurs when the selected item in the drop-down portion of the /// has /// changed. /// public event EventHandler SelectionChanged { add { AddHandler(SelectionChangedEvent, value); } remove { RemoveHandler(SelectionChangedEvent, value); } } /// /// Raises the /// /// event. /// /// A /// that /// contains the event data. protected virtual void OnPopulating(PopulatingEventArgs e) { Populating?.Invoke(this, e); } /// /// Raises the /// /// event. /// /// A /// /// that contains the event data. protected virtual void OnPopulated(PopulatedEventArgs e) { Populated?.Invoke(this, e); } /// /// Raises the /// /// event. /// /// A /// /// that contains the event data. protected virtual void OnSelectionChanged(SelectionChangedEventArgs e) { RaiseEvent(e); } /// /// Raises the /// /// event. /// /// A /// /// that contains the event data. protected virtual void OnDropDownOpening(CancelEventArgs e) { DropDownOpening?.Invoke(this, e); } /// /// Raises the /// /// event. /// /// A /// /// that contains the event data. protected virtual void OnDropDownOpened(EventArgs e) { DropDownOpened?.Invoke(this, e); } /// /// Raises the /// /// event. /// /// A /// /// that contains the event data. protected virtual void OnDropDownClosing(CancelEventArgs e) { DropDownClosing?.Invoke(this, e); } /// /// Raises the /// /// event. /// /// A /// /// which contains the event data. protected virtual void OnDropDownClosed(EventArgs e) { DropDownClosed?.Invoke(this, e); } /// /// Raises the /// /// event. /// /// A /// that contains the event data. protected virtual void OnTextChanged(RoutedEventArgs e) { TextChanged?.Invoke(this, e); } /// /// Begin closing the drop-down. /// /// The original value. private void ClosingDropDown(bool oldValue) { var args = new CancelEventArgs(); OnDropDownClosing(args); if (args.Cancel) { _ignorePropertyChange = true; SetValue(IsDropDownOpenProperty, oldValue); } else { CloseDropDown(); } UpdatePseudoClasses(); } /// /// 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) { var args = new CancelEventArgs(); // Opening OnDropDownOpening(args); if (args.Cancel) { _ignorePropertyChange = true; SetValue(IsDropDownOpenProperty, oldValue); } else { OpenDropDown(); } UpdatePseudoClasses(); } /// /// 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) { OnDropDownClosed(EventArgs.Empty); } } /// /// 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; if(TryPopulateAsync(SearchText)) { return; } // 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. PopulatingEventArgs populating = new PopulatingEventArgs(SearchText); OnPopulating(populating); if (!populating.Cancel) { PopulateComplete(); } } private bool TryPopulateAsync(string searchText) { _populationCancellationTokenSource?.Cancel(false); _populationCancellationTokenSource?.Dispose(); _populationCancellationTokenSource = null; if(_asyncPopulator == null) { return false; } _populationCancellationTokenSource = new CancellationTokenSource(); var task = PopulateAsync(searchText, _populationCancellationTokenSource.Token); if (task.Status == TaskStatus.Created) task.Start(); return true; } private async Task PopulateAsync(string searchText, CancellationToken cancellationToken) { try { IEnumerable result = await _asyncPopulator.Invoke(searchText, cancellationToken); var resultList = result.ToList(); if (cancellationToken.IsCancellationRequested) { return; } await Dispatcher.UIThread.InvokeAsync(() => { if (!cancellationToken.IsCancellationRequested) { Items = resultList; PopulateComplete(); } }); } catch (TaskCanceledException) { } finally { _populationCancellationTokenSource?.Dispose(); _populationCancellationTokenSource = null; } } /// /// Private method that directly opens the popup, checks the expander /// button, and then fires the Opened event. /// private void OpenDropDown() { if (DropDownPopup != null) { DropDownPopup.IsOpen = true; } _popupHasOpened = true; OnDropDownOpened(EventArgs.Empty); } /// /// Private method that directly closes the popup, flips the Checked /// value, and then fires the Closed event. /// private void CloseDropDown() { if (_popupHasOpened) { if (SelectionAdapter != null) { SelectionAdapter.SelectedItem = null; } if (DropDownPopup != null) { DropDownPopup.IsOpen = false; } OnDropDownClosed(EventArgs.Empty); } } /// /// 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 result = FormatValue(value); if(clearDataContext && _valueBindingEvaluator != null) { _valueBindingEvaluator.ClearDataContext(); } return result; } /// /// 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(); } /// /// 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. /// private void OnTextBoxTextChanged() { //Uses Dispatcher.Post to allow the TextBox selection to update before processing Dispatcher.UIThread.Post(() => { // Call the central updated text method as a user-initiated action TextUpdated(_textBox.Text, true); }); } /// /// 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) { bool callTextChanged = false; // Update the Text dependency property if ((userInitiated ?? true) && Text != value) { _ignoreTextPropertyChange++; Text = value; callTextChanged = true; } // 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 (!callTextChanged && (Text == value || Text == null)) { callTextChanged = true; } } if (callTextChanged) { OnTextChanged(new RoutedEventArgs()); } } /// /// 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 && TextBoxSelectionLength > 0 && TextBoxSelectionStart != 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; if(populateReady && MinimumPrefixLength == 0 && String.IsNullOrEmpty(newText) && String.IsNullOrEmpty(SearchText)) { populateReady = false; } _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; } } } /// /// 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 AvaloniaList(); } 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 new enumerable reference. private void OnItemsChanged(IEnumerable newValue) { // Remove handler for oldValue.CollectionChanged (if present) _collectionChangeSubscription?.Dispose(); _collectionChangeSubscription = null; // Add handler for newValue.CollectionChanged (if possible) if (newValue is INotifyCollectionChanged newValueINotifyCollectionChanged) { _collectionChangeSubscription = newValueINotifyCollectionChanged.WeakSubscribe(ItemsCollectionChanged); } // 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.Items != _view) { SelectionAdapter.Items = _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 ItemsCollectionChanged(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) && e.OldItems != null) { 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 (Items != null) { _items = new List(Items.Cast().ToList()); } } // Refresh the observable collection used in the selection adapter. RefreshView(); } /// /// 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. PopulatedEventArgs populated = new PopulatedEventArgs(new ReadOnlyCollection(_view)); OnPopulated(populated); if (SelectionAdapter != null && SelectionAdapter.Items != _view) { SelectionAdapter.Items = _view; } bool isDropDownOpen = _userCalledPopulate && (_view.Count > 0); if (isDropDownOpen != IsDropDownOpen) { _ignorePropertyChange = true; IsDropDownOpen = isDropDownOpen; } if (IsDropDownOpen) { OpeningDropDown(false); } 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 = TextBoxSelectionStart; 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.SelectionEnd = topString.Length; } } } } 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 = TextBoxSelectionStart; } } } /// /// 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, AvaloniaList view, AutoCompleteFilterPredicate predicate) { if (view != null && view.Count > 0) { foreach (object o in view) { if (predicate(searchText, FormatValue(o))) { return o; } } } return null; } private void UpdatePseudoClasses() { PseudoClasses.Set(":dropdownopen", IsDropDownOpen); } private void ClearTextBoxSelection() { if (TextBox != null) { int length = TextBox.Text?.Length ?? 0; TextBox.SelectionStart = length; TextBox.SelectionEnd = length; } } /// /// 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 ClearTextBoxSelection(); } /// /// 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; } //TODO Check UpdateTextCompletion /// /// 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 ClearTextBoxSelection(); TextBox.Focus(); } /// /// 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); } /// /// A predefined set of filter functions for the known, built-in /// AutoCompleteFilterMode enumeration values. /// private static class AutoCompleteSearch { /// /// Index function that retrieves the filter for the provided /// AutoCompleteFilterMode. /// /// The built-in search mode. /// Returns the string-based comparison function. public static AutoCompleteFilterPredicate GetFilter(AutoCompleteFilterMode FilterMode) { switch (FilterMode) { case AutoCompleteFilterMode.Contains: return Contains; case AutoCompleteFilterMode.ContainsCaseSensitive: return ContainsCaseSensitive; case AutoCompleteFilterMode.ContainsOrdinal: return ContainsOrdinal; case AutoCompleteFilterMode.ContainsOrdinalCaseSensitive: return ContainsOrdinalCaseSensitive; case AutoCompleteFilterMode.Equals: return Equals; case AutoCompleteFilterMode.EqualsCaseSensitive: return EqualsCaseSensitive; case AutoCompleteFilterMode.EqualsOrdinal: return EqualsOrdinal; case AutoCompleteFilterMode.EqualsOrdinalCaseSensitive: return EqualsOrdinalCaseSensitive; case AutoCompleteFilterMode.StartsWith: return StartsWith; case AutoCompleteFilterMode.StartsWithCaseSensitive: return StartsWithCaseSensitive; case AutoCompleteFilterMode.StartsWithOrdinal: return StartsWithOrdinal; case AutoCompleteFilterMode.StartsWithOrdinalCaseSensitive: return StartsWithOrdinalCaseSensitive; case AutoCompleteFilterMode.None: case AutoCompleteFilterMode.Custom: default: return null; } } /// /// An implementation of the Contains member of string that takes in a /// string comparison. The traditional .NET string Contains member uses /// StringComparison.Ordinal. /// /// The string. /// The string value to search for. /// The string comparison type. /// Returns true when the substring is found. private static bool Contains(string s, string value, StringComparison comparison) { return s.IndexOf(value, comparison) >= 0; } /// /// Check if the string value begins with the text. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool StartsWith(string text, string value) { return value.StartsWith(text, StringComparison.CurrentCultureIgnoreCase); } /// /// Check if the string value begins with the text. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool StartsWithCaseSensitive(string text, string value) { return value.StartsWith(text, StringComparison.CurrentCulture); } /// /// Check if the string value begins with the text. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool StartsWithOrdinal(string text, string value) { return value.StartsWith(text, StringComparison.OrdinalIgnoreCase); } /// /// Check if the string value begins with the text. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool StartsWithOrdinalCaseSensitive(string text, string value) { return value.StartsWith(text, StringComparison.Ordinal); } /// /// Check if the prefix is contained in the string value. The current /// culture's case insensitive string comparison operator is used. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool Contains(string text, string value) { return Contains(value, text, StringComparison.CurrentCultureIgnoreCase); } /// /// Check if the prefix is contained in the string value. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool ContainsCaseSensitive(string text, string value) { return Contains(value, text, StringComparison.CurrentCulture); } /// /// Check if the prefix is contained in the string value. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool ContainsOrdinal(string text, string value) { return Contains(value, text, StringComparison.OrdinalIgnoreCase); } /// /// Check if the prefix is contained in the string value. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool ContainsOrdinalCaseSensitive(string text, string value) { return Contains(value, text, StringComparison.Ordinal); } /// /// Check if the string values are equal. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool Equals(string text, string value) { return value.Equals(text, StringComparison.CurrentCultureIgnoreCase); } /// /// Check if the string values are equal. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool EqualsCaseSensitive(string text, string value) { return value.Equals(text, StringComparison.CurrentCulture); } /// /// Check if the string values are equal. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool EqualsOrdinal(string text, string value) { return value.Equals(text, StringComparison.OrdinalIgnoreCase); } /// /// Check if the string values are equal. /// /// The AutoCompleteBox prefix text. /// The item's string value. /// Returns true if the condition is met. public static bool EqualsOrdinalCaseSensitive(string text, string value) { return value.Equals(text, StringComparison.Ordinal); } } /// /// A framework element that permits a binding to be evaluated in a new data /// context leaf node. /// /// The type of dynamic binding to return. public class BindingEvaluator : Control { /// /// Gets or sets the string value binding used by the control. /// private IBinding _binding; #region public T Value /// /// Identifies the Value dependency property. /// public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register, T>(nameof(Value)); /// /// Gets or sets the data item value. /// public T Value { get { return GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } #endregion public string Value /// /// Gets or sets the value binding. /// public IBinding ValueBinding { get { return _binding; } set { _binding = value; AvaloniaObjectExtensions.Bind(this, ValueProperty, value); } } /// /// Initializes a new instance of the BindingEvaluator class. /// public BindingEvaluator() { } /// /// Initializes a new instance of the BindingEvaluator class, /// setting the initial binding to the provided parameter. /// /// The initial string value binding. public BindingEvaluator(IBinding binding) : this() { ValueBinding = binding; } /// /// Clears the data context so that the control does not keep a /// reference to the last-looked up item. /// public void ClearDataContext() { DataContext = null; } /// /// Updates the data context of the framework element and returns the /// updated binding value. /// /// The object to use as the data context. /// If set to true, this parameter will /// clear the data context immediately after retrieving the value. /// Returns the evaluated T value of the bound dependency /// property. public T GetDynamicValue(object o, bool clearDataContext) { DataContext = o; T value = Value; if (clearDataContext) { DataContext = null; } return value; } /// /// Updates the data context of the framework element and returns the /// updated binding value. /// /// The object to use as the data context. /// Returns the evaluated T value of the bound dependency /// property. public T GetDynamicValue(object o) { DataContext = o; return Value; } } } }