ListBox SelectedValueChanged/SelectedIndexChanged not firing when data source changes - c#

I need to keep track of the selected item on a ListBox to update/disable other controls according to the currently selected value.
This is the code to reproduce the issue:
public partial class Form1 : Form
{
private readonly BindingList<string> List = new BindingList<string>();
public Form1()
{
InitializeComponent();
listBox1.DataSource = List;
listBox1.SelectedValueChanged += (s, e) => System.Diagnostics.Debug.WriteLine("VALUE");
listBox1.SelectedIndexChanged += (s, e) => System.Diagnostics.Debug.WriteLine("INDEX");
addButton.Click += (s, e) => List.Add("Item " + (List.Count + 1));
removeButton.Click += (s, e) => List.RemoveAt(List.Count - 1);
logSelectionButton.Click += (s, e) =>
{
System.Diagnostics.Debug.WriteLine("Selected Index: " + listBox1.SelectedIndex);
System.Diagnostics.Debug.WriteLine("Selected Value: " + listBox1.SelectedValue);
};
}
}
My form has a list box listBox1 and three buttons: addButton, removeButton and logSelectionButton.
If you press addButton (starting with an empty list), then removeButton and finally addButton again, neither SelectedValueChanged nor SelectedIndexChanged will fire at the last addButton press, even though if you press logSelectionButton before and after the last addButton press, you'll see that the values of both SelectedIndex and SelectedValue have changed from -1 to 0 and from null to "Item 1" respectively, and that "Item 1" looks selected on the list box.
This would cause any other controls I need to update according to the selected item to stay disabled until the user manually selects an item on the list box, even though the first item is already selected.
I can't think of any workaround. Perhaps also subscribing to my BindingList's ListChanged event to see whether the list is empty or not, but then I don't know if the items in the list box will be updated before or after my event handler fires, which will cause other problems.

