Hey I'm trying to create a combobox that allows a user to select an item from the list (shown in list as fx. "2324 - James - 21"), put a property from that item in the textbox (fx. "James"), but also allow for user input of that one property. (fx. "Jack")
What I've got so far is a combobox that allows for user input, but when an item is selected, instead of the property (As specified in SelectedValuePath), the items ToString is put in the combobox text field, instead of just the name.
Any ideas how to fix it? This is my setup now:
Person class:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return $"{Id} - {Name} - {Age}";
}
}
XAML:
<ComboBox Text="{Binding PersonName, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding People, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
SelectedValue="{Binding PersonName, UpdateSourceTrigger=PropertyChanged}"
IsEditable="True" SelectedValuePath="Name" />
Edit:
People is an ObservableCollection<Person>, PersonName is a string bound to a field personName and OnPropertyChanged(); is called in its set method. The ViewModel Inherits from an ObservableObject class that implements INotifyPropertychanged.
Edit 2:
Okay, so I've been trying this instead:
<ComboBox Text="{Binding PersonName, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding People, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
SelectedItem="{Binding Person, UpdateSourceTrigger=PropertyChanged}"
IsEditable="True" />
Aaand in the viewmodel:
public Person SelectedPerson
{
get => selectedPerson;
set
{
selectedPerson = value;
OnPropertyChanged();
if (SelectedPerson != null) PersonName = SelectedPerson.Name;
}
}
Buuut, when I select a person from the list, the combbox text field is set to Persons ToString value, not SelectedPerson.Name... Or rather; it's probably set to SelectedPerson.Name first, but then SelectedPerson.ToString() automatically by the ComboBox :/
Edit 3:
Alright, I've been trying to interrupt the text change, when it comes from SelectionChanged in a really hacky way, but I can't figure out how to "Cancel" a text change. Here's my code so far, but TextChanged doesn't seem to allow for reverting the text, as far as I can see, and PreviewTextInput is only triggered when the change is form user input :/.
Useless code removed. Almost identical to edit 4, but without the "oldtext" field.
Edit 4:
I solved it, in a horrible way. It works, but I really wish there was a better way. Maybe one of you has a better solution? :) Anyway this is the solution I came to:
Dependency property thing that reverts text after it's been forcefully changed by the combobox:
public class ComboBoxBehaviour
{
private static bool overrideTextChange;
private static string oldText;
public static bool GetDisconnectTextFromSelectedItem(ComboBox comboBox)
{
return (bool)comboBox.GetValue(DisconnectTextFromSelectedItemProperty);
}
public static void SetDisconnectTextFromSelectedItem(ComboBox comboBox, bool value)
{
comboBox.SetValue(DisconnectTextFromSelectedItemProperty, value);
}
public static readonly DependencyProperty DisconnectTextFromSelectedItemProperty =
DependencyProperty.RegisterAttached(
"DisconnectTextFromSelectedItem",
typeof(bool),
typeof(ComboBoxBehaviour),
new UIPropertyMetadata(false, OnDisconnectTextFromSelectedItemChanged));
private static void OnDisconnectTextFromSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var comboBox = d as ComboBox;
if (comboBox == null) return;
if (e.NewValue is bool == false) return;
if ((bool)e.NewValue)
{
comboBox.SelectionChanged += HandleSelection;
comboBox.Loaded += ComboBoxOnLoaded;
}
else
{
comboBox.SelectionChanged -= HandleSelection;
comboBox.Loaded -= ComboBoxOnLoaded;
}
}
private static void ComboBoxOnLoaded(object sender, RoutedEventArgs e)
{
ComboBox comboBox = (ComboBox)e.Source;
TextBox txtBox = (TextBox)comboBox.Template.FindName("PART_EditableTextBox", comboBox);
txtBox.TextChanged += TxtBoxOnTextChanged;
}
private static void TxtBoxOnTextChanged(object sender, TextChangedEventArgs e)
{
if (overrideTextChange)
{
((TextBox) e.Source).Text = oldText;
overrideTextChange = false;
e.Handled = true;
}
}
private static void HandleSelection(object sender, RoutedEventArgs e)
{
ComboBox comboBox = (ComboBox)e.Source;
TextBox txtBox = (TextBox)comboBox.Template.FindName("PART_EditableTextBox", comboBox);
oldText = txtBox.Text;
overrideTextChange = true;
e.Handled = true;
}
}
Xaml combobox:
<ComboBox Text="{Binding PersonName, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding People, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
SelectedItem="{Binding Person, UpdateSourceTrigger=PropertyChanged}"
IsEditable="True" helpers:ComboBoxBehaviour.DisconnectTextFromSelectedItem="True" />
Edit 5:
Close, but no cigar. turns out calling
((TextBox) e.Source).Text = oldText;
does not update my combobox text binding, so it shows the correct text, but the string in the viewmodel is wrong - it's the ToString text, instead of the short text. So how do I force the combobox binding to refresh, when I don't have access to the combobox in the text change context? More "hacking"? Hmm...
Edit 6:
Alright I got it working by changing TxtBoxOnTextChanged to this;
private static void TxtBoxOnTextChanged(object sender, TextChangedEventArgs e)
{
if (overrideTextChange) //The combo box changed the text of the textbox.
{
TextBox textBox = (TextBox) e.Source; //The textbox
ComboBox comboBox = (ComboBox)textBox.TemplatedParent; //The combobox.
comboBox.Text = oldText; //Set the text of the combobox to update the property on the viewmodel.
textBox.Text = oldText; //Set the text of the textbox manually, because setting the combobox text doesn't change it for some reason.
overrideTextChange = false;
e.Handled = true; //Done!
}
}
Still hoping someone's got a better solution, but it works!
If you want to display the value of the Name property instead of ToString(), you should set the DisplayMemberPath property to "Name".
I need to show ToString in the list, but Name in the input field.
Then you should set PersonName to the string that you want to display in the input field and don't bind SelectedValue to PersonName.
You can only actually select a value that's in the source collection.
Related
I am trying to implement WPF Combobox Autocomplete TextSearch like "Contains" instead of "Start with".
Couple of question threads are there but could not find any concrete solution.
I was following the answer by #Evgenii:
WPF combobox textsearch with contains
In the SetText(DependencyObject element, string text) method, the value of "text" parameter is always a "DeviceNumber" string. So my text is not reflecting there.
Here is my own sample code
https://drive.google.com/open?id=1eqK5bh5SQJPxHeb-zzOuBHIpYapv-h18
Any reason?
Is anyone successfully implemented Text Search with Contains?
Please guide.
I thank you for every answer I get but working code is much appreciable :)
i recommend using AutoCompleteBox, it 's just like ComboBox, it has ItemsSource and SelectedItem and all like ComboBox
you can use it property 'AutoCompleteBox.FilterMode' which take AutoCompleteFilterMode enumeration, the enumerations include:Contains, ContainsCaseSensitive, ContainsOrdinal
and other helpful ...
here is how you use it:
https://www.broculos.net/2014/04/wpf-autocompletebox-autocomplete-text.html
and here it an example of using filter mode:
https://learn.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/dd833103(v=vs.95)?redirectedfrom=MSDN
make custom combobox control.
public class SearchComboBox : ComboBox
{
TextBox editableTextBox;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
editableTextBox = GetTemplateChild("PART_EditableTextBox") as TextBox;
editableTextBox.TextChanged += EditableTextBox_TextChanged;
}
private void EditableTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
ICollectionView ICV = ItemsSource as ICollectionView;
if(ICV != null)
{
if (string.IsNullOrEmpty(editableTextBox.Text.Trim()))
ICV.Filter = null;
else
ICV.Filter = new Predicate<object>(i => ((Equipment)i).equipmentLabel.Contains(editableTextBox.Text));
IsDropDownOpen = true;
}
}
}
modify you EquipmentScreenViewModel Code. add ICollectionView type property
public class EquipmentScreenViewModel
{
public string SelectedEquipmentRego { get; set; }
public ObservableCollection<Equipment> AllEquipments { get; set; }
private ICollectionView _allEquipCollection = null;
public ICollectionView AllEquipCollection
{
get
{
if (_allEquipCollection == null && AllEquipments != null)
{
_allEquipCollection = CollectionViewSource.GetDefaultView(AllEquipments);
}
return _allEquipCollection;
}
}
}
XAML
<Grid>
<local:SearchComboBox x:Name="cmbAlternativeAsset"
Width="200" IsEditable="True"
FontSize="12" Foreground="#494949"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
SelectedItem="{Binding SelectedEquipmentRego, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding AllEquipCollection}" SelectedValuePath="equipmentRego"
DisplayMemberPath="equipmentLabel" IsTextSearchEnabled="False"
/>
</Grid>
Binding ItemsSource to CollectionView and IsTextSearchEnabled false. Good Luck
I have a combobox binded to a table called Tenderness through MVVM. I'm using Entity Framework.It displays all the records properly, but I need to add another functionality to it. Suppose the user types in text which is not contained inside the combobox's Itemssource, I want to be able to add it directly to the table and then update the Itemssource as well. Now I have been able to do this without MVVM, I would want to know, how to achieve it using MVVM.
Just do what you did previously in the LostFocus event handler in the setter of a source property that you bind to the Text property of the ComboBox.
View Model:
public ObservableCollection<string> Items { get; } = new ObservableCollection<string>() { "a", "b", "c" };
private string _text;
public string Text
{
get { return _text; }
set
{
_text = value;
OnPropertyChanged(nameof(Text));
//add the missing value...
if (!Items.Contains(_text))
Items.Add(_text);
}
}
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged(nameof(SelectedItem));
}
}
View:
<ComboBox IsEditable="True" Text="{Binding Text, UpdateSourceTrigger=LostFocus}" ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}" />
In two combobox A and B.
A's ItemsSource is Custom list. and B's ItemsSource is UserControl list.
When manually setting the SelectedItem, A combobox works well, but B combobox UI do not show the selected Item. (In debugging, SelectedItem's value mapping is right, but the combobox B's UI do not be changed.)
All the other structure is same between A and B. What is the reason?
MainWindow.xaml
...
<ComboBox ItemsSource="{Binding FruitList}" SelectedItem="{Binding SelectedFruit}"
DisplayMemberPath="FruitName" />
<Button Content="Button" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="75" Click="Button_Click"/>
<ComboBox ItemsSource="{Binding UserControlList}" SelectedItem="{Binding SelectedUserControl}" DisplayMemberPath="ItemName" />
<Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" Click="Button_Click2"/>
</Grid>
MainWindow.xaml.cs
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
FruitList.Add(f1);
FruitList.Add(f2);
FruitList.Add(f3);
UserControlList.Add(u1);
UserControlList.Add(u2);
UserControlList.Add(u3);
}
Fruit f1 = new Fruit { FruitName = "Apple" };
Fruit f2 = new Fruit { FruitName = "Banana" };
Fruit f3 = new Fruit { FruitName = "Lemon" };
MyUserControl u1 = new MyUserControl { ItemName = "Apple" };
MyUserControl u2 = new MyUserControl { ItemName = "Banana" };
MyUserControl u3 = new MyUserControl { ItemName = "Lemon" };
ObservableCollection<Fruit> _FruitList = new ObservableCollection<Fruit>();
public ObservableCollection<Fruit> FruitList
{
get { return _FruitList; }
set
{
_FruitList = value;
OnPropertyChanged();
}
}
Fruit _SelectedFruit;
public Fruit SelectedFruit
{
get { return _SelectedFruit; }
set
{
_SelectedFruit = value;
OnPropertyChanged();
}
}
ObservableCollection<MyUserControl> _UserControlList = new ObservableCollection<MyUserControl>();
public ObservableCollection<MyUserControl> UserControlList
{
get
{
return _UserControlList;
}
set
{
_UserControlList = value;
OnPropertyChanged();
}
}
MyUserControl _SelectedUserControl;
public MyUserControl SelectedUserControl
{
get { return _SelectedUserControl; }
set
{
_SelectedUserControl = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged([CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.SelectedFruit = f3;
}
private void Button_Click2(object sender, RoutedEventArgs e)
{
this.SelectedUserControl = u3;
}
}
public class Fruit
{
public string FruitName { get; set; }
}
}
UserControl
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
}
public string ItemName { get; set; }
}
This is not the good way of achieving this. Better define the ItemTemplate for the combobox to have the UserControl in it like:
<ComboBox ItemsSource="{Binding ItemList}" SelectedItem="{Binding SelectedItem}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<myControls:MyUserControl/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
and define the class Item
public class Item
{
public string ItemName { get; set; }
}
ObservableCollection<Item> _ItemsList = new ObservableCollection<Item>();
public ObservableCollection<Item> ItemsList
{
get
{
return _ItemsList ;
}
set
{
_ItemsList = value;
OnPropertyChanged();
}
}
Here DataContext of your UserControl will be Item object. you can bind the ItemName within you user control to show it in anyway you want.
in your user control you can have:
<TextBlock Text="{Binding ItemName}"></TextBlock>
Since you have asked "What is the reason?":
The reason why the second combo box does not show any selection is that ComboBox handles items of type ContentControl specially. In the read-only selection box, it is not the ContentControl that is used to display the value, but the content of the ContentControl. Since a UserControl is a ContentControl, the content of the UserControl is displayed inside the selection box, and therefore you have lost the data context of the UserControl; in the end, an empty string is displayed even though SelectedItem contains a reference to the UserControl that still has a valid data context. (As far as I know this behavior is undocumented; but you can see that it works like this by examining the ComboBox's code on http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/ComboBox.cs, especially the UpdateSelectionBoxItem() method).
By setting IsEditable="True" on the second ComboBox, you can see that everything works fine if the combo box has no read-only selection box.
Therefore, you generally should avoid adding UI elements to combo boxes, especially if you are using the DisplayMemberPath property, i.e. if you never want to actually display the UI element.
The recommended way to display ComboBox items with non-standard appearance (e.g. with UserControls) is described in the answer of #nit.
If you, however, insist on passing a UserControl item list to the ComboBox, you might remove DisplayMemberPath and use something like this:
<ComboBox ItemsSource="{Binding UserControlList}" SelectedItem="{Binding SelectedUserControl}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemName}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Furthermore, in the constructor of your UserControl, you must place this line:
((FrameworkElement) Content).DataContext = this;
This is necessary to make sure that the correct data context is available in the read-only selection box, which only contains the content of the user control, not the user control itself.
Please note, that with the above example, the drop-down list contains text only (i.e. the item names), but the selection box will contain the fully rendered user control.
I am using MVVM light and have a list box with multiple selection. In my Mainpage.xaml I have
<ListBox Name="ListBox1" ItemsSource="{Binding Items}" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent" Margin="15,15,18,0" SelectionMode="Multiple" Height="100" />
In MainPage.xaml.cs I have (I do not want to use dependency property for some reason).
MainPage()
{
ListBox1.SelectionChanged = new SelectionChangedEventHandler(ListBox1_SelectionChanged);
}
void ListBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listBox = sender as ListBox;
var viewModel = listBox.DataContext as MainViewModel;
viewModel.SelectedItems.Clear();
foreach (string item in listBox.SelectedItems)
viewModel.SelectedItems.Add(item);
}
and this works fine and binds to my MainViewModel. But When the page is loaded I want the first item of the the collection items to be selected by default. Please let me know how to implement this
I'd recommend using the ListBox's Loaded event and then bind to the first item in the collection:
MainPage()
{
ListBox1.Loaded += new RoutedEventHandler( OnListBox1Loaded );
ListBox1.SelectionChanged += new SelectionChangedEventHandler(ListBox1_SelectionChanged);
}
private void OnListBox1Loaded( object sender, RoutedEventArgs e )
{
// make sure the selection changed event doesn't fire
// when the selection changes
ListBox1.SelectionChanged -= MyList_SelectionChanged;
ListBox1.SelectedIndex = 0;
e.Handled = true;
// re-hook up the selection changed event.
ListBox1.SelectionChanged += MyList_SelectionChanged;
}
Edit
If you can not use the Loaded event, then you'll need to create another property in your Model that will hold the item you want selected and then assign that property to the SelectedItem property of the ListBox.
public class MyModel : INotifyPropertyChanged
{
private ObservableCollection<SomeObject> _items;
public ObservableCollection<SomeObject> Items
{
get { return _items; }
set
{
_items = value;
NotifyPropertyChanged( "Items" );
}
}
private SomeObject _selected;
public SomeObject Selected
{
get { return _selected; }
set
{
_selected = value;
NotifyPropertyChanged( "Selected" );
}
}
public void SomeMethodThatPopulatesItems()
{
// create/populate the Items collection
Selected = Items[0];
}
// Implementation of INotifyPropertyChanged excluded for brevity
}
XAML
<ListBox ItemsSource="{Binding Path=Items}"
SelectedItem="{Binding Path=Selected}"/>
By having another property that holds the currently selected item, you also have access in your Model to that item as well whenever the selected item is changed by the user.
I have a ComboBox like this:
<ComboBox IsEditable="True"
ItemsSource="{Binding SelectableValues}"
SelectedValue="{Binding SelectedValue}" />
SelectableValues is a list of double and SelectedValue is a double. If the user chooses a value from the list or manually enters one of those values, my SelectedValue property is updated. But if the user manually enters another value, it isn't. How do I allow SelectedValue to take other values than those in ItemsSource?
Edit: Think of it like the font size box in MS Word. I can choose a value from the list, or provide my own value.
Create a user control inheriting comboBox. Add a dependency property as 'SelectedText'. create event handler for LostFocus on combo box, in event handler assign the entered value dependency property 'SelectedText'.
do binding on 'SelectedText', in its setter if value is new then ad to collection and set SelectedValue to new one.
Indirectly you have to update source by adding new property in ComboBox.
public class ExtendedComboBox : ComboBox
{
public ExtendedComboBox()
{
this.IsEditable = true;
this.LostFocus += ComboBox_LostFocus;
}
public string SelectedText
{
get
{
return (string)GetValue(SelectedTextProperty);
}
set
{
SetValue(SelectedTextProperty, value);
}
}
public static readonly DependencyProperty SelectedTextProperty = DependencyProperty.Register("SelectedText", typeof(string), typeof(ExtendedComboBox), new FrameworkPropertyMetadata(string.Empty, new PropertyChangedCallback(OnSelectedTextPropertyChanged)));
private static void OnSelectedTextPropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
{
}
private void ComboBox_LostFocus(object sender, RoutedEventArgs e)
{
SelectedText = (e.Source as ComboBox).Text??string.Empty;
}
}
// Binding Example
%gt%local:ExtendedComboBox Margin="3" x:Name="ecb" SelectedText="{Binding SelectedText,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding SelectedTextList}">%gt/local:ExtendedComboBox>