324 lines
10 KiB
C#
324 lines
10 KiB
C#
|
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
|
|||
|
}
|
|||
|
}
|