Seems like you found a bug in ListControl internal handling of the PositionChanged event when data bound (if you turn Exceptions on in VS, you'll see an exception when the first item is added to the empty list).
Since ListControl derived classes like ListBox, ComboBox etc. in data bound mode synchronize their selection with the Position property of the BindingManagerBase, the reliable workaround (and basically a more general abstract solution) is to handle CurrentChanged event of the underlying data source binding manager:
listBox1.BindingContext[List].CurrentChanged += (s, e) =>
System.Diagnostics.Debug.WriteLine("CURRENT");

I found a workaround that seems to work fine. Since ListBox updates the selected index by setting the SelectedIndex property and the property is virtual I can override it to keep track of it:
public class ListBoxThatWorks : ListBox
{
private int LatestIndex = -1;
private object LatestValue = null;
public EqualityComparer<object> ValueComparer { get; set; }
public override int SelectedIndex
{
get { return base.SelectedIndex; }
set { SetSelectedIndex(value); }
}
private void NotifyIndexChanged()
{
if (base.SelectedIndex != LatestIndex)
{
LatestIndex = base.SelectedIndex;
base.OnSelectedIndexChanged(EventArgs.Empty);
}
}
private void NotifyValueChanged()
{
if (!(ValueComparer ?? EqualityComparer<object>.Default).Equals(LatestValue, base.SelectedValue))
{
LatestValue = base.SelectedValue;
base.OnSelectedValueChanged(EventArgs.Empty);
}
}
private void SetSelectedIndex(int value)
{
base.SelectedIndex = value;
NotifyIndexChanged();
NotifyValueChanged();
}
}

Related

DataGrid auto scroll to bottom for newly added item

I use an ObservableCollection in my ViewModel to add a new record in my DataGrid, so I don't have an access to this control. I wanted to scroll to the bottom of the DataGrid every time a new item is added.
Normally I can just hook into INotifyCollectionChanged from my View, then scroll to the bottom, something like;
public MyView(){
InitializeComponent();
CollectionView myCollectionView = (CollectionView)CollectionViewSource.GetDefaultView(MyDataGrid.Items);
((INotifyCollectionChanged)myCollectionView).CollectionChanged += new NotifyCollectionChangedEventHandler(DataGrid_CollectionChanged);
}
private void DataGrid_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e){
if (MyDataGrid.Items.Count > 0){
if (VisualTreeHelper.GetChild(MyDataGrid, 0) is Decorator border){
if (border.Child is ScrollViewer scroll) scroll.ScrollToEnd();
}
}
}
My problem now is that I have a function to Duplicate and Delete an item, this whole thing is being done in my ViewModel. With the approach above, the DataGrid will always scroll to the bottom even if I deleted or duplicate an item in any position which I don't want to happen. Scrolling to the bottom should only be working for the newly added items.
What should be the approach for this?
You can try to check if NotifyCollectionChangedEventArgs.NewStartingIndex is at the end of your collection. You should scroll to the end only if the change has happened at the end.
private void DataGrid_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e){
if (MyDataGrid.Items.Count > 0 && e.NewStartingIndex == MyDataGrid.Items.Count - 1){
if (VisualTreeHelper.GetChild(MyDataGrid, 0) is Decorator border){
if (border.Child is ScrollViewer scroll) scroll.ScrollToEnd();
}
}
}
You can use the passed NotifyCollectionChangedEventArgs to identify whether a Duplicate or Delete has occurred.
Delete is easy, simply:
if (e.Action == NotifyCollectionChangedAction.Remove)
{
// An item has been removed from your collection. Do not scroll.
}
Duplicate depends on your definition of what a "duplicate" or "copy" is exactly, but most likely you can use a quick Linq check like:
if (e.Action == NotifyCollectionChangedAction.Add)
{
// An item has been added, let's see if it's a duplicate.
CollectionView changedCollection = (CollectionView)sender;
foreach (myRecordType record in e.NewItems)
{
if (changedCollection.Contains(record))
{
// This was likely a duplicate added
}
}
}
It's worth noting that EventArgs of any type are really there for this very purpose. They'll generally provide you with more information regarding the event for exactly this kind of logical handling.
You have two options, first is to compare the difference between the old and new items. Then simply get that item and using DataGrid.ScrollIntoView(item) to scroll into that specific position.
private void DataGrid_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e){
if (YourDataGrid.Items.Count > 0 && e.Action.Equals(NotifyCollectionChangedAction.Add)){ //Do this only when new item is added.
if (sender is not CollectionView oldItems) return;
foreach (var oldItem in oldItems) {
foreach (var newItem in (e.NewItems)??new ObservableCollection<TransactionItem>()){
if (!newItem.Equals(oldItem)){
YourDataGrid.ScrollIntoView(newItem); //Scroll into this specific item.
return; //No need to do further checking
}
}
}
}
}
}
However, the same behavior with the answer of #Paolo Iommarini will be expected when you duplicate the last item. Making his approach much better compared to this in terms of performance.
The second option and much better is by using Events. You simply need to define it into your ViewModel, then invoke the event every time a new item is added passing the argument as the newly added item. From your View, subscribe into that event and use DataGrid.ScrollIntoView(item) to scroll.
Here is a more detailed example, in your ViewModel.
public class ViewModel{
//define the event in the ViewModel.
public delegate void MyEventAction(YourObjectModelItem item);
public event MyEventAction? MyEvent;
private void AddNewItem(){ //Assuming this function is being called to add the item into your ObservableCollection.
var new_item = .... //(1) create your item.
YourObServableCollection.Add(new_item); //(2) add the item into your collection.
MyEvent?.Invoke(new_item); //(3) invoke the event and pass the new item as the argument.
}
}
Then in your View.
public YourView(){
InitializeComponent();
...
this.Loaded += (s, a) => { //You should subscribe only when the view is loaded, otherwise you might get a null issue with your DataContext. You may also use DataContextChanged if you want.
var vm = (ViewModel)DataContext; //(1) Get the ViewModel from your DataContext.
vm.MyEvent += (item) =>{ //(2) Subscribe to the event from the ViewModel.
YourDataGrid.ScrollIntoView(item); //(3) Scroll to that item.
};
};
}
With this approach, you don't have to make any comparison. You just need to know which item you will be scrolling.

C# Winforms: Set text in ComboBox based on selected Item

