C#: Binding ListBox Items to property of API Call - c#

I'm building an application in which I run the following code to display the results in a listbox from an API call. Link below directs to full extent of script that runs app.
https://dotnetfiddle.net/gFL2M0
I am able to get the results from the API call, match them to Class1 properties, and display them in the listbox.
Nevertheless, I want to bind the EntryID property results to the values displayed in the listbox because once the user presses the button upon a selected value, I'm looking to get that value to run another command of another method or within the button's method itself.
This is where I'm here asking y'all for help (binding the API's parsed EntryID results to the selected values (displayed members)) of the listbox.
Two things:
The route I pursued to call and parse the API data is one I chose based on my current knowledge of C#. I apologize if this method is making this much more difficult. I'm still new to C#.
If you take a look at the link above, I will eventually make the API call a class of its own. I just went ahead and provided it twice in the script for context reasons.
Here's the code at the part of the button. Thank you in advance for the help!
private void Button_Click(object sender, RoutedEventArgs e)
{
StarRezApiClient call2 = new StarRezApiClient("BaseURL", "UserName", "Password");
var selection = call2.Select("Entry", Criteria.Equals("NameLast", "Rincon Recio"));
var transfer = JsonConvert.SerializeObject(selection);
var output = JsonConvert.DeserializeObject<Class1[]>(transfer);
foreach (var id in output)
{
testListBox.SelectedItem.Equals(id.EntryID);
///Assigns SelectedItem of ListBox to EntryID [WHERE I NEED HELP PLEASE]
}
//TODO
//MessageBox.Show(SelectedValue.ToString()); for testing
//System.Diagnostics.Process.Start("BaseURL?EntryID=" + SelectedItem); takes user to webpage of api results
}
https://dotnetfiddle.net/yq45jU --- Class Properties
https://codebeautify.org/jsonviewer/cbfb31d2 --- Json Results

Answer
You want to add instances of Class1 to your listbox, not just the text display. Which for proper display in a listbox wants the .ToString() method overridden.
Line 34 in your fiddle shows testListBox.Items.Add(name.NameFirst + ' ' + name.NameLast);
which should be testListBox.Items.Add(name);
The second part is making Class1 whose code you did not include have a method like the following
public override string ToString()
{
return this.NameFirst + ' ' + this.NameLast;
}
Side note
I use a wrapper access around listboxes or comboboxes like so for easier type-safe access.
I flesh out the interfaces as needed and fallback to direct access where strong doesn't necessarily make things any simpler.
public class ListBoxT<T>
{
readonly ListBox _lb;
public T SelectedItem { get => (T)_lb.SelectedItem; set => _lb.SelectedItem = value; }
public void AddItem(T item) => _lb.Items.Add(item);
public ListBoxT(ListBox lb) => this._lb = lb;
}
public class ComboBoxT<T>
{
readonly ComboBox _cb;
public ComboBoxT(ComboBox cb) => this._cb = cb;
public T SelectedItem { get => (T)_cb.SelectedItem; set => _cb.SelectedItem = value; }
public Rectangle Bounds => this._cb.Bounds;
public int Count => this._cb.Items.Count;
public IReadOnlyCollection<T> Items { get => new ReadOnlyCollection<T>(_cb.Items.Cast<T>().ToList()); }
public void BeginUpdate() => _cb.BeginUpdate();
public void Clear() => _cb.Items.Clear();
public void AddRange(IEnumerable<T> items)
{
_cb.SuspendLayout();
_cb.Items.AddRange(items.Cast<object>().ToArray());
_cb.ResumeLayout();
}
public void EndUpdate() => _cb.EndUpdate();
public bool Enabled { get => _cb.Enabled; set => _cb.Enabled = value; }
public int SelectedIndex { get => _cb.SelectedIndex; set => _cb.SelectedIndex = value; }
public ComboBox Value => _cb;
public T this[int x]
{
get => (T)this._cb.Items[x];
set => this._cb.Items[x] = value;
}
}

Here is a simple example. As Maslow noted, you should first add objects of Class1 to your list, not plain strings. Then, you simply add the items that match your search criteria to the SelectedItems collection:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded">
<Grid>
<ListBox Name="list" />
</Grid>
</Window>
class Class1
{
public int EntryID { get; set; }
public string NameFirst { get; set; }
public string NameLast { get; set; }
public override string ToString()
{
return NameFirst;
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
list.SelectionMode = SelectionMode.Multiple;
//fill list with dummy items
var items = new Class1[10];
for(int i = 0; i < 10; ++i)
items[i] = new Class1() { EntryID = i, NameFirst = ((char)('A' + (char)i)).ToString() };
list.ItemsSource = items;
//simulate second API call
var selection = new Class1[3];
selection[0] = new Class1() { EntryID = 3 };
selection[1] = new Class1() { EntryID = 4 };
selection[2] = new Class1() { EntryID = 8 };
list.SelectedItems.Clear();
foreach (var sel in selection)
foreach (var item in items.Where(i => i.EntryID == sel.EntryID))
list.SelectedItems.Add(item);
}
}
Instead of arrays, you might want to employ more flexible or more efficient data structures like List<Class1> or Dictionary<int, Class1>. I.e., when you get your result as an array, simply fill in these data structures from it. If you have a dictionary, you can directly get the element with a specific key instead of using the LINQ query (.Where(...)).
Btw, please do not use .Net Fiddle to simply add more code to your question. The question should be self-contained and show all relevant code. .Net Fiddles can be used as supplementary material to link to a running example. However, linking to a non-compiling fiddle is just making your question harder to follow.

Related

ObserableCollection not triggering Change when Items are added/removed from list

I have a problem with ReactiveUI ObservableCollection. I am trying to update the UI based on a list that changes in a ReactiveObject, but for some reason my list change is not triggered and I am not sure what I am doing wrong.
There is a full repo here : https://github.com/SebiCiuca/ObserableCollection
App has a button, that when it's clicked calls a "RandomService" that removes items from a list and then adds a random number of items back into the list.
This list is an ObservableCollection that has a Subscribe on it, so I would like to see in my ViewModel that the list change happening in RandomService is triggering my ObservableCollection ModelList change.
Code below:
MainWindow
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public MainWindow(MainWindowViewModel mainWindowViewModel)
{
InitializeComponent();
ViewModel = mainWindowViewModel;
DataContextChanged += (sender, args) => ViewModel = DataContext as MainWindowViewModel;
this.WhenActivated(cleanup =>
{
this.BindCommand(ViewModel, vm => vm.RandonListCommand, view => view.RandomButton).DisposeWith(cleanup);
});
}
}
MainWindowViewModel
public class MainWindowViewModel : ReactiveObject
{
private readonly IRandomService m_RandomService;
public ReactiveCommand<Unit, Unit> RandonListCommand { get; set; }
public MainWindowViewModel(IRandomService randomService)
{
m_RandomService = randomService;
RandonListCommand = ReactiveCommand.Create(() => { CallRandomService(); });
randomService.WhenAnyValue(rs => rs.ModelList).WhereNotNull().Subscribe(_ => TriggerUpdateUI());
}
private void CallRandomService()
{
Debug.WriteLine("Random is called");
var random = new Random();
var take = random.Next(1, 4);
Debug.WriteLine($"Take is {take}");
m_RandomService.UpdateList(take);
}
private void TriggerUpdateUI()
{
Debug.WriteLine("List changed");
foreach (var model in m_RandomService.ModelList)
{
Debug.WriteLine($"{model.Id} {model.Name}");
}
}
}
RandomService
public class RandomService : ReactiveObject, IRandomService
{
private List<RandomModel> _privateList;
public RandomService()
{
_privateList = new List<RandomModel>
{
new RandomModel { Id = 1, Name = "FirstRandom" },
new RandomModel { Id = 2, Name = "SecondRandom" },
new RandomModel { Id = 3, Name = "SecondRandom" },
new RandomModel { Id = 4, Name = "SecondRandom" }
};
_modelList = new();
}
private ObservableCollection<RandomModel> _modelList;
public ObservableCollection<RandomModel> ModelList
{
get => _modelList;
set => this.RaiseAndSetIfChanged(ref _modelList, value);
}
public void UpdateList(int take)
{
_modelList.Clear();
Debug.WriteLine($"ModelList count {_modelList.Count}");
var addToUI = _privateList.Take(take).ToList();
addToUI.Shuffle();
addToUI.ForEach(p => ModelList.Add(p));
Debug.WriteLine($"ModelList count {_modelList.Count}");
}
}
IRandomService
public interface IRandomService
{
ObservableCollection<RandomModel> ModelList { get; }
void UpdateList(int take);
}
From my point of view everything is correct if I read the definiton of ObservableCollection
"Represents a dynamic data collection that provides notifications when items get added or removed, or when the whole list is refreshed."
So my question is, why is my TriggerUpdateUI() never called. ( except at the start of the app when it's initialized).
randomService.WhenAnyValue(rs => rs.ModelList).WhereNotNull().Subscribe(_ => TriggerUpdateUI());
WhenAnyValue is just watching randomService for property change notifications for ModelList. You haven't set up anything to look for collection related changes.
From my point of view everything is correct if I read the definiton of ObservableCollection
"Represents a dynamic data collection that provides notifications when items get added or removed, or when the whole list is refreshed."
Your quote comes from the MS Docs on ObservableCollection. It is capable of producing collection change notifications, but you still need to setup something to react to them via ReactiveUI / DynamicData.
ReactiveUI Handbook - Collections

Filtering ListBox using combobox and LINQ

I have a winform that includes a ListBox and a Combobox. In this ListBox appears a clients list on first run.
I want to filter the "Clients" in the ListBox with a Combobox.
To fill the ListBox using the string selected in the Combobox i'm using :
private void FillListBox()
{
this.lstClient.Items.Clear();
foreach (Client c in this.client)
{
if (this.cBox.Text == "All")
this.lstClient.Items.Add(c.ToString());
else
if (this.cBox.Text == "Retail" && c.GetType() == typeof(RetailClient))
this.lstClient.Items.Add(c.ToString());
}
this.lstClient.Sorted = true;
}
After that i call this method from the event of the ComboBox :
private void cBox_TextChanged(object sender, EventArgs e)
{
this.FillListBox();
}
It works "great" but my code is not really dynamic and too long (lots of differents clients) that's why i would like to use LINQ.
I read on microsoft's documentation but i'm pretty confused on how using it.
Does anyone have some times to show me the way ?
Adding infos :
My form :
I select the type i want in the ComboBox :
The result :
Thanks
Ok let's give it a try. If you want to implement filtering you should think about a proper structure how to represent your filter criterion. In this case you have a label in your combobox which is bound to a unique filter criterion. This could be represented by a custom class:
public class SortingRepresentation
{
public string DisplayLabel { get; set; }
public Type ClientType { get; set; }
}
Now you can create a List of those criteria and shove it into the combobox:
List<SortingRepresentation> sortingFields = new List<SortingRepresentation>();
public Form1()
{
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "All", TypeCriterion = typeof(Client) });
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "Only Retail", TypeCriterion = typeof(Client_A) });
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "Only Wholesale", TypeCriterion = typeof(Client_B) });
sortingFields.Add(new SortingRepresentation{ DisplayLabel = "Only Human Wholesale", TypeCriterion = typeof(Client_C) });
cBox.DisplayMember = "DisplayLabel";
cBox.DataSource = sortingFields;
}
When the selection changes in the combobox you can catch now the selected item (which will be of type SortingRepresentation and pass it as a filter to FillListBox:
private void cBox_SelectedIndexChanged(object sender, EventArgs e)
{
FillListBox((SortingRepresentation)cBox.SelectedItem);
}
Now you can use the Type TypeCriterion inside this object to filter your list:
private void FillListBox(SortingRepresentation sortcriterion)
{
this.lstClient.DataSource = null;
this.lstClient.DataSource = client
.Where(x => x.GetType() == sortcriterion.TypeCriterion || // either you are of this type
x.GetType().BaseType == sortcriterion.TypeCriterion // or your parent is of this type
).ToList();
}
Since you are using a listbox, you can bind the sorted list directly to the DataSource and be done with it. For the proper display, you need to override the ToString method in your Client class and the ListBox will take care of the display accordingly. But as I see you've done it already

Changing the data source of ListView dynamically

I have a ListView and data source for it which it populate from the Internet. Once it's populate it should remain static unless a user makes a new http request. Now I have something like this:
class MyDataItem {
public int Field1 { get; set; }
public string Field2 { get; set; }
}
class Window1: Window {
private List<MyDataItem> dataSource = new ...
void sendHttpRequest(...) {
dataSource = getFromInternet();
myListView.ItemsSource = dataSource ;
}
}
And say, I have a checkbox. When I click on it, I want to filter the data by some filter.
//.........
// the checkbox is checked
var filterDataSource = dataSource.Where(....)
How can I make my ListView update its data with data source to be filterDataSource? And then when the checkbox is unchecked again, how will I make it show the initial data source?
Here is some code to help you. Please note that this was not tested nor compiled but it can give you some hints on how to handle your case. The trick is to use a CollectionViewSource that lets you filter your data.
class Window1: Window {
private readonly ObservableCollection<MyDataItem> _children;
private readonly CollectionViewSource _viewSource;
public Window1()
{
// ...
_children = new ObservableCollection<MyDataItem>();
_viewSource = new CollectionViewSource
{
Source = _children
};
myListView.ItemsSource = _viewSource;
// ...
}
// This method needs to be called when your checkbox state is modified.
// "filter = null" means no filter
public void ApplyFilter(Func<MyDataItem, bool> filter)
{
if (_viewSource.View.CanFilter)
{
_viewSource.View.Filter = (filter == null) ? (o => true): (o => filter((MyDataItem) o));
}
}

How to delete a selected listbox item from both the listbox and the original list in c#

I'm having problems removing the data in a list which was entered by a user in my textboxes and then stored in a listbox. I know how to remove the selected item in the listbox but when I click my button to show everything in the list, the selected item I just deleted is still in the list.
Here is my code for removing the selected item in the listbox:
for (int i = 0; i < VehicleListBox.SelectedItems.Count; i++)
VehicleListBox.Items.Remove(VehicleListBox.SelectedItems[i]);
My list is in a class called company and the list is named vehicles. I have looked everywhere for the most part for my answer and cannot seem to find it. I should also mention it's a generic list.
With your class design as I understand it, I wrote the following code in a WPF project. I added a listbox called "listbox1" and a button to the form. I believe the following code does what you want, or at least would guide you to the answer.
public class Company
{
public List<Vehicle> Vehicles;
public Company()
{
Vehicles = new List<Vehicle>() { new Vehicle(1), new Vehicle(2), new Vehicle(3) };
}
}
public class Vehicle
{
private string _vehicleNum;
public Vehicle(int num)
{
_vehicleNum = "Vehicle" + num.ToString();
}
public string getDetails()
{
return _vehicleNum;
}
}
Company ACompany = new Company();
public MainWindow()
{
InitializeComponent();
foreach(Vehicle v in ACompany.Vehicles)
listbox1.Items.Add(v.getDetails());
}
private void Button_Click(object sender, RoutedEventArgs e)
{
for (int i = 0; i < listbox1.SelectedItems.Count; i++)
{
foreach(Vehicle v in ACompany.Vehicles)
{
if (String.Equals(v.getDetails(), listbox1.SelectedItems[i].ToString()))
{
ACompany.Vehicles.Remove(v);
break;
}
}
listbox1.Items.Remove(listbox1.SelectedItems[i]);
}
}
So as I understand you're using getDetails to get the list, then one-by-one adding them to the VehicleListBox.
An option would be to remove the selected items from the List and then update the ListBox accordingly.
You add a simple method to do this:
private void UpdateListBox(List<string> vehicles);
Replacing `List' with the type you're using.
Alternatively have you tried binding the ItemsSource of the ListBox?
In WPF (for example):
<ListBox Name=ExampleListBox, ItemsSource={Binding} />
The in code:
ExampleListBox.DataContext = myList;
This best goes in the Window_Loaded method after you've populated the List.
If necessary then update this when the list changes.
Ok this works I tested it.
foreach (string thing in listBox1.SelectedItems){
myList.remove(thing);
}
Given your current code, you could parse the string back out by reversing whatever getDetails() is doing, and then select the appropriate object from your list.
for (int i = 0; i < VehicleListBox.SelectedItems.Count; i++)
{
var item = (string)VehicleListBox.SelectedItems[i];
VehicleListBox.Items.Remove(item);
// assuming your company class has only a Name and ID...
string name = ... // parse name from item
int id = ... // parse id from item
vehicles.Remove(vehicles.Single(x => x.Name == name && x.ID == id));
}

Show ComboBox group header for Silverlight

I want to show a ComboBox with OPTGROUP style header gruopings in Silverlight. Every website I find (including questions on SO) that sovle this link to an outdated link and, handily, show no code snippets for me to work from.
E.g.:
So how do I do this?
See my similar question: How to show group header for items in Silverlight combobox?
I put dummy entries in collection source to indicate the group header and then modified DataTemplate to show the group headers in different way and normal entries in separate way.
Here is one generalized approach. It's not bad, and should be flexible enough -- although it has the weakness of requiring an extended collection class (cannot make use of CollectionViewSource).
Step 1 ComboBoxGroupHeader object
This plays the role of "dummy entry" mentioned by #NiteshChordiya.
public class ComboBoxGroupHeader
{
public ComboBoxGroupHeader(object header)
{
Header = header;
}
public object Header { get; protected set; }
}
Step 2 Extended ComboBox
This overrides PrepareContainerForItemOverride, in order to tinker with the dummy items' containers. It also provides an (optional) "HeaderTemplate".
public class GroupedComboBox : ComboBox
{
public DataTemplate HeaderTemplate
{
get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
set { SetValue(HeaderTemplateProperty, value); }
}
public static readonly DependencyProperty HeaderTemplateProperty =
DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(GroupedComboBox), new PropertyMetadata(null));
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
var container = element as ComboBoxItem;
if (container != null && item is ComboBoxGroupHeader)
{
// prevent selection
container.IsHitTestVisible = false;
// adjust the container to display the header content
container.ContentTemplate = HeaderTemplate
container.Content = ((ComboBoxGroupHeader)item).Header;
}
}
}
Step 3 Extended collection class
This does the grouping, and adds the dummy "ComboBoxGroupHeader" entries. This implementation is essentially read-only (groupings would break if you tried to add new items), but it would be straight-forward to support operations like "Add", "Insert", etc.
public class GroupedCollection<T, TGroup> : ObservableCollection<object>
{
private Func<T, TGroup> _grouping;
public IEnumerable<T> BaseItems
{
get { return base.Items.OfType<T>(); }
}
public GroupedCollection(IEnumerable<T> initial, Func<T, TGroup> grouping)
: base(GetGroupedItems(initial, grouping))
{
_grouping = grouping;
}
private static IEnumerable<object> GetGroupedItems(IEnumerable<T> items, Func<T, TGroup> grouping)
{
return items
.GroupBy(grouping)
.SelectMany(grp =>
new object[] { new ComboBoxGroupHeader(grp.Key) }
.Union(grp.OfType<object>())
);
}
}
Usage
It works like a normal ComboBox, with the optional HeaderTemplate:
<local:GroupedComboBox ItemsSource="{Binding Source}">
<local:GroupedComboBox.HeaderTemplate>
<TextBlock FontSize="9" TextDecorations="Underline" Foreground="DarkGray"
Text="{Binding}" />
</local:GroupedComboBox.HeaderTemplate>
</local:GroupedComboBox>
For the grouped to take effect, the source must use the "GroupedCollection" above, so as to include the dummy header items. Example:
public class Item
{
public string Name { get; set; }
public string Category { get; set; }
public Item(string name, string category)
{
Name = name;
Category = category;
}
}
public class Items : GroupedCollection<Item, string>
{
public Items(IEnumerable<Item> items)
: base(items, item => item.Category) { }
}
public class ViewModel
{
public IEnumerable<Item> Source { get; private set; }
public ViewModel()
{
Source = new Items(new Item[] {
new Item("Apples", "Fruits"),
new Item("Carrots", "Vegetables"),
new Item("Bananas", "Fruits"),
new Item("Lettuce", "Vegetables"),
new Item("Oranges", "Fruits")
});
}
}
Group header is not supported in ComboBox. Alternate way for to implement it with DataGrid or use TreeView to show grouped data.
However, you can try something like this,
Silverlight Custom ComboBox

Categories

Resources