I have a weird issue with the DataGridView control in virtual mode and threading. I use a Cache class to stored 2 pages of data. When I scroll down and I have to load a new page in cache, there is a little delay before data is display (it's ok). But, instead of having
the next row displayed(199th,200th,201th), the control skip many rows (CellValueNeeded). By example, I cached 1 to 200 rows, when I scrolling at 199, I'm loading a page to 200 à 299. However, in the datagrid it's display the row 320.. instead of 300. (I kept the mouse button pressed 2-3 secondes, before releasing it). I notice I have the problem only when I load the cache in a particular thread. Does anyone have a idea how I can fix this?
private void datagridView_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
{
e.Value = m_rfiCache.RetrieveElement(e.RowIndex, e.ColumnIndex);
}
....
public DataTable SupplyPageOfData(int lowerPageBoundary, int rowsPerPage)
{
e : " + lowerPageBoundary);
DataTable dt = new DataTable();
if (m_useThreading) //THIS DOESN'T WORK WELL
{
Thread MulThread = new Thread(delegate()
{
dt = this.LoadData(lowerPageBoundary, rowsPerPage);
});
MulThread.Start();
MulThread.Join();
}
else //OK.
{
dt = this.LoadData(lowerPageBoundary, rowsPerPage);
}
return dt;
}
P.S. : This is only a snippet, I used an external API to extract data from ODB. This API used thread to load data.
Does anyone have a idea or I can fix this?
You can load the data in a background thread, but you can't add it to your main DataTable in the background thread itself.
One potential approach would be to load the data as you're doing, but into a separate DataTable. You could then, on the UI thread, call DataSet.Merge to merge this data in with your bound data correctly (which will be reasonable fast).
Finally, I found the solution. When the tread is processing and the scroll down button is pressed, my event for scrolling aren't sent to the datagrid unless my thread is finished. So I received a bunch of calls for drawing cells and the index was wrong. To avoid this, I removed the vs scrollbar from the datagridview and added a VSscrollbar control. When I'm loading data in my thread, I set to false the enable property on my scrollbar. When the thread is completed, I enable the scrollbar. Is not the BEST solution, but at least, there is now row skipped.
Note : Virtual Mode works very well if you don't load your data in a separate thread.
Related
I have a very simple app that loads an excel file with several thousand rows into a DataGridView component. This table is then scanned for duplicates and various other issues by pressing the Scan button. I chose to have this intensive task run in a BackgroundWorker so the UI remains responsive and so I can report its progress. The code for my Scan button simply calls the RunWorkerAsync() method on the background worker component, which then does this :
Scanner scanner = new Scanner(this, dgvScancodes, dgvErroredScancodes, BgwScanner);
This calls another class to do the actual work, passing the scanner as a parameter. The Scanner class then does this :
foreach (DataGridViewRow row in table.Rows)
{
//Long computations on each row
worker.ReportProgress(row.Index / table.RowCount * 100);
}
The scan executes perfectly fine and produces the expected output but my ProgressBar never updates with the following code on the BackgroundWorker's ProgressChanged event :
private void BgwScanner_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pgbProgress.Value = e.ProgressPercentage;
}
Thinking this could be an issue with the way I calculate the percentage, I replaced my call to ReportProgress and just sent a value of 50 to see what would happen. The progress bar does update, but only when the entire scan is completed! No exception is raised and it's behaving like the UI thread is too busy doing other things (which, aside from looping over every row in a table, it is not to the best of my knowledge). Any idea why I'm seeing this behavior?
******EDIT******
I found my culprit. I forgot that, during the scan, the table's rows can be updated with a tooltip and a background color. I commented these 2 lines and sure enough the progress bar works perfectly fine now. This proves my theory that indeed the UI thread is being overwhelmed. Is there potentially a way around this? Some sort of higher priority for the progress bar updates?
For integer division the value rounds towards zero to the next nearest integer.
In your case it will always round towards exactly zero before being multiplied by 100, since row.index is presumably between 0 and table.RowCount - 1.
Being totally verbose:
row.Index / table.RowCount * 100
Could become:
(int)(((double)row.Index / (double)table.RowCount) * 100)
I addressed this issue by doing the processing on the underlying DataSource for my tables until the very last minute when it needs to be displayed. This way there are no UI updates until the table is displayed. I still need to iterate over the table itself to set the background color and tooltip for certain cells at the end (since you obviously can't do this on the DataSource itself) but that takes a relatively short time compared to the processing that is being done on the DataSource beforehand. My ProgressBar now works properly.
******EDIT******
You can actually use the DataGridView's CellFormatting event to color cells, no need to iterate over the rows and this has the bonus effect of keeping the tooltip and the background color when re-ordering the table :
private void dgvErroredScancodes_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
{
DataGridViewCell scancodeCell = dgvErroredScancodes.Rows[e.RowIndex].Cells[1];
if (e.Value.Equals((int) ErrorType.DUPLICATE))
{
scancodeCell.Style.BackColor = ErrorType.DUPLICATE.BackgroundColor();
scancodeCell.ToolTipText = ErrorType.DUPLICATE.ErrorMessage();
}
...
}
I have inherited a form and have been tasked with speeding it up. Basically, it is a form which makes a WCF call to a service for some data, and then displays the data in a DataGridView.
I originally thought the bottleneck was the WCF call, so I moved that to a BackgroundWorker, to stop the UI from freezing up.
That helped a little bit, but it turns out that the bottleneck is actually this line:
DumpInfoGrid.DataSource = dumpGridBinding;
where the data retrieved from the WCF is assigned to the DataSource property.
The code basically boils down to this:
private BackgroundWorker dataGatherer;
dataGatherer.DoWork += dataGatherer_DoWork;
dataGatherer.RunWorkerCompleted += dataGatherer_RunWorkerCompleted;
private void dataGatherer_DoWork(object sender, DoWorkEventArgs e)
{
dumpGridBinding = WCF.GetDataFromService();
}
private void dataGatherer_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
DumpInfoGrid.DataSource = dumpGridBinding;
}
Is there anything I can do to speed this up? Or any part I can move to the BackgroundWorker ? The assignment freezes the UI for far too long.
One possible solution for this would be to use FastDataGridView (or FastDataListView) which is found in ObjectListView, which greatly improves the speed of the DataGridView control by not attempting to process all the records at one time.
Another solution would be to page the DataGridView you currently have, spreading out the load time across the pages as you use them as seen here
You could use virtual mode -
MSDN: http://msdn.microsoft.com/en-us/library/15a31akc.aspx
This mode shows just the rows the user is looking at through the viewable region - you only load the rows actually being viewed - which is fast.
You handle the CellValueNeeded event and provide data from your list when the user scrolls through the list. Tex example shows you how to do that, but it's easy to write a linq statement to skip, take whatever is requested from your datasource (returned from the WCF service).
I have a problem with populating my comboBoxes in a WinForm application that I'm writing. The data that I use to populate these comboBoxes is pulled from database. The problem is that there is a lot of data that needs to be binded to the comboBoxes, so this process takes a very long time during which the entire application is locked (the entire process of binding data takes over 9 seconds, while the process of pulling the data from the database takes only 400 milliseconds). I'm trying to speed things up by splitting the processes of creating the controls (main thread) and populating the comboBoxes (background worker), but naturally I get the cross thread error.
This is the part of code that I use:
private void Populate()
{
comboBox1.BindingContext = new System.Windows.Forms.BindingContext();
comboBox1.DataSource = MyClass.dtMyDataTable;
comboBox1.DisplayMember = "TitleColumn";
.//I repeat the same code for each comboBox
.//I use the BiningContext because some of the comboBoxes have to display the
.//same data.
}
I created a class that contains all DataTables that I need in this form - there are multiple forms that use the same data from the database, so I created a class and created an object that fills all of these DataTables on the parent Form.Load(), and then I pass them to the children forms when I create them. This way I load the data when the application loads (it doesen't even take that long), so it should be ready to use when I call it from child forms. I've tried to call the Populate() method from backgroundWorker.DoWork() method, and there I get the cross thread error.
My question is - is there a way to make this work, and if not, what could I use as alternative solution.
Thank you
I'm not a full bottle on invoking but try this:
PopulateData()
{
if (combobox1.InvokeRequired)
{
combobox1.Invoke(new EventHandler(delegate(object o, EventArgs a)
{
PopulateData();
}
));
}
else
{
// Do your updates here...
}
}
I believe this will find the thread responsible for combobox1, which is going to be the same thread for the other combo's, and then run.
I'm sure someone else will chime in with a better way to invoke perhaps at the form level ?
I've found a good alternative, and it sped things up from 9 seconds to 1.5 seconds. The solution is to place the comboBox.DisplayMember BEFORE the 'comboBox.DataSource' line because when you change the DisplayMember (or ValueMember) the data source repopulates itself. So if the comboBox.DisplayMember is after the 'comboBox.DataSource' line, the data source populates itself 2 times (I think that ClearBeforeFill is enabled by default when binding data sources, that is why there are no duplicates in the binded data).
Thanks anyway.
We display our data on datagrids, bound to a dataset, which is in turn fed from a Progress database on the server. During processing, we need to make a change to the data-set and refresh it's value from the server. So far, all well and good and no problems.
The problem is that when we come back with the new data, we want the selection in the datagrid to remain on the same row it was on before. We've managed this with the following code:
int iPostingPos = dgridPostings.CurrentRow.Index;
// process data on server
dataContTranMatch.RunBoProcedure(dataContTranMatch.BoProcedure,
transactionMatchingDataSet);
// Reload Data
LoadData();
if (iPostingPos > ttPOSTingsRowBindingSource.Count)
{
iPostingPos = ttPOSTingsRowBindingSource.Count;
}
if (ttPOSTingsRowBindingSource.Count > 0)
{
ttPOSTingsRowBindingSource.Position = iPostingPos;
dgridPostings.Rows[iPostingPos].Selected = true;
}
This works, but we get the selected line jumping about on the screen, which is really annoying the users.
For example, if you select row 7, then run this code, you have row 7 selected, selection then jumps to row 0, then jumps back to row 7. This isn't acceptable.
In an attempt to fix this, we've tried enclosing the above code in the following additional lines:
chTableLayoutPanel1.SuspendLayout();
*DO CODE*
chTableLayoutPanel1.ResumeLayout();
But this didn't help.
So far, the most acceptable solution that we've been able to reach is to change the colour on the selection so that you can't see it, letting it leap about and then putting the colours back as they should be. This makes the flicker more acceptable.
dgridPostings.RowsDefaultCellStyle.SelectionBackColor =
SystemColors.Window;
dgridPostings.RowsDefaultCellStyle.SelectionForeColor =
SystemColors.ControlText;
DO CODE
dgridPostings.RowsDefaultCellStyle.SelectionBackColor =
SystemColors.Highlight;
dgridPostings.RowsDefaultCellStyle.SelectionForeColor =
SystemColors.HighlightText;
We beleive that the issue is caused by the binding source being temporarily empty as the dataset is refreshed, we then re-navigate one it's got data in it again.
Can anyone offer any ideas on how we can prevent this unpleasent flicker from occuring?
Many thanks
Colin
It may be a bit heavy handed but one option would be to suspend painting of the control. A user asked how to achieve this here: How Do I Suspend Painting For a Control and Its' Children. I've used the selected answer there to achieve something similar.
In my application I update a treeview in a backgrounworker thread. While updating the treeview, the combobox values are not visible. When the treeview is updated, the values appear.
Here's my backgroundworker code:
void _bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
tvCategories.Invoke((MethodInvoker)delegate()
{
FillCategoryTreeView(); // Fills the treeview
}
);
}
The code that fills my combobox:
private void FillCategoryCombo()
{
Category categorie = new Category();
List<Category> categories = categorie.GetQuestionCategories();
cmbCategories.DataSource = categories;
cmbCategories.DisplayMember = "Description";
cmbCategories.ValueMember = "Id";
}
The combobox is filled in the constructor of the form.
The reason that I've put the treeview in a seperate thread is because the treeview must be updated. In the meantime I want to continue using the application. Therefore I need access to a combobox. But the values of the combobox are invisible while the treeview is being updated.
What to do to solve this?
I'm not quite sure there is enough information in your post to fully answer the question... but assuming that you create the Background worker thread in the Constructor prior to calling the FillCategoryCombo() method... this makes sense.
In your background worker method, you immediately call Invoke which switches control right back to the UI thread, which will then be doing the work of FillCategoryTreeView() before FillCategoryCombo() has a chance to run.
If you want to asynchronously fill your treeview (assuming it comes from a long running database call), then what you need to do is actually have separate Invoke calls in FillCategoryTreeView when you specifically need to add a tree view item. That way as each database call (or whatever takes a long time) finishes, it only does an operation on the UI thread when it needs to add a physical tree node.