簡體   English   中英

帶有過濾器和自動完成功能的組合框

[英]ComboBox with both filter and auto-complete

有沒有人成功使用 WPF 的 ComboBox 自動完成和過濾功能? 我現在已經花了幾個小時,但還是沒能搞定。 這是 WPF + MVVM Light。 這是我的設置。

虛擬機層

提供以下屬性的 ViewModel:

  • FilterText ( string ):用戶在 TextBox 區域中輸入的用於過濾的文本。 FilteredItems上觸發更改通知。
  • Items ( List<string> ):這是包含所有選項的主要數據源。
  • FilteredItems :使用FilterText過濾的Items列表。
  • SelectedOption ( string ):當前選擇的選項。

視圖層

一個 ComboBox,用戶只能從下拉選項中進行選擇。 但是,應允許用戶在文本框區域中鍵入文本,並且下拉列表應過濾掉不以鍵入文本開頭的項目。 第一個匹配項應自動附加到文本框(即自動完成)。 這是我的綁定:

  • ItemsSource :綁定到FilteredItems ,單向
  • Text綁定到FilterText ,雙向
  • SelectedItem綁定到SelectedOption ,雙向

IsTextSearchEnabled設置為 true 以啟用自動完成。

此設置的問題在於,一旦用戶鍵入第一個字母,就會觸發自動完成並嘗試定位第一個匹配的條目,如果找到,則將SelectedItem設置為該條目,這將ComboBoxText屬性設置為該項目,這反過來會觸發過濾器操作,並且下拉列表只剩下一個完全匹配Text條目,這不是它應該的樣子。

例如,如果用戶鍵入“C”,自動完成將嘗試定位以“C”開頭的第一個條目。 假設第一個匹配條目是“客戶”。 自動完成將選擇該條目,這會將SelectedItem設置為“Customer”,因此Text也將變為“Customer”。這將調用FilterText因為綁定,這將更新FilteredItems ,它現在將只返回一個條目,而不是返回所有以“C”開頭的條目。

我在這里缺少什么?

我覺得你的方法太復雜了。
在啟用自動完成功能時,您可以實現一個簡單的附加行為來實現過濾的建議列表。

除了ComboBox.ItemsSource的公共源集合之外,此示例不需要任何其他屬性。 過濾是通過使用ICollectionView.Filter屬性完成的。 這將只修改ItemsControl的內部源集合的視圖,而不是底層綁定源集合本身。 不需要將IsTextSearchEnabled設置為True來啟用自動完成。

基本思想是觸發過濾而不是在TextBox.TextChanged上而不是在ComboBox.SelectedItemChanged (或一般的ComboBox.SelectedItem )上。

組合框.cs

class ComboBox : DependencyObject
{
  #region IsFilterOnAutoCompleteEnabled attached property

  public static readonly DependencyProperty IsFilterOnAutocompleteEnabledProperty =
    DependencyProperty.RegisterAttached(
      "IsFilterOnAutocompleteEnabled",
      typeof(bool),
      typeof(ComboBox),
      new PropertyMetadata(default(bool), ComboBox.OnIsFilterOnAutocompleteEnabledChanged));

  public static void SetIsFilterOnAutocompleteEnabled(DependencyObject attachingElement, bool value) =>
    attachingElement.SetValue(ComboBox.IsFilterOnAutocompleteEnabledProperty, value);

  public static bool GetIsFilterOnAutocompleteEnabled(DependencyObject attachingElement) =>
    (bool)attachingElement.GetValue(ComboBox.IsFilterOnAutocompleteEnabledProperty);

  #endregion

  // Use hash tables for faster lookup
  private static Dictionary<TextBox, System.Windows.Controls.ComboBox> TextBoxComboBoxMap { get; }
  private static Dictionary<TextBox, int> TextBoxSelectionStartMap { get; }
  private static Dictionary<System.Windows.Controls.ComboBox, TextBox> ComboBoxTextBoxMap { get; }
  private static bool IsNavigationKeyPressed { get; set; }

  static ComboBox()
  {
    ComboBox.TextBoxComboBoxMap = new Dictionary<TextBox, System.Windows.Controls.ComboBox>();
    ComboBox.TextBoxSelectionStartMap = new Dictionary<TextBox, int>();
    ComboBox.ComboBoxTextBoxMap = new Dictionary<System.Windows.Controls.ComboBox, TextBox>();
  }

  private static void OnIsFilterOnAutocompleteEnabledChanged(
    DependencyObject attachingElement,
    DependencyPropertyChangedEventArgs e)
  {
    if (!(attachingElement is System.Windows.Controls.ComboBox comboBox
      && comboBox.IsEditable))
    {
      return;
    }

    if (!(bool)e.NewValue)
    {
      ComboBox.DisableAutocompleteFilter(comboBox);
      return;
    }

    if (!comboBox.IsLoaded)
    {
      comboBox.Loaded += ComboBox.EnableAutocompleteFilterOnComboBoxLoaded;
      return;
    }
    ComboBox.EnableAutocompleteFilter(comboBox);
  }

