Removing items from a List(Of ) results in System.InvalidOperationException - vb.net

It happens so often to me that I need to remove items from a List(Of) if a certain condition is met.
I prefer to do it this way:
For Each nClass As SomeClass In MyList
If (Something) Then
MyList.Remove(nClass)
End If
Next
However, when I remove an item, the collection is changed, and the For Next Statement can't proceed, and a System.InvalidOperationException is thrown.
I wonder if there's any general way to do this properly without writing big workarounds.
Can anybody tell how this should be done correctly?
I'm attaching a test code to see the error:
Public Class Form1
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim nList As New List(Of SomeClass)
For i As Integer = 0 To 5
Dim nNewItem As New SomeClass
nNewItem.Int = i
nNewItem.Text = "My text " + i.ToString
nList.Add(nNewItem)
Next
For Each nItem As SomeClass In nList
If nItem.Int > 1 And nItem.Int < 5 Then
nList.Remove(nItem)
End If
Next
End Sub
End Class
Public Class SomeClass
Private _iInt As Integer = 0
Private _sText As String = String.Empty
Public Property Int() As Integer
Get
Return _iInt
End Get
Set(value As Integer)
_iInt = value
End Set
End Property
Public Property Text() As String
Get
Return _sText
End Get
Set(value As String)
_sText = value
End Set
End Property
End Class

