using System; using System.Collections; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; namespace Developpez.Dotnet.Windows.Behaviors { /// /// Fournit des propriétés attachées pour gérer l'autocomplétion dans une TextBox /// public static class AutoCompleteBehavior { #region Attached properties /// /// Identifiant de la propriété attachée ItemsSource /// public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.RegisterAttached( "ItemsSource", typeof(IEnumerable), typeof(AutoCompleteBehavior), new UIPropertyMetadata( null, ItemsSourceChanged)); /// /// Identifiant de la propriété attachée DisplayMemberPath /// public static readonly DependencyProperty DisplayMemberPathProperty = DependencyProperty.RegisterAttached("DisplayMemberPath", typeof(string), typeof(AutoCompleteBehavior), new UIPropertyMetadata(string.Empty)); /// /// Identifiant de la propriété attachée SearchMemberPath /// public static readonly DependencyProperty SearchMemberPathProperty = DependencyProperty.RegisterAttached("SearchMemberPath", typeof(string), typeof(AutoCompleteBehavior), new UIPropertyMetadata(string.Empty)); /// /// Identifiant de la propriété attachée ComparisonType /// public static readonly DependencyProperty ComparisonTypeProperty = DependencyProperty.RegisterAttached("ComparisonType", typeof(StringComparison), typeof(AutoCompleteBehavior), new UIPropertyMetadata(StringComparison.CurrentCultureIgnoreCase)); /// /// Identifiant de la propriété attachée Suggest /// public static readonly DependencyProperty SuggestProperty = DependencyProperty.RegisterAttached("Suggest", typeof(bool), typeof(AutoCompleteBehavior), new UIPropertyMetadata(false)); /// /// Identifiant de la propriété attachée MatchMode /// public static readonly DependencyProperty MatchModeProperty = DependencyProperty.RegisterAttached("MatchMode", typeof(StringMatchMode), typeof(AutoCompleteBehavior), new UIPropertyMetadata(StringMatchMode.StartsWith)); /// /// Identifiant de la propriété attachée ItemTemplate /// public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.RegisterAttached("ItemTemplate", typeof(DataTemplate), typeof(AutoCompleteBehavior), new UIPropertyMetadata(null)); private static readonly DependencyPropertyKey AutoCompletePopupPropertyKey = DependencyProperty.RegisterAttachedReadOnly("AutoCompletePopup", typeof(AutoCompletePopup), typeof(AutoCompleteBehavior), new UIPropertyMetadata(null)); #endregion #region Attached property accessors /// /// Obtient la valeur de la propriété attachée ItemsSource pour l'élément spécifié. /// /// L'objet pour lequel on veut obtenir la valeur de la propriété /// La liste utilisée pour l'autocomplétion [AttachedPropertyBrowsableForType(typeof(TextBox))] public static IEnumerable GetItemsSource(TextBox obj) { return (IEnumerable)obj.GetValue(ItemsSourceProperty); } /// /// Définit la valeur de la propriété attachée ItemsSource pour l'élément spécifié. /// /// L'objet pour lequel on veut définir la valeur de la propriété /// La liste à utiliser pour l'autocomplétion public static void SetItemsSource(TextBox obj, IEnumerable value) { obj.SetValue(ItemsSourceProperty, value); } /// /// Obtient la valeur de la propriété attachée ItemTemplate pour l'élément spécifié. /// /// L'objet pour lequel on veut obtenir la valeur de la propriété /// Le DataTemplate utilisé pour les éléments de la liste [AttachedPropertyBrowsableForType(typeof(TextBox))] public static DataTemplate GetItemTemplate(DependencyObject obj) { return (DataTemplate)obj.GetValue(ItemTemplateProperty); } /// /// Définit la valeur de la propriété attachée ItemTemplate pour l'élément spécifié. /// /// L'objet pour lequel on veut définir la valeur de la propriété /// Le DataTemplate à utiliser pour les éléments de la liste public static void SetItemTemplate(DependencyObject obj, DataTemplate value) { obj.SetValue(ItemTemplateProperty, value); } /// /// Obtient la valeur de la propriété attachée DisplayMemberPath pour l'élément spécifié. /// /// L'objet pour lequel on veut obtenir la valeur de la propriété /// Le membre des éléments de la liste à afficher [AttachedPropertyBrowsableForType(typeof(TextBox))] public static string GetDisplayMemberPath(DependencyObject obj) { return (string)obj.GetValue(DisplayMemberPathProperty); } /// /// Définit la valeur de la propriété attachée DisplayMemberPath pour l'élément spécifié. /// /// L'objet pour lequel on veut définir la valeur de la propriété /// Le membre des éléments de la liste à afficher public static void SetDisplayMemberPath(DependencyObject obj, string value) { obj.SetValue(DisplayMemberPathProperty, value); } /// /// Obtient la valeur de la propriété attachée SearchMemberPath pour l'élément spécifié. /// /// L'objet pour lequel on veut obtenir la valeur de la propriété /// Le membre des éléments de la liste utilisé pour la recherche [AttachedPropertyBrowsableForType(typeof(TextBox))] public static string GetSearchMemberPath(DependencyObject obj) { return (string)obj.GetValue(SearchMemberPathProperty); } /// /// Définit la valeur de la propriété attachée SearchMemberPath pour l'élément spécifié. /// /// L'objet pour lequel on veut définir la valeur de la propriété /// Le membre des éléments de la liste à utiliser pour la recherche public static void SetSearchMemberPath(DependencyObject obj, string value) { obj.SetValue(SearchMemberPathProperty, value); } /// /// Obtient la valeur de la propriété attachée Suggest pour l'élément spécifié. /// /// L'objet pour lequel on veut obtenir la valeur de la propriété /// True si la suite du texte est suggérée automatiquement, sinon false [AttachedPropertyBrowsableForType(typeof(TextBox))] public static bool GetSuggest(DependencyObject obj) { return (bool)obj.GetValue(SuggestProperty); } /// /// Définit la valeur de la propriété attachée Suggest pour l'élément spécifié. /// /// L'objet pour lequel on veut définir la valeur de la propriété /// True pour suggérer automatiquement la suite du texte, sinon false public static void SetSuggest(DependencyObject obj, bool value) { obj.SetValue(SuggestProperty, value); } /// /// Obtient la valeur de la propriété attachée MatchMode pour l'élément spécifié. /// /// L'objet pour lequel on veut obtenir la valeur de la propriété /// Le mode de correspondance de chaine utilisé pour la recherche [AttachedPropertyBrowsableForType(typeof(TextBox))] public static StringMatchMode GetMatchMode(DependencyObject obj) { return (StringMatchMode)obj.GetValue(MatchModeProperty); } /// /// Définit la valeur de la propriété attachée MatchMode pour l'élément spécifié. /// /// L'objet pour lequel on veut définir la valeur de la propriété /// Le mode de correspondance de chaine à utiliser pour la recherche public static void SetMatchMode(DependencyObject obj, StringMatchMode value) { obj.SetValue(MatchModeProperty, value); } /// /// Obtient la valeur de la propriété attachée ComparisonType pour l'élément spécifié. /// /// L'objet pour lequel on veut obtenir la valeur de la propriété /// Le type de comparaison de chaine pour le filtrage [AttachedPropertyBrowsableForType(typeof(TextBox))] public static StringComparison GetComparisonType(DependencyObject obj) { return (StringComparison)obj.GetValue(ComparisonTypeProperty); } /// /// Définit la valeur de la propriété attachée ComparisonType pour l'élément spécifié. /// /// L'objet pour lequel on veut définir la valeur de la propriété /// Le type de comparaison de chaine pour le filtrage public static void SetComparisonType(DependencyObject obj, StringComparison value) { obj.SetValue(ComparisonTypeProperty, value); } #endregion private static void ItemsSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { TextBox textBox = o as TextBox; if (textBox == null) return; if (e.OldValue != null && e.NewValue == null) { var popup = (AutoCompletePopup)textBox.GetValue(AutoCompletePopupPropertyKey.DependencyProperty); if (popup != null) popup.Dispose(); } else if (e.OldValue == null && e.NewValue != null) { new AutoCompletePopup(textBox); } } private class AutoCompletePopup : Popup, IDisposable { private readonly ListBox _listBox; private readonly TextBox _textBox; private string _currentSearch; private bool _textChanging; private bool _deleting; public AutoCompletePopup(TextBox textBox) { _textBox = textBox; SetBinding( MinWidthProperty, new Binding { Path = new PropertyPath("ActualWidth"), Source = textBox }); StaysOpen = true; _listBox = new ListBox(); _listBox.SetBinding( ItemsControl.ItemsSourceProperty, new Binding { Path = new PropertyPath(ItemsSourceProperty), Source = _textBox }); _listBox.SetBinding( ItemsControl.DisplayMemberPathProperty, new Binding { Path = new PropertyPath(DisplayMemberPathProperty), Source = _textBox }); _listBox.SetBinding( ItemsControl.ItemTemplateProperty, new Binding { Path = new PropertyPath(ItemTemplateProperty), Source = _textBox }); _listBox.AddHandler(Mouse.MouseDownEvent, new MouseButtonEventHandler(_listBox_MouseLeftButtonDown), true); Child = _listBox; PlacementTarget = _textBox; Placement = PlacementMode.Bottom; MaxHeight = 200; AllowsTransparency = true; SetResourceReference(PopupAnimationProperty, SystemParameters.ComboBoxPopupAnimationKey); Focusable = false; _textBox.LostFocus += _textBox_LostFocus; _textBox.TextChanged += _textBox_TextChanged; _textBox.PreviewKeyDown += _textBox_PreviewKeyDown; Window window = Window.GetWindow(_textBox); if (window != null) window.Deactivated += window_Deactivated; _textBox.SetValue(AutoCompletePopupPropertyKey, this); } protected override void OnOpened(EventArgs e) { base.OnOpened(e); RefreshFilter(); } private void RefreshFilter() { var view = CollectionViewSource.GetDefaultView(_listBox.ItemsSource); if (view != null) { view.SortDescriptions.Clear(); string displayMemberPath = GetDisplayMemberPath(_textBox); if (!string.IsNullOrEmpty(displayMemberPath)) view.SortDescriptions.Add(new System.ComponentModel.SortDescription(displayMemberPath, System.ComponentModel.ListSortDirection.Ascending)); if (string.IsNullOrEmpty(_textBox.Text)) { view.Filter = null; } else { view.Filter = FilterItem; } } } private bool FilterItem(object obj) { if (string.IsNullOrEmpty(_currentSearch)) return true; var comparisonType = GetComparisonType(_textBox); var matchMode = GetMatchMode(_textBox); string itemText = GetItemText(obj); if (itemText != null) { switch (matchMode) { case StringMatchMode.StartsWith: return itemText.StartsWith(_currentSearch, comparisonType); case StringMatchMode.EndsWith: return itemText.EndsWith(_currentSearch, comparisonType); case StringMatchMode.Contains: return itemText.Contains(_currentSearch, comparisonType); default: break; } } return false; } private string GetItemText(object obj) { string path = GetSearchMemberPath(_textBox); if (string.IsNullOrEmpty(path)) path = GetDisplayMemberPath(_textBox); if (!string.IsNullOrEmpty(path) && obj != null) { obj = PropertyPathHelper.GetValue(obj, path); } if (obj != null) return obj.ToString(); return null; } private void _listBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (IsInListBoxItem(e.OriginalSource as DependencyObject)) { ChooseSelectedText(); IsOpen = false; } } private static bool IsInListBoxItem(DependencyObject o) { if (o == null) return false; if (o is ListBoxItem) return true; DependencyObject parent = VisualTreeHelper.GetParent(o); return IsInListBoxItem(parent); } private void window_Deactivated(object sender, EventArgs e) { IsOpen = false; } private void _textBox_TextChanged(object sender, RoutedEventArgs e) { if (_textChanging) return; string searchText = _textBox.Text; int start = _textBox.SelectionStart; int length = _textBox.SelectionLength; if (length != 0) searchText = _textBox.Text.Substring(0, _textBox.SelectionStart); IsOpen = true; if (searchText != _currentSearch) { _currentSearch = searchText; RefreshFilter(); } if (GetSuggest(_textBox) && !_deleting) { object firstSuggestion = _listBox.Items.Cast().FirstOrDefault(); if (firstSuggestion != null) { string suggestedText = GetItemText(firstSuggestion); if (!string.IsNullOrEmpty(suggestedText)) { try { _textChanging = true; _textBox.Text = suggestedText; _textBox.Select(start, suggestedText.Length - start); } finally { _textChanging = false; } } } } _deleting = false; } private void _textBox_LostFocus(object sender, RoutedEventArgs e) { IsOpen = false; } private void _textBox_PreviewKeyDown(object sender, KeyEventArgs e) { switch (e.Key) { case Key.Down: IsOpen = true; MoveSelection(1); break; case Key.Up: IsOpen = true; MoveSelection(-1); break; case Key.Escape: IsOpen = false; break; case Key.Enter: ChooseSelectedText(); IsOpen = false; break; case Key.Delete: case Key.Back: _deleting = true; break; } } private void MoveSelection(int direction) { if (direction < 0) { if (_listBox.SelectedIndex < 0) _listBox.SelectedIndex = _listBox.Items.Count - 1; else _listBox.SelectedIndex--; } else if (direction > 0) { if (_listBox.SelectedIndex < 0) _listBox.SelectedIndex = 0; else _listBox.SelectedIndex++; } _listBox.ScrollIntoView(_listBox.SelectedItem); } private void ChooseSelectedText() { string itemText = GetItemText(_listBox.SelectedItem); if (itemText != null) _textBox.Text = itemText; } public void Dispose() { _textBox.LostFocus -= _textBox_LostFocus; _textBox.TextChanged -= _textBox_TextChanged; _textBox.PreviewKeyDown -= _textBox_PreviewKeyDown; Window window = Window.GetWindow(_textBox); if (window != null) window.Deactivated -= window_Deactivated; _textBox.ClearValue(AutoCompletePopupPropertyKey); } } } }