Adding rows inside a loop - c#

If I execute the following code fragment:
IXLRange dataRange = worksheet.Range(1, 1, myData.Count, 4);
foreach (var row in dataRange.Rows()) {
//int cells = row.CellCount();
if (isEndOfGroup) {
row.InsertRowsBelow(1);
var rowBelow = row.RowBelow();
// Do stuff with the added row, like adding a subtotal
}
}
What's actually happening here? It seems like the row collection being iterated on is being updated, which is what I want to happen, because the commented-out line throws an exception with the error "Range is invalid" on the next iteration, as if the row somehow hasn't been initialised yet.
I understand modifying a collection as you iterate through it is bad practice, but I've done this before without any issues.
What would be an alternative way of achieving this?

Never modify a collection while iterating... It is a bid.
Take advantage of the for loop.
When it did not raise an exception before, then you just changed the content of the row but not the reference of to row itself.

I ended up using a for loop as Bagerfahrer suggested and having a second counter to keep track of the row number:
int rowNum = 1; // Row numbering starts at 1
for (int i = 0; i < myData.Count; i++) {
var row = worksheet.Row(rowNum);
if (isEndOfGroup) {
row.InsertRowsBelow(1);
rowNum++;
var rowBelow = row.RowBelow();
// Do stuff with the added row, like adding a subtotal
}
rowNum++;
}
This allows me to index into myData for retrieving data and also keep track of where I'm writing to in the spreadsheet.

Related

clear datagridview duplicate rows and keep unique rows

so when filling my datagridview I normally do something like
public void FillTable(CoBRAMetaField[] metaFields)
{
dataGridView.Rows.Clear();
// do something with metaFields
}
Important:
CoBRAMetaField is a object with a ID and other stuff
Each row in the grid holds a metafield object
My grid gets filled correctly (sorry, the language is german)
When I fill the grid another time I only want to remove the rows with metaFields that don't exist in the new metaFields array. I want this behaviour because when a user selected a value for this row I don't want it to get removed and created again because then the selected value is removed too.
I came up with this
public void FillTable(CoBRAMetaField[] metaFields)
{
for (int i = 0; i < dataGridView.Rows.Count; i++) // loop through the grid rows
{
double metaFieldID = (dataGridView.Rows[i].Cells[0].Tag as CoBRAMetaField).ID; // get the ID from the row metaField
if (metaFields.Any(field => field.ID == metaFieldID)) // Does it exist?
metaFields = metaFields.Where(field => field.ID != metaFieldID).ToArray(); // Remove it from the new array
else // it doesn't exist
dataGridView.Rows.Remove(dataGridView.Rows[i]); // remove the row
}
// Fill the grid with the remaining metaFields
}
The first run gets initialized correctly
the second run seems to crash, some fields remain empty
when I press the button on this row I get a nullpointer exception. I only get this error when using the "new code" so am I missing something? Is there something I didn't think about?
I will provide a full example here
At first create a DataGridView and Button on the form. Create a file for all required classes and take this snippet
https://pastebin.com/BFmr2ps9
After that fill the forms code with some test data
https://pastebin.com/Yz84Akkj
and now setup the DataGridView logic
https://pastebin.com/qH6kZKZv
I added
dataGridView.AllowDrop = false;
dataGridView.AllowUserToAddRows = false;
dataGridView.AllowUserToDeleteRows = false;
dataGridView.AllowUserToOrderColumns = false;
dataGridView.AllowUserToResizeRows = false;
if you just want to copy paste but you can also do it by the forms designer. Have a look at dataGridView.Rows.Clear(); this provides a working example. Comment it out and use the code above to test the incorrect example
The main problem is in the code for adding rows (taken from the link):
// Fill the grid with the remaining metaFields
for (int i = 0; i < metaFields.Length; i++)
{
MetaField currentMetaField = metaFields[i];
dataGridView.Rows.Add(currentMetaField.Name, null);
DataGridViewRow newRow = dataGridView.Rows[i]; // <-- Problem!
DataGridViewCell metaFieldCell = newRow.Cells[0];
metaFieldCell.Tag = currentMetaField;
(newRow.Cells[1] as DataGridViewAllocationCell).Initialize(releaseSetupData);
}
In the marked line you are assuming that the index of the added row is the same as i, which is true when you start with empty grid, bit not when the grid is updated and some old records are kept.
The proper way of handling it is to not assume the new row index - it is returned by the Add method:
int rowIndex = dataGridView.Rows.Add(currentMetaField.Name, null);
DataGridViewRow newRow = dataGridView.Rows[rowIndex];
This will solve the original issue from the question.
There is also a problem in the remove part of the code - the for loop will miss checking the rows next to the removed ones. Anytime you want to iterate some list and remove items during the iteration, use reverse for loop and RemoveAt:
for (int i = dataGridView.Rows.Count - 1; i >= 0; i--) // <--
{
double metaFieldID = (dataGridView.Rows[i].Cells[0].Tag as MetaField).ID;
if (metaFields.Any(field => field.ID == metaFieldID))
metaFields = metaFields.Where(field => field.ID != metaFieldID).ToArray();
else
dataGridView.Rows.RemoveAt(i); // <--
}
The removal code could further be improved (currently looks inefficient with these Any, Where + ToArray), but at least with the above changes it will work correctly.

