Files
ShrlAlgoToolkit/Melskin/Controls/AutoComplete.xaml.cs
2026-03-01 10:42:42 +08:00

249 lines
9.2 KiB
C#

using System.Collections;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
namespace Melskin.Controls
{
/// <summary>
/// AutoComplete 控件提供了一个文本框,用户可以在其中输入文本,并从一个下拉列表中选择建议项。该控件适用于需要快速完成或查找功能的场景。
/// </summary>
/// <remarks>
/// 通过设置 ItemsSource 属性,可以为 AutoComplete 控件提供数据源,用于填充建议列表。当用户在文本框中输入时,会根据输入的内容显示匹配的建议项。
/// </remarks>
[TemplatePart(Name = "PART_TextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]
[TemplatePart(Name = "PART_ListBox", Type = typeof(ListBox))]
public class AutoComplete : Control
{
private TextBox? textBox;
private Popup? popup;
private ListBox? listBox;
private bool isSelectionChanging = false;
private const int filterDelay = 200;
private CancellationTokenSource? cts;
#region Dependency Properties
/// <summary>
/// 用于设置或获取 AutoComplete 控件的数据源。该属性接受实现了 IEnumerable 接口的任何集合,这些数据将作为建议项显示给用户。
/// </summary>
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(AutoComplete), new PropertyMetadata(null, OnItemsSourceChanged));
/// <summary>
/// 用于设置或获取 AutoComplete 控件的数据源。该属性接受实现了 IEnumerable 接口的任何集合,这些数据将作为建议项显示给用户。
/// </summary>
public IEnumerable ItemsSource
{
get => (IEnumerable)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
/// <summary>
/// 用于设置或获取 AutoComplete 控件中文本框显示的文本。此属性支持双向数据绑定,允许在控件与数据源之间同步文本内容。
/// </summary>
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(AutoComplete), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnTextChanged));
/// <summary>
/// 用于获取或设置 AutoComplete 控件中显示的文本。此属性支持双向数据绑定,允许用户输入的文本与外部数据源之间进行同步。
/// </summary>
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>
/// 用于设置或获取 AutoComplete 控件中当前选中的项。该属性支持双向数据绑定,可以用来反映用户从建议列表中选择的项目。
/// </summary>
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(nameof(SelectedItem), typeof(object), typeof(AutoComplete), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
/// <summary>
/// 获取或设置当前在 AutoComplete 控件中选中的项。此属性支持双向数据绑定,允许用户选择的项能够被外部逻辑访问和修改。
/// </summary>
public object? SelectedItem
{
get => GetValue(SelectedItemProperty);
set => SetValue(SelectedItemProperty, value);
}
/// <summary>
/// 用于设置或获取 AutoComplete 控件的下拉列表是否展开的状态。该属性为布尔值,当设置为 true 时,表示下拉列表处于打开状态;反之则关闭。
/// </summary>
public static readonly DependencyProperty IsDropDownOpenProperty =
DependencyProperty.Register(nameof(IsDropDownOpen), typeof(bool), typeof(AutoComplete), new PropertyMetadata(false));
/// <summary>
///
/// </summary>
public bool IsDropDownOpen
{
get => (bool)GetValue(IsDropDownOpenProperty);
set => SetValue(IsDropDownOpenProperty, value);
}
/// <summary>
///
/// </summary>
public static readonly DependencyProperty PlaceholderTextProperty =
DependencyProperty.Register(nameof(PlaceholderText), typeof(string), typeof(AutoComplete), new PropertyMetadata(string.Empty));
/// <summary>
///
/// </summary>
public string PlaceholderText
{
get => (string)GetValue(PlaceholderTextProperty);
set => SetValue(PlaceholderTextProperty, value);
}
#endregion
static AutoComplete()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoComplete), new FrameworkPropertyMetadata(typeof(AutoComplete)));
}
/// <inheritdoc />
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (textBox != null)
{
textBox.TextChanged -= OnTextBoxTextChanged;
textBox.PreviewKeyDown -= OnTextBoxPreviewKeyDown;
}
if (listBox != null)
{
listBox.SelectionChanged -= OnListBoxSelectionChanged;
}
textBox = GetTemplateChild("PART_TextBox") as TextBox;
popup = GetTemplateChild("PART_Popup") as Popup;
listBox = GetTemplateChild("PART_ListBox") as ListBox;
if (textBox != null)
{
textBox.TextChanged += OnTextBoxTextChanged;
textBox.PreviewKeyDown += OnTextBoxPreviewKeyDown;
}
if (listBox != null)
{
listBox.SelectionChanged += OnListBoxSelectionChanged;
}
}
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (AutoComplete)d;
if (control.listBox != null)
{
control.listBox.ItemsSource = e.NewValue as IEnumerable;
}
}
private static async void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (AutoComplete)d;
if (control.isSelectionChanging) return;
if (control.textBox != null && control.textBox.Text != (string)e.NewValue)
{
control.textBox.Text = (string)e.NewValue;
}
control.cts?.Cancel();
control.cts = new CancellationTokenSource();
try
{
await Task.Delay(filterDelay, control.cts.Token).ConfigureAwait(true);
control.FilterItemsSource();
}
catch (TaskCanceledException)
{
// Ignore cancellation
}
}
private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
if (isSelectionChanging) return;
// 同步Text属性
if (textBox != null && Text != textBox.Text)
{
Text = textBox.Text;
}
// 清空我们自己控件的选中项
SelectedItem = null;
// 同时清空ListBox内部的选中项
if (listBox is { SelectedItem: not null })
{
listBox.SelectedItem = null;
}
}
private void OnTextBoxPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Down && listBox is { Items.Count: > 0 })
{
listBox.Focus();
listBox.SelectedIndex = 0;
}
else if (e.Key == Key.Escape)
{
IsDropDownOpen = false;
}
}
private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0 || listBox?.SelectedItem == null) return;
isSelectionChanging = true;
SelectedItem = listBox.SelectedItem;
Text = SelectedItem?.ToString() ?? string.Empty;
IsDropDownOpen = false;
textBox?.Focus();
textBox?.Select(textBox.Text.Length, 0);
isSelectionChanging = false;
}
private void FilterItemsSource()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
if (string.IsNullOrEmpty(Text))
{
view.Filter = null;
}
else
{
view.Filter = item => item.ToString()?.IndexOf(Text, StringComparison.OrdinalIgnoreCase) >= 0;
}
view.Refresh();
// !string.IsNullOrEmpty(Tip) 的判断
if (!string.IsNullOrEmpty(Text) && !view.IsEmpty && textBox is { IsKeyboardFocused: true })
{
IsDropDownOpen = true;
}
else
{
IsDropDownOpen = false;
}
}
}
}