WPF Combobox search typing speed - wpf

I have a WPF combobox
<ComboBox BorderThickness="0" Name="cmb_songs_head" HorizontalAlignment="Right" SelectedItem="{Binding Path=T.SelectedSong, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding Path=T.SelectedSet.Songs, UpdateSourceTrigger=PropertyChanged}" />
When the combobox is selected and I type, it selects from the dropdown - which is what I want. e.g:
Hall
Hold
Hollow
Lead
So When I type H the first item is selected, Ho selects the second item, Holl selects the third.
But, users of my program complain that they are often too slow in typing and so they end up typing Hol which selects Hold, followed by l, which selects Lead; instead of seeing it as one input for Hollow.
Is there any way to extend the timeout between words?

You can set Binding.Delay, when binding the ComboBox.SelectedItem.
The following example sets the delay of the binding to 1500ms. Every change of the binding target or source that occurred before the delay has elapsed, will reset the delay timer:
<ComboBox Name="cmb_songs_head"
StaysOpenOnEdit="True"
IsEditable="True"
SelectedItem="{Binding T.SelectedSong, Delay=1500}"
ItemsSource="{Binding T.SelectedSet.Songs}" />
Remarks
The bindings can be simplified to enhance readability:
ComboBox.SelectedItem binds TwoWay by default.
UpdateSourceTrigger.PropertyChanged is the default trigger for properties of ItemsControl.
Update
That's the default search behavior. By typing, the matching item is searched and the match is actually selected.
Since the match is immediately assigned to ComboBox.SelectedItem, it could have unwanted side effects to select something not matching. Especially when the selection triggers an operation.
If you want to auto-select the closest match or make suggestions, I recommend to use collection filtering instead.
I would write an Attached Behavior, which listens to the ComboBox.PreviewTextInput and the TextBoxBase.PreviewKeyUp events to handle the filtering.
The following example handles this events in code-behind instead and assumes that the ComboBox item type is string. The Binding.Delay is set to 5s:
View
<ComboBox ItemsSource="{Binding T.SelectedSet.Songs}"
SelectedItem="{Binding T.SelectedSong, Delay=5000}"
StaysOpenOnEdit="True"
IsEditable="True"
TextBoxBase.PreviewKeyUp="EditTextBox_OnPreviewKeyUp"
PreviewTextInput="ComboBox_OnPreviewTextInput" />
Code-behind
private void EditTextBox _OnPreviewKeyUp(object sender, KeyEventArgs e)
{
var editTextBox = e.OriginalSource as TextBox;
var comboBox = sender as ComboBox;
switch (e.Key)
{
case Key.Back:
{
MainWindow.FilterComboBoxItemsSource(sender as ComboBox, editTextBox.Text, editTextBox);
int selectionStart = comboBox.SelectedItem == null
? editTextBox.CaretIndex
: Math.Max(0, editTextBox.SelectionStart - 1);
int selectionLength = comboBox.SelectedItem == null
? 0
: editTextBox.Text.Length - selectionStart;
editTextBox.Select(selectionStart, selectionLength);
break;
}
case Key.Space:
{
MainWindow.FilterComboBoxItemsSource(sender as ComboBox, editTextBox.Text, editTextBox);
break;
}
case Key.Delete:
{
int currentCaretIndex = editTextBox.CaretIndex;
MainWindow.FilterComboBoxItemsSource(sender as ComboBox, editTextBox.Text, editTextBox);
editTextBox.CaretIndex = currentCaretIndex;
break;
}
}
}
private void ComboBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = true;
var editTextBox = e.OriginalSource as TextBox;
string oldText = editTextBox.Text.Substring(0, editTextBox.SelectionStart);
string newText = oldText + e.Text;
FilterComboBoxItemsSource(sender as ComboBox, newText, editTextBox);
}
private void FilterComboBoxItemsSource(ComboBox comboBox, string predicateText, TextBox editTextBox)
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(comboBox.ItemsSource);
if (!string.IsNullOrWhiteSpace(predicateText)
&& !collectionView.SourceCollection
.Cast<string>()
.Any(item => item.StartsWith(predicateText, StringComparison.OrdinalIgnoreCase)))
{
int oldCaretIndex = editTextBox.CaretIndex == editTextBox.Text.Length
? predicateText.Length
: editTextBox.CaretIndex;
editTextBox.Text = predicateText;
editTextBox.CaretIndex = oldCaretIndex;
return;
}
collectionView.Filter = item => (item as string).StartsWith(string.IsNullOrWhiteSpace(predicateText)
? string.Empty
: predicateText, StringComparison.OrdinalIgnoreCase);
collectionView.MoveCurrentToFirst();
editTextBox.Text = collectionView.CurrentItem as string;
editTextBox.Select(predicateText.Length, editTextBox.Text.Length - predicateText.Length);
}