Index out of range error when adding a cloned DataGridViewRow to a DataGridViewRowCollection

I got the error:
Index was out of range. Must be non-negative and less than the size of the collection.\r\nParameter name: index
This error happens when I add a cloned row to a DataGridViewRowCollection, at the line collection.Add(coleRowWithValues(row)). However, it does not happen immediately, as the function gets called a few times first on which it completes successfully.
My code is:
public DataGridViewRowCollection getSelectedRows()
{
object value = null;
DataGridViewRowCollection collection = new DataGridViewRowCollection(this);
foreach (DataGridViewRow row in Rows)
{
value = row.Cells[columnCheckBox.Index].Value;
if (value != null && (bool) value == true)
{
collection.Add(cloneRowWithValues(row)); //error happens here
}
}
return collection;
}
where this is a class derived from DataGridView.
cloneRowWithValues is defined as:
private DataGridViewRow cloneRowWithValues(DataGridViewRow oldRow)
{
DataGridViewRow newRow = (DataGridViewRow)oldRow.Clone();
for (Int32 i = 0; i < oldRow.Cells.Count; i++)
{
newRow.Cells[i].Value = oldRow.Cells[i].Value;
}
return newRow;
}
This last function I got from the MSDN documentation.
So my question is: What do I do wrong here, and how can I fix it?
EDIT:
I have added some extra information which came from the debugger.
SECOND EDIT:
The error still remains exactly the same when changing the name of variable index to i in function cloneRowWithValues(), which I therefore altered to avoid confusion.
Your problem lies in this:
DataGridViewRowCollection collection = new DataGridViewRowCollection(this);
This creates a DGVRowCollection that is linked to (source)
The DataGridView that owns the DataGridViewRowCollection.
Changing this object will change the DataGridView. You seem to not want to change the current DataGridView, but merely to read some data from it.
The way it is now, collection is linked to the same DGV as Rows. Changing a collection while foreach iterating it is a compiler error for most collections. But because you didn't change Rows, but collection, with collection.Add(cloneRowWithValues(row)); this weird construction evaded that, and instead blew up.
Change it to a List<DataGridViewRow> list = new List<DataGridViewRow>() instead, and add those rows that match the condition. Return the list.
Or with LINQ, as DGVR is an Enumerable:
var selected = Rows.Where(row => row.Cells[columnCheckBox.Index].Value as bool? == true);
Also, those debug screenshots are useless, what's interesting is the variables at time of crash. (PMing with OP got me better info).
EDIT: And I now read the actual cloning function:
DataGridViewRow newRow = (DataGridViewRow)oldRow.Clone();
for (Int32 i = 0; i < oldRow.Cells.Count; i++)
{
newRow.Cells[i].Value = oldRow.Cells[i].Value;
}
return newRow;
DataGridViewRow is like an array of DataGridViewCell reference objects. Cloning it gives a new Row object with references to the same cells. newRow.Cells[i] is the exact same object as oldRow.Cells[i]. The assignment here does literally nothing, as it stores a value over itself.

move datagridview Row to another datagridView

I've gone through the other answers for this kind of question, nothing has really worked well so far.
I have a foreach loop that should add the the row from the source datagridview to the destination datagridview then remove that row from the source.
The invalid operation exception is: Row provided already belongs to a DataGridView control.
I couldn't get ...Rows.Copy() to work either. Any ideas?
foreach (DataGridViewRow selRow in fromDataGridView.SelectedRows)
{
toDataGridView.Rows.Add(selRow);
fromDataGridView.Rows.Remove(selRow);
}
You need to remove the row from fromDataGridView before you add it to toDataGridView.
But you're modifying the collection inside the foreach loop - that won't work.
The workaround is to copy the collection for use in the foreach.
Try this:
foreach (DataGridViewRow selRow in fromDataGridView.SelectedRows.OfType<DataGridViewRow>().ToArray())
{
fromDataGridView.Rows.Remove(selRow);
toDataGridView.Rows.Add(selRow);
}
Here is an example of what you could do or try..
its happening when one row is removed the rows count decrements too so if you put your code in for loop and run it in reverse it would work fine have a look:
for (int selRow = dataGridView1.Rows.Count -1; selRow >= 0 ; selRow--)
{
toDataGridView.Rows.Add(selRow);
fromDataGridView.Rows.Remove(selRow);
}
Adding a new row directly to DataGridView is not possible when there is BindingSource.
You should add row to the BindingSource of second view and let it to add the row to its grid.
I've tested the below code in a working solution.
var selectedRows = dataGridView1.SelectedRows;
int count = dataGridView1.SelectedRows.Count;
for (int i = 0; i < count; i++)
{
bindingSource2.Add(bindingSource1[selectedRows[i].Index]);
dataGridView1.Rows.Remove(selectedRows[i]);
}