As Plutonix said, you need to work backwards through your loop -
Change
For Each nItem As SomeClass In nList
If nItem.Int > 1 And nItem.Int < 5 Then
nList.Remove(nItem)
End If
Next
to
For I As Integer = nList.Count to 0 Step -1
If nlist(I).Int > 1 And nList(I).Int < 5 Then
nList.RemoveAt(I)
End If
Next
The reason being that in a For Each..Next loop, the number of items in the object is recorded at the start of the loop and does not change. Even if you add or remove items in the object.
For example. Say you start off with a list of 10 letters.
A
B
C
D
E
F
G
H
I
J
So. At the start of your For Each loop, the number of items is recorded (10). As the loop iterates through the list, let's say you remove "E".
For the purpose of this explanation is doesn't matter too much which one. Anyway, you'll end up with all the subsequent items being moved back one. So what was at index position 5 will be at position 4 and so on to what was at index 10 is now at index 9 . The loop carries on and when it tries to access the item at index 10, it doesn't exist because it's outside the bounds of the list that now has 9 items.
If instead you use a `For..Next' loop stepping backwards, and start at the list item and head backwards, when you get to say, "E" and want to remove it, and then step backwards again and again until you get to the first item,the first item is still at the same index position, so no problem.

Related

unable to add items to a list in VB.NET

Good morning,
so i have received this homework for the summer where i have to create a program to store a list of movies and display them, but the problem is that there isn't a defined number of movie so i can't use the constant method i've always used, so i tried doing that with variables instead, but whenever i press the input button twice the app crashes and i get the error "Index over the matrix limits"
Here's the code in the module
Module Module1
Public Structure Film
Public Titolo As String
Public Autore As String
Public Incasso As Integer
Public Nazionalita As String
End Structure
Public i As Integer = 0
Public Flm(i) As Film
End Module
And here's the input part
Public Class frmInput
Private Sub btnInserisci_Click(sender As Object, e As EventArgs) Handles btnInserisci.Click
If IsNumeric(txtIncasso.Text) = False Then
MsgBox("L'incasso deve essere un valore numerico", MsgBoxStyle.Exclamation, "Attenzione")
ElseIf txtTitolo.Text = "" Or txtAutore.Text = "" Or txtNazionalita.Text = "" Then
MsgBox("Uno o piĆ¹ valori sono vuoti", MsgBoxStyle.Exclamation, "Attenzione")
Else
Flm(i).Titolo = txtTitolo.Text
Flm(i).Autore = txtAutore.Text
Flm(i).Incasso = txtIncasso.Text
Flm(i).Nazionalita = txtNazionalita.Text
i += 1
End If
End Sub
End Class
You should use a List(Of Film) to store the inputs received.
A generic List like that has no practical limits and can grow while you add elements to it
Public Flm As List(Of Film) = new List(Of Film)
....
Else
Dim f as Film = new Film()
f.Titolo = txtTitolo.Text
f.Autore = txtAutore.Text
f.Incasso = txtIncasso.Text
f.Nazionalita = txtNazionalita.Text
Flm.Add(f)
End If
A List(Of Film) could be used like it was an array
For x As Integer = 0 To Flm.Count -1 Step 1
Console.WriteLine("Film #" & x+1)
Console.WriteLine("Titolo = " & Flm(x).Titolo)
.....
Next
And of course you can iterate over it using a simpler foreach
For Each Film f in Flm
Console.WriteLine("Film #" & x+1)
Console.WriteLine("Titolo = " & f.Titolo)
.....
Next
Although others have mentioned using List, which would probably be appropriate, you also mentioned it was a homework task, so maybe you need, or have to, use the more traditional arrays as you have shown. Bearing this in mind, and also to let you know what your problem is. You are incrementing i but not the array.
Public i As Integer = 0
Public Flm(i) As Film
Thus, Flm is 0 to 0, one element.
You add to this, all is OK.
You increment i, good, i += 1
However, you don't then increment the array, Flm(). Incrementing i doesn't automatically increment the array, Flm().
You need to use: ReDim Preserve
Thus... Change:
Else
Flm(i).Titolo = txtTitolo.Text
to:
Else
ReDim Preserve Flm(i)
Flm(i).Titolo = txtTitolo.Text
Lastly, IsNumeric and MsgBox are remnants of the VB6 days, there are vb.net equivalents. Also, using i as a global/public variable is really not good. It's very common, standard, to use i in all local subroutines and functions for little loops etc, it gets used and lost. If you look at almost all examples of code, you'll see i being used as the integer counter.

vb.net fastest way to remove duplicates from listview?

Does anyone have a quicker and better way to remove duplicates from a listview? I am currently doing it like this: I sort the items alphabetically, and then it checks the item below and compares it with the one above.
This is time consuming though.. When I enter 20.000 records into an excel sheet, and remove duplicates it takes a few miliseconds, but with this code below it takes hours to check 20.000 items in vb.net. Does anyone know of a faster method?
Dim max As Integer = ListView2.Items.Count
Dim i As Integer = 0
For Each item As ListViewItem In ListView2.Items
If i = max Then
Exit For
End If
If i > 0 Then
If item.Text = ListView2.Items(i - 1).Text Then
max -= 1
item.Remove()
i -= 1
End If
End If
i += 1
Label4.Text = "Total domains: " & ListView2.Items.Count
Next
Here is a Linq based solution to get distinct items based on item.Text and sort the items. If sorting is not required, you can remove the OrderBy part.
Private Shared Sub RemoveDuplicatesAndSort(lv As ListView)
Dim distictItems As ListViewItem() = lv.Items.Cast(Of ListViewItem)().Distinct(New LVItemComparer()).OrderBy(Function(item As ListViewItem) item.Text).ToArray
lv.BeginUpdate() ' suppress screen updates
lv.Items.Clear()
lv.Items.AddRange(distictItems)
lv.EndUpdate()
End Sub
Private Class LVItemComparer : Implements IEqualityComparer(Of ListViewItem)
Public Function Equals1(x As ListViewItem, y As ListViewItem) As Boolean Implements IEqualityComparer(Of ListViewItem).Equals
Return x.Text.Equals(y.Text)
End Function
Public Function GetHashCode1(obj As ListViewItem) As Integer Implements IEqualityComparer(Of ListViewItem).GetHashCode
Return obj.Text.GetHashCode
End Function
End Class
Use HashSet which will accept only unique values.
Dim itemsText = ListView2.Items.Cast(Of ListViewItem).Select(Function(item) item.Text)
Dim uniquesValues As HashSet(Of String) = New HashSet(Of String)(itemsText)
Then set items from HashSet to the ListView.

Generate a property name in a loop

I'm trying to find a way of loading a single record with 25 columns into a datatable.
I could list all 25 variables called SPOT1 to SPOT25 (columns in the datatable) but I'm looking for more concise method like using a loop or dictionary.
The code below shows two methods, a 'long' method which is cumbersome, and a 'concise' method which I'm trying to get help with.
Public dctMC As Dictionary(Of String, VariantType)
Dim newMC As New MONTE_CARLO()
'long method: this will work but is cumbersome
newMC.SPOT1=999
newMC.SPOT2=887
...
newMC.SPOT25=5
'concise method: can it be done more concisely, like in a loop for example?
Dim k As String
For x = 1 To 25
k = "SPOT" & CStr(x)
newMC.K = dctMC(k) 'convert newMC.k to newMC.SPOT1 etc
Next
'load record
DATA.MONTE_CARLOs.InsertOnSubmit(newMC)
Per the others, I think there are better solutions, but it is possible...
Public Class MONTE_CARLO
Private mintSpot(-1) As Integer
Property Spot(index As Integer) As Integer
Get
If index > mintSpot.GetUpperBound(0) Then
ReDim Preserve mintSpot(index)
End If
Return mintSpot(index)
End Get
Set(value As Integer)
If index > mintSpot.GetUpperBound(0) Then
ReDim Preserve mintSpot(index)
End If
mintSpot(index) = value
End Set
End Property
End Class
Usage...
Dim newMC As New MONTE_CARLO
For i As Integer = 0 To 100
newMC.Spot(i) = i
Next i
MsgBox(newMC.Spot(20))

More effective loop

I'm making a player-match-up program in Visual Basic. The program is supposed to pick random registered players and and pair them. I'm currently working on the odd-number-of-players-part.
The solution I have is working but is perhaps not that effective. Is there a better way for me to write this code?
The code is supposed to pick the random players and make sure they are not picked again. As you see, for the code to work i must make it loop thousands of times. If I don't some of the players won't show up in the listbox. Is there a better solution???
In case it's confusing "spiller" is norwegian for "player"
For i As Integer = 0 To 100000
Dim spiller1 As Integer
Dim spiller2 As Integer
Do
spiller1 = CInt(Math.Floor(Rnd() * spillerListe.Count))
spiller2 = CInt(Math.Floor(Rnd() * spillerListe.Count))
Loop Until CBool(spiller1 <> spiller2)
If brukteSpillere(spiller1) = False And brukteSpillere(spiller2) = False Then
brukteSpillere(spiller1) = True
brukteSpillere(spiller2) = True
lstSpillere.Items.Add(spillerListe(spiller1).ToString + " VS. " + spillerListe(spiller2).ToString())
End If
Next i
This is a mess... Have a List(Of Integer) with all the available index.
Loop while availableIndex.Count > 1
Pick a random index from availableIndex and remove it from that list
Pick a random index from availableIndex and remove it from that list
Add these two index to the list of pairs
End Loop
that way you don't need to check if the random values are the same or if they were already picked.
Now, if you don't want to create a list. Then threat the random number not as an index, but as the number of items to check.
Delta = RandomNumber
x = 0
For i As Integer = 0 To itemList.Count-1
If Not itemList(i).IsChoosen Then
x += 1
If x = Delta Then
' i is now your item to pick
itemList(i).IsChoosen = True
Exit For
End If
End If
Next
There are two efficient ways in approaching this problem:
Sort your player list by random number, then match up 1 with 2, 3 with 4 and so on.
Dim r As New Random
Dim randomListe = spillerListe.OrderBy(Function() r.Next).ToList
Generate two random numbers from your range, match up those players into a separate List, remove players from the original list. General two more random numbers from a smaller range (original minus 2), match up, etc.
EDIT: Having looked at MSDN, List has O(n) performance for RemoveAt, so it's not quite efficient, better be using a dictionary, which is O(1) at removing items, so instead of spillerListe have some spillerDicte, where you would add entries in a form (key = index, value = item).
Instead of working with integers, what if you keep your players name in a list and, after picking a player you remove it from the list. Probably this will not be the best performant solution, but it is clear what are you trying to do
Dim lstSpillere = new List(Of String)() ' Just for the example below
Dim spillerListe = new List(Of String)() from {"Marc", "John", "Steve", "George", "David", "Jeremy", "Andrew" }
Dim rnd = new Random()
While spillerListe.Count > 1
Dim firstPlayer = spillerListe(rnd.Next(0, spillerListe.Count))
spillerListe.Remove(firstPlayer)
Dim secondPlayer = spillerListe(rnd.Next(0, spillerListe.Count))
spillerListe.Remove(secondPlayer)
lstSpillere.Add(firstPlayer + " VS. " + secondPlayer)
' for debug purpose....
Console.WriteLine(firstPlayer & " VS. " & secondPlayer)
End While
if spillerListe.Count > 0 Then
Console.WriteLine("Excluded from play list is:" & spillerListe(0))
End if
The important key here is the generation of Random instance that should be outside the loop to avoid to generate the same number in the short time period required by the loop to execute.
Try this:
Module Module1
Dim rnd As New Random
Sub Main()
Dim RegisteredPlayers As New List(Of Player)
' Fill List (example 100 players)
For index As Integer = 1 To 100
RegisteredPlayers.Add(New Player(String.Format("Player{0}", index)))
Next
'Sort Players using a random number
Dim SortedPlayersArray = RandomSortItems(RegisteredPlayers.ToArray())
'Pair players by selecting 2 consequative ones from randomly sorted array
Dim Matches As New List(Of Match)
For index As Integer = 1 To SortedPlayersArray.Length Step 2
Dim m As Match = New Match(SortedPlayersArray(index - 1), SortedPlayersArray(index))
Matches.Add(m)
Debug.WriteLine(m.ToString())
Next
' Match Player48 vs. Player79
' Match Player3 vs. Player53
' Match Player18 vs. Player43
' Match Player85 vs. Player1
' Match Player47 vs. Player56
' Match Player23 vs. Player66
' etc..
End Sub
Public Function RandomSortItems(Of T)(ByVal items As T()) As T()
Dim sorted As T() = New T(items.Length-1) {}
Array.Copy(items, sorted, sorted.Length)
Dim keys As Double() = New Double(items.Length-1) {}
For i As Integer = 1 To items.Length
keys(i - 1) = rnd.NextDouble()
Next
Array.Sort(keys, sorted)
Return sorted
End Function
End Module1
Public Class Player
Dim m_name As String
Public Sub New(ByVal player_name As String)
m_name = player_name
End Sub
Public ReadOnly Property Name() As String
Get
Return m_name
End Get
End Property
Public Overrides Function ToString() As String
Return m_name
End Function
End Class
Public Class Match
Dim m_player_1 As Player, m_player_2 As Player
Public Sub New(ByVal player_1 As Player, ByVal player_2 As Player)
m_player_1 = player_1
m_player_2 = player_2
End Sub
Public ReadOnly Property Player1() As Player
Get
Return m_player_1
End Get
End Property
Public ReadOnly Property Player2() As Player
Get
Return m_player_2
End Get
End Property
Public Overrides Function ToString() As String
Return String.Format("Match {0} vs. {1}", Player1, Player2)
End Function
End Class
Edit 1:
An alternate random sorter (which should be faster) is
Public Function RandomSortItems(Of T)(ByVal items As T()) As T()
Dim slist As New SortedList(Of Double, T)
For i As Integer = 1 to items.Length
slist.Add(rnd.NextDouble(), items(i-1) )
Next i
return slist.Values.ToArray()
End Function

Adding a variable to a listBox in VB.net Windows Form

I am making a dvd database system in windows form and trying to display the dvd's entered by a user. Then display the Title, Director and Genre in 3 separate listBoxes.
When the user enters the information through 3 separate text boxes, the information is stored in a structure I made called TDvd. This means I can call for example dvd.Title or dvd.Director. I also use the variable index to add this information to an array I made called Dvd(100) (just a random number I used to test).
Here is the code I currently have for adding the items to the ListBox:
For i = 1 To noOfAddedDvds
lstTitle.Items.Add(dvd(i).Title)
lstDirector.Items.Add(dvd(i).Director)
lstGenre.Items.Add(dvd(i).Genre)
Next
The variable NoOfDvdsAdded is just a way of keeping track of the number of dvd's the user has already entered.
I run this and enter the Title, Director and Genre, but when I try and display this information across the 3 listboxes, I get the error:
An unhandled exception of type 'System.ArgumentNullException' occurred in System.Windows.Forms.dll
Public Class Form1
Structure TDvd
Dim Title As String
Dim Director As String
Dim Genre As String
End Structure
Dim dvd(100) As TDvd
Dim index As Integer = 0
Dim noOfAddedDvds As Integer
Private Sub btnAddToDatabase_Click(sender As Object, e As EventArgs) Handles btnAddToDatabase.Click
If txtDirector.Text <> "" Or txtGenre.Text <> "" Or txtTitle.Text <> "" Then
txtTitle.Text = dvd(index).Title
txtDirector.Text = dvd(index).Director
txtGenre.Text = dvd(index).Genre
index += 1
noOfAddedDvds += 1
End If
End Sub
Private Sub btnDisplayDatabase_Click(sender As Object, e As EventArgs) Handles btnDisplayDatabase.Click
Dim i As Integer
For i = 0 To noOfAddedDvds
MessageBox.Show(index & ", " & i)
lstTitle.Items.Add(dvd(i).Title)
lstDirector.Items.Add(dvd(i).Director)
lstGenre.Items.Add(dvd(i).Genre)
MessageBox.Show(index & ", " & i)
Next
End Sub
End Class
According to the documentation, an ArgumentNullException is thrown by the Add() method if the argument passed to it is null. (Or Nothing in VB.) So one of these is Nothing at runtime:
dvd(i).Title
dvd(i).Director
dvd(i).Genre
You'll have to debug to determine which. It would seem that the error is because you're starting your iteration at 1 instead of 0, I would think it should be:
For i = 0 To noOfAddedDvds - 1
So when you get to the index of noOfAddedDvds in your collection, that element will be an uninitialized struct with Nothing strings.
You'll definitely want to fix the iteration (indexes start at 0). Additionally, you may also benefit from initializing the String properties in your struct to String.Empty internally. Depends on whether you want similar errors to manifest as an exception or as an empty record. Sometimes the latter makes the problem more obvious since at runtime you'd see that your output started on the second record.
Just a few pointers...
The Items collection on the ListBox is actually 0 indexed, by which I mean that instead of going "1,2,3", it actually goes (0,1,2).
That's what your problem is.
Hint - think about perhaps using a List instead of an array as well... (for dvd)
Your thing cries out for being rewritten in OO form:
Friend DVDGenres
Undefined
Comedy
Action
Adventure
Sci-Fi
End Enum
Friend Class DVD
Public Property Title As String
Public Property Director As String
Public Property Genre As DVDGenres
Public Sub New
Title = ""
Director = ""
Genre = DVDGenres.Undefined
' other stuff too
End Sub
Public Overrides Function ToString As String
Return Title
End Sub
End Class
Now something to store them in. Arrays went out with Rubik's Cubes, so a List:
Private myDVDs As New List(of DVD)
A list and a class can do what arrays and structures can without the headaches. Add a DVD:
Dim d As New DVD
d.Name = TextBoxName.Text
d.Director = TextBoxDir.Text
d.Genre = comboboxGenre.SelectedItem
' add to the container:
myDVDs.Add(d)
Display all the DVDs in a ListBox to pick from:
AllDVDsLB.DataSource = myDVDs
AllDVDsLB.DisplayMember = "Title"
This will set your list as the datasource for the listbox. Whatever is in the List is automatically displayed without copying data into the Items collection. Then, say from selectedindex changed event, display the selected item details to some labels:
Label1.Text = Ctype(AllDVDsLB.SelectedItem, DVD).Title
Label2.Text = Ctype(AllDVDsLB.SelectedItem, DVD).Director
Label3.Text = Ctype(AllDVDsLB.SelectedItem, DVD).Genre.ToString
Iterate to do something like what is in the Question:
For Each d As DVD in myDVDs ' CANT run out of data
lstTitle.Items.Add(d.Title)
lstDirector.Items.Add(d.Director)
lstGenre.Items.Add(d.Genre.ToString)
Next
Or iterate and reference with an Int32:
For n As Integer = 0 To myDVDs.Count - 1
lstTitle.Items.Add(myDVDs(n).Title)
' etc
Next n
HTH