Increamental loading in grid using DataTable as DataSource - c#

In my window application there are many screens with grid. And I have used DataTable as DataSource of the grid and DataTable have some really large data sets (> 50,000), which take a lot time to load data on screen if we load all at a time while loading the UI get un-responsive till all data not get loaded, So that I have implemented incremental loading in that grid using Background Worker.
Here is the code :
// DoWork Event of the background Wroker.
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
try
{
while (bgstop)
{
e.Result = addNewRecord();
if (Convert.ToBoolean(e.Result) == false)
{
e.Cancel = true;
bgstop = false;
killBGWorker();
break;
}
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
// to add/merge the records in the DataTable
private bool addNewRecord()
{
int flag = 0;
try
{
Thread.Sleep(500); //optional
DataTable tableAdd = getTableData();
if (tableAdd.Rows.Count > 0)
{
dtRecords.Merge(tableAdd); // dtRecords is the DataTable which attached to grid
flag++;
}
else
backgroundWorker1.WorkerSupportsCancellation = true;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
if (flag > 0)
return true;
else
return false;
}
// To get the next slot of Records from the DataBase
private DataTable getTableData()
{
DataTable dt = new DataTable();
start = nextRows * noOfRows;
stop = start + noOfRows;
dt = SQLHelper.getAllRecords(totalRows,noOfRows, start + 1, stop);
nextRows++;
return dt;
}
// kill the backgroudworker after the all data/records get loaded from database to grid/DataTable
private void killBGWorker()
{
backgroundWorker1.WorkerSupportsCancellation = true;
backgroundWorker1.CancelAsync();
}
Above code get the first defined number of records (say 200) and after that in the background worker started and start fetching the data in a slot and merge that with grid DataSource till all data (say >50,000 records) get loaded into the grid.
But still have some issue with UI interaction, UI not get hang for 2-3 seconds many time till all records from DataBase get loaded into the grid.
I gone through this but in that example DataModel was used but in my case there is no DataModel they just fetched in DataTable from DataBase and right now we can't move to DataModel. Is there any other way to achieve incremental Loading with good UI interaction ?OR Is there any way to implement IBindingList in current scenario ?

You can achieve that by changing the DataGridView from BindingMode to VirtualMode.
The following changes will re-use as much as possible what you already have and you will see that the DataGridView gets loaded incrementally. I don't know how much records you fetch at once, but you can keep that number low.
Set the property VirtualMode to true. Remove any values from the property DataSource. Add as many Unbounded columns to your DataGridView as you have columns in your DataGrid (this could be done automatic if needed).
Add an eventhandler for CellValueNeeded.
Add the following code to that handler:
private void dataGridView1_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
e.Value = dtRecords.Rows[e.RowIndex][e.ColumnIndex];
}
On you backgroundworker1 set the property WorkerReportsProgress to True
Add an eventhandler to your backgroundworker for ProgressChanged.with the following code:
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.dataGridView1.RowCount = (int) e.UserState;
}
In your method addNewRecord add below this line:
dtRecords.Merge(tableAdd); // dtRecords is the DataTable which attached to grid
// added to bring the number of records to the UI thread
backgroundWorker1.ReportProgress(42, dtRecords.Rows.Count);
And with that your datagridview should now load its data incrementally. The trick really is setting the RowCount property. That signals to the datagrid if it can show a record and it adapts its scrollbar to the same.

My solution is using BindingSource like this:
// To take data in silent
BackgroundWorker m_oWorker;
// To hold my data. tblDuToanPhanBo is my data type
List<tblDuToanPhanBo> lst2 = new List<tblDuToanPhanBo>();
BindingSource bs = new BindingSource();
// replace 50000 with your total data count
int totalData = 500000;
// No of rows to load a time by BackgroundWorker
int RowsToTake = 2000;
// No of rows loaded
int RowsTaken = 0;
Take first portion of data and let BackgroundWorker do the rest:
private void UserControl1_Load(object sender, EventArgs e)
{
m_oWorker = new BackgroundWorker();
m_oWorker.DoWork += new DoWorkEventHandler(m_oWorker_DoWork);
m_oWorker.ProgressChanged += new ProgressChangedEventHandler (m_oWorker_ProgressChanged);
m_oWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler (m_oWorker_RunWorkerCompleted);
// QLQT is my DataContext
using (QLQT db = new QLQT())
{
lst2.AddRange(db.tblDuToanPhanBos.Skip(RowsTaken).Take(RowsToTake).ToList());
}
RowsTaken = lst2.Count;
bs.DataSource = lst2;
dataGridView1.DataSource = bs;
m_oWorker.RunWorkerAsync();
}
BackgroundWorker to take one portion of data:
void m_oWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Load data
using (QLQT db = new QLQT())
{
lst2.AddRange(db.tblDuToanPhanBos.Skip(RowsTaken).Take(RowsToTake).ToList());
}
// Update number of rows loaded
RowsTaken = lst2.Count;
if (((BackgroundWorker)sender).CancellationPending)
{
e.Cancel = true;
return;
}
}
When BackgroundWorker is completed, update BindingSource, run BackgroundWorker against until all data loaded:
void m_oWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
MessageBox.Show("Loading Cancelled.");
}
else if (e.Error != null)
{
MessageBox.Show("Error while performing background operation.");
}
else
{
if (lst2.Count < totalData)
{
bs.ResetBindings(false);
m_oWorker.RunWorkerAsync();
}
else
{
bs.ResetBindings(false);
}
}
}
Hope this help :)