Remove a row from a DataTable within a DataSet

I have a DataSet with several DataTables inside. I'm displaying the DataTables in a ListView (I didn't know about databinding when I wrote the code). Anyway, I would like to remove rows from the DataTables inside the DataSet.
I have tried this:
foreach (DataRow row in dsData.Tables["Table1"].Rows)
{
//find the row that contains the username I'm after
if (item.SubItems[2].Text == row["LoginName"].ToString())
{
dsData.Tables["Table1"].Rows.Remove(row); //<- main code of interest
}
}
I've also tried
dsData.Tables["Table1".Rows.Delete(row);
The problem I'm experiencing is that the when you remove a row I get the exception:
Collection was modified; enumeration operation might not execute.
From what I understand it's because when you remove a row from a ListView the row below it moves up and causes all this trouble. The code itself does what it's supposed to but it's not nice to see that exception when when you run it.
I was about to rewrite the whole class with a DataGridView but would rather correct that single line if possible :).
EDIT: I'm not even sure a DataGridView would solve the problem anyway.
change the loop to a for loop counting backwards so you don't get that message.
for(int i = dsData.Tables["TAble1"].Rows; i > 0; i--)
{
if(item.SubItems[2].Text == dsData.Tables["Table1"].Rows[i - 1]["LoginName"].ToString())
dsData.Tables["Table1"].Rows.Remove(i - 1)
}
Try:
DataSet dsData = new DataSet();
List<DataRow> rowsToDelete = new List<DataRow>();
foreach (DataRow row in dsData.Tables["Table1"].Rows)
{
if (item.SubItems[2].Text == row["LoginName"].ToString())
{
rowsToDelete.Add(row);
}
}
foreach(DataRow row in rowsToDelete)
{
dsData.Tables["Table1"].Rows.Remove(row);
}
You need a backwards for loop if you're going to be removing things
(explanation of why here)
for (int i = dsData.Tables["Table1"].Rows.Count - 1; i >= 0; i--)
{
DataRow row = dsData.Tables["Table1"].Rows[i];
//find the row that contains the username I'm after
if (item.SubItems[2].Text == row["LoginName"].ToString())
{
dsData.Tables["Table1"].Rows.Remove(row); //<- main code of interest
}
}
in general you can't remove items from a collection within a loop that is iterating on it
what you could do is keep a list of all the row you want to remove (creating it within the loop) and remove all of them OUTSIDE the loop
You can't modify a collection that you are iterating though with a foreach loop, from inside the loop. Do this instead:
for (int i = 0; i < dsData.Tables["Table1"].Rows.Count; i++)
{
DataRow row = dsData.Tables["Table1"].Rows[i];
if (item.SubItems[2].Text == row["LoginName"].ToString())
{
dsData.Tables["Table1"].Rows.Remove(row); //<- main code of interest
}
}

How to properly update a datatable while inside a loop

In my below code I'm comparing values (email addresses) in my Arrarylist (called emails) with values in my Datatable (called dtResult). I was hoping to be able to remove rows from the datatable if it has an email address that is found in the arraylist but I'm starting to think I'm better off creating a new datatable that does not have those rows instead (I just don't know how to do that). Obviously my code below bombs after a row is deleted because it loses its place in the foreach loop. How can I fix this?
Thanks.
foreach (DataRow row in dtResult.Rows)
{
var tmpl = row[3];
for (int x = 0; x < emails.Count; x++) //emails is an ArrayList of email addresses
{
string one = emails[x].ToString().ToUpper();
string two = tmpl.ToString().ToUpper();
//compare datatable value to arraylist value..
if (one == two)
{
//if they are equal then I want to remove them (or maybe create a new datatable??)
dtResult.Rows.Remove(row);
dtResult.AcceptChanges();
}
}
}
The easiest way would be to loop through the DataTable backward:
for (int i = dtResult.Rows.Count - 1; i >= 0; i--)
{
var row = dtResult.Rows[i];
// ...
}
You also might want to consider whether you can just mark the row state on those rows as deleted (and then observe that status later) instead of actually removing them from the table.
You could go through the DataTable in reverse
for(int i = dtResult.Rows.Count; i >= 0; i--)
{
foreach(object email in emails)
{
if(email.ToString().ToUpper() == dtResult[i].ToString().ToUpper())
{
dtResult.Rows.Remove(dtResult.Rows[i]);
dtResult.AcceptChanges();
}
}
}
Why don't you do it in two steps?
I didn't try this, so let me know how it goes.
var deleteRows = new List<DataRow>();
then in your loop, instead of deleting the row right away, just add the row to your list:
deleteRows.Add(row);
and after you loop through your database, you loop through the deleteRows list and delete each row one by one.

Categories

Resources