Relatively new to C# threading and ran into a situation whereby an async call is made using cefsharp that scrapes a table from a site (using a headless call). That part works fine, however, I need to clear and repopulate a DataGridView on the main form, and get an error
System.InvalidOperationException
HResult=0x80131509
Message=Cross-thread operation not valid: Control 'dataGridView1' accessed from a thread other than the thread it was created on.
Source=System.Windows.Forms
when I try to do so. I understand the problem, but not sure how to go about it. I figured I could use an event, but have no idea where to begin. (still pretty green with C# Winforms).
I'm running a test winforms app in DotNetCore 5.0 to get this to work. Here's my single page code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using CefSharp;
using CefSharp.OffScreen;
using HtmlAgilityPack;
using HtmlDocument = HtmlAgilityPack.HtmlDocument;
using System.IO;
using static ParseWebPage.XUtilities;
using System.Threading.Tasks;
namespace ParseWebPage
{
public partial class Form6 : Form
{
private ChromiumWebBrowser browser;
public Form6()
{
InitializeComponent();
var settings = new CefSettings()
{
//By default CefSharp will use an in-memory cache, you need to specify a Cache Folder to persist data
CachePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CefSharp\\Cache")
};
//Perform dependency check to make sure all relevant resources are in our output directory.
Cef.Initialize(settings, performDependencyCheck: true, browserProcessHandler: null);
}
private void PopulateDataGridView(BindingList<cRecord> cr)
{
dataGridView1.DataSource = null;
dataGridView1.DataSource = cr;
}
private void button1_Click(object sender, EventArgs e)
{
// Create the offscreen Chromium browser.
browser = new ChromiumWebBrowser(txtTestURL.Text.Trim());
// An event that is fired when the first page is finished loading.
// This returns to us from another thread.
browser.LoadingStateChanged += BrowserLoadingStateChanged;
}
private void BrowserLoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
// Check to see if loading is complete - this event is called twice, one when loading starts
// second time when it's finished
// (rather than an iframe within the main frame).
if (!e.IsLoading)
{
// Remove the load event handler, because we only want one snapshot of the initial page.
browser.LoadingStateChanged -= BrowserLoadingStateChanged;
var task2 = browser.GetSourceAsync();
task2.ContinueWith(x =>
{
//need to parse data here
ProcessScrapedRawPageData(task2.Result); //problem - this is running on another thread - need an event to show this is done - fire an event or something
}, TaskScheduler.Default);
}
}
void ProcessScrapedRawPageData(string pagedata)
{
var doc = new HtmlDocument();
doc.LoadHtml(pagedata);
HtmlNode table = doc.DocumentNode.SelectSingleNode("//table[starts-with(@class, 'tbl1')]");
HtmlNodeCollection rows = table.SelectNodes(".//tr");
if (rows != null)
{
if (rows.Count > 0)
{
BindingList<cRecord> cRecords = new BindingList<cRecord>();
foreach (HtmlNode row in rows)
{
HtmlNodeCollection cells = row.SelectNodes(".//td");
if (cells != null)
{
if (cells.Count == 7)
{
int i = 0;
cRecord r = new cRecord();
foreach (HtmlNode cell in cells)
{
string cellval = cell.InnerText;
i++;
switch (i)
{
case 1:
r.MarketDate = Convert.ToDateTime(cellval);
break;
case 2:
r.Open = StrUtils.StrToDec(cellval);
break;
case 3:
r.High = StrUtils.StrToDec(cellval);
break;
case 4:
r.Low = StrUtils.StrToDec(cellval);
break;
case 5:
r.Close = StrUtils.StrToDec(cellval);
break;
case 6:
r.Volume = StrUtils.StrToInt64(cellval);
break;
case 7:
r.MarketCap = StrUtils.StrToInt64(cellval);
break;
default:
break;
}
}
cRecords.Add(r);
}
}
}
// we have all the records -- need to update the DataGridView with the results
// problem is this is on another thread, so an error occurs (need to do this on the main thread)
ClearDataGridView();
PopulateDataGridView(cRecords);
}
}
}
private void ClearDataGridView()
{
if (dataGridView1 != null)
{
if (dataGridView1.Rows != null)
{
dataGridView1.Rows.Clear();
}
}
}
}
}
-- UPDATE ----------------------------------------------------------------- Okay, I tried to create an eventhandler that is invoked call OnScrapeComplete as so:
public event EventHandler<string> OnScrapeComplete;
then changed the method to invoke it as so:
private void BrowserLoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
this.OnScrapeComplete += OnScrape_Complete;
if (!e.IsLoading)
{
browser.LoadingStateChanged -= BrowserLoadingStateChanged;
var task2 = browser.GetSourceAsync();
task2.ContinueWith(x =>
{
OnScrapeComplete?.Invoke(this, task2.Result);
}, TaskScheduler.Default);
}
}
private void OnScrape_Complete(object sender, string e)
{
ProcessScrapedRawPageData(e);
}
But no joy. I have the exact same problem: The ProcessScrapedRawPageData is still executing on the other thread, so when it calls either ClearDataGridView() OR PopulateDataGridView(cRecords), I still get the 'Cross-thread operation not valid: Control 'dataGridView1' accessed ... message. UGH! I'm stumped. I have no idea how to get the data to the main thread!
User contributions licensed under CC BY-SA 3.0