249 lines
9.2 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
} |