High-Performance WinForms DataGridView with Virtual Mode
Loading tens of thousands of records into a DataGridView at once quickly becomes a bottleenck: the control allocates every row and cell object up-front, memory pressure rises, and the UI freezes. Virtual mode solves this by keeping the data in a separate collection and asking the grid for only the rows that are actually visible.
Preparing the data source
Use a BindingList<T> so the grid can be notified of inserts/removes without extra plumbing.
private readonly BindingList<Document> _cache = new BindingList<Document>();
Enabling virtual mode
Flip the switch in the designer or code:
grid.VirtualMode = true;
Virtual mode disables double-buffering by default, so turn it on via reflection to avoid flicker:
public static void EnableDoubleBuffer(Control c, bool on = true)
{
var prop = typeof(Control).GetProperty("DoubleBuffered",
BindingFlags.Instance | BindingFlags.NonPublic);
prop?.SetValue(c, on);
}
// in Form_Load
EnableDoubleBuffer(grid);
Keeping counts in sync
Whenever the underlying list changes, update the grid’s RowCount and invalidate the visible area. The helper below marshals to the UI thread if necessary.
private void WireEvents()
{
_cache.ListChanged += (s, e) =>
{
this.InvokeIfRequired(() =>
{
grid.RowCount = _cache.Count;
grid.Invalidate();
});
};
}
Adding or removing items
All mutations go through the list; the grid never touches the data directly.
lock (_syncRoot)
{
_cache.Add(new Document { Name = "New file", Status = Status.Pending });
}
Supplying cell values on demand
Handle CellValueNeeded. The grid calls this once for every visible cell, passing row and column indices.
private void grid_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
if (_cache == null || e.RowIndex >= _cache.Count) return;
var doc = _cache[e.RowIndex];
switch (e.ColumnIndex)
{
case 0: // icon
e.Value = IconIndex.Normal;
break;
case 1: // row number
e.Value = e.RowIndex + 1;
break;
case 2: // status text
e.Value = doc.State == 0
? doc.StateRemark
: EnumHelper.GetDisplayName(doc.ExceptionType);
break;
case 3: // upload flag
e.Value = doc.IsUploaded && doc.IsCloudSynced ? "Uploaded" : "Pending";
break;
case 4: // file name
e.Value = doc.Name;
break;
case 5: // serial number
e.Value = doc.Meta?.Serial ?? "-";
break;
default: // dynamic columns
int dataIndex = e.ColumnIndex - 6;
if (doc.Answers != null && dataIndex < doc.Answers.Count)
e.Value = string.IsNullOrWhiteSpace(doc.Answers[dataIndex].Content)
? "-"
: doc.Answers[dataIndex].Content;
else
e.Value = "-";
break;
}
}
Because the grid is in virtual mode, you never touch Rows or Cells collections; all interaction is routed through CellValueNeeded. The result is a responsive UI that can comfortably handle hundreds of thousadns of rows with minimal memory footprint.