[英]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
設置為該條目,這將ComboBox
的Text
屬性設置為該項目,這反過來會觸發過濾器操作,並且下拉列表只剩下一個完全匹配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.