I'm new to c# and I'm now learning how to trigger events based on some form actions.
This is part of view:
private void comboGoodsName_TextChanged(object sender, EventArgs e)
{
controller.selectName(comboGoodsName.Text);
}
public void nameChanged(object sender, MeasurementEventArgs e)
{
comboGoodsName.TextChanged -= comboGoodsName_TextChanged;
comboGoodsName.Text = e.value;
comboGoodsName.TextChanged += comboGoodsName_TextChanged;
}
And this is part of controller:
public void selectName(string name)
{
model.Name = name.Split('|')[0].Trim();
if (name.Contains(" | "))
{
string code = name.Split('|')[1].Trim();
model.NameCode = code;
}
}
The scenario is as follows:
I want to have a ComboBox with some items in it (doesn't matter what's the source). Items are combination of name and code in following format: NAME | CODE. When I enter some text in ComboBox (type it in), comboGoodsName_TextChanged is triggered, which in turn calls selectName which sets model's property, which in turn raises an event which is observed by nameChanged. This works fine, as expected (puts NAME in ComboBox and CODE to TextBox - not shown as not relevant). Problem shows up when I select item from ComboBox drop-down list. When I select item, instead of showing NAME in ComboBox, I see NAME | CODE.
Edit: In the model, property is set correctly, which I confirmed by printing its value. So, issue is related only to displaying proper value in ComboBox.
Try this:
private void comboGoodsName_SelectedIndexChanged(object sender, EventArgs e)
{
// if combobox has selected item then continue
if (comboGoodsName.SelectedIndex > -1)
{
// split the selecteditem text on the pipe into a string array then pull the first element in the array i.e. NAME
string nameOnly = comboGoodsName.GetItemText(this.comboGoodsName.SelectedItem).Split('|')[0];
// handing off the reset of the combobox selected value to a delegate method - using methodinvoker on the forms main thread is an efficient to do this
// see https://msdn.microsoft.com/en-us/library/system.windows.forms.methodinvoker(v=vs.110).aspx
this.BeginInvoke((MethodInvoker)delegate { this.comboGoodsName.Text = nameOnly; });
}
}

c# preventing custom combobox from autoselecting an item

i am trying to implement my own ComboBox class in C# because, untill 3.5 NET Framework (if i'm not mistaking) suggestion lookup is made with a "StartWith" function (i.e. if the list contains "Doe, John" and user types "John", that item is not displayed). Basically i'm adding or removing items on text change event, getting them from the initial content of the list. Everything works pretty fine for what i am looking for, the only issue is, when ComboBox is clicked out, an item is still being selected even though it is not equal to the inserted text. Following the example i did, i want that "Doe, John" is selected (and set as ComboBox.Text property) only if user clicked on it, if user just typed "John" and no item is strictly equal to it (not just contain it), then Text property must remain as the user inserted it. Here's the code of my derived class
public class customTB : ComboBox
{
private object[] startlist;
public customTB() : base()
{
this.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.None;
this.AutoCompleteSource = System.Windows.Forms.AutoCompleteSource.None;
this.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDown;
this.Sorted = true;
this.KeyPress += customTB_KeyPress;
this.TextChanged += customTB_TextChanged;
this.Enter += customTB_Enter;
}
void customTB_Enter(object sender, EventArgs e)
{
this.DroppedDown = (this.Items.Count > 0);
}
void customTB_TextChanged(object sender, EventArgs e)
{
UpdateList();
}
void customTB_KeyPress(object sender, KeyPressEventArgs e)
{
this.DroppedDown = (this.Items.Count>0);
}
void UpdateList()
{
if (this.startlist == null)
{
//get starting lists elems
this.startlist = new Object[this.Items.Count];
this.Items.CopyTo(this.startlist, 0);
}
this.BeginUpdate();
foreach (object o in startlist)
{
if (o.ToString().Contains(this.Text))
{
if (!this.Items.Contains(o))
this.Items.Add(o);
}
else if (this.Items.Contains(o))
this.Items.Remove(o);
}
this.EndUpdate();
}
}
If tried, any time you try to exit the ComboBox, Text is highlighted and its value is set to an item.
As example of what i would like to have is:
items contains "Doe John", "Smith John", "Smith Marie".
if user types "John", then dropdown items are "Doe John" and "Smith John" but if he doesn't click any of the dropdown elements and exit the ComboBox (i.e. clicking outside), the Text remains "John"
Have one boolean variable itemClicked
Set itemClicked to false inside Enter event handler
Set itemClicked to true inside SelectionChangeCommitted event handler
Set Text property to string.Empty if not itemClicked inside DropDownClosed event handler