Related

Programmatically setting drop down list selection does not await in event handler

I have a winform with a SfComboBox. When a selection is made a list of dates is retrieved from a cloud data base, and those dates are used to populated another combobox.
The combobox has .DropDownStyle set to DropDownList and SelectedIndexChanged handled by
private async void Sf_collectionDDL_SelectedIndexChanged(object sender, EventArgs e)
{
var cmb = sender as SfComboBox;
if (cmb.SelectedIndex < 0)
{
sf_datesDDL.SelectedItems.Clear();
return;
}
string collectionName = cmb.SelectedItem.ToString();
await GetCollectionDates(collectionName);
sf_datesDDL.DataSource = CollectionDates;
sf_datesDDL.ValueMember = "Value";
sf_datesDDL.DisplayMember = "Formatted";
sf_datesDDL.MaxDropDownItems = 12;
}
private Task GetCollectionDates(string collectionName)
{
return Task.Run(() =>
{
var builder = Builders<BsonDocument>.Filter;
var filter = builder.Eq("Type", "Header");
var headerDocuments =
Database
.GetCollection<BsonDocument>(collectionName)
.Find(filter)
.ToList()
;
CollectionDates = new SortedSet<ListItem_Date>();
foreach (BsonDocument doc in headerDocuments)
{
DateTime rangeStart = doc["DateStart"].ToUniversalTime().Date;
DateTime rangeEnd = doc["DateEnd"].ToUniversalTime().Date;
for (DateTime dt = rangeStart; dt < rangeEnd; dt = dt.AddDays(1))
{
CollectionDates.Add(new ListItem_Date(dt));
}
}
});
}
Everything works fine when events triggered by person driven UI operations, mouse clicks, etc.
To speed up some debugging (by reaching a specific state of selections and data retrievals) I am trying to make the selections programmatically from inside the form constructor.
private SortedSet<ListItem_Date> CollectionDates { get; set; }
public Form1()
{
InitializeComponent();
WindowState = FormWindowState.Maximized;
sfDataGrid1.EnableDataVirtualization = true;
// host during debugging
sf_hostDDL.DataSource = new List<string>() { "###hidden###" };
sf_hostDDL.SelectedIndex = 0; // loads database names into DDL
sf_databaseDDL.SelectedIndex = 0; // automatically select first database
radioXYZ.Checked = true;
CollectionsDDL_Update(); // do same "DDL_update" UI click on radioXYZ would have done
// Changing the selected index triggers the async/await method
sf_collectionDDL.SelectedIndex = 0; // automatically select first collection
// At this point the CollectionDates property is still null and exception ensues
// The selectedIndex assignment did not 'await' the innards of
// Sf_collectionDDL_SelectedIndexChanged() as I expected.
// Check first three dates
sf_datesDDL.SelectedItems.Add(CollectionDates.Take(3));
}
What is a good strategy to avoid the exception and programmatically achieve the selections I want in order to reach the 'state' I need to be at.
You shouldn't do that on the constructor because events aren't being fired yet.
Try the OnLoad override or the Load event.
The problem is that the event handler is async void.
private async void Sf_collectionDDL_SelectedIndexChanged(object sender, EventArgs e)
An async void method can't be awaited, so you must find some indirect way to await the handler to complete. There must surely exist better ways to do it, but here is a not particularly pretty one. You can declare a TaskCompletionSource member inside the Form, that will represent the asynchronous completion of the event handler:
private TaskCompletionSource<bool> Sf_collectionDDL_SelectedIndexChangedAsync;
private async void Sf_collectionDDL_SelectedIndexChanged(object sender, EventArgs e)
{
Sf_collectionDDL_SelectedIndexChangedAsync = new TaskCompletionSource<bool>();
try
{
var cmb = sender as SfComboBox;
//...
await GetCollectionDates(collectionName);
//...
sf_datesDDL.MaxDropDownItems = 12;
Sf_collectionDDL_SelectedIndexChangedAsync.SetResult(true);
}
catch (Exception ex)
{
Sf_collectionDDL_SelectedIndexChangedAsync.SetException(ex);
}
}
Then in the constructor after changing the SelectedIndex, wait for the completion of the asynchronous operation:
sf_collectionDDL.SelectedIndex = 0;
Sf_collectionDDL_SelectedIndexChangedAsync.Wait();