Related

WPF listbox removing item properly

Hi I am making an WPF application and have a problem with a listbox/listview, MVVM is implemented. I am creating a list of a class that is displayed on the listbox and I am editing the items through selecting an item in the listbox. The problem is when I am deleting an item it doesn't trigger onpropertychanged event to the UI, but is however working in the code, the values are right. When I close the window and reopens it then the list is updated, but not directly when the item is deleted, it never triggers onpropertychanged event for some reason.
It does work to just filter the quicknotelist like
quicknotelist = quicknotelist.where(x => x.id != selecteditem.id);
It works only once though and the UI updates however the selecteditem doesn't seem to work properly even though I am declaring
selecteditem = new quicknote() {*values*};
Part of relevant code, I am using INotifyPropertyChanged
private QuickNote selectedNote = new QuickNote(); // weeeeeee
public QuickNote SelectedNote
{
get
{
return selectedNote;
}
set
{
if (SelectedNote != null)
{
selectedNote = value;
OnPropertyChanged("SelectedNote");
EnableEditNoteBox = true;
}
}
}
private List<QuickNote> quickNoteList = new List<QuickNote>();
public List<QuickNote> QuickNoteList
{
get { return quickNoteList; }
set { quickNoteList = value; OnPropertyChanged("QuickNoteList"); }
}
here is the method that deletes the item
private void DeleteNote(object obj)
{
if (SelectedNote != null)
{
QuickNoteList.Remove(SelectedNote);
// I want this to trigger onpropertychanged without using myclasslist = newclasslist; since it messes up selecteditem to null.
}
}
heres the xaml part.
<ListBox
Width="713"
Height="230"
SelectedItem="{Binding SelectedNote, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
ItemsSource="{Binding QuickNoteList,BindsDirectlyToSource=True, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
DisplayMemberPath="Notes"
Foreground="Black"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.CanContentScroll="False"/>
I'd leave a comment if I could. You should lookup ObservableCollection. I think QuickNoteList should be of this type.

Removing last character from WPF editable combobox

I am using Editable Combo box
<ComboBox Width="200" Height="30" IsEditable="True">
<ComboBoxItem Content="true"/>
<ComboBoxItem Content="false"/>
</ComboBox>
1st issue: If i select true and then delete the last character 'e' then the text box has just tru but the selected Item property change is never fired, I mean the setter of the property data bind to selected item is never called.
2nd issue: If i now open the drop down and try to select true, the text in the text box remains same 'tru' it does not change to true
Vikas
You can "adjust" the behaviour e.g. by using an attached property like this:
The behaviour will be: If the text from the Text property is different from the selected item's text => set selected index to -1 (this also makes selected item null etc.). Tweak to your preferences.
Note: I'm not sure if this works correctly if you bind a value to Enable and change this several times (memory leaks etc.). It's also hardwired to string items. You might need a more general apporach to be really reusable.
public class StrictComboxBox
{
public static readonly DependencyProperty EnableProperty = DependencyProperty.RegisterAttached(
"Enable", typeof (bool), typeof (StrictComboxBox), new PropertyMetadata(defaultValue: default(bool), propertyChangedCallback: EnableChanged));
private static void EnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var cb = d as ComboBox;
if (cb == null)
return;
var value = GetEnable(cb);
if (value)
{
DependencyPropertyDescriptor
.FromProperty(ComboBox.TextProperty, typeof(ComboBox))
.AddValueChanged(cb, TextChanged);
}
else
{
DependencyPropertyDescriptor
.FromProperty(ComboBox.TextProperty, typeof(ComboBox))
.RemoveValueChanged(cb, TextChanged);
}
}
private static void TextChanged(object sender, EventArgs e)
{
var cb = sender as ComboBox;
var selectedTextMatches = cb.SelectedValue != null && ( (cb.SelectedValue as ComboBoxItem).Content as string) == cb.Text;
if (selectedTextMatches == false)
{
cb.SelectedIndex = -1;
}
}
public static void SetEnable(DependencyObject element, bool value)
{
element.SetValue(EnableProperty, value);
}
public static bool GetEnable(DependencyObject element)
{
return (bool) element.GetValue(EnableProperty);
}
}
Usage in xaml would be:
<Window xmlns:my ="clr-namespace:YourNameSpace.ContainingTheStrictComboBoxClass" ...>
<ComboBox Width="200" Height="30" IsEditable="True" my:StrictComboxBox.Enable="True">
<ComboBoxItem Content="true"/>
<ComboBoxItem Content="false"/>
</ComboBox>

How do I prevent keyboard navigation in an AutoCompleteBox from firing the SelectionChanged event?

I'm trying to build a search field using the AutoCompleteBox from the WPF Toolkit. The AutoCompleteBox's Text property is bound to a property in a ViewModel that implements INotifyPropertyChanged. When the property is changed, it fetches new suggestions to show to the user.
This gets mucked up if the user uses arrow keys to scan through the list of autocomplete suggestions before choosing one - the moment the cursor move into the popup, SelectionChanged is fired, the text field gets a new value, and the autocomplete suggestions are re-collected. This also interferes with my desire to use the SelectionChanged event to kick off a search.
Is there any way to prevent the SelectionChanged event from firing on keyboard navigation?
Here's how I have things set up. Note sc:SearchField is a subclass of AutoCompleteBox that only provides a way to access the TextBox property on the AutoCompleteBox so I can call functions like SelectAll()
XAML:
<sc:SearchField x:Name="SearchField" DataContext="{Binding SearchBoxVm}" Text="{Binding Query, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding QuerySuggestions, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" IsTextCompletionEnabled="False" Margin="54,10,117,67" Grid.RowSpan="2" BorderThickness="0" FontSize="14" PreviewKeyUp="searchField_OnKeyup" Foreground="{Binding Foreground, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" FontStyle="{Binding QueryFont, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" >
</sc:SearchField>
ViewModel:
void GetQuerySuggestions()
{
if (!string.IsNullOrEmpty(Query) && !Query.Equals(DEFAULT_TEXT))
{
QueryFont = FontStyles.Normal;
Foreground = Brushes.Black;
QuerySuggestions = SearchAssistant.GetQueryRecommendations(_query);
}
}
public string _query = DEFAULT_TEXT;
public string Query
{
get
{
return _query;
}
set
{
_query = value;
GetQuerySuggestions();
NotifyPropertyChanged("Query");
}
}
List<string> querySuggestions = new List<string>();
public List<string> QuerySuggestions
{
get { return querySuggestions; }
set
{
querySuggestions = value;
NotifyPropertyChanged("QuerySuggestions");
}
}
SearchField subclass:
public class SearchField : AutoCompleteBox
{
public TextBox TextBox
{
get
{
return (this.GetTemplateChild("Text") as TextBox);
}
}
}
Not sure if this is what you are wanting to do but I have the following code which only changes the selection when the 'Enter' key is pressed or the mouse is used to select an item from the list (left mouse button clicked). I can arrow up and down the list without issue and only fire the selection changed event when the user presses enter or clicks on the desired entry.
Note that I am using the AutoCompleteBox and not the SearchField as you are using.
In XAML:
<toolkit:AutoCompleteBox Name="OmniSearchTextBox"
ItemsSource="{Binding CompanyList}"
SelectedItem="{Binding SelectedObject, Mode=TwoWay}"
IsTextCompletionEnabled="False"
FilterMode="Contains"
KeyUp="OmniSearch_KeyUp"
MouseLeftButtonUp="OmniSearch_MouseLeftButtonUp"
Margin="10,94,10,0"
RenderTransformOrigin="0.518,1.92" Height="35"
VerticalAlignment="Top" />
In code behind:
private void OmniSearch_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
BindingExpression exp = this.OmniSearchTextBox.GetBindingExpression(AutoCompleteBox.SelectedItemProperty);
exp.UpdateSource();
}
}
private void OmniSearch_MouseLeftButtonUp(object sender, MouseEventArgs e)
{
BindingExpression exp = this.OmniSearchTextBox.GetBindingExpression(AutoCompleteBox.SelectedItemProperty);
exp.UpdateSource();
}
In ViewModel:
private const string CompanyListPropertyName = "CompanyList";
private ObservableCollection<Company> _companyList;
public ObservableCollection<Company> CompanyList
{
get
{
return _companyList;
}
set
{
if (_companyList == value)
{
return;
}
_companyList = value;
RaisePropertyChanged(CompanyListPropertyName);
}
}
private Company _selectedObject;
public Company SelectedObject
{
get
{
return _selectedObject;
}
set
{
if (_selectedObject != value)
{
_selectedObject = value;
}
}
}

Expand/Collapse button in a Silverlight DataGrid

