I am having issues with a ComboBoxselecting the first entered character, which then causes the a problem where the second entered character overwrites the first one.
EDIT: A small explanation of what I an trying to do.
I have set up the ComboBox to act as an autocomplete control. When I enter a character, I am using CollectionView class to filter any names that match each entered character. Upon entered text the ComboBox drop down menu needs to open up, which is why I am binding to IsDropDownOpen. This is how it is supposed to look here.
This is beyond me, I can't research what I need to do to stop this behavior.
Here is a screen shot of what I mean.
This is the ComboBox XAML:
<ComboBox Style="{StaticResource ComboBoxToggleHidden}"
DisplayMemberPath="FullName" SelectedValuePath="Key"
IsTextSearchEnabled="False"
IsEditable="True"
StaysOpenOnEdit="True"
Text="{Binding Path=EnteredText, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Path=Employees}"
SelectedItem="{Binding UpdateSourceTrigger=PropertyChanged, Path=SelectedEmployee}"
IsDropDownOpen="{Binding IsDropDown}">
</ComboBox>
EDIT: I have narrowed it down to this, IsDropDown = true;, commenting this out fixes the issue. But I need the drop down when editing the ComboBox
In the EnteredText property
private string _enteredText;
public string EnteredText
{
get { return _enteredText; }
set
{
_enteredText = value;
Filter(value);
IsDropDown = true;
OnPropertyChanged();
}
}
public bool IsDropDown { get; set; }
OK, I solved this doing a hack, but it will have to do until I can figure out why this behavior is happening.
I created an KeyUpEvent Event in the constructor,
EventManager.RegisterClassHandler(typeof(TextBox), TextBox.KeyUpEvent,
new RoutedEventHandler(DeselectText));
Then in the Handler I just deselected the text.
private void DeselectText(object sender, RoutedEventArgs e)
{
var textBox = e.OriginalSource as TextBox;
if (textBox == null) return;
if (textBox.Text.Length >= 2) return;
textBox.SelectionLength = 0;
textBox.SelectionStart = 1;
}
I know this is a hack, but I have no choice until the correct solution is posted.
This is how it looks with the hack.
Consider this as another solution. It mights resove the side effect of above mentioned problem.
I am expecting the TemplateChild to have the Name (PART_EditableTextBox). If you are changing the name in the Template then please do the necessary changes here as well.
private TextBox EditableTextBox => (TextBox)GetTemplateChild("PART_EditableTextBox");
protected override void OnDropDownOpened(EventArgs e)
{
EditableTextBox.Select(Text.Length, 0);
base.OnDropDownOpened(e);
}
Related
I've got a simple DataGrid bound to an ObservableCollection of view models. One column is a DataGridTemplateColumn whose CellTemplate is a TextBlock and CellEditingTemplate is a TextBox (I realize I could use a DataGridTextColumn, but I want to be explicit about which controls to use). The TextBox is configured to validate on errors.
<DataGrid
ItemsSource="{Binding People}"
AutoGenerateColumns="False"
>
<DataGrid.Columns>
<DataGridTemplateColumn Header="First Name">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<TextBox Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
The view model for each item (PersonViewModel) simply defines a FirstName property, and implements IDataErrorInfo for validation. I'm using the MVVM Light Toolkit for property notification.
public class PersonViewModel : ViewModelBase, IDataErrorInfo
{
private string firstName;
public string FirstName
{
get => firstName;
set => Set(nameof(FirstName), ref firstName, value);
}
public string this[string columnName]
{
get
{
if (columnName == nameof(FirstName))
{
if (string.IsNullOrEmpty(FirstName))
return "First name cannot be empty";
}
return null;
}
}
public string Error => null;
}
In addition, I've got a button that, when clicked, sets the FirstName of every person to "Hello".
public ICommand HelloCommand =>
new RelayCommand(() =>
{
foreach (var person in People)
person.FirstName = "Hello";
});
Here's the issue: when I enter an invalid FirstName (that is, I set it to the empty string), and then click on the Hello button, it correctly replaces the FirstName with "Hello". But then if I try to edit it again, the FirstName is immediately replaced with the empty string.
As a test, I made it so that it's an error to have the string "a". Doing the same steps as above, the "Hello" string is replaced with "a" in the TextBox. It's as if the TextBox doesn't know that FirstName was changed to "Hello," even though the TextBlock correctly displays it and they're both bound to the same property.
Does anyone know what's going on or ways in which to solve this issue? The behavior I expected was for the TextBox to contain the value the bound property changed to (regardless of whether there was a validation error on that TextBox).
Note: I'm using .NET 4.0 because I have to.
Obviously, the reason of this behavior is that at the time of HelloCommand executing, the TextBox that initiated validation error does not already exist. And hence clearing of validation error can't proceed usual way.
It is hard to say why the new value is not taken from the ViewModel at the moment of new TextBox creating and binding restoring. Even more interesting is where this erroneous value comes from. One may think, that if we take the new value of the FirstName property from the ViewModel and set it to TextBox.Text property in the DataContext_Changed event handler of the TextBox, then it should solve problem, because at this moment both the TextBox and the ViewModel are having a new (valid) value, and there are just no room where to take the wrong one from. But magically it still comes from somewhere :)
Happily, there is a trick that helps to get around the problem. The idea is to cancel all pending binding operations before executing HelloCommand. It is not too obvious why it should work. After all at this point the BindingExpression that caused the error is not exists. Nevertheless it works.
Give name to DataGrid:
<DataGrid x:Name="myGrid" ItemsSource="{Binding People}" AutoGenerateColumns="False">
Add click handler to the button:
<Button Content="Hello" Command="{Binding HelloCommand}" Click="Hello_Click"/>
with that code
private void Hello_Click(object sender, RoutedEventArgs e)
{
foreach (var bg in BindingOperations.GetSourceUpdatingBindingGroups(myGrid))
bg.CancelEdit();
}
UPD
As GetSourceUpdatingBindingGroups is not available in .NET 4.0, you could try this way:
private void Hello_Click(object sender, RoutedEventArgs e)
{
for (int i = 0; i < myGrid.Items.Count; i++)
{
DataGridRow row = (DataGridRow)myGrid.ItemContainerGenerator.ContainerFromIndex(i);
if (row != null && Validation.GetHasError(row))
{
row.BindingGroup?.CancelEdit();
}
}
}
It is not so elegant, but does literally the same.
I am trying to make a WPF listbox replicate the behaviour of an old Winforms CheckedListBox, or the checked list box used in e.g. AnkhSVN. I have seen examples that show how to use a DataTemplate to create a check box for every time (e.g. Wpf CheckedListbox - how to get selected item), but this feels very clunky compared to the winforms control:
The logic of "If the user changes a check state, ensure that check state changes for all selected items" is not present by default.
The hit area to change an item from checked to unchecked is the box /and/ the title, rather than just the box as in Winforms
I can handle the first issue by adding a listener to the PropertyChanged event on each item in the bound collection, and if IsChecked changes, then set IsChecked to the same value for all currently selected items.
However, I cannot find a good solution to the second issue. By splitting the DataTemplate into a Checkbox with no title, and a TextBlock with the title, I can reduce the hit area to change the check state to only the desired square. However, all mouse interaction which hits the TextBlock does nothing - I would like it to behave the same as in a normal listbox, or in the dead space outside of the Textblock: If the user is holding shift, then select everything up to and including this item, if not, then clear the selection and select only this item. I could try to implement something where I handled Mouse* events on the TextBlock, but that seems brittle and inelegant - I'd be trying to recreate the exact behaviour of the ListBox, rather than passing events to the listbox.
Here's what I've got currently:
XAML:
<ListBox x:Name="_lstReceivers" SelectionMode="Extended" Margin="10,41,6,15"
ItemsSource="{Binding Receivers}">
<ListBox.ItemTemplate>
<DataTemplate>
<ListBoxItem>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsChecked}" IsHitTestVisible="True"/>
<TextBlock Text="{Binding Item}" Background="{x:Null}" IsHitTestVisible="False"/><!--Attempt to make it pass mouse events through. Doesn't work. Yuk.-->
</StackPanel>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Code behind to get the "Change all checks at the same time" logic (removed some error handling for clarity):
private void ListBoxItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var item = sender as CheckableItem<Receiver>;
if (item == null)
return;
if (e.PropertyName == nameof(CheckableItem<Receiver>.IsChecked))
{
bool newVal = item.IsChecked;
foreach (CheckableItem<Receiver> changeItem in _lstReceivers.SelectedItems)
{
changeItem.IsChecked = newVal;
}
}
}
By trying various combinations of Background = "{x:Null}" and IsHitTestVisible="False", I did manage to get the entire item to not respond to mouse click events - but I could not make it have only the Checkbox respond to mouse events, while everything else is passed to the ListBox for proper selection processing.
Any help would be greatly appreciated.
Answering my own question again.
Well, I couldn't find a clean way to do it, so I ended up setting the ListBoxItem to have IsHitTestVisible="False", and manually tracing mouse events using PreviewMouseDown.
Final code:
XAML:
<ListBox x:Name="_lstReceivers" SelectionMode="Extended" Margin="10,41,6,15"
ItemsSource="{Binding Receivers}" PreviewMouseDown="_lstReceivers_MouseDown">
<ListBox.ItemTemplate>
<DataTemplate>
<ListBoxItem IsSelected="{Binding IsSelected}" IsHitTestVisible="False">
<StackPanel Orientation="Horizontal" Background="{x:Null}">
<CheckBox IsChecked="{Binding IsChecked}" IsHitTestVisible="True" Checked="CheckBox_Checked" Unchecked="CheckBox_Checked"/>
<TextBlock Text="{Binding Item}" Background="{x:Null}" IsHitTestVisible="False"/>
</StackPanel>
</ListBoxItem>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Code behind:
//Logic to handle allowing the user to click the checkbox, but have everywhere else respond to normal listbox logic.
private void _lstReceivers_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
Visual curControl = _lstReceivers as Visual;
ListBoxItem testItem = null;
//Allow normal selection logic to take place if the user is holding shift or ctrl
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
return;
//Find the control which the user clicked on. We require the relevant ListBoxItem too, so we can't use VisualTreeHelper.HitTest (Or it wouldn't be much use)
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(curControl); i++)
{
var testControl = (Visual)VisualTreeHelper.GetChild(curControl, i);
var rect = VisualTreeHelper.GetDescendantBounds(testControl);
var pos = e.GetPosition((IInputElement)curControl) - VisualTreeHelper.GetOffset(testControl);
if (!rect.Contains(pos))
continue;
else
{
//There are multiple ListBoxItems in the tree we walk. Only take the first - and use it to remember the IsSelected property.
if (testItem == null && testControl is ListBoxItem)
testItem = testControl as ListBoxItem;
//If we hit a checkbox, handle it here
if (testControl is CheckBox)
{
//If the user has hit the checkbox of an unselected item, then only change the item they have hit.
if (!testItem.IsSelected)
dontChangeChecks++;
((CheckBox)testControl).IsChecked = !((CheckBox)testControl).IsChecked;
//If the user has hit the checkbox of a selected item, ensure that the entire selection is maintained (prevent normal selection logic).
if (testItem.IsSelected)
e.Handled = true;
else
dontChangeChecks--;
return;
}
//Like recursion, but cheaper:
curControl = testControl;
i = -1;
}
}
}
//Guard variable
int dontChangeChecks = 0;
//Logic to have all selected listbox items change at the same time
private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
if (dontChangeChecks > 0)
return;
var newVal = ((CheckBox)sender).IsChecked;
dontChangeChecks++;
try
{
//This could be improved by making it more generic.
foreach (CheckableItem<Receiver> item in _lstReceivers.SelectedItems)
{
item.IsChecked = newVal.Value;
}
}
finally
{
dontChangeChecks--;
}
}
This solution works, but I don't like the coupling it introduces between my code and the exact behaviour of the ListBox implementation:
Checking the Keyboard state
It won't handle dragging if the user starts dragging inside a checkbox
It should happen on mouseup, not mousedown. But it's close enough for my needs.
PS: The bound class, even though it's irrelevant and obvious what it would have:
public class CheckableItem<T> : INotifyPropertyChanged
{
public T Item { get; set; }
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected == value)
return;
_isSelected = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected)));
}
}
private bool _checked;
public bool IsChecked
{
get => _checked;
set
{
if (_checked == value)
return;
_checked = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsChecked)));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Basically what I am trying to do is have it where you can type something into a ComboBox and it autocompletes to something from it's drop down, but the user shouldn't be able to enter their own entry.
I know that by default if you have "isEditable" equal to true then it autocompletes on it's own. However you can still enter in whatever you want. I want to prevent this.
This is how I am pulling my names in
void populateNames()
{
nameBox = this.nameTextBox;
APICaller Caller = new APICaller();
try
{
List<string> listOfNames = Caller.APIGetNames();
foreach (string a in listOfNames)
{
Console.WriteLine(a);
nameBox.Items.Add(a);
}
}
catch (Exception e)
{
Console.WriteLine("Something went wrong: " + e);
}
nameBox.SelectedIndex = 0;
}
and the ComboBox in the XAML
<ComboBox
Name="nameTextBox" Height="23" Width="Auto" Margin="10,0,10,97" VerticalAlignment="Bottom" IsEditable="True"
PreviewTextInput="tbxPreviewTextInput" DataObject.Pasting="tbxPasting" LostFocus="nameTbxLostFocus"
GotFocus="nameTbxGotFocus" PreviewKeyDown="classTextBox_PreviewKeyDown" HorizontalAlignment="Stretch"/>
Under your properties tab, set your AutoComplete source to the list of items that populates the combo box. Then, set your AutoCompleteMode to Suggest or SuggestAppend.
I have a Combobox in WPF, I have set Is Editable="true" which allows me enter any text in the combobox. I would like to restrict users from entering text outside datasource.
Xaml:
<ComboBox Name="service" Margin="0,0,0,4"
IsEditable="True"
Grid.Column="1"
Grid.ColumnSpan="2" Grid.Row="4"
SelectedValuePath="Id"
DisplayMemberPath="Service"
SelectedValue="{Binding Controller.Service1}"
ItemsSource="{Binding}" />
C#:
System.Data.DataView vw = tableAdapterServices.GetData().DefaultView;
service.ItemsSource = vw;
service.SelectedIndex = 0;
I do not want to allow users to enter text which is not present in the datasource, or handle it if the user enters any other text.
Update:
Thanks for the solution #Vishal, LostFocus event is handling the issue, but it gave rise to another issue. I have a button which is used to submit the combobox value along with other textbox values to the server. I am setting default value in the combobox in lostfocus event. But I need to prevent the button click event if some value other that datasource value is added in combobox.
You can check for selectedIndex in Lostfocus event :
private void ComboBox_LostFocus(object sender, EventArgs e)
{
if(((ComboBox)sender).SelectedIndex == -1)
{
//Text entered by user is not a part your ItemsSource's Item
SaveButton.IsEnabled = false;
}
else
{
//Text entered by user is a part your ItemsSource's Item
SaveButton.IsEnabled = true;
}
}
You can try handling the ComboBox's TextInput or PreviewTextInput events, doing the text search yourself, selecting the most appropriate item, and setting "e.Handled = true."
This works for a single character (i.e. if you enter the letter "j", it will select the first item that contains a "j" or "J"), but I'm sure there's a way to do this with your control. Just include a little more logic to achieve this.
private void MyComboBox_PreviewTextInput(object sender, TextCompositionEventArgs e) {
foreach (ComboBoxItem i in MyComboBox.Items) {
if (i.Content.ToString().ToUpper().Contains(e.Text.ToUpper())) {
MyComboBox.SelectedItem = i;
break;
}
}
e.Handled = true;
}
Ok, from what I understand the behaviour of the combobox should disregard an inserted character if there is no item in the datasource that contains the resulted string.
I believe you are doing it in a MVVM style since you are using binding. What you can do is to bind the ComboBox text to a property and the PreviewTextInput event to a command and do the filtering there.
XAML:
<ComboBox Name="service"
Margin="0,0,0,4"
IsEditable="True"
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="4"
SelectedValuePath="Id"
DisplayMemberPath="Service"
SelectedValue="{Binding Controller.Service1}"
ItemsSource="{Binding}"
Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewTextInput">
<cmd:EventToCommand Command="{Binding TextInputCommand}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Combobox>
C# ViewModel:
public RelayCommand<object> TextInputCommand { get; set; }
public bool CanExecuteTextInputCommand(object param)
{
return true;
}
public void ExecuteTextInputCommand(object param)
{
TextCompositionEventArgs e = param as TextCompositionEventArgs;
string currentText = this.Text;
string entireText = string.Format("{0}{1}", currentText, e.Text);
var item = this.Items.Where(d => d.StartsWith(entireText)).FirstOrDefault();
if (item == null)
{
e.Handled = true;
this.Text = currentText;
}
}
Where Items is the ObservableCollection containing the items (in this case it's a list of strings) and Text is the property binded to the Combobox text.
EDIT: Ok so what you need to do to make it work is to go to your project, right click on References, choose Manage NuGet Packages, search and install MVVM Light. Two dlls that start with GalaSoft will be added to your references. After this, in your xaml code add these namespaces:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
What this allows you to do is to bind an event to a ICommand object.
I have a text box that will retain the last 10 entries entered, similar to the search box in Internet Explorer. The user can click on the dropdown menu to see last 10 entries. The drop down menu is a combo box. I created an Observable collection of strings that is bound to the combo box Itemssource.Below is the code.
Xaml
<Grid x:Name="TextBox_grid" Margin="0,0,40,0" Width="360" Height="23">
<ComboBox Name="cb" Margin="0,0,-29,0" Style="{DynamicResource Onyx_Combo}" ItemsSource="{Binding TextEntries, ElementName=TheMainWindow, Mode=OneWay}" IsEditable="False" Visibility="Visible" />
<Rectangle Fill="#FF131210" Stroke="Black" RadiusX="2" RadiusY="2"/>
<TextBox Name=UniversalTextBox Margin="0" Background="{x:Null}" BorderBrush="{x:Null}" FontSize="16" Foreground="#FFA0A0A0" TextWrapping="Wrap" PreviewKeyDown="TextBox_PreviewKeyDown"/>
</Grid>
Code
public partial class Window1 : Window
{
private ObservableCollection<string> m_TextEntries = new ObservableCollection<string>();
public Window1()
{
InitializeComponent();
}
public ObservableCollection<string> TextEntries
{
get { return m_TextEntries; }
}
private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null)
return;
if (e.Key == Key.Enter)
{
PopulateHistoryList(textBox.Text);
e.Handled = true;
}
if (e.Key == Key.Escape)
{
e.Handled = true;
}
}
private void PopulateHistoryList(string text)
{
m_TextEntries.Add(text);
}
private event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
The above code will populate the TextEntries collection when the Enter Key is pressed on the textbox. I need two things
How do I set the Selected Item of the combo box and how can I bind that to my text box.
The combobox(dropmenu) should only show the last 10 entries from the drop down menu.
Thanks in advance,
Using Expesssion Blend, binding the value of a property of one control to the property value of another control is easy, and it is known as ElementProperty Binding, here is a screenshot of where you access the ability to create this within Blend, note that the Textbox is the selected element in the Objects and Timeline panel, and it's the 'little box' to the right of the Text property in the properties panel which has been clicked to produce the context menu pictured...
Once you have selected 'Element Property Binding' for the text property of your textbox, your cursor will become a little bullseye icon, which you now will use to indicate what you want to bind to, by clicking it in either the design canvas or the Objects and Timeline panel, while the cursor appears that way...
Here we see the 'SelectedValue' property of the combo box being selected as the source of what is displayed in the textbox. Once done, the textbox will automagically be immediately set to display whatever is selected in the combo. Be sure to take a look at what Blend is doing in your XAML when you do this, as it will help you better understand what is actually going on, and might even teach you a thing or two about the binding syntax of XAML.
As for the list only ever having the last ten entries...there are several ways to do this, each one more or less appropriate, depending on the surrounding context, but here is one way; simply run a procedure similar to this one, whenever the box has entries added to it:
// assuming 'listItems' is your ObservableCollection
string[] items = listItems.ToArray();
// prepare a new array for the current ten
string[] tenItems = new string[10];
// copy a subset of length ten, to the temp array, the set your ObservableCollection to this array.
Array.Copy(items, (items.Length - 10), tenItems, 0, 10);
Note: The Array .Copy assumes the only way items are getting added to the observable collection is by some form of .Add, which always adds them to the end of the list...
Part of the answer
<TextBlock Text="{Binding ElementName=cb, Path=SelectedValue}" />
<ComboBox x:Name="cb" ItemsSource="{Binding Path=Fields}" SelectedValue="{Binding Path=SelectedValue}" />
And if you set the datacontext of the window
DataContext="{Binding RelativeSource={RelativeSource self}}">