DataGridView not updating through Event

I have an EventHandler DonorUpdatedHandler() in a c# WinForm that updates a datagridview through a method RefreshGridData() in which I assign the datasource again. The RefreshGridData() is called when event fired but the grid never updates. BTW, RefreshGridData() works fine from a Form.Activated event handler. Can you suggest why?
private void gridDonors_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
var sendergrid = (DataGridView)sender;
if(sendergrid.Columns[e.ColumnIndex] is DataGridViewButtonColumn && e.RowIndex >=0)
{
int id = (int) sendergrid.Rows[e.RowIndex].Cells["id"].Value;
frmAddDonor frmDonor = new frmAddDonor();
frmDonor.editMode = true;
frmDonor.DonorUpdated += new System.EventHandler(this.DonorUpdatedHandler);
frmDonor.Show();
frmDonor.LoadRecordById(id);
}
}
private void RefreshGridData()
{
BindingSource source = new BindingSource();
BindingList<Donor> list = new BindingList<Donor>(donorModel.all());
source.DataSource = list;
gridDonors.DataSource = source;
}
private void DonorUpdatedHandler(Object sender, EventArgs e)
{
RefreshGridData();
}
I have figured out the problem. Nothing wrong in the code. It was the issue of synchronization of writes and reads with the Jet OLE DB Provider. I was writing and reading through different OleDbConnections. I have used a Singleton and that apparently solved the problem. Thanks.
Details:
https://support.microsoft.com/en-us/help/200300/how-to-synchronize-writes-and-reads-with-the-jet-ole-db-provider-and-a

DataGridView with BindingSource inline edit with stored procedure

I have dataGridView1 with DataSource defined as BindingSource:
BindingSource bs = new BindingSource();
bs.DataSource = dsGrid.Tables[0];
dataGridView1.DataSource = bs;
where dsGrid is DataSet read from MSSQL DB with stored procedure.
As I set dataGridView1.ReadOnly = false, user can inline edit data.
I want to send whole edited row to stored procedure after user finishes editing, where parameters will be edited data. How can I do it?
I have class, which runs this procedure on actual SqlConnection. So my goal is to hook the moment of posting data and read edited data from grid. Than I can send them to DB.
I didn't find easy straightforward solution. So I added editing column of DataGridViewButtonColumn class named Action, buttons with text Edit.
int _rowIndex = -1;
bool _edited = false;
.
.
.
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex == 0) // it is button - column Action
{
if (_rowIndex >= 0) // not first editing
{
if (_rowIndex != e.RowIndex) // row change - cancel and begin elsewhere
{
// TODO: ask for save edited values
endInlineEdit(_rowIndex);
beginInlineEdit(e.RowIndex);
}
else // the same row
{
if (_edited) // is edited, so save
{
saveToDB(e.RowIndex);
}
else // repeating same row editation
{
beginInlineEdit(e.RowIndex);
}
}
}
else // editing first time
{
beginInlineEdit(e.RowIndex);
}
}
}
private void saveToDB(int rowIndex)
{
save to DB
...
endInlineEdit(rowIndex);
}
private void beginInlineEdit(int rowIndex)
{
dataGridView1.Rows[rowIndex].Cells[0].Value = "Save";
dataGridView1.CurrentCell = dataGridView1.Rows[rowIndex].Cells["FirstEditedColumn"];
dataGridView1.EditMode = DataGridViewEditMode.EditOnEnter;
dataGridView1.BeginEdit(true);
_rowIndex = rowIndex;
_edited = true;
}
private void endInlineEdit(int rowIndex)
{
dataGridView1.Rows[rowIndex].Cells[0].Value = "Edit";
dataGridView1.EditMode = DataGridViewEditMode.EditProgrammatically;
dataGridView1.EndEdit();
_edited = false;
}
private void dataGridView1_RowLeave(object sender, DataGridViewCellEventArgs e)
{
if (_edited)
{
endInlineEdit(_rowIndex);
}
}
So I did it manually. Not nice, but functionally.