  private static async void FilterOnTextInput(object sender, TextChangedEventArgs e)
  {
    await Application.Current.Dispatcher.InvokeAsync(
      () =>
      {
        if (ComboBox.IsNavigationKeyPressed)
        {
          return;
        }

        var textBox = sender as TextBox;
        int textBoxSelectionStart = textBox.SelectionStart;
        ComboBox.TextBoxSelectionStartMap[textBox] = textBoxSelectionStart;

        string changedTextOnAutocomplete = textBox.Text.Substring(0, textBoxSelectionStart);
        if (ComboBox.TextBoxComboBoxMap.TryGetValue(
          textBox,
          out System.Windows.Controls.ComboBox comboBox))
        {
          comboBox.Items.Filter = item => item.ToString().StartsWith(
            changedTextOnAutocomplete,
            StringComparison.OrdinalIgnoreCase);
        }
      },
      DispatcherPriority.Background);
  }

  private static async void HandleKeyDownWhileFiltering(object sender, KeyEventArgs e)
  {
    var comboBox = sender as System.Windows.Controls.ComboBox;
    if (!ComboBox.ComboBoxTextBoxMap.TryGetValue(comboBox, out TextBox textBox))
    {
      return;
    }

    switch (e.Key)
    {
      case Key.Down 
        when comboBox.Items.CurrentPosition < comboBox.Items.Count - 1 
             && comboBox.Items.MoveCurrentToNext():
      case Key.Up 
        when comboBox.Items.CurrentPosition > 0 
             && comboBox.Items.MoveCurrentToPrevious():
      {
        // Prevent the filter from re-apply as this would override the
        // current selection start index
        ComboBox.IsNavigationKeyPressed = true;

        // Ensure the Dispatcher en-queued delegate 
        // (and the invocation of the SelectCurrentItem() method)
        // executes AFTER the FilterOnTextInput() event handler.
        // This is because key input events have a higher priority
        // than text change events by default. The goal is to make the filtering 
        // triggered by the TextBox.TextChanged event ignore the changes 
        // introduced by this KeyDown event.
        // DispatcherPriority.ContextIdle will force to "override" this behavior.
        await Application.Current.Dispatcher.InvokeAsync(
          () =>
          {
            ComboBox.SelectCurrentItem(textBox, comboBox);
            ComboBox.IsNavigationKeyPressed = false;
          }, 
          DispatcherPriority.ContextIdle);

        break;
      }
    }
  }

  private static void SelectCurrentItem(TextBox textBox, System.Windows.Controls.ComboBox comboBox)
  {
    comboBox.SelectedItem = comboBox.Items.CurrentItem;
    if (ComboBox.TextBoxSelectionStartMap.TryGetValue(textBox, out int selectionStart))
    {
      textBox.SelectionStart = selectionStart;
    }
  }

  private static void EnableAutocompleteFilterOnComboBoxLoaded(object sender, RoutedEventArgs e)
  {
    var comboBox = sender as System.Windows.Controls.ComboBox;
    ComboBox.EnableAutocompleteFilter(comboBox);
  }

  private static void EnableAutocompleteFilter(System.Windows.Controls.ComboBox comboBox)
  {
    if (comboBox.TryFindVisualChildElement(out TextBox editTextBox))
    {
      ComboBox.TextBoxComboBoxMap.Add(editTextBox, comboBox);
      ComboBox.ComboBoxTextBoxMap.Add(comboBox, editTextBox);
      editTextBox.TextChanged += ComboBox.FilterOnTextInput;

      // Need to receive handled KeyDown event
      comboBox.AddHandler(UIElement.PreviewKeyDownEvent, new KeyEventHandler(HandleKeyDownWhileFiltering), true);
    }
  }

  private static void DisableAutocompleteFilter(System.Windows.Controls.ComboBox comboBox)
  {
    if (comboBox.TryFindVisualChildElement(out TextBox editTextBox))
    {
      ComboBox.TextBoxComboBoxMap.Remove(editTextBox);
      editTextBox.TextChanged -= ComboBox.FilterOnTextInput;
    }
  }
}

擴展.cs

public static class Extensions
{ 
  /// <summary>
  /// Traverses the visual tree towards the leafs until an element with a matching element type is found.
  /// </summary>
  /// <typeparam name="TChild">The type the visual child must match.</typeparam>
  /// <param name="parent"></param>
  /// <param name="resultElement"></param>
  /// <returns></returns>
  public static bool TryFindVisualChildElement<TChild>(this DependencyObject parent, out TChild resultElement)
    where TChild : DependencyObject
  {
    resultElement = null;

    if (parent is Popup popup)
    {
      parent = popup.Child;
      if (parent == null)
      {
        return false;
      }
    }

    for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
    {
      DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
      if (childElement is TChild child)
      {
        resultElement = child;
        return true;
      }

      if (childElement.TryFindVisualChildElement(out resultElement))
      {
        return true;
      }
    }

    return false;
  }
}

使用示例

<ComboBox ItemsSource="{Binding Items}" 
          IsEditable="True"
          ComboBox.IsFilterOnAutocompleteEnabled="True" />

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM