简体   繁体   中英

Deleting selected row from DataTable bound DataGridView

Consider a DataGridView whose DataSource is a DataTable . The dataGridView allows one row to be selected at a time. There is a "delete" button, which should delete the selected rows, by one of the columns, from the bound dataTable.

I have configured the DataGridView to be uneditable, from the user's point of view. It should only be possible to delete a row (from the dataTable and the grid) via a separate "delete" Button I have created. The event handler for this is shown below:

private void delete_Click(object sender, EventArgs args)
{
var usersTable = myGridView.DataSource as DataTable;
            foreach (DataGridViewRow selected in myGridView.SelectedRows)
            {
                string name = (string)selectedRow.Cells[0].Value;
                foreach (DataRow tableRow in usersTable.Rows)
                {
                    if (tableRow["Name"].ToString().Equals(name))
                        tableRow.Delete();
                }
                usersTable.AcceptChanges();
            }

            myGridView.DataSource = usersTable;
}

How to does this properly, without "collection was modified, enumeration might not execute" errors?

The grid is being populated as follows:

private DataTable usersTable = new DataTable("Users");

void Populate()
{
    usersTable.Columns.Clear();
    usersTable.Rows.Clear();
    usersTable.Columns.Add("Name", typeof(string));
    // addd other columns...

    foreach (var user in usersCollection)
    {
        usersTable.Rows.Add(user.GetName()),
        // add other rows...
    }

    // bind the data
    myGridView.DataSource = usersTable;
}

... The dataGridView allows one row to be selected at a time... which should delete the selected rows, by one of the columns...

The DataGridView.SelectedRows property requires the DataGridView.SelectionMode is set to FullRowSelect or RowHeaderSelect to return the selected rows. The entire row should be selected, not just some cells like in RowHeaderSelect option. Read the Remarks section of the property.

In this case, you can delete a selected row like so:

private void delete_Click(object sender, EventArgs e)
{
    if (myGridView.SelectedRows.Count > 0 && !myGridView.SelectedRows[0].IsNewRow))
    {
        myGridView.Rows.Remove(myGridView.SelectedRows[0]);

        // Don't call this if you have a DB to update.
        // (myGridView.DataSource as DataTable).AcceptChanges();
    }
}

Or, if you have the DataGridView.MultiSelect property enabled:

private void delete_Click(object sender, EventArgs e)
{
    if (myGridView.SelectedRows.Count > 0)
    {
        foreach (var r in myGridView.SelectedRows
            .Cast<DataGridViewRow>()
            .Where(r => !r.IsNewRow))
            myGridView.Rows.Remove(r);

        // Don't call this if you have a DB to update.
        // (myGridView.DataSource as DataTable).AcceptChanges();
    }
}

Now, if you have the SelectionMode property set to one of the other values, then you can't count on the SelectedRows property. You need to get the selected rows through the DataGridView.SelectedCells which is always populated regardless of the SelectionMode property value. Remarks

private void delete_Click(object sender, EventArgs e)
{
   var selRows = myGridView.SelectedCells
        .Cast<DataGridViewCell>()
        .Where(c => !c.OwningRow.IsNewRow)
        .Select(c => c.OwningRow)
        .Distinct();

    foreach (var r in selRows)
        myGridView.Rows.Remove(r);

     // Don't call this if you have a DB to update.
     // (myGridView.DataSource as DataTable).AcceptChanges();
}

Or, get the DataRow objects from the selected rows through the DataGridViewRow.DataBoundItem property and remove them from the DataTable .

private void delete_Click(object sender, EventArgs e)
{
    var dt = myGridView.DataSource as DataTable;

    var selRows = myGridView.SelectedCells
        .Cast<DataGridViewCell>()
        .Where(c => !c.OwningRow.IsNewRow)
        .Distinct()
        .Select(c => ((DataRowView)c.OwningRow.DataBoundItem).Row);

    foreach (var r in selRows)
        dt.Rows.Remove(r);

    // Don't call this if you have a DB to update.
    // dt.AcceptChanges();
}

Important

As @Jimi commented, if you have a database in your project, then the DataTable.AcceptChanges method should only be called right after:

  1. Populating the DataTable , loading the data from the database.
  2. Commiting the changes, updating the database.

Your problem is modifying collections during enumeration. Specifically both foreach loops.

You can get around this by marking things for deletion and then doing the actual deletes after you're done enumerating.

var rowsToDelete = new List<DataRow>();
foreach (DataGridViewRow selected in myGridView.SelectedRows)
{
    string name = (string)selectedRow.Cells[0].Value;
    foreach (DataRow tableRow in usersTable.Rows)
    {
        if (tableRow["Name"].ToString().Equals(name))
            rowsToDelete.Add(tableRow);
    }
}
foreach (var row in rowsToDelete)
    row.Delete();

You could use for loops instead (this is not enumeration). But then you have to track index changes so I prefer this method.

This requires few code changes. You can simply delete rows directly from the grid, not the data source.

Regardless of method, you still need to avoid modifying the collection during enumeration with foreach .

In your code you are trying to delete the row while iterating thru the collection of rows which is causing the collection to be modified. That's why you are getting this error.

You need a way to delete the row from DataTable directly without iterating thru the Rows collection.

DataTable.Rows collection has Remove method which takes an instance of DataRow as a parameter.

So if you can get hold of the DataRow to be deleted, then all you need to do is call usersTable.Rows.Remove method.

As you can get hold of the original DataSource of GridView by doing

var usersTable = myGridView.DataSource as DataTable;

You can get hold of the individual DataItem bound to individual rows of DataGridView. Here in this case DataItem which is bound to rows of DataGridView will be DataRowView .

DataRowView rowView = selectedRow.DataBoundItem as DataRowView;

And DataRowView has a property Row which represents the DataRow from the table which is bound to the GridVeiw. So you can use rowView.Row to call Remove method on usersTable.Rows.Remove method.

So the code of delete_Click can be changed to following to solve your issue.

private void delete_Click(object sender, EventArgs args)
{
    foreach (DataGridViewRow selectedRow in myGridView.SelectedRows)
    {
        DataRowView rowView = selectedRow.DataBoundItem as DataRowView;

        if(rowView != null && rowView.Row != null)
        {
            // Removing row from the table created at the Form level.
            usersTable.Rows.Remove(rowView.Row);
        }
    }

    myGridView.Refresh();
}

I hope this will help you to solve your issue.

PS : This solution is applicable only when you have DataTable used as DataSource of GridView. For any other type of DataSource a different solution may be needed.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM