c# - 具有过滤器和自动完成功能的组合框
问题描述
有没有人成功使用 WPF 的 ComboBox 自动完成和过滤功能?我现在已经花了几个小时,但无法确定它。这是 WPF + MVVM Light。这是我的设置。
虚拟机层
提供以下属性的 ViewModel:
FilterText
(string
):用户在 TextBox 区域中输入的用于过滤的文本。在 上触发更改通知FilteredItems
。Items
(List<string>
):这是包含所有选项的主要数据源。FilteredItems
: 过滤的Items
使用列表FilterText
。SelectedOption
(string
):当前选择的选项。
视图层
一个组合框,用户只能从下拉选项中进行选择。但是,应该允许用户在文本框区域输入文本,并且下拉菜单应该过滤掉不以输入文本开头的项目。第一个匹配项应自动附加到文本框(即自动完成)。这是我的绑定:
ItemsSource
: 绑定到FilteredItems
, 单向Text
绑定到FilterText
,双向SelectedItem
绑定到SelectedOption
,双向
IsTextSearchEnabled
设置为 true 以启用自动完成。
此设置的问题在于,一旦用户键入第一个字母,就会触发自动完成并尝试找到第一个匹配条目,如果找到,则设置SelectedItem
为该条目,其中将Text
属性设置ComboBox
为该项目,其中turn 触发过滤器操作,下拉菜单只剩下一个完全匹配的条目Text
,这不是它应该的样子。
例如,如果用户键入“C”,自动完成将尝试定位以“C”开头的第一个条目。假设第一个匹配条目是“客户”。自动完成将选择该条目,该条目将设置SelectedItem
为“客户”,因此也将变为“客户。由于绑定,Text
这将调用,它将更新,现在将只返回一个条目,而不是返回所有以“开头的条目C”。FilterText
FilteredItems
我在这里想念什么?
解决方案
我认为你的方法太复杂了。
您可以实现一个简单的附加行为来实现过滤建议列表,同时启用自动完成。
此示例不需要任何其他属性,除了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" />
推荐阅读
- java - 如何清除导航标题视图?
- javascript - CSS 变换旋转仅适用于 Firefox
- angular - “对象”类型上不存在属性“数据”
- powershell - 定期检查日志是否有错误,但要防止发送重复通知
- javascript - 获取未定义表格单元格javascript子项的值
- jhipster - jhipster项目时出现elasticsearch错误
- ruby - 如何使用 Ruby 比较两个 XML 文件并以 html 格式显示差异
- php - Laravel:碳缩短 diffForHumans()
- javascript - html 页面作为小部件
- reactjs - 反应:如果未保存数据,如何取消 componentWillUnmount() 方法