YunNan_DM/SearchableComboBox/SearchableComboBox.cs

324 lines
10 KiB
C#
Raw Normal View History

2023-11-13 14:20:30 +08:00
using System;
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;
using System.Diagnostics;
namespace SearchableComboBox
{
/// <summary>
/// Represents a ComboBox with search functionality.
/// </summary>
public class SearchableComboBox : ComboBox
{
#region Fields and Constructors
private readonly DispatcherTimer _debounceTimer;
private bool debug = true;
private TextBox _searchTermTextBox;
private CollectionViewSource _collectionViewSource;
private object _lastSelectedItem;
private bool _isDropDownOpen = false;
private TextBlock _placeholderTextBlock;
/// <summary>
/// Initializes a new instance of the <see cref="SearchableComboBox"/> class.
/// </summary>
public SearchableComboBox()
{
Debug.WriteLineIf(debug, "Constructor");
_debounceTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) };
_debounceTimer.Tick += DebounceTimer_Tick;
this.DropDownClosed += OnDropDownClosed;
}
static SearchableComboBox()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchableComboBox), new FrameworkPropertyMetadata(typeof(SearchableComboBox)));
}
public override void OnApplyTemplate()
{
Debug.WriteLineIf(debug, "OnApplyTemplate");
base.OnApplyTemplate();
_searchTermTextBox = Template.FindName("SearchTermTextBox", this) as TextBox;
_placeholderTextBlock = Template.FindName("PlaceholderTextBlock", this) as TextBlock;
UpdatePlaceholderVisibility();
}
#endregion
#region CustomSort Support
public static readonly DependencyProperty CustomSortProperty =
DependencyProperty.Register(
"CustomSort",
typeof(IComparer),
typeof(SearchableComboBox),
new PropertyMetadata(null, OnCustomSortChanged));
/// <summary>
/// Gets or sets the custom sort comparer.
/// </summary>
public IComparer CustomSort
{
get => (IComparer)GetValue(CustomSortProperty);
set => SetValue(CustomSortProperty, value);
}
private static void OnCustomSortChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var comboBox = d as SearchableComboBox;
if (comboBox._collectionViewSource != null && comboBox._collectionViewSource.View is ListCollectionView lcv)
{
lcv.CustomSort = e.NewValue as IComparer;
}
}
#endregion
#region IsSearchEnabled
public static readonly DependencyProperty IsSearchEnabledProperty =
DependencyProperty.Register(
"IsSearchEnabled",
typeof(bool),
typeof(SearchableComboBox),
new PropertyMetadata(true));
/// <summary>
/// Gets or sets a value indicating whether search is enabled.
/// </summary>
public bool IsSearchEnabled
{
get => (bool)GetValue(IsSearchEnabledProperty);
set => SetValue(IsSearchEnabledProperty, value);
}
#endregion
#region Placeholder Text
public static readonly DependencyProperty PlaceholderStyleProperty =
DependencyProperty.Register("PlaceholderStyle", typeof(Style), typeof(SearchableComboBox), new PropertyMetadata(null));
/// <summary>
/// Gets or sets the style for the placeholder text block.
/// </summary>
public Style PlaceholderStyle
{
get => (Style)GetValue(PlaceholderStyleProperty);
set => SetValue(PlaceholderStyleProperty, value);
}
public static readonly DependencyProperty PlaceholderProperty =
DependencyProperty.Register("Placeholder", typeof(string), typeof(SearchableComboBox), new PropertyMetadata(string.Empty));
/// <summary>
/// Gets or sets the placeholder text.
/// </summary>
public string Placeholder
{
get => (string)GetValue(PlaceholderProperty);
set => SetValue(PlaceholderProperty, value);
}
private void UpdatePlaceholderVisibility()
{
if (_placeholderTextBlock == null) return;
if (SelectedItem == null || SelectedItem.Equals(string.Empty))
{
_placeholderTextBlock.Visibility = Visibility.Visible;
}
else
{
_placeholderTextBlock.Visibility = Visibility.Collapsed;
}
}
#endregion
#region OnItemsSourceChanged event
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
Debug.WriteLineIf(debug, "OnItemsSourceChanged");
base.OnItemsSourceChanged(oldValue, newValue);
if (_collectionViewSource == null)
{
_collectionViewSource = new CollectionViewSource { Source = newValue };
_collectionViewSource.View.Filter = FilterPredicate;
if (_collectionViewSource.View is ListCollectionView lcv && CustomSort != null)
{
lcv.CustomSort = CustomSort;
}
ItemsSource = _collectionViewSource.View;
}
}
#endregion
#region Changes only when selected
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
Debug.WriteLineIf(debug, "OnSelectionChanged");
if (_isDropDownOpen && e.AddedItems.Count == 0 && e.RemovedItems.Count > 0)
{
// If the dropdown is open and the selection was removed due to filtering, revert the selection to the last selected item
SelectedItem = _lastSelectedItem;
return; // Exit early to avoid additional processing and raising events
}
if (e.AddedItems.Count > 0)
{
_lastSelectedItem = e.AddedItems[0];
}
UpdatePlaceholderVisibility();
base.OnSelectionChanged(e);
}
#endregion
#region Error and border visibility
public static readonly DependencyProperty NotFoundLabelVisibilityProperty =
DependencyProperty.Register("NotFoundLabelVisibility", typeof(Visibility), typeof(SearchableComboBox), new PropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty DropDownBorderVisibilityProperty =
DependencyProperty.Register("DropDownBorderVisibility", typeof(Visibility), typeof(SearchableComboBox), new PropertyMetadata(Visibility.Visible));
/// <summary>
/// Gets or sets the visibility of the not found label.
/// </summary>
public Visibility NotFoundLabelVisibility
{
get => (Visibility)GetValue(NotFoundLabelVisibilityProperty);
set => SetValue(NotFoundLabelVisibilityProperty, value);
}
/// <summary>
/// Gets or sets the visibility of the dropdown border.
/// </summary>
public Visibility DropDownBorderVisibility
{
get => (Visibility)GetValue(DropDownBorderVisibilityProperty);
set => SetValue(DropDownBorderVisibilityProperty, value);
}
private void UpdateVisibility()
{
Debug.WriteLineIf(debug, "UpdateVisibility");
if (_collectionViewSource.View.IsEmpty)
{
NotFoundLabelVisibility = Visibility.Visible;
DropDownBorderVisibility = Visibility.Collapsed;
}
else
{
NotFoundLabelVisibility = Visibility.Collapsed;
DropDownBorderVisibility = Visibility.Visible;
}
}
#endregion
#region Filtering logic
public static readonly DependencyProperty SearchTermProperty =
DependencyProperty.Register("SearchTerm", typeof(string), typeof(SearchableComboBox), new PropertyMetadata(string.Empty, OnSearchTermChanged));
/// <summary>
/// Gets or sets the search term.
/// </summary>
public string SearchTerm
{
get => (string)GetValue(SearchTermProperty);
set => SetValue(SearchTermProperty, value);
}
private static void OnSearchTermChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is SearchableComboBox searchableComboBox)
{
searchableComboBox.DebounceFilter();
}
}
private void DebounceFilter()
{
_debounceTimer.Stop();
_debounceTimer.Start();
}
private void DebounceTimer_Tick(object sender, EventArgs e)
{
_debounceTimer.Stop();
_collectionViewSource.View.Refresh();
UpdateVisibility();
}
private bool FilterPredicate(object item)
{
if (string.IsNullOrEmpty(SearchTerm))
{
return true;
}
string strItem = item.ToString();
if (strItem.IndexOf(SearchTerm, StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
return false;
}
protected override void OnDropDownOpened(EventArgs e)
{
Debug.WriteLineIf(debug, "OnDropDownOpened");
_isDropDownOpen = true;
_lastSelectedItem = SelectedItem; // Store the current selected item.
this.Dispatcher.BeginInvoke((Action)(() =>
{
_searchTermTextBox?.Focus();
}), DispatcherPriority.ContextIdle);
base.OnDropDownOpened(e);
}
#endregion
#region Clear Search
private void OnDropDownClosed(object sender, EventArgs e)
{
Debug.WriteLineIf(debug, "OnDropDownClosed");
SearchTerm = string.Empty;
_collectionViewSource.View.Refresh();
if (_lastSelectedItem != null)
{
SelectedItem = _lastSelectedItem;
}
_isDropDownOpen = false;
}
#endregion
}
}