Creating a functional Double Control - wpf

I have a custom control called DoubleNumericBox that validates and accepts user input like 23,00, 0,9, 23.900,01, 34... etc.
The problem starts when I try to binding something to it. The binding is not reliable enough, some times the control won't display the new value, but if I set the DataContext one more time it will set the value, etc.
So, I must be doing something very wrong with my custom properties and events.
Custom Properties/Events
Value : Double
MinValue : Double
MaxValue : Double
ValueChanged : Event
Expected Behaviour
Validate typed keys: Numbers, Commas and Points (Decimal separator and digit grouping glyph). My culture uses Comma as decimal separator.
Validate the whole text if (return to the latest Value if number not valid):
Text pasted.
Lost Focus.
Validate Min/Max limit.
Accept binding from Text or Value, and validate the binding value.
Code
public class DoubleNumericBox : TextBox
{
Variables:
public readonly static DependencyProperty MinValueProperty;
public readonly static DependencyProperty ValueProperty;
public readonly static DependencyProperty MaxValueProperty;
Properties:
public double MinValue
{
get { return (double)GetValue(MinValueProperty); }
set { SetCurrentValue(MinValueProperty, value); }
}
public double Value
{
get { return (double)GetValue(ValueProperty); }
set
{
SetCurrentValue(ValueProperty, value);
RaiseValueChangedEvent();
}
}
public double MaxValue
{
get { return (double)GetValue(MaxValueProperty); }
set { SetCurrentValue(MaxValueProperty, value); }
}
Event:
public static readonly RoutedEvent ValueChangedEvent;
public event RoutedEventHandler ValueChanged
{
//Provide CLR accessors for the event
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
public void RaiseValueChangedEvent()
{
var newEventArgs = new RoutedEventArgs(ValueChangedEvent);
RaiseEvent(newEventArgs);
}
Constructor/Override:
static DoubleNumericBox()
{
MinValueProperty = DependencyProperty.Register("MinValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D));
ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D, ValueCallback));
MaxValueProperty = DependencyProperty.Register("MaxValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(Double.MaxValue));
ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DoubleNumericBox));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PreviewTextInput += DoubleNumericBox_PreviewTextInput;
ValueChanged += DoubleNumericBox_ValueChanged;
TextChanged += DoubleNumericBox_TextChanged;
LostFocus += DoubleNumericBox_LostFocus;
AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(PastingEvent));
}
Events:
private static void ValueCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBox = d as DoubleNumericBox;
if (textBox == null) return;
//textBox.Text = String.Format("{0:###,###,##0.0###}", textBox.Value);
textBox.RaiseValueChangedEvent();
}
private void DoubleNumericBox_ValueChanged(object sender, RoutedEventArgs e)
{
var textBox = sender as DoubleNumericBox;
if (textBox == null) return;
ValueChanged -= DoubleNumericBox_ValueChanged;
TextChanged -= DoubleNumericBox_TextChanged;
if (Value > MaxValue)
Value = MaxValue;
else if (Value < MinValue)
Value = MinValue;
textBox.Text = Text = String.Format("{0:###,###,##0.0###}", Value);
ValueChanged += DoubleNumericBox_ValueChanged;
TextChanged += DoubleNumericBox_TextChanged;
}
private void DoubleNumericBox_TextChanged(object sender, TextChangedEventArgs e)
{
var textBox = sender as TextBox;
if (textBox == null) return;
if (String.IsNullOrEmpty(textBox.Text)) return;
if (IsTextDisallowed(textBox.Text)) return;
ValueChanged -= DoubleNumericBox_ValueChanged;
var newValue = Convert.ToDouble(textBox.Text);
if (newValue > MaxValue)
Value = MaxValue;
else if (newValue < MinValue)
Value = MinValue;
else
{
Value = newValue;
}
ValueChanged += DoubleNumericBox_ValueChanged;
}
private void DoubleNumericBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (String.IsNullOrEmpty(e.Text))
{
e.Handled = true;
return;
}
//Only Numbers, comma and points.
if (IsEntryDisallowed(sender, e.Text))
{
e.Handled = true;
}
}
private void PastingEvent(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(String)))
{
var text = (String)e.DataObject.GetData(typeof(String));
if (IsTextDisallowed(text))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
private void DoubleNumericBox_LostFocus(object sender, RoutedEventArgs e)
{
TextChanged -= DoubleNumericBox_TextChanged;
Text = String.Format("{0:###,###,##0.0###}", Value);
TextChanged += DoubleNumericBox_TextChanged;
}
Methods:
private bool IsEntryDisallowed(object sender, string text)
{
var regex = new Regex(#"^[0-9]|\.|\,$");
if (regex.IsMatch(text))
{
return !CheckPontuation(sender, text);
}
//Not a number or a Comma/Point.
return true;
}
private bool IsTextDisallowed(string text)
{
var regex = new Regex(#"^((\d+)|(\d{1,3}(\.\d{3})+)|(\d{1,3}(\.\d{3})(\,\d{3})+))((\,\d{4})|(\,\d{3})|(\,\d{2})|(\,\d{1})|(\,))?$");
return !regex.IsMatch(text); //\d+(?:,\d{1,2})?
}
private bool CheckPontuation(object sender, string next)
{
var textBox = sender as TextBox;
if (textBox == null) return true;
if (Char.IsNumber(next.ToCharArray()[0]))
return true;
if (next.Equals("."))
{
var textAux = textBox.Text;
if (!String.IsNullOrEmpty(textBox.SelectedText))
textAux = textAux.Replace(textBox.SelectedText, "");
//Check if the user can add a point mark here.
var before = textAux.Substring(0, textBox.SelectionStart);
var after = textAux.Substring(textBox.SelectionStart);
//If no text, return true.
if (String.IsNullOrEmpty(before) && String.IsNullOrEmpty(after)) return true;
if (!String.IsNullOrEmpty(before))
{
if (before.Contains(',')) return false;
if (after.Contains("."))
{
var split = before.Split('.');
if (split.Last().Length != 3) return false;
}
}
if (!String.IsNullOrEmpty(after))
{
var split = after.Split('.', ',');
if (split.First().Length != 3) return false;
}
return true;
}
//Only one comma.
if (next.Equals(","))
{
return !textBox.Text.Any(x => x.Equals(','));
}
return true;
}
}
Can you guys help me out to make this custom control work better?

So a couple of gotchas I see in your code:
Do not use += / -= to hook up events in WPF controls, it can and will break routed events, use Addhandler / RemoveHandler instead.
I removed the unhooking and rehooking of events and used a member level flag instead for change loop issues. Here is the code I came up with, seem to bind fine to Value field.
A side note, you failed to account for multiple "." entry in your textbox so a user could type 345.34.434.23 which would not be prevented. I know to check this because I wrote a WPF FilterTextBox years ago and this came up in my testing.
public class DoubleNumericBox : TextBox
{
public readonly static DependencyProperty MinValueProperty;
public readonly static DependencyProperty ValueProperty;
public readonly static DependencyProperty MaxValueProperty;
public bool _bIgnoreChange = false;
public double MinValue
{
get { return (double)GetValue(MinValueProperty); }
set { SetCurrentValue(MinValueProperty, value); }
}
public double Value
{
get { return (double)GetValue(ValueProperty); }
set
{
SetCurrentValue(ValueProperty, value);
RaiseValueChangedEvent();
}
}
public double MaxValue
{
get { return (double)GetValue(MaxValueProperty); }
set { SetCurrentValue(MaxValueProperty, value); }
}
public static readonly RoutedEvent ValueChangedEvent;
public event RoutedEventHandler ValueChanged
{
//Provide CLR accessors for the event
add { AddHandler(ValueChangedEvent, value); }
remove { RemoveHandler(ValueChangedEvent, value); }
}
public void RaiseValueChangedEvent()
{
var newEventArgs = new RoutedEventArgs(ValueChangedEvent);
RaiseEvent(newEventArgs);
}
static DoubleNumericBox()
{
MinValueProperty = DependencyProperty.Register("MinValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D));
ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(0D, ValueCallback));
MaxValueProperty = DependencyProperty.Register("MaxValue", typeof(double), typeof(DoubleNumericBox), new FrameworkPropertyMetadata(Double.MaxValue));
ValueChangedEvent = EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DoubleNumericBox));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
AddHandler(TextBox.PreviewTextInputEvent, new TextCompositionEventHandler(DoubleNumericBox_PreviewTextInput));
AddHandler(TextBox.TextChangedEvent, new TextChangedEventHandler(DoubleNumericBox_TextChanged));
AddHandler(TextBox.LostFocusEvent, new RoutedEventHandler(DoubleNumericBox_LostFocus));
AddHandler(DataObject.PastingEvent, new DataObjectPastingEventHandler(PastingEvent));
}
private static void ValueCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var textBox = d as DoubleNumericBox;
if (textBox == null) return;
//textBox.Text = String.Format("{0:###,###,##0.0###}", textBox.Value);
textBox.DoubleNumericBox_ValueChanged();
}
private void DoubleNumericBox_ValueChanged()
{
if (Value > MaxValue)
Value = MaxValue;
else if (Value < MinValue)
Value = MinValue;
if (!_bIgnoreChange)
this.Text = Text = String.Format("{0:###,###,##0.0###}", Value);
}
private void DoubleNumericBox_TextChanged(object sender, TextChangedEventArgs e)
{
var textBox = sender as TextBox;
if (textBox == null) return;
if (String.IsNullOrEmpty(textBox.Text)) return;
if (IsTextDisallowed(textBox.Text)) return;
//ValueChanged -= DoubleNumericBox_ValueChanged;
_bIgnoreChange = true;
Value = Convert.ToDouble(textBox.Text);
//if (newValue > MaxValue)
// Value = MaxValue;
//else if (newValue < MinValue)
// Value = MinValue;
//else
//{
// Value = newValue;
//}
_bIgnoreChange = false;
//ValueChanged += DoubleNumericBox_ValueChanged;
}
private void DoubleNumericBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (String.IsNullOrEmpty(e.Text))
{
e.Handled = true;
return;
}
//Only Numbers, comma and points.
if (IsEntryDisallowed(sender, e.Text))
{
e.Handled = true;
}
}
private void PastingEvent(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(String)))
{
var text = (String)e.DataObject.GetData(typeof(String));
if (IsTextDisallowed(text))
{
e.CancelCommand();
}
}
else
{
e.CancelCommand();
}
}
private void DoubleNumericBox_LostFocus(object sender, RoutedEventArgs e)
{
//TextChanged -= DoubleNumericBox_TextChanged;
Text = String.Format("{0:###,###,##0.0###}", Value);
//TextChanged += DoubleNumericBox_TextChanged;
}
private bool IsEntryDisallowed(object sender, string text)
{
var regex = new Regex(#"^[0-9]|\.|\,$");
if (regex.IsMatch(text))
{
return !CheckPontuation(sender, text);
}
//Not a number or a Comma/Point.
return true;
}
private bool IsTextDisallowed(string text)
{
var regex = new Regex(#"^((\d+)|(\d{1,3}(\.\d{3})+)|(\d{1,3}(\.\d{3})(\,\d{3})+))((\,\d{4})|(\,\d{3})|(\,\d{2})|(\,\d{1})|(\,))?$");
return !regex.IsMatch(text); //\d+(?:,\d{1,2})?
}
private bool CheckPontuation(object sender, string next)
{
var textBox = sender as TextBox;
if (textBox == null) return true;
if (Char.IsNumber(next.ToCharArray()[0]))
return true;
if (next.Equals("."))
{
var textAux = textBox.Text;
if (!String.IsNullOrEmpty(textBox.SelectedText))
textAux = textAux.Replace(textBox.SelectedText, "");
//Check if the user can add a point mark here.
var before = textAux.Substring(0, textBox.SelectionStart);
var after = textAux.Substring(textBox.SelectionStart);
//If no text, return true.
if (String.IsNullOrEmpty(before) && String.IsNullOrEmpty(after)) return true;
if (!String.IsNullOrEmpty(before))
{
if (before.Contains(',')) return false;
if (after.Contains("."))
{
var split = before.Split('.');
if (split.Last().Length != 3) return false;
}
}
if (!String.IsNullOrEmpty(after))
{
var split = after.Split('.', ',');
if (split.First().Length != 3) return false;
}
return true;
}
//Only one comma.
if (next.Equals(","))
{
return !textBox.Text.Any(x => x.Equals(','));
}
return true;
}
}

Related

Keep WPF Calendar open after date selection from DatePicker

I'm attempting to customise the WPF DatePicker and it's calendar popup component to keep the popup open after the user has selected a date. This is due to the extension to the style I have made that allows the user to select a time.
The calendar should only be closed once the user has clicked a 'finished' button in the style.
I've made multiple attempts at doing this, each with varying degrees of success but I have to admit, a fully working solution has eluded me.
Example solution using an override on IsDropDownOpen;
IsDropDownOpenProperty.OverrideMetadata(typeof(CustomDatePicker), new
FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnIsDropDownOpenChanged, OnCoerceIsDropDownOpen));
private static object OnCoerceIsDropDownOpen(DependencyObject d, object baseValue)
{
var instance = d as CustomDatePicker;
if (instance != null)
{
if (instance.CalendarPopup.IsOpen == false)
{
return true;
}
var coalesed = (bool) baseValue;
if (coalesed == false)
{
return instance.PopupStaysOpenAfterSelection;
}
}
return baseValue;
}
'PopupStaysOpenAfterSelection' is a dependency property I have created so that this behaviour is customizable.
Is an example of the popup staying open and closing when the user clicks 'done' however the popup will no longer reopen after interacting with the datepicker button.
Am I missing something obvious with this approach / is there a working solution?
EDIT:
ROOT CONTROL
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using Calendar = System.Windows.Controls.Calendar;
namespace CommonControls.Calendar
{
[TemplatePart(Name = NavigationPreviousButtonPartName, Type = typeof(Button))]
[TemplatePart(Name = NavigationNextButtonPartName, Type = typeof(Button))]
[TemplatePart(Name = NavigationTodayButtonPartName, Type = typeof(Button))]
[TemplatePart(Name = NavigationDoneButtonPartName, Type = typeof(Button))]
[TemplatePart(Name = DatePickerButtonPartName, Type = typeof(Button))]
[TemplatePart(Name = DatePickerTextBoxPartName, Type = typeof(DatePickerTextBox))]
[TemplatePart(Name = CalendarPopupPartName, Type = typeof(Popup))]
[TemplatePart(Name = DatePickerDisplayTextBoxPartName, Type = typeof(TextBlock))]
public class ExtendedDatePicker : DatePicker
{
public const string DatePickerDisplayTextBoxPartName = "PART_DisplayDateTextBox";
public const string DatePickerButtonPartName = "PART_Button";
public const string DatePickerTextBoxPartName = "PART_TextBox";
public const string CalendarPopupPartName = "PART_Popup";
public const string NavigationPreviousButtonPartName = "PART_NavPreviousButton";
public const string NavigationNextButtonPartName = "PART_NavNextButton";
public const string NavigationTodayButtonPartName = "PART_TodayButton";
public const string NavigationDoneButtonPartName = "PART_DoneButton";
public static readonly DependencyProperty BlackoutDatesInPastProperty =
DependencyProperty.Register("BlackoutDatesInPast", typeof(bool), typeof(ExtendedDatePicker), new PropertyMetadata(true, BlackoutDatesInPastPropertyChanged));
public static readonly DependencyProperty ThrowExceptionOnBlackoutDateSelectionProperty =
DependencyProperty.Register("ThrowExceptionOnBlackoutDateSelection", typeof(bool), typeof(ExtendedDatePicker), new PropertyMetadata(false));
public static readonly DependencyProperty DatePickerButtonTemplateProperty =
DependencyProperty.Register("DatePickerButtonTemplate", typeof(ControlTemplate), typeof(ExtendedDatePicker), new PropertyMetadata(null));
public static readonly DependencyProperty CustomDateFormatProperty =
DependencyProperty.Register("CustomDateFormat", typeof(string), typeof(ExtendedDatePicker), new PropertyMetadata("d", CustomDateFormatPropertyChanged));
public static readonly DependencyProperty UseCustomDateFormatProperty =
DependencyProperty.Register("UseCustomDateFormat", typeof(bool), typeof(ExtendedDatePicker), new PropertyMetadata(false, UseCustomDateFormatPropertyChanged));
public static readonly DependencyProperty ShowTodayButtonProperty =
DependencyProperty.Register("ShowTodayButton", typeof(bool), typeof(ExtendedDatePicker), new PropertyMetadata(false, ShowTodayButtonPropertyChanged));
public static readonly DependencyProperty DaysToJumpProperty =
DependencyProperty.Register("DaysToJump", typeof(int), typeof(ExtendedDatePicker), new PropertyMetadata(1));
public static readonly DependencyProperty MonthsToJumpProperty =
DependencyProperty.Register("MonthsToJump", typeof(int), typeof(ExtendedDatePicker), new PropertyMetadata(1));
public static readonly DependencyProperty PopupStaysOpenAfterSelectionProperty =
DependencyProperty.Register("PopupStaysOpenAfterSelection", typeof(bool),
typeof(ExtendedDatePicker), new PropertyMetadata(null));
protected DatePickerTextBox DatePickerTextBox;
protected Calendar InternalCalendar;
protected Popup CalendarPopup;
protected bool PendingClose = false;
private Button _navigationPreviousButton;
private Button _navigationNextButton;
private Button _navigationTodayButton;
private Button _navigationDoneButton;
private Button _datePickerButton;
private TextBlock _displayDateTextBlock;
static ExtendedDatePicker()
{
IsDropDownOpenProperty.OverrideMetadata(typeof(ExtendedDatePicker), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ExtendedOnIsDropDownOpenChanged, OnCoerceIsDropDownOpen));
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedDatePicker), new FrameworkPropertyMetadata(typeof(ExtendedDatePicker)));
}
private static object OnCoerceIsDropDownOpen(DependencyObject d, object basevalue)
{
var instance = d as ExtendedDatePicker;
if (!instance.IsEnabled)
{
return false;
}
if (!instance.PopupStaysOpenAfterSelection)
{
return basevalue;
}
if (instance.PendingClose)
{
instance.PendingClose = false;
return false;
}
return true;
}
/**
* OLD
private static object OnCoerceIsDropDownOpen(DependencyObject d, object baseValue)
{
var instance = d as ExtendedDatePicker;
if (instance != null)
{
if (instance.IsDropDownOpen == false)
{
return true;
}
var coalesed = (bool) baseValue;
if (coalesed == false)
{
return instance.PopupStaysOpenAfterSelection;
}
}
return baseValue;
}
*/
private static void ExtendedOnIsDropDownOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var instance = (ExtendedDatePicker) d;
instance.CoerceValue(IsDropDownOpenProperty);
}
public bool BlackoutDatesInPast
{
get { return (bool)GetValue(BlackoutDatesInPastProperty); }
set { SetValue(BlackoutDatesInPastProperty, value); }
}
public bool ThrowExceptionOnBlackoutDateSelection
{
get { return (bool)GetValue(ThrowExceptionOnBlackoutDateSelectionProperty); }
set { SetValue(ThrowExceptionOnBlackoutDateSelectionProperty, value); }
}
public ControlTemplate DatePickerButtonTemplate
{
get { return (ControlTemplate)GetValue(DatePickerButtonTemplateProperty); }
set { SetValue(DatePickerButtonTemplateProperty, value); }
}
public string CustomDateFormat
{
get { return (string)GetValue(CustomDateFormatProperty); }
set { SetValue(CustomDateFormatProperty, value); }
}
public bool UseCustomDateFormat
{
get { return (bool)GetValue(UseCustomDateFormatProperty); }
set { SetValue(UseCustomDateFormatProperty, value); }
}
public bool PopupStaysOpenAfterSelection
{
get { return (bool) GetValue(PopupStaysOpenAfterSelectionProperty); }
set { SetValue(PopupStaysOpenAfterSelectionProperty, value);}
}
public bool ShowTodayButton
{
get { return (bool)GetValue(ShowTodayButtonProperty); }
set { SetValue(ShowTodayButtonProperty, value); }
}
public int DaysToJump
{
get { return (int)GetValue(DaysToJumpProperty); }
set { SetValue(DaysToJumpProperty, value); }
}
public int MonthsToJump
{
get { return (int)GetValue(MonthsToJumpProperty); }
set { SetValue(MonthsToJumpProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_datePickerButton = GetTemplateChild(DatePickerButtonPartName) as Button;
_datePickerButton.CreateBinding(TemplateProperty, this, nameof(DatePickerButtonTemplate));
DatePickerTextBox = GetTemplateChild(DatePickerTextBoxPartName) as DatePickerTextBox;
_displayDateTextBlock = GetTemplateChild(DatePickerDisplayTextBoxPartName) as TextBlock;
CalendarPopup = GetTemplateChild(CalendarPopupPartName) as Popup;
InternalCalendar = CalendarPopup.Child as Calendar;
InternalCalendar.ApplyTemplate();
CalendarPopup.Loaded += CalendarPopup_Loaded;
_navigationPreviousButton = GetTemplateChild(NavigationPreviousButtonPartName) as Button;
_navigationNextButton = GetTemplateChild(NavigationNextButtonPartName) as Button;
_navigationTodayButton = InternalCalendar.Template.FindName(NavigationTodayButtonPartName, InternalCalendar) as Button;
_navigationDoneButton = InternalCalendar.Template.FindName(NavigationDoneButtonPartName, InternalCalendar) as Button;
if (_navigationTodayButton != null)
{
_navigationTodayButton.Click += TodayButton_Click;
}
if (_navigationPreviousButton != null)
{
_navigationPreviousButton.Click += NavPreviousButton_Click;
}
if (_navigationNextButton != null)
{
_navigationNextButton.Click += NavNextButton_Click;
}
if (_navigationDoneButton != null)
{
_navigationDoneButton.Click += NavDoneButton_Click;
}
RefreshBlackOutDates();
RefreshDisplayDate();
RefreshTextControlsVisibility();
RefreshTodayButtonVisibility();
}
private void CalendarPopup_Loaded(object sender, RoutedEventArgs e)
{
}
protected override void OnSelectedDateChanged(SelectionChangedEventArgs e)
{
base.OnSelectedDateChanged(e);
RefreshDisplayDate();
}
private static void BlackoutDatesInPastPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var source = (ExtendedDatePicker)d;
source.RefreshBlackOutDates();
}
private static void CustomDateFormatPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var source = (ExtendedDatePicker)d;
source.RefreshDisplayDate();
}
private static void UseCustomDateFormatPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var source = (ExtendedDatePicker)d;
source.RefreshTextControlsVisibility();
source.RefreshDisplayDate();
}
private static void ShowTodayButtonPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var source = (ExtendedDatePicker)d;
source.RefreshTodayButtonVisibility();
}
private void RefreshTextControlsVisibility()
{
if (DatePickerTextBox != null
&& _displayDateTextBlock != null)
{
DatePickerTextBox.Visibility = UseCustomDateFormat ? Visibility.Collapsed : Visibility.Visible;
_displayDateTextBlock.Visibility = !UseCustomDateFormat ? Visibility.Collapsed : Visibility.Visible;
}
}
private void RefreshTodayButtonVisibility()
{
if (_navigationTodayButton != null)
{
_navigationTodayButton.Visibility = ShowTodayButton ? Visibility.Visible : Visibility.Collapsed;
}
}
private void RefreshDisplayDate()
{
if (_displayDateTextBlock != null)
{
if (UseCustomDateFormat
&& !string.IsNullOrEmpty(CustomDateFormat))
{
if (SelectedDate.HasValue)
{
var d = SelectedDate.Value;
_displayDateTextBlock.Text = string.Format(CultureInfo.CurrentCulture, d.ToString(CustomDateFormat));
}
else
{
_displayDateTextBlock.Text = "Select a date";
}
}
}
}
private void RefreshBlackOutDates()
{
if (BlackoutDatesInPast)
{
BlackoutDates.AddDatesInPast();
}
}
private void GoToToday()
{
var newDate = DateTime.Today;
GoToDate(newDate);
}
private void GoToNextDate()
{
var selectedDate = SelectedDate;
if (selectedDate.HasValue)
{
var newDate = selectedDate.Value;
if (DaysToJump > 0)
{
newDate = newDate.AddDays(DaysToJump);
}
if (MonthsToJump > 0)
{
newDate = newDate.AddMonths(MonthsToJump);
}
GoToDate(newDate);
}
}
private void GoToPreviousDate()
{
var selectedDate = SelectedDate;
if (selectedDate.HasValue)
{
var newDate = selectedDate.Value;
if (DaysToJump > 0)
{
newDate = newDate.AddDays(DaysToJump * -1);
}
if (MonthsToJump > 0)
{
newDate = newDate.AddMonths(MonthsToJump * -1);
}
GoToDate(newDate);
}
}
private void GoToDate(DateTime date)
{
if (!BlackoutDates.Contains(date))
{
SelectedDate = date;
}
}
private void NavNextButton_Click(object sender, RoutedEventArgs e)
{
GoToNextDate();
}
private void NavPreviousButton_Click(object sender, RoutedEventArgs e)
{
GoToPreviousDate();
}
private void TodayButton_Click(object sender, RoutedEventArgs e)
{
GoToToday();
}
private void NavDoneButton_Click(object sender, RoutedEventArgs e)
{
IsDropDownOpen = false;
}
}
}
WITH TIME
public class CustomDatePickerWithTime : ExtendedDatePicker
{
public static readonly DependencyProperty MinuteIntervalProperty = DependencyProperty.Register(
"MinuteInterval", typeof(int), typeof(ExtendedDatePickerWithTime), new FrameworkPropertyMetadata(30));
public static readonly DependencyPropertyKey AvailableTimesPropertyKey = DependencyProperty.RegisterReadOnly(
"AvailableTimes", typeof(List<TimeSpan>), typeof(ExtendedDatePickerWithTime),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));
public static readonly DependencyProperty AvailableTimesProperty = AvailableTimesPropertyKey.DependencyProperty;
public static readonly DependencyProperty SelectedTimeProperty = DependencyProperty.Register(
"SelectedTime", typeof(TimeSpan), typeof(ExtendedDatePickerWithTime), new PropertyMetadata(default(TimeSpan), OnSelectedTimePropertyChanged));
static ExtendedDatePickerWithTime()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ExtendedDatePickerWithTime),
new FrameworkPropertyMetadata(typeof(ExtendedDatePickerWithTime)));
}
public int MinuteInterval
{
get { return (int) GetValue(MinuteIntervalProperty); }
set { SetValue(MinuteIntervalProperty, value);}
}
public List<TimeSpan> AvailableTimes
{
get { return (List<TimeSpan>) GetValue(AvailableTimesProperty); }
protected set { SetValue(AvailableTimesPropertyKey, value);}
}
public TimeSpan SelectedTime
{
get { return (TimeSpan) GetValue(SelectedTimeProperty); }
set { SetValue(SelectedTimeProperty, value);}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
CheckIntervalConstraintsAndConstruct();
OverrideTextBoxEntryStyle();
}
private void OverrideTextBoxEntryStyle()
{
}
private static void OnSelectedTimePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var instance = (ExtendedDatePickerWithTime) d;
instance.SelectedDate = instance.SelectedDate?.Date.Add(instance.SelectedTime) ?? DateTime.Today.Add(instance.SelectedTime);
}
private void CheckIntervalConstraintsAndConstruct()
{
if (60 % MinuteInterval != 0)
{
throw new ArgumentOutOfRangeException(nameof(MinuteInterval), #"The supplied interval in minutes must be divisible by 60");
}
var times = new List<TimeSpan>();
var starting = DateTime.Today;
var ending = DateTime.Today.AddDays(1);
for (var ts = starting; ts <= ending.AddMinutes(MinuteInterval *-1); ts = ts.AddMinutes(MinuteInterval))
{
times.Add(ts.TimeOfDay);
}
AvailableTimes = times;
}
}
XAML: (length exceeds allowed body)
https://pastebin.com/W9KrtM8E

Editable ComboBox behavior on arrow navigation in drop down

The default behavior for non-editable Combobox when you navigate through drop down list with Up and Down keys is, that the current item is highlighted but not selected. Only on Enter Key the Item gets selected.
If you set IsEditable="True" then the behavior is different. Currently selected item (and or Text input) changes by keyboard navigation in the drop down.
My problem with this is, that I'm filtering the items depending on text input. And when you select, you have one exact match and items count goes to one.
So it's not possible to select a correct item with a keyboard.
Inspired by blog post below (Thank you Diederik Krols) I'm finaly found a solution for my problem.
http://dotbay.blogspot.de/2009/04/building-filtered-combobox-for-wpf.html
It needed some extra work, but with a little bit Reflection and Binding suspendig, I have now combobox behavior like expected.
Here is my code
public enum FilterMode
{
Contains,
StartsWith
}
public class FilteredComboBoxBehavior : ManagedBehaviorBase<ComboBox>
{
private ICollectionView currentView;
private string currentFilter;
private Binding textBinding;
private TextBox textBox;
private PropertyInfo HighlightedInfoPropetyInfo { get; set; }
public static readonly DependencyProperty FilterModeProperty = DependencyProperty.Register("FilterMode", typeof(FilterMode), typeof(FilteredComboBoxBehavior), new PropertyMetadata(default(FilterMode)));
public FilterMode FilterMode
{
get
{
return (FilterMode)this.GetValue(FilterModeProperty);
}
set
{
this.SetValue(FilterModeProperty, value);
}
}
public static readonly DependencyProperty OpenDropDownOnFocusProperty = DependencyProperty.Register("OpenDropDownOnFocus", typeof(bool), typeof(FilteredComboBoxBehavior), new PropertyMetadata(true));
public bool OpenDropDownOnFocus
{
get
{
return (bool)this.GetValue(OpenDropDownOnFocusProperty);
}
set
{
this.SetValue(OpenDropDownOnFocusProperty, value);
}
}
protected override void OnSetup()
{
base.OnSetup();
this.AssociatedObject.KeyUp += this.AssociatedObjectOnKeyUp;
this.AssociatedObject.IsKeyboardFocusWithinChanged += this.OnIsKeyboardFocusWithinChanged;
this.textBox = this.AssociatedObject.FindChild<TextBox>();
this.textBinding = BindingOperations.GetBinding(this.AssociatedObject, ComboBox.TextProperty);
this.HighlightedInfoPropetyInfo = typeof(ComboBox).GetProperty(
"HighlightedInfo",
BindingFlags.Instance | BindingFlags.NonPublic);
var pd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(ComboBox));
pd.AddValueChanged(this.AssociatedObject, this.OnItemsSourceChanged);
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.KeyUp -= this.AssociatedObjectOnKeyUp;
if (this.currentView != null)
{
// ReSharper disable once DelegateSubtraction
this.currentView.Filter -= this.TextInputFilter;
}
BindingOperations.ClearAllBindings(this);
}
private void OnItemsSourceChanged(object sender, EventArgs eventArgs)
{
this.currentFilter = this.AssociatedObject.Text;
if (this.currentView != null)
{
// ReSharper disable once DelegateSubtraction
this.currentView.Filter -= this.TextInputFilter;
}
if (this.AssociatedObject.ItemsSource != null)
{
this.currentView = CollectionViewSource.GetDefaultView(this.AssociatedObject.ItemsSource);
this.currentView.Filter += this.TextInputFilter;
}
this.Refresh();
}
private void OnIsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
if (this.AssociatedObject.IsKeyboardFocusWithin)
{
this.AssociatedObject.IsDropDownOpen = this.AssociatedObject.IsDropDownOpen || this.OpenDropDownOnFocus;
}
else
{
this.AssociatedObject.IsDropDownOpen = false;
this.currentFilter = this.AssociatedObject.Text;
this.Refresh();
}
}
private void AssociatedObjectOnKeyUp(object sender, KeyEventArgs keyEventArgs)
{
if (!this.IsTextManipulationKey(keyEventArgs)
|| (Keyboard.Modifiers.HasAnyFlag() && Keyboard.Modifiers != ModifierKeys.Shift)
)
{
return;
}
if (this.currentFilter != this.AssociatedObject.Text)
{
this.currentFilter = this.AssociatedObject.Text;
this.Refresh();
}
}
private bool TextInputFilter(object obj)
{
var stringValue = obj as string;
if (obj != null && !(obj is string))
{
var path = (string)this.GetValue(TextSearch.TextPathProperty);
if (path != null)
{
stringValue = obj.GetType().GetProperty(path).GetValue(obj) as string;
}
}
if (stringValue == null)
return false;
switch (this.FilterMode)
{
case FilterMode.Contains:
return stringValue.IndexOf(this.currentFilter, StringComparison.OrdinalIgnoreCase) >= 0;
case FilterMode.StartsWith:
return stringValue.StartsWith(this.currentFilter, StringComparison.OrdinalIgnoreCase);
default:
throw new ArgumentOutOfRangeException();
}
}
private bool IsTextManipulationKey(KeyEventArgs keyEventArgs)
{
return keyEventArgs.Key == Key.Back
|| keyEventArgs.Key == Key.Space
|| (keyEventArgs.Key >= Key.D0 && keyEventArgs.Key <= Key.Z)
|| (Keyboard.IsKeyToggled(Key.NumLock) && keyEventArgs.Key >= Key.NumPad0 && keyEventArgs.Key <= Key.NumPad9)
|| (keyEventArgs.Key >= Key.Multiply && keyEventArgs.Key <= Key.Divide)
|| (keyEventArgs.Key >= Key.Oem1 && keyEventArgs.Key <= Key.OemBackslash);
}
private void Refresh()
{
if (this.currentView != null)
{
var tempCurrentFilter = this.AssociatedObject.Text;
using (new SuspendBinding(this.textBinding, this.AssociatedObject, ComboBox.TextProperty))
{
this.currentView.Refresh();
//reset internal highlighted info
this.HighlightedInfoPropetyInfo.SetValue(this.AssociatedObject, null);
this.AssociatedObject.SelectedIndex = -1;
this.AssociatedObject.Text = tempCurrentFilter;
}
if (this.textBox != null && tempCurrentFilter != null)
{
this.textBox.SelectionStart = tempCurrentFilter.Length;
this.textBox.SelectionLength = 0;
}
}
}
}
/// <summary>
/// Temporarely suspend binding on dependency property
/// </summary>
public class SuspendBinding : IDisposable
{
private readonly Binding bindingToSuspend;
private readonly DependencyObject target;
private readonly DependencyProperty property;
public SuspendBinding(Binding bindingToSuspend, DependencyObject target, DependencyProperty property)
{
this.bindingToSuspend = bindingToSuspend;
this.target = target;
this.property = property;
BindingOperations.ClearBinding(target, property);
}
public void Dispose()
{
BindingOperations.SetBinding(this.target, this.property, this.bindingToSuspend);
}
}
public abstract class ManagedBehaviorBase<T> : Behavior<T> where T : FrameworkElement
{
private bool isSetup;
private bool isHookedUp;
private WeakReference weakTarget;
protected virtual void OnSetup() { }
protected virtual void OnCleanup() { }
protected override void OnChanged()
{
var target = this.AssociatedObject;
if (target != null)
{
this.HookupBehavior(target);
}
else
{
this.UnHookupBehavior();
}
}
private void OnTargetLoaded(object sender, RoutedEventArgs e) { this.SetupBehavior(); }
private void OnTargetUnloaded(object sender, RoutedEventArgs e) { this.CleanupBehavior(); }
private void HookupBehavior(T target)
{
if (this.isHookedUp) return;
this.weakTarget = new WeakReference(target);
this.isHookedUp = true;
target.Unloaded += this.OnTargetUnloaded;
target.Loaded += this.OnTargetLoaded;
if (target.IsLoaded)
{
this.SetupBehavior();
}
}
private void UnHookupBehavior()
{
if (!this.isHookedUp) return;
this.isHookedUp = false;
var target = this.AssociatedObject ?? (T)this.weakTarget.Target;
if (target != null)
{
target.Unloaded -= this.OnTargetUnloaded;
target.Loaded -= this.OnTargetLoaded;
}
this.CleanupBehavior();
}
private void SetupBehavior()
{
if (this.isSetup) return;
this.isSetup = true;
this.OnSetup();
}
private void CleanupBehavior()
{
if (!this.isSetup) return;
this.isSetup = false;
this.OnCleanup();
}
}
XAML
<ComboBox IsEditable="True"
Text="{Binding Path=ZipCode, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
ItemsSource="{Binding Path=PostalCodes}"
IsTextSearchEnabled="False"
behaviors:AttachedMaxLength.ChildTextBoxMaxLength="{Binding Path=ZipCodeMaxLength}">
<i:Interaction.Behaviors>
<behaviors:FilteredComboBoxBehavior FilterMode="StartsWith"/>
</i:Interaction.Behaviors>

