简体   繁体   中英

Uncatchable exception in DataGridView when deleting rows in datatable

I have a data table in which rows are added based on data available on the internet. Each time I add a row, I also put a timestamp on the row. This has been working for a number of years, with hundreds of users with no errors occurring. I now wish to delete rows from the data table based on the age of the data. I have a timer that fires every user defined interval and then I search for all rows that are older and I delete them. The problem is, that I am receiving various errors that seem to change and even though I have try catch statements on all methods, the errors are not caught. The error is caught only on an error catch on the whole application in program.cs The errors are "Exit error: Object reference not set to an instance of an object. at System.Windows.Forms.DataGridViewCell.PaintWork, or Exit error: Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index at System.Collections.ArrayList.get_Item(Int32 index)

or Exit error: Object reference not set to an instance of an object. at System.Windows.Forms.DataGridViewTextBoxCell.PaintPrivate

but there is no useful information in the rest of the errors. Here is the code:

    public void ageTimer_Elapsed(object source, System.Timers.ElapsedEventArgs e)
    {
        try
        {
            spotAge = Convert.ToDouble(Properties.Settings.Default.SpotAge);

            //Select all rows in datatable where the age is older than spotAge and delete them.
            //date is stored in dt as 20:12:2017: 21:49:02 derived from DateTime.UtcNow.ToString("dd:MM:yyyy: HH:mm:ss")

            //spotTime is the time from the table

            DateTime spotTime;
            dt.AcceptChanges();
            var rowCount = dt.Rows.Count;

            for (int i = rowCount -1; i >= 0; i--)
            {
                DataRow dr = dt.Rows[i];

                spotTime = DateTime.ParseExact(Convert.ToString(dr["date"]), "dd:MM:yyyy: HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);

                if (spotTime.AddMinutes(spotAge) <= DateTime.UtcNow)
                {
                    dr.Delete();

                }
            }

            dt.AcceptChanges();

            dataGridView1.Update();
            dataGridView1.Refresh();
            dataGridView1.FirstDisplayedScrollingRowIndex = 0;
        }

        catch (Exception x)
        {

        }
    }

Now the interesting thing is that on my system this will not crash at all ever. However, if I run visual studio in debug mode it will. It will also crash on a small number of users as well. But not everyone.

I tried the same code except I always will delete only one row and it works perfectly fine. I tried deleting every row except for the first, the first two and it still crashes.

Based on the comment below which pointed out that the timer was running on a separate thread, I added the following:

    public void ageTimer_Elapsed(object source, System.Timers.ElapsedEventArgs e)
    {
        DeleteRows("");
    }

and the above code now now has the following:

private void DeleteRows(string details)
{
  if (InvokeRequired)
  {
     this.Invoke(new Action<string>(DeleteRows), new object[] { details});
     return;
  }

and the rest of the code is as above. This solves the issue completely. Many thanks!

Just read your question again. This was working for years. Now it's not working, cause you added timer, running in a new, separate thread. This might be a race condition. Obviously, dt is a shared resource which is not properly used in a multithreaded environment. Assuming dt is source of your data grid view, in one situation, user might be doing something with it (just scrolling would be enough to read from dt in order to paint newly visible rows - thus, I suppose you get the PaintWork error from there, as the state was changed in the mean time by a timer event handler). Imagine user deletes row from the grid, that reflects to the underlying source dt , but, your timer just happened to delete those rows just a moment ago and you get index out of range exception.

Instead of working with dt in your timer event handler, can you try and make a copy of it dt_copy (just pay attention not to make a shallow copy). Make your changes, and once you're done, just bind that copy as a new source of datagridview and all should be good.

Another approach, which also might perform faster would be to call stored procedure and do deletion by database directly and just refresh your dt once that call returns (just repopulate dt ).

In both cases, depending how fast deletion is, you should check if 'dt' is dirty (updated by a user in the mean time) and might want to merge those changes so they are not lost.

The problem over here is, that you´r calling the delete row.delete statement inside your for loop. This is manipulating the length of your row collection thus you get the index out of bound exception.

One quote from msdn:

Delete should not be called in a foreach loop while iterating through a DataRowCollection object. Delete modifies the state of the collection.

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