I'm attempting to create a searchable combobox where the dropdown items (in an ObservableCollection) update as the user text.
For example:
Let's say I have a list of all the cities in the world (10,000+), something which is too large to display in a drodown list. This list (max 100 items) is retrieved through an API which sends a search term within the GET request.
So as the user searches for a specific city, the observable collection is updated to show the related search terms and the data-bound combobox is updated.
When search = "", returns first 100 cities (those beginning with A) from API. Observablecollection/combobox is updated to show.
When search = "London", returns x cities with London in the name from API. Observablecollection/combobox is updated to show.
Currently, I have the following code to update the observable collection when the search value is changed (this works using INotifyPropertyChanged). However when the collection and by association the data-bound combobox is updated, the user's search term is removed.
public ObservableCollection<string> cities { get; set; }
private string searchString;
public string SearchString
{
get
{
return searchString;
}
set
{
searchString = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged("SearchString");
}
}
// Create the OnPropertyChanged method to raise the event
protected void OnPropertyChanged(string name)
{
if (name == "SearchString")
{
UpdateCitiesDropdown();
}
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
public async void UpdateCitiesDropdown()
{
// Retrieve new cities
List<string> newCities = await GetCities(searchString);
// Save old cities to remove (from observable collection)
List<string> oldCities = cities.ToList<string>();
foreach (string city in oldCities )
{
cities.Remove(city );
}
// Add new
foreach (string city in newCities)
{
cities.Add(city );
}
}
This the current combobox with it's bound data.
<ComboBox ItemsSource="{Binding cities}" Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}" IsEditable="True"/>
Is there any way to update the observable collection and have the combobox updated without removing the user search?
The text which is preselected in the ComboBox is the text of the currently selected item.
In UpdateCitiesDropdown method you are removing all old cities including the selected item. That's the reason why the search is set to blank later.
The solution can be to add SelectedCity property to your view model.
private string selectedCity;
public string SelectedCity
{
get { return selectedCity; }
set
{
selectedCity = value;
OnPropertyChanged("SelectedCity");
}
}
Bind the property to ComboBox's SelectedItem
<ComboBox ItemsSource="{Binding cities}" Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding SelectedCity}" IsEditable="True"/>
And modify UpdateCitiesDropdown method not to remove SelectedItem from cities collection
...
List<string> oldCities = cities.Where(x => x != SelectedCity).ToList<string>();
...
foreach (string city in newCities)
{
if (city != SelectedCity)
{
cities.Add(city);
}
}
Related
Am I missing something or is there more to it that I am not getting? I'm working on a mobile app and have to use pickers for choices from a data table. To start, I have many such pickers that are key/value based. I have an internal ID and a corresponding Show value. The IDs do not always have 1, 2, 3 values such as originating from a lookup table and may have things as
KeyID / ShowValue
27 = Another Thing
55 = Many More
12 = Some Item
Retrieved as simple as
select * from LookupTable where Category = 'demo'
So I have this class below that is used for binding the picker via a list of records
public class CboIntKeyValue
{
public int KeyID { get; set; } = 0;
public string ShowValue { get; set; } = "";
}
Now, the data record that I am trying to bind to has only the ID column associated to the lookup. Without getting buried into XAML, but in general, I have my ViewModel. On that I have an instance of my data record that has the ID column.
public class MyViewModel : BindableObject
{
public MyViewModel()
{
// Sample to pre-load list of records from data server of KVP
PickerChoices = GetDataFromServerForDemo( "select * from LookupTable where Category = 'demo'" );
ShowThisRecord = new MyDataRec();
// for grins, I am setting the value that SHOULD be defaulted
// in picker. In this case, ID = 12 = "Some Item" from above
ShowThisRecord.MyID = 12;
}
// this is the record that has the "ID" column I am trying to bind to
public MyDataRec ShowThisRecord {get; set;}
// The picker is bound to this list of possible choices
public List<CboIntKeyValue> PickerChoices {get; set;}
}
I can’t bind to the index of the list, because that would give me 0, 1, 2, when I would be expecting the corresponding "ID" to be the basis of proper record within the list.
In WPF, I have in the past, been able to declare the show value for the screen, but also the bind value to the ID column in similar. So, the binding of the INT property on my "ShowThisRecord" would drive and properly refresh.
I can see the binding of SelectedItem, but that is the whole item of the KVP class which is not part of the MyDataRec. Only the ID is the common element between them.
What is the proper bindings to get this to work?
<Picker ItemDisplayBinding="{Binding ShowValue}"
SelectedItem="{Binding ???}" />
Just to confirm my record bindings are legitimate, my page has binding context to MyViewModel as I can properly see the ID via a sample text entry I added to the page via.
<Entry Text="{Binding Path=ShowThisRecord.MyID}"/>
I created a demo to test your code, and it works properly. The full demo is here. I also added a function to verify the selected item.
If you want to get the SelectedItem object synchronously, the MyViewModel should implement INotifyPropertyChanged, and I created a selectedRecordfield for SelectedItem, so you can do like this:
public class MyViewModel : ViewModelBase
{
public MyViewModel()
{
// Sample to pre-load list of records from data server of KVP
//PickerChoices = GetDataFromServerForDemo("select * from LookupTable where Category = 'demo'");
PickerChoices = new ObservableCollection<TestModel>() {
new TestModel{MyID = 5, ShowValue="test1"}, new TestModel{MyID = 9, ShowValue="test2"},
new TestModel{MyID = 18, ShowValue="test18"}, new TestModel{MyID = 34, ShowValue="test4"}
};
// Set the default selected item
// foreach (TestModel model in PickerChoices) {
// if (model.MyID == 18) { // Default value
// SelectedRecord = model;
// break;
// }
// }
ShowThisRecord = new TestModel();
// For grins, I am setting the value that SHOULD be defaulted
// in picker. In this case, ID = 12 = "Some Item" from above
ShowThisRecord.MyID = 12;
}
// This is the record that has the "ID" column I am trying to bind to
public TestModel ShowThisRecord { get; set; }
//*****************************************
TestModel selectedRecord; // Selected item object
public TestModel SelectedRecord
{
get { return selectedRecord; }
set
{
if (selectedRecord != value)
{
selectedRecord = value;
OnPropertyChanged();
}
}
}
//*****************************************
// The picker is bound to this list of possible choices
public ObservableCollection<TestModel> PickerChoices { get; set; }
}
class ViewModelBase
public class ViewModelBase: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And the XAML content:
<Picker Title="Select a value" x:Name="mypicker"
ItemsSource="{Binding Path= PickerChoices}"
SelectedItem="{Binding SelectedRecord}"
ItemDisplayBinding="{Binding MyID}"/>
File xaml.cs:
public partial class MainPage : ContentPage
{
ObservableCollection<TestModel> items = new ObservableCollection<TestModel>();
MyViewModel testModel = null;
public MainPage()
{
InitializeComponent();
testModel = new MyViewModel();
BindingContext = testModel;
// This will also work
//if (testModel!=null && testModel.PickerChoices!=null) {
// for (int index=0; index< testModel.PickerChoices.Count; index++) {
// TestModel temp = testModel.PickerChoices[index];
// if (18 == temp.MyID) {
// mypicker.SelectedIndex = index;
// break;
// }
// }
//}
foreach (TestModel model in testModel.PickerChoices)
{
if (model.MyID == 18)
{ // Default value
testModel.SelectedRecord = model;
break;
}
}
}
// To show the selected item
private void Button_Clicked(object sender, EventArgs e)
{
if (testModel.SelectedRecord!=null) {
DisplayAlert("Alert", "selected Item MyID: " + testModel.SelectedRecord.MyID + "<--> ShowValue: " + testModel.SelectedRecord.ShowValue, "OK");
}
}
}
The result is:
You need to set the ItemsSource property to your list of CboIntValue items:
<Picker Title="Select a value"
ItemsSource="{Binding PickerChoices}"
ItemDisplayBinding="{Binding ShowValue}" />
After much work, I ended up writing my own separate class and template style for what I needed. Due to the length of it, and posting the source code for anyone to use, review, assess, whatever, I posted that out on The Code Project.
Again, the primary issue I had is if I have an integer key ID coming from a data source, the picker would not automatically refresh itself by just the given ID (int or string).
Basic question from a novice. I've been stuck on this and have read through a lot of material and several similar questions on SO; hopefully not a completely duplicate question. I simplified the code as much as I know how to.
I'm trying to make the ListView show a filtered ObservableCollection) property (as the ItemsSource?), based on the selection in the ComboBox.
Specifically, which "meetings" have this "coordinator" related to it.
I'm not seeing any data errors in the output while it's running and debugging shows the properties updating correctly, but the ListView stays blank. I'm trying to avoid any code-behind on the View, there is none currently.
Thanks!
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<Meeting> meetings;
public ObservableCollection<Meeting> Meetings
{
get
{
return meetings;
}
set
{
meetings = value;
OnPropertyChanged("ListProperty");
OnPropertyChanged("Meetings");
}
}
private string coordinatorSelected;
public string CoordinatorSelected
{
get
{
return coordinatorSelected;
}
set
{
coordinatorSelected = value;
Meetings = fakeDB.Where(v => v.CoordinatorName == CoordinatorSelected) as ObservableCollection<Meeting>;
}
}
private ObservableCollection<string> comboProperty = new ObservableCollection<string> { "Joe", "Helen", "Sven" };
public ObservableCollection<string> ComboProperty
{
get
{
return comboProperty;
}
}
private ObservableCollection<Meeting> fakeDB = new ObservableCollection<Meeting>() { new Meeting("Joe", "Atlas"), new Meeting("Sven", "Contoso"), new Meeting("Helen", "Acme") };
public ObservableCollection<Meeting> ListProperty
{
get
{
return Meetings;
}
}
public class Meeting
{
public string CoordinatorName { get; set; }
public string ClientName { get; set; }
public Meeting(string coordinatorName, string clientName)
{
CoordinatorName = coordinatorName;
ClientName = clientName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<Window.Resources>
<local:ViewModel x:Key="VM"></local:ViewModel>
</Window.Resources>
<DockPanel DataContext="{StaticResource ResourceKey=VM}">
<ComboBox Margin="10" ItemsSource="{Binding ComboProperty}" SelectedItem="{Binding CoordinatorSelected}" DockPanel.Dock="Top"/>
<ListView Margin="10" ItemsSource="{Binding ListProperty, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="ClientName"/>
</DockPanel>
Update:
This seems to show that the lambda is returning a Meeting object but the assignment to Meetings is failing. Is this an error in casting maybe?
Thanks again.
You always have to change a property's backing field before you fire a PropertyChanged event. Otherwise a consumer of the event would still get the old value when it reads the property.
Change the Meetings property setter like this:
public ObservableCollection<Meeting> Meetings
{
get
{
return meetings;
}
set
{
meetings = value;
OnPropertyChanged("ListProperty");
OnPropertyChanged("Meetings");
}
}
I believe I've found two solutions to the same problem. The error pointed out #Clemens was also part of the solution. The Meetings property problem is solved if I change ListProperty and Meetings to IEnumerable. Alternatively this approach without changing the type, which I believe invokes the collection's constructor with the filtered sequence as an argument.
set
{
coordinatorSelected = value;
var filteredList = fakeDB.Where(v => v.CoordinatorName == coordinatorSelected);
Meetings = new ObservableCollection<Meeting>(filteredList);
OnPropertyChanged("ListProperty");
}
Context
On the network are servers that advertise their names with UDP at regular intervals.
The datagrams come in on port 1967 and contain a string like this:
UiProxy SomeServerMachineName
New entries are added, existing entries are updated and stale entries age out of an observable collection that serves as the ItemsSource of a XAML combo box.
This is the combo box
<ComboBox x:Name="comboBox" ItemsSource="{Binding Directory}" />
and this is the supporting code. Exception handlers wrap everything dangerous but are here omitted for brevity.
public class HostEntry
{
public string DisplayName { get; set;}
public HostName HostName { get; set; }
public DateTime LastUpdate { get; set; }
public HostEntry(string displayname, HostName hostname)
{
DisplayName = displayname;
HostName = hostname;
LastUpdate = DateTime.Now;
}
public override string ToString()
{
return DisplayName;
}
}
HostEntry _selectedHost;
public HostEntry SelectedHost
{
get { return _selectedHost; }
set
{
_selectedHost = value;
UpdateWriter();
}
}
async UpdateWriter() {
if (_w != null)
{
_w.Dispose();
_w = null;
Debug.WriteLine("Disposed of old writer");
}
if (SelectedHost != null)
{
_w = new DataWriter(await _ds.GetOutputStreamAsync(SelectedHost.HostName, "1967"));
Debug.WriteLine(string.Format("Created new writer for {0}", SelectedHost));
}
}
ObservableCollection<HostEntry> _directory = new ObservableCollection<HostEntry>();
public ObservableCollection<HostEntry> Directory
{
get { return _directory; }
}
private async void _ds_MessageReceived(DatagramSocket sender, DatagramSocketMessageReceivedEventArgs args)
{
if (_dispatcher == null) return;
await _dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
var dr = args.GetDataReader();
var raw = dr.ReadString(dr.UnconsumedBufferLength);
var s = raw.Split();
if (s[0] == "UiProxy")
{
if (_directory.Any(x => x.ToString() == s[1]))
{ //update
_directory.Single(x => x.ToString() == s[1]).LastUpdate = DateTime.Now;
}
else
{ //insert
_directory.Add(new HostEntry(s[1], args.RemoteAddress));
}
var cutoff = DateTime.Now.AddSeconds(-10);
var stale = _directory.Where(x => x.LastUpdate < cutoff);
foreach (var item in stale) //delete
_directory.Remove(item);
}
});
}
The collection starts empty.
The UpdateWrite method called from the setter of SelectedHost destroys (if necessary) and creates (if possible) a DataWriter around a DatagramSocket aimed at the address described by the value of SelectedHost.
Goals
Automatically select when a value is added and the list ceases to be empty.
The list can also become empty. When this happens the selection must return to null with a selected index of -1.
As things stand, the list is managed and it is possible to interactively pick a server from the list.
At the moment I am setting SelectedHost like this but I am sure it could be done with binding.
private void comboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedHost = comboBox.SelectedItem as HostEntry;
}
The setter method for SelectedHost calls CreateWriter which manages the object used elsewhere to send data to the selected host. I've called this from the setter because it must always happen right after the value changes, and at no other time. It's in a method so it can be async.
I could move it to the SelectionChanged handler but if I do that then how can I guarantee order of execution?
Problem
I get errors when I try to programmatically set the selection of the combo box. I am marshalling onto the UI thread but still things aren't good. What is the right way to do this? I've tried setting SelectedIndex and SelectedValue.
I get errors when I try to programmatically set the selection of the combo box.
How are you doing it? In code-behind this should work so long as the collection you are bound to has an item at that index:
myComboBox.SelectedIndex = 4;
but I am sure it could be done with binding
Yes it can, looks like you forgot to implement INotifyPropertyChanged. Also since you are using UWP there is a new improved binding syntax Bind instead of Binding learn more here: https://msdn.microsoft.com/en-us/windows/uwp/data-binding/data-binding-in-depth
<ComboBox x:Name="comboBox" ItemsSource="{Binding Directory}"
SelectedItem="{Binding SelectedHost}" />
public event PropertyChangedEventHandler PropertyChanged;
HostEntry _selectedHost;
public HostEntry SelectedHost
{
get { return _selectedHost; }
set
{
_selectedHost = value;
RaiseNotifyPropertyChanged();
// What is this? propertys should not do things like this CreateWriter();
}
}
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
FileRecord is the observable collection that is being binded with my wpf datagrid in MVVM model.
I have one checkbox for each column above my datagrid. Checkbox name is "SelectUnique--Columnname--". When I click those checkboxes it should show unique values for the column in my grid.
When I click unique check box for TId, I do below logic
var grpd = FileRecord.GroupBy(item => item.TID).Select(grp => grp.First());
FileRecord= new ObservableCollection<FileData>(grpd); // will refresh the grid.
Then again When I click unique check box for CId, I do below logic
var grpd = FileRecord.GroupBy(item => item.CID).Select(grp => grp.First());
FileRecord= new ObservableCollection<FileData>(grpd);// will refresh the grid.
and so on. In this case, for example, if I do unique selection for all my columns, then again If I want to deselect the checkbox randomly(not in the order I selected unique checkboxes) I would like to undo what I have done for that particular column. For example, if I unselect CID unique check box, then the grid should so proper result.
How to acheive this? Please help.
When I want to filter a collection like this I have a property like this:
public IEnumerable<FileData> FilteredFiles
{
get
{
if (Unique)
{
return Files.GroupBy(item => item.TID).Select(grp => grp.First());
}
else
{
return Files.GroupBy(item => item.CID).Select(grp => grp.First());
}
}
}
public ObservableCollection<FileData> Files
{
get; set;
}
public bool Unique
{
get
{
return unique;
}
set
{
unique = value;
RaisePropertyChanged("FilteredFiles");
}
}
Bind to FilteredFiles and when you add/remove from the collection just call RaisePropertyChanged("FilteredFiles") to notify the UI.
You should have a reference of the original collection somewhere, and do all calculations over that one.
For instance, you could have a single method that gets called whenever a CheckBox is checked or unchecked, and have that method filter/group the original collection.
// Simplified properties
private IEnumerable<FileData> FileRecordCollection;
public ObservableCollection<FileData> FileRecord { get; set; }
// Event handlers for the CheckBoxes
private void TID_CheckBox_Checked(object sender, RoutedEventArgs e)
{
UpdateFileRecord();
}
private void TID_CheckBox_Unchecked(object sender, RoutedEventArgs e)
{
UpdateFileRecord();
}
// etc.
// Method that updates FileRecord
private void UpdateFileRecord()
{
IEnumerable<FileData> groupedCollection = FileRecordCollection;
if (TID_CheckBox.IsChecked)
groupedCollection = groupedCollection.GroupBy(item => item.TID).Select(grp => grp.First());
if (CID_CheckBox.IsChecked)
groupedCollection = groupedCollection.GroupBy(item => item.CID).Select(grp => grp.First());
// etc.
FileRecord = new ObservableCollection<FileData>(groupedCollection);
}
This isn't exactly optimal, but I can't think of something better (performance-wise) right now.
I am having a problem with my Combobox. From the database I grab data from a programs column based on two other combo boxes (status and campus) and attempt to fill the combobox. The data from the first row in the db is the only data filling the combobox. The first row in db contains 172. A 1, 7, and 2 are added to the combo box. (Since I am new to Stackoverflow I cannot post images yet - sorry in adavance)
To get an idea here's how it is looking when I fill the combo box:
https://www.dropbox.com/s/et4ty82suk9nkr9/tuitionEstimatorApp.jpg
Program Data:
https://www.dropbox.com/s/my07trwvh6pxr3l/programData.jpg
XAML
<telerik:RadComboBox x:Name="ProgramComboBox" ItemsSource="{Binding ProgramName}" />
CS
DataContext = Program.GetPrograms(status, campusSelection);
Program Class
public class Program : INotifyPropertyChanged
{
private string _programname;
public string ProgramName
{
get { return _programname; }
set
{
_programname = value; RaisePropertyChanged();
}
}
private void RaisePropertyChanged([CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public static List<Program> GetPrograms(string status, string campus)
{
var context = new FTEDatabaseEntities();
var query = context.Entity1
.Where(p => p.dependency_status == status && p.campus == campus)
.Select(p => p.program);
return query.Distinct().Select(program => new Program() { ProgramName = program }).ToList();
}
}
Any insight would be greatly appreciated.
You have to bind the combo box to an List (or enumerable) type. Your binding sees that the property "ProgramName" is of type IEnumerable<char> (aka string), and so populates itself that way.
It looks like you will want to create a new property of type List<Program> Programs or similar, and then use:
<telerik:RadComboBox x:Name="ProgramComboBox" ItemsSource="{Binding Programs}" />