Getting the old selected index in Winform's Combo box

I have a combo box (winform). This combo box has some items (eg. 1,2,3,4).
Now, when I change the selection within this combo, I wish to know the old index and the new index.
How do I get this?
Possible approaches that I wish to AVOID.
Add an enter event, cache the current index and then on selection index change get the new index.
Using the selected text/selected item property received by the sender of the event.
What I ideally want:
In the event args that are received, I want something like:
e.OldIndex;
e.newIndex;
Right now the event args which are received in the SelectionIndex Change event are totally useless.
I don't want to use more than one event.
If C#, does not offer this, can I have my event which passes the old index and new index as event args?
Seems like this is a possible duplicate
ComboBox SelectedIndexChanged event: how to get the previously selected index?
There is nothing built in, you will need to listen for this event and keep track in a class variable.
But this answer seems to suggest a sensible way of extending the combobox to keep track of the previous index
https://stackoverflow.com/a/425323/81053
1-Make a List of integers
2-Bind a Button to switch to previous Screen (button Name "prevB")
3-change the ComboBox Index as Per described in the code
//initilize List and put current selected index in it
List<int> previousScreen = new List<int>();
previousScreen.Add(RegionComboBox.SelectedIndex);
//Button Event
private void prevB_Click(object sender, EventArgs e)
{
if (previousScreen.Count >= 2)
{
RegionComboBox.SelectedIndex = previousScreen[previousScreen.Count - 2];
}
}
You will need to replace the ComboBox with the following control:
public class AdvancedComboBox : ComboBox
{
private int myPreviouslySelectedIndex = -1;
private int myLocalSelectedIndex = -1;
public int PreviouslySelectedIndex { get { return myPreviouslySelectedIndex; } }
protected override void OnSelectedIndexChanged(EventArgs e)
{
myPreviouslySelectedIndex = myLocalSelectedIndex;
myLocalSelectedIndex = SelectedIndex;
base.OnSelectedIndexChanged(e);
}
}
Now you can get the PreviouslySelectedIndex property.
You can use YourComboBox.Tag (or other unused string/int property) to store old selected index...
I use such pair
comboBox.SelectedItem new item
comboBox.SelectionBoxItem old item

Generic.List: How to set/get position/currentitem for DataGridView?

I'm currenty working on a DataGridView-extentsion with custom DataSource handling. If I bind a list to two normal System.Windows.Forms.DataGridView and select an item in dataGridView1, dataGridView2 automatically also sets the position to the item.
If I assign a BindingSource I can handle the PositionChanged event, but a Generic.List doesn't have a CurrencyManager, so how does the dataGridView2 know the new position?
You want to get the current position of some DataGridView (having the as list as DataSource) from the List?
Then the answer is: you cannot. The list knows nothing of the connected view - shown element included (of course)
Alternative to get the info from the DataGridView:
subscribe to the SelectionChanged event of the DataGridView and set the index of the second accordingly - for both you should be able to use the CurrentCell-property
You cannot do such things as you described in your comments below without knowing something of the DataGridView.
It's a different design - you could implement your own "ShowableList" or something and try creating your own DataGridView that shows the indicated item from your ShowableList and sets the ShownIndex in there too - but you have to do this yourself.
Finally i found the answer: BindingContext!
A simple example:
public class ModifiedCollection : BindingSource {
BindingSource Source {get;set;}
BindingManagerBase bmb;
Control Parent;
public ModifiedCollection(object Source, Control Parent) {
if ((this.Source = Source as BindingSource) == null) {
this.Source = new BindingSource();
this.Source.DataSource = Source;
}
this.Source.ListChanged += new ListChangedEventHandler(Source_ListChanged);
this.Parent = Parent;
this.Parent.BindingContextChanged += new EventHandler(Parent_BindingContextChanged);
}
void Parent_BindingContextChanged(object sender, EventArgs e) {
if (bmb != null) {
bmb.PositionChanged -= bmb_PositionChanged;
}
if (Parent.FindForm().BindingContext.Contains(this.Source.DataSource)) {
bmb = Parent.BindingContext[this.Source.DataSource];
if (bmb != null) {
bmb.PositionChanged += new EventHandler(bmb_PositionChanged);
}
}
}
}

Categories

Resources