I'm using below code for fix my screen when i insert listboxItem.
public partial class KeepItemsInViewListBox : ListBox
{
private ScrollViewer ScrollViewer { get; set; }
#region Overrides of FrameworkElement
/// <inheritdoc />
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (TryFindVisualChildElement(this, out ScrollViewer scrollViewer))
{
this.ScrollViewer = scrollViewer;
}
}
#endregion
#region Overrides of ListView
/// <inheritdoc />
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
if (this.ScrollViewer == null)
{
return;
}
double verticalOffset;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add when e.NewItems != null:
// Check if insert or add
verticalOffset = e.NewStartingIndex < this.ScrollViewer.VerticalOffset
? this.ScrollViewer.VerticalOffset + e.NewItems.Count
: this.ScrollViewer.VerticalOffset;
break;
case NotifyCollectionChangedAction.Remove when e.OldItems != null:
verticalOffset = this.ScrollViewer.VerticalOffset - e.OldItems.Count;
break;
default:
verticalOffset = this.ScrollViewer.VerticalOffset;
break;
}
this.ScrollViewer?.ScrollToVerticalOffset(verticalOffset);
}
#endregion
public bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild childElement)
where TChild : FrameworkElement
{
childElement = null;
if (parent == null)
{
return false;
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is TChild resultElement)
{
childElement = resultElement;
return true;
}
if (TryFindVisualChildElement(child, out childElement))
{
return true;
}
}
return false;
}
}
that class makes my listbox's scroll to item-by-item.
see the below picture.
before upper Class(KeepItemsInViewListBox), my scrolling condition is Pixel. but after using that code my scrolling condition is changed.
because all of my listboxitem's size is diffrent,
I added VirtualizingPanel.ScrollUnit="Pixel" or ScrollViewer.CanContentScroll = "False" in xaml, or ScrollViewer.CanContentScroll = false; in .cs file
then my screen is also move...
First thanks to Mr.Code
I modify Mr.Code's class, but my Code is not a perfect.
public class KeepItemsInViewListBox : ListBox
{
private ScrollViewer ScrollViewer { get; set; }
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (insertCheck)
{
ScrollViewer.ScrollToVerticalOffset(ScrollViewer.VerticalOffset + e.ExtentHeightChange);
insertCheck = false;
}
}
#region Overrides of FrameworkElement
/// <inheritdoc />
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (TryFindVisualChildElement(this, out ScrollViewer scrollViewer))
{
this.ScrollViewer = scrollViewer;
ScrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
}
}
#endregion
private bool insertCheck = false;
#region Overrides of ListView
/// <inheritdoc />
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
switch (e.Action)
{
case NotifyCollectionChangedAction.Add when e.NewItems != null:
bool isInsert = this.ScrollViewer.CanContentScroll
? e.NewStartingIndex < this.ScrollViewer.VerticalOffset
: e.NewStartingIndex + 1 != this.Items.Count - 1;
if (!isInsert)
{
return;
}
insertCheck = isInsert;
break;
}
}
#endregion
public bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild childElement)
where TChild : FrameworkElement
{
childElement = null;
if (parent == null)
{
return false;
}
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is TChild resultElement)
{
childElement = resultElement;
return true;
}
if (TryFindVisualChildElement(child, out childElement))
{
return true;
}
}
return false;
}
}
I just redefine "ScrollViewer_ScrollChanged" method and set verticalOffset to changed offset value.
It works in my test Project. but in my real Project doesn't work. Then i use ScrollViewer's ExtentHeight value.
I checked the first ExtentHeight and it was saved. After scrolling, I compare two of ExtentHeight and set the diffrence to verticalOffset.
But it isn't work Beautifully
Related
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>
I'm currently working on a kind of VirtualizedWrapPanel to use as the ItemsPanel in a ListView.
After following this guy's instructions, and borrowing heavily from this guy's implementation found on codeproject but I don't have the reputation to post the link so sorry..., I have something that is nicely shaping up to be exactly what I need.
The item size is fixed so the scrolling is pixel based. the orientation is always horizontal.
the ListView :
<ListView Name="lv"
ItemsSource="{Binding CV}"
IsSynchronizedWithCurrentItem="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<i:Interaction.Behaviors>
<local:ScrollToSelectionListViewBehavior/> <!-- Behavior calling ScrollIntoView whenever the selection changes -->
<local:ListViewSelectedItemsBehavior SelectedItems="{Binding SelectedItems}"/> <!-- Behavior exposing the attached ListView's SelectedItems array -->
</i:Interaction.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Height="100" Width="100" Orientation="Vertical">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="5" Width="90" Height="90">
<TextBlock Text ="{Binding ItemText}" FontWeight="Bold" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<local:VirtualizingWrapPanel ItemHeight="100" ItemWidth="110" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListView.ItemContainerStyle>
</ListView>
the local:VirtualizingWrapPanel :
public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
{
private ScrollViewer _owner;
private const bool _canHScroll = false;
private bool _canVScroll = false;
private Size _extent = new Size(0, 0);
private Size _viewport = new Size(0, 0);
private Point _offset;
UIElementCollection _children;
ItemsControl _itemsControl;
IItemContainerGenerator _generator;
Dictionary<UIElement, Rect> _realizedChildLayout = new Dictionary<UIElement, Rect>();
#region Properties
private Size ChildSlotSize
{
get { return new Size(ItemWidth, ItemHeight); }
}
#endregion
#region Dependency Properties
[TypeConverter(typeof(LengthConverter))]
public double ItemHeight
{
get { return (double)base.GetValue(ItemHeightProperty); }
set { base.SetValue(ItemHeightProperty, value); }
}
[TypeConverter(typeof(LengthConverter))]
public double ItemWidth
{
get { return (double)base.GetValue(ItemWidthProperty); }
set { base.SetValue(ItemWidthProperty, value); }
}
public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(double.PositiveInfinity));
private int LineCapacity
{ get { return Math.Max((_viewport.Width != 0) ? (int)(_viewport.Width / ItemWidth) : 0, 1); } }
private int LinesCount
{ get { return (ItemsCount > 0) ? ItemsCount / LineCapacity : 0 ; } }
private int ItemsCount
{ get { return _itemsControl.Items.Count; } }
public int FirstVisibleLine
{ get { return (int)(_offset.Y / ItemHeight); } }
public int FirstVisibleItemVPos
{ get { return (int)((FirstVisibleLine * ItemHeight) - _offset.Y); } }
public int FirstVisibleIndex
{ get { return (FirstVisibleLine * LineCapacity); } }
#endregion
#region VirtualizingPanel overrides
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
_itemsControl = ItemsControl.GetItemsOwner(this);
_children = InternalChildren;
_generator = ItemContainerGenerator;
this.SizeChanged += new SizeChangedEventHandler(this.Resizing);
}
protected override Size MeasureOverride(Size availableSize)
{
if (_itemsControl == null || _itemsControl.Items.Count == 0)
return availableSize;
if (availableSize != _viewport)
{
_viewport = availableSize;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
Size childSize = new Size(ItemWidth, ItemHeight);
Size extent = new Size(availableSize.Width, LinesCount * ItemHeight);
if (extent != _extent)
{
_extent = extent;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
foreach (UIElement child in this.InternalChildren)
{
child.Measure(childSize);
}
_realizedChildLayout.Clear();
Size realizedFrameSize = availableSize;
int firstVisibleIndex = FirstVisibleIndex;
GeneratorPosition startPos = _generator.GeneratorPositionFromIndex(firstVisibleIndex);
int childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;
int current = firstVisibleIndex;
using (_generator.StartAt(startPos, GeneratorDirection.Forward, true))
{
bool stop = false;
double currentX = 0;
double currentY = FirstVisibleItemVPos;
while (current < ItemsCount)
{
bool newlyRealized;
// Get or create the child
UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement;
if (newlyRealized)
{
// Figure out if we need to insert the child at the end or somewhere in the middle
if (childIndex >= _children.Count)
{
base.AddInternalChild(child);
}
else
{
base.InsertInternalChild(childIndex, child);
}
_generator.PrepareItemContainer(child);
child.Measure(ChildSlotSize);
}
else
{
// The child has already been created, let's be sure it's in the right spot
Debug.Assert(child == _children[childIndex], "Wrong child was generated");
}
childSize = child.DesiredSize;
Rect childRect = new Rect(new Point(currentX, currentY), childSize);
if (childRect.Right > realizedFrameSize.Width) //wrap to a new line
{
currentY = currentY + ItemHeight;
currentX = 0;
childRect.X = currentX;
childRect.Y = currentY;
}
if (currentY > realizedFrameSize.Height)
stop = true;
currentX = childRect.Right;
_realizedChildLayout.Add(child, childRect);
if (stop)
break;
current++;
childIndex++;
}
}
CleanUpItems(firstVisibleIndex, current - 1);
return availableSize;
}
public void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
{
for (int i = _children.Count - 1; i >= 0; i--)
{
GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
int itemIndex = _generator.IndexFromGeneratorPosition(childGeneratorPos);
if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
{
//var c = _children[i] as ListViewItem;
//if(c!= null && c.IsSelected)
//{
//}
_generator.Remove(childGeneratorPos, 1);
RemoveInternalChildRange(i, 1);
}
}
}
protected override Size ArrangeOverride(Size finalSize)
{
if (finalSize != _viewport)
{
_viewport = finalSize;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
Size childSize = new Size(ItemWidth, ItemHeight);
Size extent = new Size(finalSize.Width, LinesCount * ItemHeight);
if (extent != _extent)
{
_extent = extent;
if (_owner != null)
_owner.InvalidateScrollInfo();
}
if (_children != null)
{
foreach (UIElement child in _children)
{
var layoutInfo = _realizedChildLayout[child];
child.Arrange(layoutInfo);
}
}
return finalSize;
}
protected override void BringIndexIntoView(int index)
{
SetVerticalOffset((index / LineCapacity) * ItemHeight);
}
protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
{
base.OnItemsChanged(sender, args);
_offset.X = 0;
_offset.Y = 0;
switch (args.Action)
{
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
break;
case NotifyCollectionChangedAction.Move:
RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount);
break;
}
}
#endregion
#region EventHandlers
public void Resizing(object sender, EventArgs e)
{
var args = e as SizeChangedEventArgs;
if(args.WidthChanged)
{
int lineCapacity = LineCapacity;
int previousLineCapacity = (int)(args.PreviousSize.Width / ItemWidth);
if (previousLineCapacity != lineCapacity)
{
int previousFirstItem = ((int)(_offset.Y / ItemHeight) <= 0) ? 0 : ((int)(_offset.Y / ItemHeight) * previousLineCapacity);
BringIndexIntoView(previousFirstItem);
}
}
if (_viewport.Width != 0)
{
MeasureOverride(_viewport);
}
}
#endregion
#region IScrollInfo Implementation
public ScrollViewer ScrollOwner
{
get { return _owner; }
set { _owner = value; }
}
public bool CanHorizontallyScroll
{
get { return false; }
set { if (value == true) throw (new ArgumentException("VirtualizingWrapPanel does not support Horizontal scrolling")); }
}
public bool CanVerticallyScroll
{
get { return _canVScroll; }
set { _canVScroll = value; }
}
public double ExtentHeight
{
get { return _extent.Height;}
}
public double ExtentWidth
{
get { return _extent.Width; }
}
public double HorizontalOffset
{
get { return _offset.X; }
}
public double VerticalOffset
{
get { return _offset.Y; }
}
public double ViewportHeight
{
get { return _viewport.Height; }
}
public double ViewportWidth
{
get { return _viewport.Width; }
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
var gen = (ItemContainerGenerator)_generator.GetItemContainerGeneratorForPanel(this);
var element = (UIElement)visual;
int itemIndex = gen.IndexFromContainer(element);
while (itemIndex == -1)
{
element = (UIElement)VisualTreeHelper.GetParent(element);
itemIndex = gen.IndexFromContainer(element);
}
Rect elementRect = _realizedChildLayout[element];
if (elementRect.Bottom > ViewportHeight)
{
double translation = elementRect.Bottom - ViewportHeight;
_offset.Y += translation;
}
else if (elementRect.Top < 0)
{
double translation = elementRect.Top;
_offset.Y += translation;
}
InvalidateMeasure();
return elementRect;
}
public void LineDown()
{
SetVerticalOffset(VerticalOffset + 50);
}
public void LineUp()
{
SetVerticalOffset(VerticalOffset - 50);
}
public void MouseWheelDown()
{
SetVerticalOffset(VerticalOffset + 50);
}
public void MouseWheelUp()
{
SetVerticalOffset(VerticalOffset - 50);
}
public void PageDown()
{
int fullyVisibleLines = (int)(_viewport.Height / ItemHeight);
SetVerticalOffset(VerticalOffset + (fullyVisibleLines * ItemHeight));
}
public void PageUp()
{
int fullyVisibleLines = (int)(_viewport.Height / ItemHeight);
SetVerticalOffset(VerticalOffset - (fullyVisibleLines * ItemHeight));
}
public void SetVerticalOffset(double offset)
{
if (offset < 0 || _viewport.Height >= _extent.Height)
{
offset = 0;
}
else
{
if (offset + _viewport.Height >= _extent.Height)
{
offset = _extent.Height - _viewport.Height;
}
}
_offset.Y = offset;
if (_owner != null)
_owner.InvalidateScrollInfo();
InvalidateMeasure();
}
public void LineLeft() { throw new NotImplementedException(); }
public void LineRight() { throw new NotImplementedException(); }
public void MouseWheelLeft() { throw new NotImplementedException(); }
public void MouseWheelRight() { throw new NotImplementedException(); }
public void PageLeft() { throw new NotImplementedException(); }
public void PageRight() { throw new NotImplementedException(); }
public void SetHorizontalOffset(double offset) { throw new NotImplementedException(); }
#endregion
#region methods
#endregion
}
Now my problem is : An Item Selection should always Deselect the previously selected item, when using a normal WrapPanel, the previously selected ListViewItem's IsSelected property is always set to false before the new selected ListViewItem's IsSelected is set to true.
This deselection does not happen with my VirtualizingPanel when the previously selected item is no longer realized (when it is not visible in the viewport), so I end up with two or more selected items at once and the panel's behavior becomes really weird. Sometimes it even settles into an infinite loop, the two selected items yanking visibility from each other in a never ending battle.
I searched a bit for a solution to this problem but I don't really know where to start or what to search for.
Here is a test project if you want to experiment with it.
Thanks
I found a way by preventing the virtualization of selected items. Now the control behaves correctly.
public void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
{
for (int i = _children.Count - 1; i >= 0; i--)
{
GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
int itemIndex = _generator.IndexFromGeneratorPosition(childGeneratorPos);
if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
{
//I don't much like this cast
// how do I access the IsSelectedProperty
// of a UIElement of any type ( ListBoxItem, TreeViewItem...)?
var c = _children[i] as ListViewItem;
c.IsEnabled = false;
if (c != null && c.IsSelected)
{
var layoutInfo = new Rect(0, 0, 0, 0);
c.Arrange(layoutInfo);
}
else
{
_generator.Remove(childGeneratorPos, 1);
RemoveInternalChildRange(i, 1);
}
}
}
}
In ArrangeOverride :
if (_children != null)
{
foreach (UIElement child in _children)
{
if (child.IsEnabled)
{
var layoutInfo = _realizedChildLayout[child];
child.Arrange(layoutInfo);
}
}
}
In MeasureOverride:
while (current < ItemsCount)
{
bool newlyRealized;
// Get or create the child
UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement;
child.IsEnabled = true;
I'm still curious to know if there's a better way. In the meantime this will do.
btw I'll update the test project with this fix in case anyone wants a virtualizing wrappanel.
We have a ComboBox with a DataGrid, based on this article.
Now we wan't to have the possibility to filter the values. So I implemented this one.
Filtering is working fine and it's possible to select suggestions with up and down on the keyboard. But it is not possible to select one with the mouse.
Why is this not possible? How can I fix that?
Here is the code of our combobox:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
namespace CustomControls {
[DefaultProperty("Columns")]
[ContentProperty("Columns")]
[TemplatePart(Name = s_partPopupDataGrid, Type = typeof(DataGrid))]
public class GridCombo : ComboBox {
#region Static
internal static readonly DependencyProperty ReplaceColumnsProperty =
DependencyProperty.Register(
"ReplaceColumns",
typeof(IEnumerable<DataGridBoundColumn>),
typeof(GridCombo),
new FrameworkPropertyMetadata());
public static readonly DependencyProperty CellStyleProperty =
DependencyProperty.Register(
"CellStyle",
typeof(Style),
typeof(GridCombo),
new FrameworkPropertyMetadata());
public static readonly DependencyProperty MinimumSearchLengthProperty =
DependencyProperty.Register(
"MinimumSearchLength",
typeof(int),
typeof(GridCombo),
new UIPropertyMetadata(1));
static GridCombo() {
DefaultStyleKeyProperty.OverrideMetadata(
typeof(GridCombo), new FrameworkPropertyMetadata(typeof(GridCombo)));
}
#endregion
// ======================================================================
#region Fields & Constructors
private const string s_partPopupDataGrid = "PART_PopupDataGrid";
// Columns of DataGrid
private ObservableCollection<DataGridBoundColumn> _columns;
private readonly Dictionary<Type, List<PropertyInfo>> _properties = new Dictionary<Type, List<PropertyInfo>>();
// Attached DataGrid control
private DataGrid _popupDataGrid;
private Popup _popup;
private string _oldFilter = string.Empty;
private string _currentFilter = string.Empty;
#endregion
// ======================================================================
#region Public
public Style CellStyle {
get { return (Style)GetValue(CellStyleProperty); }
set { SetValue(CellStyleProperty, value); }
}
/// <summary>
/// If set, the "Columns" property is ignored. Useful if you need
/// a dependency property.
/// </summary>
internal IEnumerable<DataGridBoundColumn> ReplaceColumns {
get { return (ObservableCollection<DataGridBoundColumn>)GetValue(ReplaceColumnsProperty); }
set { SetValue(ReplaceColumnsProperty, value); }
}
// The property is default and Content property for CustComboBox
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ObservableCollection<DataGridBoundColumn> Columns {
get {
if (_columns == null) {
_columns = new ObservableCollection<DataGridBoundColumn>();
}
return _columns;
}
}
// Apply theme and attach columns to DataGrid popup control
public override void OnApplyTemplate() {
if (_popupDataGrid == null) {
_popupDataGrid = Template.FindName(s_partPopupDataGrid, this) as DataGrid;
if (_popupDataGrid != null && (_columns != null || ReplaceColumns != null)) {
if (ReplaceColumns != null) {
foreach (var column in ReplaceColumns) {
var copy = DataGridFix.CopyDataGridColumn(column);
_popupDataGrid.Columns.Add(copy);
}
} else {
// Add columns to DataGrid columns
for (int i = 0; i < _columns.Count; i++)
_popupDataGrid.Columns.Add(_columns[i]);
}
// Add event handler for DataGrid popup
_popupDataGrid.MouseDown += PopupDataGridMouseDown;
_popupDataGrid.SelectionChanged += PopupDataGridSelectionChanged;
}
}
if (_popup == null) {
_popup = Template.FindName("PART_Popup", this) as Popup;
if (_popup != null && _popupDataGrid != null) {
_popup.Opened += PopupOpened;
_popup.Focusable = true;
}
}
// Call base class method
base.OnApplyTemplate();
}
[Description("Length of the search string that triggers filtering.")]
[Category("Filtered ComboBox")]
[DefaultValue(1)]
public int MinimumSearchLength {
[DebuggerStepThrough]
get { return (int)GetValue(MinimumSearchLengthProperty); }
[DebuggerStepThrough]
set { SetValue(MinimumSearchLengthProperty, value); }
}
#endregion
// ======================================================================
#region Protected
// When selection changed in combobox (pressing arrow key down or up) must be synchronized with opened DataGrid popup
protected override void OnSelectionChanged(SelectionChangedEventArgs e) {
base.OnSelectionChanged(e);
if (_popupDataGrid == null)
return;
if (!DesignerProperties.GetIsInDesignMode(this)) {
if (IsDropDownOpen) {
_popupDataGrid.SelectedItem = SelectedItem;
ScrollIntoView(SelectedItem);
}
}
}
protected override void OnDropDownOpened(EventArgs e) {
if (_popupDataGrid == null)
return;
_popupDataGrid.SelectedItem = SelectedItem;
base.OnDropDownOpened(e);
}
protected TextBox EditableTextBox {
get { return Template.FindName("PART_EditableTextBox", this) as TextBox; }
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e) {
if (!IsEditable) {
base.OnPreviewLostKeyboardFocus(e);
return;
}
ClearFilter();
int temp = SelectedIndex;
SelectedIndex = -1;
Text = string.Empty;
SelectedIndex = temp;
base.OnPreviewLostKeyboardFocus(e);
}
protected override void OnKeyUp(KeyEventArgs e) {
if (!IsEditable) {
base.OnKeyUp(e);
return;
}
if (e.Key == Key.Up || e.Key == Key.Down) {
// Navigation keys are ignored
} else if (e.Key == Key.Tab || e.Key == Key.Enter) {
// Explicit Select -> Clear Filter
ClearFilter();
} else {
// The text was changed
if (Text != _oldFilter) {
// Clear the filter if the text is empty,
// apply the filter if the text is long enough
if (Text.Length == 0 || Text.Length >= MinimumSearchLength) {
RefreshFilter();
IsDropDownOpen = true;
// Unselect
EditableTextBox.SelectionStart = int.MaxValue;
}
}
base.OnKeyUp(e);
_currentFilter = Text;
}
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) {
if (!IsEditable) {
base.OnItemsSourceChanged(oldValue, newValue);
return;
}
if (newValue != null) {
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterPredicate;
}
if (oldValue != null) {
var view = CollectionViewSource.GetDefaultView(oldValue);
view.Filter -= FilterPredicate;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
protected override void OnPreviewKeyDown(KeyEventArgs e) {
if (!IsEditable) {
base.OnPreviewKeyDown(e);
return;
}
if (e.Key == Key.Tab || e.Key == Key.Enter) {
// Explicit Selection -> Close ItemsPanel
IsDropDownOpen = false;
} else if (e.Key == Key.Escape) {
// Escape -> Close DropDown and redisplay Filter
IsDropDownOpen = false;
SelectedIndex = -1;
Text = _currentFilter;
} else {
if (e.Key == Key.Down) {
// Arrow Down -> Open DropDown
IsDropDownOpen = true;
}
base.OnPreviewKeyDown(e);
}
_oldFilter = Text;
}
#endregion
// ======================================================================
#region Private
private void RefreshFilter() {
if (ItemsSource != null) {
var view = CollectionViewSource.GetDefaultView(ItemsSource);
view.Refresh();
}
}
private void ClearFilter() {
_currentFilter = string.Empty;
RefreshFilter();
}
private bool FilterPredicate(object value) {
if (value == null) {
return false;
}
if (Text.Length == 0) {
return true;
}
var properties = GetProperties(value.GetType());
foreach (var property in properties) {
var propertyValue = (property.GetValue(value, null) ?? string.Empty).ToString();
if (propertyValue.ToLowerInvariant().Contains(Text.ToLowerInvariant())) {
return true;
}
}
return false;
}
private IEnumerable<PropertyInfo> GetProperties(Type type) {
if (!_properties.ContainsKey(type)) {
_properties.Add(type, new List<PropertyInfo>());
foreach (var column in _columns) {
if (column.Binding != null && column.Binding is Binding) {
var path = ((Binding)column.Binding).Path.Path;
var property = type.GetProperty(path);
if (property != null) {
_properties[type].Add(property);
}
}
}
}
return _properties[type];
}
private void PopupOpened(object sender, EventArgs e) {
ScrollIntoView(SelectedItem);
}
private void ScrollIntoView(object item) {
if (item != null && _popupDataGrid.Items.Contains(item))
_popupDataGrid.ScrollIntoView(item);
}
// Synchronize selection between Combo and DataGrid popup
private void PopupDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) {
// When open in Blend prevent raising exception
if (!DesignerProperties.GetIsInDesignMode(this)) {
var grid = sender as DataGrid;
if (grid != null && grid.IsVisible) {
SelectedItem = grid.SelectedItem;
}
}
}
// Event for DataGrid popup MouseDown
private void PopupDataGridMouseDown(object sender, MouseButtonEventArgs e) {
DataGrid dg = sender as DataGrid;
if (dg != null) {
var dep = (DependencyObject)e.OriginalSource;
// iteratively traverse the visual tree and stop when dep is one of ..
while ((dep != null) &&
!(dep is DataGridCell) &&
!(dep is DataGridColumnHeader)) {
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null)
return;
if (dep is DataGridColumnHeader) {
// do something
}
// When user clicks to DataGrid cell, popup have to be closed
if (dep is DataGridCell) {
IsDropDownOpen = false;
}
}
}
#endregion
}
}
The following Xaml can be used to test it:
<Window x:Class="GridComboTestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:combo="clr-namespace:CustomControls;assembly=CustomControls"
Height="300" Width="300">
<StackPanel>
<Button Content="ChangeElements" Command="{Binding ChangeElements}" />
<combo:GridCombo
x:Name="GridCombo"
ItemsSource="{Binding Elements}"
DisplayMemberPath="Number"
IsEditable="True"
SelectedItem="{Binding SelectedElement}"
MaxDropDownHeight="100">
<DataGridTextColumn Binding="{Binding Number, Mode=OneWay}" />
<DataGridTextColumn Binding="{Binding Name, Mode=OneWay}" />
</combo:GridCombo>
</StackPanel>
</Window>
You are not calling base() on PopupDataGridMouseDown. Not sure this will fix it but something to look at.
I need to get the position of a WPF DataGridCell, obtained in a DataGrid cell changed event, but only can get the vertical (Y-axis).
The horizontal remains the same, despite a different column is pointed.
Here is the almost working code.
Test by clicking on different cells.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
List<Person> Persons = new List<Person>();
public MainWindow()
{
InitializeComponent();
Persons.Add(new Person { Id = 1, Name = "John", City = "London" });
Persons.Add(new Person { Id = 2, Name = "Charles", City = "Rome" });
Persons.Add(new Person { Id = 3, Name = "Paul", City = "Chicago" });
this.EditingDataGrid.ItemsSource = Persons;
this.EditingDataGrid.CurrentCellChanged += new EventHandler<EventArgs>(EditingDataGrid_CurrentCellChanged);
}
void EditingDataGrid_CurrentCellChanged(object sender, EventArgs e)
{
DataGridCell Cell = GetCurrentCell(this.EditingDataGrid);
var Position = Cell.PointToScreen(new Point(0, 0));
// WHY X NEVER CHANGES??!!
MessageBox.Show("X=" + Position.X.ToString() + ", Y=" + Position.Y.ToString(), "Position");
}
/// <summary>
/// Returns, for this supplied Source Data-Grid, the current Data-Grid-Cell.
/// May return null if no associated Cell is found.
/// </summary>
public static DataGridCell GetCurrentCell(DataGrid SourceDataGrid)
{
if (SourceDataGrid.CurrentCell == null)
return null;
var RowContainer = SourceDataGrid.ItemContainerGenerator.ContainerFromItem(SourceDataGrid.CurrentCell.Item);
if (RowContainer == null)
return null;
var RowPresenter = GetVisualChild<System.Windows.Controls.Primitives.DataGridCellsPresenter>(RowContainer);
if (RowPresenter == null)
return null;
var Container = RowPresenter.ItemContainerGenerator.ContainerFromItem(SourceDataGrid.CurrentCell.Item);
var Cell = Container as DataGridCell;
// Try to get the cell if null, because maybe the cell is virtualized
if (Cell == null)
{
SourceDataGrid.ScrollIntoView(RowContainer, SourceDataGrid.CurrentCell.Column);
Container = RowPresenter.ItemContainerGenerator.ContainerFromItem(SourceDataGrid.CurrentCell.Item);
Cell = Container as DataGridCell;
}
return Cell;
}
/// <summary>
/// Returns the nearest child having the specified TRet type for the supplied Target.
/// </summary>
public static TRet GetVisualChild<TRet>(DependencyObject Target) where TRet : DependencyObject
{
if (Target == null)
return null;
for (int ChildIndex = 0; ChildIndex < VisualTreeHelper.GetChildrenCount(Target); ChildIndex++)
{
var Child = VisualTreeHelper.GetChild(Target, ChildIndex);
if (Child != null && Child is TRet)
return (TRet)Child;
else
{
TRet childOfChild = GetVisualChild<TRet>(Child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
}
The DataGrid is just defined by...
<DataGrid x:Name="EditingDataGrid"/>
Maybe exists there an alternative to get that DataGridCell position?
You can get the DataGridCell from CurrentCell like this
void EditingDataGrid_CurrentCellChanged(object sender, EventArgs e)
{
DataGridCell Cell = GetDataGridCell(EditingDataGrid.CurrentCell);
var Position = Cell.PointToScreen(new Point(0, 0));
MessageBox.Show("X=" + Position.X.ToString() + ", Y=" + Position.Y.ToString(), "Position");
}
public static DataGridCell GetDataGridCell(DataGridCellInfo cellInfo)
{
if (cellInfo.IsValid == false)
{
return null;
}
var cellContent = cellInfo.Column.GetCellContent(cellInfo.Item);
if (cellContent == null)
{
return null;
}
return cellContent.Parent as DataGridCell;
}
You could also create an extension method on the DataGrid to do this
DataGridExtensions.cs
public static class DataGridExtensions
{
public static DataGridCell GetCurrentDataGridCell(this DataGrid dataGrid)
{
DataGridCellInfo cellInfo = dataGrid.CurrentCell;
if (cellInfo.IsValid == false)
{
return null;
}
var cellContent = cellInfo.Column.GetCellContent(cellInfo.Item);
if (cellContent == null)
{
return null;
}
return cellContent.Parent as DataGridCell;
}
}
Which you can use like this everytime you want to get the current DataGridCell
DataGridCell Cell = EditingDataGrid.GetCurrentDataGridCell();
I guess what's going on is that your grid's default selection mode is full row, the code you're using to get the DataGridCell is getting the first selected cell which holds the "Id" column value.
What you can try to do is changing the grid's selection mode to "Cell" and this trigger the message box with correct coordinates.
<DataGrid x:Name="EditingDataGrid" SelectionUnit="Cell"/>
Also I've changed your code a bit, see if it would work for you:
void EditingDataGrid_CurrentCellChanged(object sender, EventArgs e)
{
// this will iterate through all selected cell of the datagrid
foreach (DataGridCellInfo cellInfo in this.EditingDataGrid.SelectedCells)
{
DataGridCell Cell = GetCurrentCell(this.EditingDataGrid, cellInfo);
if (Cell != null)
{
var Position = Cell.PointToScreen(new Point(0, 0));
MessageBox.Show("X=" + Position.X.ToString() +
", Y=" + Position.Y.ToString() +
" Content = " + ((TextBlock)Cell.Content).Text.ToString(), "Position");
}
}
}
/// <summary>
/// Returns, for this supplied Source Data-Grid, the current Data-Grid-Cell.
/// May return null if no associated Cell is found.
/// </summary>
public static DataGridCell GetCurrentCell(DataGrid grid, DataGridCellInfo cellInfo)
{
DataGridCell result = null;
DataGridRow row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromItem(cellInfo.Item);
if (row != null)
{
int columnIndex = grid.Columns.IndexOf(cellInfo.Column);
if (columnIndex > -1)
{
DataGridCellsPresenter presenter = GetVisualChild<DataGridCellsPresenter>(row);
result = presenter.ItemContainerGenerator.ContainerFromIndex(columnIndex) as DataGridCell;
}
}
return result;
}
/// <summary>
/// Returns the nearest child having the specified TRet type for the supplied Target.
/// </summary>
static T GetVisualChild<T>(Visual parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
hope this helps, regards
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;
}
}
}
}