BackGroundWorker hanging Crystal Report

I have a Crystal Report which generates from various DataTables from a button on a Form with a TreeView. I wanted to run a BackGroundWorker so I can add a ProgressBar, since the Crystal Report generated takes some time. I've read that in the first place I needed to add a BackGroundWorker to the control and put all the logic code that generates que long-running process on the DoWork event of the BackGroundWorker. I did it like this:
//bgwBackThread is the name of the BackGroundWorkerObject
private void bgwBackThread_DoWork(object sender, DoWorkEventArgs e)
{
DataTable reporte = preReportDouble(Ot, Oth);
DataTable hh = reporteHH(Ot, Oth);
DataTable otNoCosto = otNoCost(Ot, Oth);
DataTable dethh = detalleHH(Ot, Oth);
//cryrepo is a Form which holds a CrystalReportViewer
InformeMaquina cryrepo = new InformeMaquina();
cryrepo.Informe = reporte;
cryrepo.Hh = hh;
cryrepo.SinCosto = otNoCosto;
cryrepo.DetHh = dethh;
cryrepo.Show();
}
and after I assigned the method RunWorkerAsync() to the button which generated the Form
before
private void btnReporte_Click(object sender, EventArgs e)
{
bgwBackThread.RunWorkerAsync();
//Below its commented because before of trying BackGroundWorker I just used the code here.
/*DataTable reporte = preReportDouble(Ot, Oth);
DataTable hh = reporteHH(Ot, Oth);
DataTable otNoCosto = otNoCost(Ot, Oth);
DataTable dethh = detalleHH(Ot, Oth);
InformeMaquina cryrepo = new InformeMaquina();
cryrepo.Informe = reporte;
cryrepo.Hh = hh;
cryrepo.SinCosto = otNoCosto;
cryrepo.DetHh = dethh;
cryrepo.Show();
*/
}
The problem is when I press the report button with the code as above. It loads the Form which holds que Crystal Report, but this Forms hangs (even in Debug). Without using BackGroundWorker it works fine, but with delay. I've read that its maybe because I'm loading the Form from a non-UI Thread, and that I have to unbind from the UI and then rebind. Is that the Problem?? If it were, how can I unbind and then rebind??
Your help is very apreciated.
Try creating a private class in your form to hold the DataTable information (which I assume is the time consuming part);
private class ReportTables {
public DataTable reporte;
public DataTable hh;
public DataTable otNoCosto;
public DataTable dethh;
}
Create the DataTables and update the results in the e.Result property:
private void bgwBackThread_DoWork(object sender, DoWorkEventArgs e)
{
ReportTables rt = new ReportTables();
rt.reporte = preReportDouble(Ot, Oth);
rt.hh = reporteHH(Ot, Oth);
rt.otNoCosto = otNoCost(Ot, Oth);
rt.dethh = detalleHH(Ot, Oth);
e.Result = rt;
}
Then in the Completed event, show the form:
void bgwBackThread_RunWorkerCompleted(object sender,
RunWorkerCompletedEventArgs e) {
if (e.Error != null) {
MessageBox.Show(e.Error.Message);
} else {
ReportsTables rt = e.Result as ReportTables;
//cryrepo is a Form which holds a CrystalReportViewer
InformeMaquina cryrepo = new InformeMaquina();
cryrepo.Informe = rt.reporte;
cryrepo.Hh = rt.hh;
cryrepo.SinCosto = rt.otNoCosto;
cryrepo.DetHh = rt.dethh;
cryrepo.Show();
}
}

I want to programmatically generate a click on a DataGridView Row in C#