How to get a WPF datagrid column resize event?

I want to dynamically load and store the Datagrid column widths from my ini file. Write to my inifile for every resize of the column width. Which event can i ues for this. Could any body give any Suggetions or sample code for this.
i use apllicationsettings within a behavior for such things and save the information on application exit.
usage
<DataGrid>
<i:Interaction.Behaviors>
<local:DataGridBehavior GridSettings="{Binding Source={x:Static local:MySettings.Instance},Mode=OneWay}" />
</i:Interaction.Behaviors>
</DataGrid>
settings
[SettingsManageabilityAttribute(SettingsManageability.Roaming)]
public sealed class MySettings: ApplicationSettingsBase, IGridSettings
{
private static readonly Lazy<MySettings> LazyInstance = new Lazy<MySettings>(() => new KadiaSettings());
public static MySettingsInstance { get { return LazyInstance.Value; } }
[UserScopedSettingAttribute()]
[DefaultSettingValue("")]
[SettingsSerializeAs(SettingsSerializeAs.Xml)]
public SerializableDictionary<string, int> GridDisplayIndexList
{
get { return (SerializableDictionary<string, int>)this["GridDisplayIndexList"]; }
set { this["GridDisplayIndexList"] = value; }
}
[UserScopedSettingAttribute()]
[DefaultSettingValue("")]
[SettingsSerializeAs(SettingsSerializeAs.Xml)]
public SerializableDictionary<string, Visibility> GridColumnVisibilityList
{
get { return (SerializableDictionary<string, Visibility>)this["GridColumnVisibilityList"]; }
set { this["GridColumnVisibilityList"] = value; }
}
[UserScopedSettingAttribute()]
[DefaultSettingValue("")]
[SettingsSerializeAs(SettingsSerializeAs.Xml)]
public SerializableDictionary<string, double> GridColumnWidthList
{
get { return (SerializableDictionary<string, double>)this["GridColumnWidthList"]; }
set { this["GridColumnWidthList"] = value; }
}
private MySettings()
{
Application.Current.Exit += OnExit;
}
private void OnExit(object sender, ExitEventArgs e)
{
this.Save();
}
}
public interface IGridSettings: INotifyPropertyChanged
{
SerializableDictionary<string, int> GridDisplayIndexList { get; }
SerializableDictionary<string, Visibility> GridColumnVisibilityList { get; }
SerializableDictionary<string, double> GridColumnWidthList { get; }
}
[XmlRoot("Dictionary")]
public class SerializableDictionary<TKey, TValue>: Dictionary<TKey, TValue>, IXmlSerializable
{
#region IXmlSerializable Members
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
var keySerializer = new XmlSerializer(typeof(TKey));
var valueSerializer = new XmlSerializer(typeof(TValue));
bool wasEmpty = reader.IsEmptyElement;
reader.Read();
if (wasEmpty)
return;
while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
{
reader.ReadStartElement("item");
reader.ReadStartElement("key");
var key = (TKey)keySerializer.Deserialize(reader);
reader.ReadEndElement();
reader.ReadStartElement("value");
var value = (TValue)valueSerializer.Deserialize(reader);
reader.ReadEndElement();
this.Add(key, value);
reader.ReadEndElement();
reader.MoveToContent();
}
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
var keySerializer = new XmlSerializer(typeof(TKey));
var valueSerializer = new XmlSerializer(typeof(TValue));
foreach (TKey key in this.Keys)
{
writer.WriteStartElement("item");
writer.WriteStartElement("key");
keySerializer.Serialize(writer, key);
writer.WriteEndElement();
writer.WriteStartElement("value");
TValue value = this[key];
valueSerializer.Serialize(writer, value);
writer.WriteEndElement();
writer.WriteEndElement();
}
}
#endregion
}
behavior
public class DataGridBehavior : Behavior<DataGrid>
{
public static readonly DependencyProperty GridSettingsProperty =
DependencyProperty.Register("GridSettings", typeof(IGridSettings), typeof(DataGridBehavior), null);
public IGridSettings GridSettings
{
get { return (IGridSettings)GetValue(GridSettingsProperty); }
set { SetValue(GridSettingsProperty, value); }
}
public DataGridICollectionViewSortMerkerBehavior()
{
Application.Current.Exit += CurrentExit;
}
private void CurrentExit(object sender, ExitEventArgs e)
{
SetSettings();
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += AssociatedObjectLoaded;
AssociatedObject.Unloaded += AssociatedObjectUnloaded;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Loaded -= AssociatedObjectLoaded;
AssociatedObject.Unloaded -= AssociatedObjectUnloaded;
}
private void AssociatedObjectUnloaded(object sender, RoutedEventArgs e)
{
SetSettings();
}
void AssociatedObjectLoaded(object sender, RoutedEventArgs e)
{
var settings = GridSettings;
var columns = AssociatedObject.Columns.ToList();
var colCount = columns.Count;
foreach (var column in columns)
{
var key = column.Header.ToString();
if (settings.GridDisplayIndexList.ContainsKey(key))
{
//manchmal wird -1 als index abgespeichert
var index = settings.GridDisplayIndexList[key];
if(index > 0 && index < colCount)
column.DisplayIndex = index;
}
if (settings.GridColumnVisibilityList.ContainsKey(key))
{
column.Visibility = settings.GridColumnVisibilityList[key];
}
if (settings.GridColumnWidthList.ContainsKey(key))
{
column.Width = new DataGridLength(settings.GridColumnWidthList[key]);
}
}
}
private void SetSettings()
{
var settings = GridSettings;
foreach (var column in AssociatedObject.Columns)
{
var key = column.Header.ToString();
var displayindex = column.DisplayIndex;
var visibility = column.Visibility;
var width = column.ActualWidth;
if (settings.GridDisplayIndexList.ContainsKey(key))
{
settings.GridDisplayIndexList[key] = displayindex;
}
else
{
settings.GridDisplayIndexList.Add(key, displayindex);
}
if (settings.GridColumnVisibilityList.ContainsKey(key))
{
settings.GridColumnVisibilityList[key] = visibility;
}
else
{
settings.GridColumnVisibilityList.Add(key, visibility);
}
if (settings.GridColumnWidthList.ContainsKey(key))
{
settings.GridColumnWidthList[key] = width;
}
else
{
settings.GridColumnWidthList.Add(key, width);
}
}
}
}