I am using a RowDetailsTemplate in a Silverlight DataGrid to show row details. Setting RowDetailsVisibilityMode="VisibleWhenSelected" does not give a good user experience (only one row can be expanded at a time, all rows cannot be collapsed). What's the easiest way to add an expand/collapse button on each row so that the rows can be expanded/collapsed independently?
I've been meaning to blog my solution to this.
I set the grid RowDetailsVisibilityMode to Collapsed and use a DataGridTemplateColumn with a styled ToggleButton in it to toggle the row visibility.
The toggle button can be wired up to toggle the row visibility using either binding or through a TriggerAction.
Binding has to be done in code-behind since you are trying to bind ToggleButton.IsChecked to an element that is generated and does not exist in XAML (DataGridRow.DetailsVisibility)
(This will be allowed in SL5 with a stronger RelativeSource binding)
For both solutions I have this extension method in a helper class:
/// <summary>
/// Walk up the VisualTree, returning first parent object of the type supplied as type parameter
/// </summary>
public static T FindAncestor<T>(this DependencyObject obj) where T : DependencyObject
{
while (obj != null)
{
T o = obj as T;
if (o != null)
return o;
obj = VisualTreeHelper.GetParent(obj);
}
return null;
}
For code-behind binding method:
private void ToggleButton_Loaded(object sender, RoutedEventArgs e)
{
ToggleButton button = sender as ToggleButton;
DataGridRow row = button.FindAncestor<DataGridRow>(); //Custom Extension
row.SetBinding(DataGridRow.DetailsVisibilityProperty, new Binding()
{
Source = button,
Path = new PropertyPath("IsChecked"),
Converter = new VisibilityConverter(),
Mode = BindingMode.TwoWay
});
}
For TriggerAction method:
public class ExpandRowAction : TriggerAction<ToggleButton>
{
protected override void Invoke(object o)
{
var row = this.AssociatedObject.FindAncestor<DataGridRow>();
if (row != null)
{
if (this.AssociatedObject.IsChecked == true)
row.DetailsVisibility = Visibility.Visible;
else
row.DetailsVisibility = Visibility.Collapsed;
}
}
}
Then in XAML:
<sdk:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ToggleButton Style="{StaticResource PlusMinusToggleButtonStyle}" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<behaviors:ExpandRowAction/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ToggleButton>
</DataTemplate>
</sdk:DataGridTemplateColumn.CellTemplate>

Silverlight Datagrid Row Click

I have a datagrid with a column containing a checkbox. I want to change the value of the bound Selected property when the row is clicked:
alt text http://lh4.ggpht.com/_L9TmtwXFtew/Sw6YtzRWGEI/AAAAAAAAGlQ/pntIr2GU6Mo/image_thumb%5B3%5D.png
NOTE: I don't want to use the SelectedItemChanged event because this doesn't work properly when there is only one row in the grid.
As is often the way i have found my own solution for this:
Add a MouseLeftButtonUp event to the datagrid:
<data:DataGrid x:Name="dgTaskLinks"
ItemsSource="{Binding TaskLinks}"
SelectedItem="{Binding SelectedTaskLink, Mode=TwoWay}"
MouseLeftButtonUp="dgTaskLinks_MouseLeftButtonUp"
>...
And walk the visual tree to get the data grid row:
private void dgTaskLinks_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
///get the clicked row
DataGridRow row = MyDependencyObjectHelper.FindParentOfType<DataGridRow>(e.OriginalSource as DependencyObject);
///get the data object of the row
if (row != null && row.DataContext is TaskLink)
{
///toggle the IsSelected value
(row.DataContext as TaskLink).IsSelected = !(row.DataContext as TaskLink).IsSelected;
}
}
Once found, it is a simple approach to toggle the bound IsSelected property :-)
Hope this helps someone else.
Here is an even simpler solution
XAML
<data:DataGrid
x:Name="dgMyDataGrid"
ItemsSource="{Binding MyList}"
SelectedItem="{Binding MyList, Mode=TwoWay}"
MouseLeftButtonUp="dgMyDataGrid_MouseLeftButtonUp">...
CS
private void dgMyDataGrid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
DataGrid dg = (sender as DataGrid);
var allObjects = dg.DataContext as List<MyCustomObject>;
foreach(var o in allObjects)
{
o.Selected = false;
}
MyCustomObject SelectedObject = (MyCustomObject)dg.SelectedItem;
SelectedObject.Selected = true;
}
Note: this as well as the other example assumes your class that you are binding to the control implements INotifyPropertyChanged

Resources