I have a DataGridView in a form and I want to programmatically click its first row. I have found code to select its rows or columns from code.
For eg.
datagridview.Columns[0].Selected = true;
datagridview.Rows[0].Selected = true;
However this code is not raising the click event on the datagridview. If any one has coded how to click a datagridview from code, please extend your kind help.
Simply call the event handler method e.g.:
datagridviewRowClickedEventHandler(new object(), new eventargs());
If you use the sender or e parameters in the event handler then you will need to work out how to pass in the correct values.
Insert the follwing code into your project where appropriate (Usually on the form which has the datagridview).
Make sure to change the name of the DataGridView from dataGridView1 to the appropriate one on your form.
private void Form1_Load(object sender, EventArgs e)
{
//call the cell click event with the first cell as the parameters.
dataGridView1_CellClick(dataGridView1, new DataGridViewCellEventArgs(0, 0));
}
private void dataGridView1_CellClick(object sender, DataGridViewCellEventArgs e)
{
//put your code for handling cell click here
}
It looks like you have the first half, setting the propers rows Selected value to true. Now you can programatically call the row click handler and it should proceed as if you had clicked it within the GUI.
datagridview.Columns[0].Selected = true;
datagridview.Rows[0].Selected = true;
It makes look the row like selected but it won't change dataGridView.CurrentRow. So it could be an issue.
dataGridView.CurrentCell = dataGridView[<column>, <row>];
will change CurrentRow value too.
Hope it will help.
I assume you want to apply DataSource and select the first row? Right?
The best way to do it like this
private async void DgvAreas_RowStateChanged(object sender, DataGridViewRowStateChangedEventArgs e)
{
}
And here is the code to simulate click on row.
DgvAreas_RowStateChanged(dgvAreas, new DataGridViewRowStateChangedEventArgs(dgvAreas.Rows[0], DataGridViewElementStates.Selected));
In my case I have 3 DataGridViews so I populate the first one easly.
The second one I populate when user click the first DataGridView and in this case I use DgvStaff_RowStateChanged event.
And in this event DgvStaff_RowStateChanged I have the code to get data async and populate the third DataGridView and after I apply data source for the second DataGridView I need to get data for the first row of this view and display it in the third DataGridView. It is cascade logic.
private async void DgvStaff_RowStateChanged(object sender, DataGridViewRowStateChangedEventArgs e)
{
try
{
// For any other operation except, StateChanged, do nothing
if (e.StateChanged != DataGridViewElementStates.Selected) return;
if (sender is MetroFramework.Controls.MetroGrid)
{
if ((sender as MetroFramework.Controls.MetroGrid).SelectedRows.Count > 0)
{
dgvGeoData.DataSource = null;
dgvAreas.DataSource = null;
metroProgressSpinnerMain.Visible = true;
panelFilter.Enabled = false;
dgvAreas.RowStateChanged -= DgvAreas_RowStateChanged;
var selectedRow = (sender as MetroFramework.Controls.MetroGrid).SelectedRows[0];
var machineModelShortView = (MachineModelShortView)selectedRow.DataBoundItem;
var startTime = Convert.ToDateTime(dateTimePickerStart.Value.ToShortDateString());
var endTime = Convert.ToDateTime(metroDateTimeEnd.Value.ToShortDateString());
var areas = await UpdateAreaItems(machineModelShortView.MachineID, startTime, endTime);
if (areas.Any())
{
BeginInvoke((Action)(() =>
{
dgvAreas.DataSource = areas.OrderBy(x => x.AreaID).ThenBy(x => x.TimeStart).ToList();
dgvAreas.RowStateChanged += DgvAreas_RowStateChanged;
// !!! This is how you simulate click to the FIRST ROW dgvAreas.Rows[0]
DgvAreas_RowStateChanged(dgvAreas,
new DataGridViewRowStateChangedEventArgs(dgvAreas.Rows[0], DataGridViewElementStates.Selected));
metroProgressSpinnerMain.Visible = false;
panelFilter.Enabled = true;
}));
}
else
{
BeginInvoke((Action)(() =>
{
metroProgressSpinnerMain.Visible = false;
panelFilter.Enabled = true;
}));
}
}
}
}
catch (Exception ex)
{
logger.Error(ex);
}
}
And here
private async void DgvAreas_RowStateChanged(object sender, DataGridViewRowStateChangedEventArgs e)
{
try
{
// For any other operation except, StateChanged, do nothing
if (e.StateChanged != DataGridViewElementStates.Selected) return;
//Get GeoData
if (sender is MetroFramework.Controls.MetroGrid)
{
if ((sender as MetroFramework.Controls.MetroGrid).SelectedRows.Count > 0)
{
dgvGeoData.DataSource = null;
metroProgressSpinnerMain.Visible = true;
panelFilter.Enabled = false;
var selectedRow = (sender as MetroFramework.Controls.MetroGrid).SelectedRows[0];
var areaItem = (AreaItem)selectedRow.DataBoundItem;
var geoData = await UpdateWDataPositionItems(areaItem.MachineID, areaItem.TimeStart, areaItem.TimeEnd.Value);
if (geoData.Any())
{
BeginInvoke((Action)(() =>
{
dgvGeoData.DataSource = geoData.OrderBy(x => x.AtTime).ToList();
metroProgressSpinnerMain.Visible = false;
panelFilter.Enabled = true;
}));
}
else
{
BeginInvoke((Action)(() =>
{
metroProgressSpinnerMain.Visible = false;
panelFilter.Enabled = true;
}));
}
}
}
}
catch (Exception ex)
{
logger.Error(ex);
}
}

Categories