Allow Numbers and decimal in a text box using WPF at KeyDown Event

As I'm new to the WPF development.
I want to know how to handle the Numeric Text Box.
As in old Windows Development Application i can handle the above mentioned scenario
in the Key Press Event but in WPF i won't have this Event.
So i have to handle this scenario in the Key Down Event.
But it was little Complicated can i know how to handle this one.
Condition :
Should Allow only Numeric and a single Decimal Point.
Should allow only 2 characters(Numbers) after the decimal point.
You have to create an extended textbox by inheriting from native TextBox. And override the OnTextInput method. Handle the input as you required. The following snippet will allow only numbers and not characters. Same way you can validate for your other needs.
protected override void OnTextInput(TextCompositionEventArgs e)
{
string text = e.Text.ToString();
double output = 0.0;
bool isnumber = Double.TryParse(text, out output);
if (!isnumber)
{
e.Handled = true;
}
base.OnTextInput(e);
}
you can use a behavior for this stuff
public class TextBoxInputBehavior : Behavior<TextBox>
{
const NumberStyles validNumberStyles = NumberStyles.AllowDecimalPoint |
NumberStyles.AllowThousands |
NumberStyles.AllowLeadingSign;
public TextBoxInputBehavior()
{
this.InputMode = TextBoxInputMode.None;
}
public TextBoxInputMode InputMode { get; set; }
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.PreviewTextInput += AssociatedObjectPreviewTextInput;
AssociatedObject.PreviewKeyDown += AssociatedObjectPreviewKeyDown;
DataObject.AddPastingHandler(AssociatedObject, Pasting);
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.PreviewTextInput -= AssociatedObjectPreviewTextInput;
AssociatedObject.PreviewKeyDown -= AssociatedObjectPreviewKeyDown;
DataObject.RemovePastingHandler(AssociatedObject, Pasting);
}
private void Pasting(object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(typeof(string)))
{
var pastedText = (string)e.DataObject.GetData(typeof(string));
if (!this.IsValidInput(this.GetText(pastedText)))
{
System.Media.SystemSounds.Beep.Play();
e.CancelCommand();
}
}
else
{
System.Media.SystemSounds.Beep.Play();
e.CancelCommand();
}
}
private void AssociatedObjectPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
if (!this.IsValidInput(this.GetText(" ")))
{
System.Media.SystemSounds.Beep.Play();
e.Handled = true;
}
}
}
private void AssociatedObjectPreviewTextInput(object sender, TextCompositionEventArgs e)
{
if (!this.IsValidInput(this.GetText(e.Text)))
{
System.Media.SystemSounds.Beep.Play();
e.Handled = true;
}
}
private string GetText(string input)
{
var txt = this.AssociatedObject;
var realtext = txt.Text.Remove(txt.SelectionStart, txt.SelectionLength);
var newtext = realtext.Insert(txt.CaretIndex, input);
return newtext;
}
private bool IsValidInput(string input)
{
switch (InputMode)
{
case TextBoxInputMode.None:
return true;
case TextBoxInputMode.DigitInput:
return CheckIsDigit(input);
case TextBoxInputMode.DecimalInput:
//minus einmal am anfang zulässig
if (input.Contains("-"))
if (input.IndexOf("-") == 0 && input.LastIndexOf("-")==0)
return true;
else
return false;
//wen mehr als ein Komma
if (input.ToCharArray().Where(x => x == ',').Count() > 1)
return false;
decimal d;
return decimal.TryParse(input,validNumberStyles,CultureInfo.CurrentCulture, out d);
default: throw new ArgumentException("Unknown TextBoxInputMode");
}
return true;
}
private bool CheckIsDigit(string wert)
{
return wert.ToCharArray().All(Char.IsDigit);
}
}
public enum TextBoxInputMode
{
None,
DecimalInput,
DigitInput
}
xaml
<TextBox Text="{Binding MyDecimalProperty}">
<i:Interaction.Behaviors>
<Behaviors:TextBoxInputBehavior InputMode="DecimalInput"/>
</i:Interaction.Behaviors>
</TextBox>
//It's not satisfy second condition
TextBox txtDecimal = sender as TextBox;
if ((e.Key == Key.OemPeriod || e.Key == Key.Decimal) && (txtDecimal.Text.Contains(".") == false))
{ e.Handled = false;}
else if ((e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9) || (e.Key == Key.Back))
{ e.Handled = false; }
else if((e.Key >= Key.D0 && e.Key <= Key.D9))
{ e.Handled = false; }
else
{ e.Handled = true; }

Dynamic filter of WPF combobox based on text input

I cant seem to find a direct method for implementing filtering of text input into a list of items in a WPF combobox.
By setting IsTextSearchEnabled to true, the comboBox dropdown will jump to whatever the first matching item is. What I need is for the list to be filtered to whatever matches the text string (e.g. If I focus on my combobox and type 'abc', I'd like to see all the items in the ItemsSource collection that start with (or contain preferably) 'abc' as the members of the dropdown list).
I doubt that it makes a difference but my display item is templated to a property of a complex type :
<ComboBox x:Name="DiagnosisComboBox" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3"
ItemsSource="{Binding Path = ApacheDxList,
UpdateSourceTrigger=PropertyChanged,
Mode=OneWay}"
IsTextSearchEnabled="True"
ItemTemplate="{StaticResource DxDescriptionTemplate}"
SelectedValue="{Binding Path = SelectedEncounterDetails.Diagnosis,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>
Thanks.
I just did this a few days ago using a modified version of the code from this site: Credit where credit is due
My full code listed below:
using System.Collections;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace MyControls
{
public class FilteredComboBox : ComboBox
{
private string oldFilter = string.Empty;
private string currentFilter = string.Empty;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null)
{
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Tab:
case Key.Enter:
IsDropDownOpen = false;
break;
case Key.Escape:
IsDropDownOpen = false;
SelectedIndex = -1;
Text = currentFilter;
break;
default:
if (e.Key == Key.Down) IsDropDownOpen = true;
base.OnPreviewKeyDown(e);
break;
}
// Cache text
oldFilter = Text;
}
protected override void OnKeyUp(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
case Key.Down:
break;
case Key.Tab:
case Key.Enter:
ClearFilter();
break;
default:
if (Text != oldFilter)
{
RefreshFilter();
IsDropDownOpen = true;
EditableTextBox.SelectionStart = int.MaxValue;
}
base.OnKeyUp(e);
currentFilter = Text;
break;
}
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
ClearFilter();
var temp = SelectedIndex;
SelectedIndex = -1;
Text = string.Empty;
SelectedIndex = temp;
base.OnPreviewLostKeyboardFocus(e);
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
view.Refresh();
}
private void ClearFilter()
{
currentFilter = string.Empty;
RefreshFilter();
}
private bool FilterItem(object value)
{
if (value == null) return false;
if (Text.Length == 0) return true;
return value.ToString().ToLower().Contains(Text.ToLower());
}
}
}
And the WPF should be something like so:
<MyControls:FilteredComboBox ItemsSource="{Binding MyItemsSource}"
SelectedItem="{Binding MySelectedItem}"
DisplayMemberPath="Name"
IsEditable="True"
IsTextSearchEnabled="False"
StaysOpenOnEdit="True">
<MyControls:FilteredComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel VirtualizationMode="Recycling" />
</ItemsPanelTemplate>
</MyControls:FilteredComboBox.ItemsPanel>
</MyControls:FilteredComboBox>
A few things to note here. You will notice the FilterItem implementation does a ToString() on the object. This means the property of your object you want to display should be returned in your object.ToString() implementation. (or be a string already) In other words something like so:
public class Customer
{
public string Name { get; set; }
public string Address { get; set; }
public string PhoneNumber { get; set; }
public override string ToString()
{
return Name;
}
}
If this does not work for your needs I suppose you could get the value of DisplayMemberPath and use reflection to get the property to use it, but that would be slower so I wouldn't recommend doing that unless necessary.
Also this implementation does NOT stop the user from typing whatever they like in the TextBox portion of the ComboBox. If they type something stupid there the SelectedItem will revert to NULL, so be prepared to handle that in your code.
Also if you have many items I would highly recommend using the VirtualizingStackPanel like my example above as it makes quite a difference in loading time
Based on this answer, I added:
The ability to limit user input to the values provided in the InputSource using OnlyValuesInList property.
Handling Esc key to clear filter
Handling Down arrow key to open the ComboBox.
Handling Backspace key does not clear selection, only filter text.
Hid auxiliar classes and methods
Deleted unnecessary methods
Added SelectionEffectivelyChanged event that only fires when the user leaves the control or presses Enter, as in the process of filtering the SelectionChanged eventfrom the standard ComboBox fires several times.
Added EffectivelySelectedItem property that only changes when the user leaves the control or presses Enter, as in the process of filtering the SelectedItem item from the standard ComboBox changes several times.
public class FilterableComboBox : ComboBox
{
/// <summary>
/// If true, on lost focus or enter key pressed, checks the text in the combobox. If the text is not present
/// in the list, it leaves it blank.
/// </summary>
public bool OnlyValuesInList {
get => (bool)GetValue(OnlyValuesInListProperty);
set => SetValue(OnlyValuesInListProperty, value);
}
public static readonly DependencyProperty OnlyValuesInListProperty =
DependencyProperty.Register(nameof(OnlyValuesInList), typeof(bool), typeof(FilterableComboBox));
/// <summary>
/// Selected item, changes only on lost focus or enter key pressed
/// </summary>
public object EffectivelySelectedItem {
get => (bool)GetValue(EffectivelySelectedItemProperty);
set => SetValue(EffectivelySelectedItemProperty, value);
}
public static readonly DependencyProperty EffectivelySelectedItemProperty =
DependencyProperty.Register(nameof(EffectivelySelectedItem), typeof(object), typeof(FilterableComboBox));
private string CurrentFilter = string.Empty;
private bool TextBoxFreezed;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
private UserChange<bool> IsDropDownOpenUC;
/// <summary>
/// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed.
/// </summary>
public event Action<FilterableComboBox, object> SelectionEffectivelyChanged;
public FilterableComboBox()
{
IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
DropDownOpened += FilteredComboBox_DropDownOpened;
IsEditable = true;
IsTextSearchEnabled = true;
StaysOpenOnEdit = true;
IsReadOnly = false;
Loaded += (s, e) => {
if (EditableTextBox != null)
new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange;
};
SelectionChanged += (_, __) => shouldTriggerSelectedItemChanged = true;
SelectionEffectivelyChanged += (_, o) => EffectivelySelectedItem = o;
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
if (e.Key == Key.Down && !IsDropDownOpen) {
IsDropDownOpen = true;
e.Handled = true;
}
else if (e.Key == Key.Escape) {
ClearFilter();
Text = "";
IsDropDownOpen = true;
}
else if (e.Key == Key.Enter || e.Key == Key.Tab) {
CheckSelectedItem();
TriggerSelectedItemChanged();
}
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnPreviewLostKeyboardFocus(e);
CheckSelectedItem();
if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox)
TriggerSelectedItemChanged();
}
private void CheckSelectedItem()
{
if (OnlyValuesInList)
Text = SelectedItem?.ToString() ?? "";
}
private bool shouldTriggerSelectedItemChanged = false;
private void TriggerSelectedItemChanged()
{
if (shouldTriggerSelectedItemChanged) {
SelectionEffectivelyChanged?.Invoke(this, SelectedItem);
shouldTriggerSelectedItemChanged = false;
}
}
public void ClearFilter()
{
if (string.IsNullOrEmpty(CurrentFilter)) return;
CurrentFilter = "";
CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
}
private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
{
if (IsDropDownOpenUC.IsUserChange)
ClearFilter();
}
private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
{
if (TextBoxFreezed) return;
var tb = EditableTextBox;
if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower();
else
CurrentFilter = tb.Text.ToLower();
RefreshFilter();
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null) {
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null) {
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
FreezTextBoxState(() => {
var isDropDownOpen = IsDropDownOpen;
//always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
IsDropDownOpenUC.Set(false);
view.Refresh();
if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen)
IsDropDownOpenUC.Set(true);
if (SelectedItem == null) {
foreach (var itm in ItemsSource)
if (itm.ToString() == Text) {
SelectedItem = itm;
break;
}
}
});
}
private void FreezTextBoxState(Action action)
{
TextBoxFreezed = true;
var tb = EditableTextBox;
var text = Text;
var selStart = tb.SelectionStart;
var selLen = tb.SelectionLength;
action();
Text = text;
tb.SelectionStart = selStart;
tb.SelectionLength = selLen;
TextBoxFreezed = false;
}
private bool FilterItem(object value)
{
if (value == null) return false;
if (CurrentFilter.Length == 0) return true;
return value.ToString().ToLower().Contains(CurrentFilter);
}
private class TextBoxBaseUserChangeTracker
{
private bool IsTextInput { get; set; }
public TextBoxBase TextBoxBase { get; set; }
private List<Key> PressedKeys = new List<Key>();
public event EventHandler UserTextChanged;
private string LastText;
public TextBoxBaseUserChangeTracker(TextBoxBase textBoxBase)
{
TextBoxBase = textBoxBase;
LastText = TextBoxBase.ToString();
textBoxBase.PreviewTextInput += (s, e) => {
IsTextInput = true;
};
textBoxBase.TextChanged += (s, e) => {
var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString();
IsTextInput = false;
LastText = TextBoxBase.ToString();
if (isUserChange)
UserTextChanged?.Invoke(this, e);
};
textBoxBase.PreviewKeyDown += (s, e) => {
switch (e.Key) {
case Key.Back:
case Key.Space:
if (!PressedKeys.Contains(e.Key))
PressedKeys.Add(e.Key);
break;
}
if (e.Key == Key.Back) {
var textBox = textBoxBase as TextBox;
if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length) {
textBox.SelectionStart--;
textBox.SelectionLength++;
e.Handled = true;
UserTextChanged?.Invoke(this, e);
}
}
};
textBoxBase.PreviewKeyUp += (s, e) => {
if (PressedKeys.Contains(e.Key))
PressedKeys.Remove(e.Key);
};
textBoxBase.LostFocus += (s, e) => {
PressedKeys.Clear();
IsTextInput = false;
};
}
}
private class UserChange<T>
{
private Action<T> action;
public bool IsUserChange { get; private set; } = true;
public UserChange(Action<T> action)
{
this.action = action;
}
public void Set(T val)
{
try {
IsUserChange = false;
action(val);
}
finally {
IsUserChange = true;
}
}
}
}
Kelly's answer is great. However, there is a small bug that if you select an item in the list (highlighting the input text) then press BackSpace, the input text will revert to the selected item and the SelectedItem property of the ComboBox is still the item you selected previously.
Below is the code to fix the bug and add the ability to automatically select the item when the input text matches it.
using System.Collections;
using System.Diagnostics;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace MyControls
{
public class FilteredComboBox : ComboBox
{
private string oldFilter = string.Empty;
private string currentFilter = string.Empty;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null)
{
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Tab:
case Key.Enter:
IsDropDownOpen = false;
break;
case Key.Escape:
IsDropDownOpen = false;
SelectedIndex = -1;
Text = currentFilter;
break;
default:
if (e.Key == Key.Down) IsDropDownOpen = true;
base.OnPreviewKeyDown(e);
break;
}
// Cache text
oldFilter = Text;
}
protected override void OnKeyUp(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
case Key.Down:
break;
case Key.Tab:
case Key.Enter:
ClearFilter();
break;
default:
if (Text != oldFilter)
{
var temp = Text;
RefreshFilter(); //RefreshFilter will change Text property
Text = temp;
if (SelectedIndex != -1 && Text != Items[SelectedIndex].ToString())
{
SelectedIndex = -1; //Clear selection. This line will also clear Text property
Text = temp;
}
IsDropDownOpen = true;
EditableTextBox.SelectionStart = int.MaxValue;
}
//automatically select the item when the input text matches it
for (int i = 0; i < Items.Count; i++)
{
if (Text == Items[i].ToString())
SelectedIndex = i;
}
base.OnKeyUp(e);
currentFilter = Text;
break;
}
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
ClearFilter();
var temp = SelectedIndex;
SelectedIndex = -1;
Text = string.Empty;
SelectedIndex = temp;
base.OnPreviewLostKeyboardFocus(e);
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
view.Refresh();
}
private void ClearFilter()
{
currentFilter = string.Empty;
RefreshFilter();
}
private bool FilterItem(object value)
{
if (value == null) return false;
if (Text.Length == 0) return true;
return value.ToString().ToLower().Contains(Text.ToLower());
}
}
}
You can try https://www.nuget.org/packages/THEFilteredComboBox/ and give feedback. I plan to get as much feedback as possible and create perfect filtered combobox we all miss in WPF.
This is my take on it. A different approach, one that I have made for myself and one that I am using. It works with IsTextSearchEnabled="true". I've just completed it so there could be some bugs.
public class TextBoxBaseUserChangeTracker
{
private bool IsTextInput { get; set; }
public TextBoxBase TextBox { get; set; }
private List<Key> PressedKeys = new List<Key>();
public event EventHandler UserTextChanged;
private string LastText;
public TextBoxBaseUserChangeTracker(TextBoxBase textBox)
{
TextBox = textBox;
LastText = TextBox.ToString();
textBox.PreviewTextInput += (s, e) =>
{
IsTextInput = true;
};
textBox.TextChanged += (s, e) =>
{
var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBox.ToString();
IsTextInput = false;
LastText = TextBox.ToString();
if (isUserChange)
UserTextChanged?.Invoke(this, e);
};
textBox.PreviewKeyDown += (s, e) =>
{
switch (e.Key)
{
case Key.Back:
case Key.Space:
case Key.Delete:
if (!PressedKeys.Contains(e.Key))
PressedKeys.Add(e.Key);
break;
}
};
textBox.PreviewKeyUp += (s, e) =>
{
if (PressedKeys.Contains(e.Key))
PressedKeys.Remove(e.Key);
};
textBox.LostFocus += (s, e) =>
{
PressedKeys.Clear();
IsTextInput = false;
};
}
}
public static class ExtensionMethods
{
#region DependencyObject
public static T FindParent<T>(this DependencyObject child) where T : DependencyObject
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
else
return parentObject.FindParent<T>();
}
#endregion
#region TextBoxBase
public static TextBoxBaseUserChangeTracker TrackUserChange(this TextBoxBase textBox)
{
return new TextBoxBaseUserChangeTracker(textBox);
}
#endregion
}
public class UserChange<T>
{
private Action<T> action;
private bool isUserChange = true;
public bool IsUserChange
{
get
{
return isUserChange;
}
}
public UserChange(Action<T> action)
{
this.action = action;
}
public void Set(T val)
{
try
{
isUserChange = false;
action(val);
}
finally
{
isUserChange = true;
}
}
}
public class FilteredComboBox : ComboBox
{
// private string oldFilter = string.Empty;
private string CurrentFilter = string.Empty;
private bool TextBoxFreezed;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
private UserChange<bool> IsDropDownOpenUC;
public FilteredComboBox()
{
IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
DropDownOpened += FilteredComboBox_DropDownOpened;
Loaded += (s, e) =>
{
if (EditableTextBox != null)
{
EditableTextBox.TrackUserChange().UserTextChanged += FilteredComboBox_UserTextChange;
}
};
}
public void ClearFilter()
{
if (string.IsNullOrEmpty(CurrentFilter)) return;
CurrentFilter = "";
CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
}
private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
{
//if user opens the drop down show all items
if (IsDropDownOpenUC.IsUserChange)
ClearFilter();
}
private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
{
if (TextBoxFreezed) return;
var tb = EditableTextBox;
if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower();
else
CurrentFilter = tb.Text.ToLower();
RefreshFilter();
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null)
{
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
FreezTextBoxState(() =>
{
var isDropDownOpen = IsDropDownOpen;
//always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
IsDropDownOpenUC.Set(false);
view.Refresh();
if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen)
IsDropDownOpenUC.Set(true);
if (SelectedItem == null)
{
foreach (var itm in ItemsSource)
{
if (itm.ToString() == Text)
{
SelectedItem = itm;
break;
}
}
}
});
}
private void FreezTextBoxState(Action action)
{
TextBoxFreezed = true;
var tb = EditableTextBox;
var text = Text;
var selStart = tb.SelectionStart;
var selLen = tb.SelectionLength;
action();
Text = text;
tb.SelectionStart = selStart;
tb.SelectionLength = selLen;
TextBoxFreezed = false;
}
private bool FilterItem(object value)
{
if (value == null) return false;
if (CurrentFilter.Length == 0) return true;
return value.ToString().ToLower().Contains(CurrentFilter);
}
}
Xaml:
<local:FilteredComboBox ItemsSource="{Binding List}" IsEditable="True" IsTextSearchEnabled="true" StaysOpenOnEdit="True" x:Name="cmItems" SelectionChanged="CmItems_SelectionChanged">
</local:FilteredComboBox>
It sounds like what you are really looking for is something similar to an auto-complete textbox, which provides completion suggestions in a popup similar to a combobox popup.
You might find this CodeProject article useful:
A Reusable WPF Autocomplete TextBox
public class FilteredComboBox : ComboBox
{
/// <summary>
/// If true, on lost focus or enter key pressed, checks the text in the combobox. If the text is not present
/// in the list, it leaves it blank.
/// </summary>
public bool OnlyValuesInList
{
get => (bool)GetValue(OnlyValuesInListProperty);
set => SetValue(OnlyValuesInListProperty, value);
}
public static readonly DependencyProperty OnlyValuesInListProperty =
DependencyProperty.Register(nameof(OnlyValuesInList), typeof(bool), typeof(FilteredComboBox));
/// <summary>
/// Selected item, changes only on lost focus or enter key pressed
/// </summary>
public object EffectivelySelectedItem
{
get => (bool)GetValue(EffectivelySelectedItemProperty);
set => SetValue(EffectivelySelectedItemProperty, value);
}
public static readonly DependencyProperty EffectivelySelectedItemProperty =
DependencyProperty.Register(nameof(EffectivelySelectedItem), typeof(object), typeof(FilteredComboBox));
private string CurrentFilter = string.Empty;
private bool TextBoxFreezed;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
private UserChange<bool> IsDropDownOpenUC;
/// <summary>
/// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed.
/// </summary>
public event Action<FilteredComboBox, object> SelectionEffectivelyChanged;
public FilteredComboBox()
{
IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
DropDownOpened += FilteredComboBox_DropDownOpened;
IsEditable = true;
IsTextSearchEnabled = true;
StaysOpenOnEdit = true;
IsReadOnly = false;
Loaded += (s, e) => {
if (EditableTextBox != null)
new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange;
};
SelectionChanged += (_, __) => shouldTriggerSelectedItemChanged = true;
SelectionEffectivelyChanged += (_, o) => EffectivelySelectedItem = o;
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
if (e.Key == Key.Down && !IsDropDownOpen)
{
IsDropDownOpen = true;
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
ClearFilter();
Text = "";
IsDropDownOpen = true;
}
else if (e.Key == Key.Back)
{
ClearFilter();
Text = "";
IsDropDownOpen = true;
}
else if (e.Key == Key.Enter || e.Key == Key.Tab)
{
CheckSelectedItem();
TriggerSelectedItemChanged();
IsDropDownOpen = false;
}
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnPreviewLostKeyboardFocus(e);
CheckSelectedItem();
if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox)
TriggerSelectedItemChanged();
}
private void CheckSelectedItem()
{
if (OnlyValuesInList)
Text = SelectedItem?.ToString() ?? "";
}
private bool shouldTriggerSelectedItemChanged = false;
private void TriggerSelectedItemChanged()
{
if (shouldTriggerSelectedItemChanged)
{
SelectionEffectivelyChanged?.Invoke(this, SelectedItem);
shouldTriggerSelectedItemChanged = false;
}
}
public void ClearFilter()
{
if (string.IsNullOrEmpty(CurrentFilter)) return;
CurrentFilter = "";
CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
}
private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
{
if (IsDropDownOpenUC.IsUserChange)
ClearFilter();
}
private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
{
if (TextBoxFreezed) return;
var tb = EditableTextBox;
if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower();
else
CurrentFilter = tb.Text.ToLower();
RefreshFilter();
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null)
{
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null)
{
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
var view = CollectionViewSource.GetDefaultView(ItemsSource);
FreezTextBoxState(() => {
var isDropDownOpen = IsDropDownOpen;
//always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
IsDropDownOpenUC.Set(false);
view.Refresh();
if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen)
IsDropDownOpenUC.Set(true);
if (SelectedItem == null)
{
foreach (var itm in ItemsSource)
if (itm.ToString() == Text)
{
SelectedItem = itm;
break;
}
}
});
}
private void FreezTextBoxState(Action action)
{
TextBoxFreezed = true;
var tb = EditableTextBox;
var text = Text;
var selStart = tb.SelectionStart;
var selLen = tb.SelectionLength;
action();
Text = text;
tb.SelectionStart = selStart;
tb.SelectionLength = selLen;
TextBoxFreezed = false;
}
private bool FilterItem(object value)
{
var _newVal = ((NewComboSearch.MainWindow.ComboData)value).text;
if (_newVal == null) return false;
if (CurrentFilter.Length == 0) return true;
return _newVal.ToString().ToLower().Contains(CurrentFilter);
}
private class TextBoxBaseUserChangeTracker
{
private bool IsTextInput { get; set; }
public TextBoxBase TextBoxBase { get; set; }
private List<Key> PressedKeys = new List<Key>();
public event EventHandler UserTextChanged;
private string LastText;
public TextBoxBaseUserChangeTracker(TextBoxBase textBoxBase)
{
TextBoxBase = textBoxBase;
LastText = TextBoxBase.ToString();
textBoxBase.PreviewTextInput += (s, e) => {
IsTextInput = true;
};
textBoxBase.TextChanged += (s, e) => {
var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString();
IsTextInput = false;
LastText = TextBoxBase.ToString();
if (isUserChange)
UserTextChanged?.Invoke(this, e);
};
textBoxBase.PreviewKeyDown += (s, e) => {
switch (e.Key)
{
case Key.Back:
case Key.Space:
if (!PressedKeys.Contains(e.Key))
PressedKeys.Add(e.Key);
break;
}
if (e.Key == Key.Back)
{
var textBox = textBoxBase as TextBox;
if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length)
{
textBox.SelectionStart--;
textBox.SelectionLength++;
e.Handled = true;
UserTextChanged?.Invoke(this, e);
}
}
};
textBoxBase.PreviewKeyUp += (s, e) => {
if (PressedKeys.Contains(e.Key))
PressedKeys.Remove(e.Key);
};
textBoxBase.LostFocus += (s, e) => {
PressedKeys.Clear();
IsTextInput = false;
};
}
}
private class UserChange<T>
{
private Action<T> action;
public bool IsUserChange { get; private set; } = true;
public UserChange(Action<T> action)
{
this.action = action;
}
public void Set(T val)
{
try
{
IsUserChange = false;
action(val);
}
finally
{
IsUserChange = true;
}
}
}
}

Resources