views:

706

answers:

2

I have a non databound DGV (no datasource, etc; rows added manually). To filter it I'd been doing a check in a loop and setting the rows visible property appropriately. This worked well with smaller test sets, but fails utterly in performance with larger ones. 1k rows filtered at 5000/sec. 10k rows filtered at only ~250/sec. 50k at a mere 40/sec. My assumption about what's going on is that each time i change a rows visibility the DGV rebuilds a list of displayed rows turning the filtering process into an O(n^2) operation.

While even 10k rows indicates that the user is abusing the system; badly behaved users need to be accounted for so I need to do something differently. Is there a faster way to filter a large number of rows than what I'm doing now without using data binding, or do I need to fall back on clearing/recreating all the rows (for reasonable amounts of data this is significantly slower)?

//psuedocode.  runs slowly if more than a few thousand rows.
foreach (DataGridViewRow row in myDGV)
{
    row.Visible = CalculateFilter(row);
}
+4  A: 

I had this issue a few years ago (before I knew about databindings) and found a bug post at Microsoft, saying that this is confirmed, but the issue will propably not be fixed.

However, there are a few possibilities to solve this.

  1. Insted of adding rows to the datagridview, add rows to a datatable and bind it to the datagridview.

    DataTable table = new DataTable();
    table.Columns.Add("Name", typeof(String));
    table.Columns.Add("...", typeof(String));
    
    
    foreach (var element in list)
       table.Rows.Add(element.Name, element.Something);
    
    
    dataGridView1.DataSource = table1;
    table.DefaultView.RowFilter = "Name Like '...'";
    
  2. Create a Class that inherits from BindingList and implements IBindingList. Then bind it to your DataGridView.

  3. Set DataGridView VirtualMode to true.

Method two is more complicated, because you have to add your own logic to implement the FindCore Method.

And you should look here: http://social.msdn.microsoft.com/Forums/en-US/winformsdatacontrols/thread/68c8b93e-d273-4289-b2b0-0e9ea644623a

SchlaWiener
Method1 looks like it's probably the best way to do it. Unfortunately it's probably too late in this dev cycle to do so. The one "cheap" fix in the msdn thread Suspend/ResumeLayout didn't have any noticable performance gain. My assumption would be that while not fixing the fundamental issue MS was kind enough to add some sort of change caching layout suspension to limit the damage that my sort of ignorance would cause.
Dan Neely
+1  A: 

The overall performance should greatly improve if you temporarily remove the rows from the dataGridView while filtering.

  1. Create a windows forms app
  2. Drop a DataGridView and four buttons to the form
  3. Copy and paste this code (don't forget to add event handlers for the button events)

public partial class Form1 : Form { public Form1() { InitializeComponent(); }

private Stopwatch watch = new Stopwatch();
private void Form1_Load(object sender, EventArgs e)
{
    // populate dataGridView
    for (int i = 0; i < 10000; i++)
        dataGridView1.Rows.Add("Column", i+1, 10000 - i);

    for (int i = 0; i < 10000; i = i + 2)
        dataGridView1.Rows[i].DefaultCellStyle.BackColor = Color.Red;

}

// remove filter
private void button1_Click(object sender, EventArgs e)
{
    watch.Reset();
    watch.Start();

    foreach (DataGridViewRow row in dataGridView1.Rows)
        row.Visible = true;


    watch.Stop();
    MessageBox.Show(watch.ElapsedMilliseconds.ToString());
}

// add filter (hide all odd rows)
private void button2_Click(object sender, EventArgs e)
{
    watch.Reset();
    watch.Start();

    foreach (DataGridViewRow row in dataGridView1.Rows)
    {
        if (Convert.ToInt32(row.Cells[1].Value) % 2 != 0)
            row.Visible = false;
    }

    watch.Stop();
    MessageBox.Show(watch.ElapsedMilliseconds.ToString());
}

// remove filter (improved)
private void button3_Click(object sender, EventArgs e)
{
    watch.Reset();
    watch.Start();

    List<DataGridViewRow> rows = new List<DataGridViewRow>();
    foreach (DataGridViewRow row in dataGridView1.Rows)
    {
        rows.Add(row);
    }

    dataGridView1.Rows.Clear();

    foreach (DataGridViewRow row in rows)
        row.Visible = true;

    dataGridView1.Rows.AddRange(rows.ToArray());

    watch.Stop();
    MessageBox.Show(watch.ElapsedMilliseconds.ToString());
}

// add filer (improved)
private void button4_Click(object sender, EventArgs e)
{
    watch.Reset();
    watch.Start();

    List<DataGridViewRow> rows = new List<DataGridViewRow>();
    foreach (DataGridViewRow row in dataGridView1.Rows)
    {
        rows.Add(row);
    }

    dataGridView1.Rows.Clear();

    foreach (DataGridViewRow row in rows)
    {
        if (Convert.ToInt32(row.Cells[1].Value) % 2 != 0)
        {
            row.Visible = false;
        }
    }

    dataGridView1.Rows.AddRange(rows.ToArray());

    watch.Stop();
    MessageBox.Show(watch.ElapsedMilliseconds.ToString());
}

}

SchlaWiener
That worked. The approach also sped up the initial grid loading process.
Dan Neely