WebBrowser not firing event DocumentCompleted - c#

So I've written a piece of code inside a console application that just simply makes a web browser from System.Windows.Forms visit a website. Now the proble I'm facing is, the DocumentCompleted event will not fire if I use a ready made WebBrowser, as you can see I pass one into the constructor.
My ideal way would be to use the WebBrowser passed in via the constructor, and just navigate on the same WebBrowser, for some reason, it wont fire the DocumentCompleted event when using the same browser, it forces me to initialize a new browser, inside a new thread every time.
Why is this?
Here is the code that works, but has to initialize a new browser every time.
public class WebController
{
private readonly ILogger _logger = new ConsoleLogger(typeof(WebController));
private WebBrowser _webBrowser;
private readonly string _webAddress;
private bool _sendingMessage;
private string _lastMessage;
public WebController(WebBrowser webBrowser)
{
_webAddress = "https://{username}.example.com/";
}
public void SendMessage(string username, string message)
{
var thread = new Thread(() =>
{
_webBrowser = new WebBrowser();
_webBrowser.DocumentCompleted += BrowserDocumentCompleted;
_webBrowser.ScriptErrorsSuppressed = true;
_webBrowser.Navigate(_webAddress);
Application.Run();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
}
private void BrowserDocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
if (e.Url.AbsolutePath != (sender as WebBrowser)?.Url.AbsolutePath)
{
return;
}
_logger.Warn("Navigated to: " + e.Url.AbsoluteUri);
}
}

Related

C# backgroundWorker cancellation and invoke

I have 2 questions about backgroundWorker: one is cancellation and another is invoking.
My code briefly looks like this:
public partial class App : Form {
//Some codes omitted
public EditProcess Process = new EditProcess(ProcessTextBox);
private void ExecuteBtn_Click (object sender, EventArgs e) {
//DnldBgWorker is a backgroundWorker.
Download Dnld = new Download(dir, Process);
DnldBgWorker.DoWork += (obj, e) => GoDownload(Dnld, urllist, e);
DnldBgWorker.RunWorkerAsync();
DnldBgWorker.RunWorkerCompleted += (obj, e) => FinishExecution();
}
private void GoDownload(Download Dnld, string[] urllist, EventArgs e) {
foreach(string url in urllist) {
Dnld.Dnld(url);
}
for (int i = 0; i < 10; i++) {
System.Threading.Thread.Sleep(50);
if (DnldBgWorker.CancellationPending) {
e.Cancel = true;
return;
}
}
}
private void StopBtn_Click(object sender, EventArgs e) {
DnldBgWorker.CancelAsync();
}
}
public class Download {
// Some codes omitted
public WebClient client = new WebClient();
public EditProcess Process;
public Download(string dir, EditProcess Process) {
this.dir = dir;
this.Process = Process;
}
public void Dnld() {
client.DownloadFile(url, dir);
EditProcess.Text(String.Format("Downloaded: {0}\r\n"));
}
}
public class EditProcess {
public TextBox Box;
public EditProcess(TextBox Box) {
this.Box = Box;
}
public void Text(string textToAdd) {
Box.Text += textToAdd;
}
}
First, while DnldBgWorker is running, I clicked StopBtn to stop the DnldBgWorker and the asynchronous work would not stop. How should I stop DnldBgWorker?
Second, EditProcess.Text(String.Format("Downloaded: {0}\r\n")); would give me an error that cross-thread operation is not valid. I know that I should make a delegate to do this, but I don't know exactly how.
++) My code looks like it's doing very simple works in very complicated way, but I put really essential elements in this code so please understand
Let's address the issue before we get into the code
For some reason, you have a completely redundant loop waiting for cancel after your actual download is done. Hence BtnStop does not work for you
When you call EditProcess.Text from Dnld which is invoked in the BackgroundWorker context, you are accessing a GUI element from a thread which does not "own" it. You can read in detail about cross-thread operation here. In your case, you should do it via your ReportProgress call.
Now you can see how I have
Removed the redundant loop from GoDownload while moving the if (DnldBgWorker.CancellationPending) check to the download loop. This should make the StopBtn work now.
Added the ProgressChanged event handler to do the GUI change in the ExecuteBtn_Click. This is triggered by DnldBgWorker.ReportProgress call in the download loop of GoDownload method. Here we pass the custom formatted string as UserState
Also make sure that you have the enabled the ReportsProgress and SupportsCancellation properties like below, perhaps in your designer property box or in code lile DnldBgWorker.WorkerReportsProgress = true; DnldBgWorker.WorkerSupportsCancellation = true;
Hope everything else is clear with the code below.
public partial class App : Form {
//Some codes omitted
public EditProcess Process = new EditProcess(ProcessTextBox);
private void ExecuteBtn_Click (object sender, EventArgs e) {
//DnldBgWorker is a backgroundWorker.
Download Dnld = new Download(dir, Process);
DnldBgWorker.DoWork += (obj, e) => GoDownload(Dnld, urllist, e);
DnldBgWorker.RunWorkerAsync();
DnldBgWorker.RunWorkerCompleted += (obj, e) => FinishExecution();
DnldBgWorker.ProgressChanged += (s, e) => EditProcess.Text((string)e.UserState);;
}
private void GoDownload(Download Dnld, string[] urllist, EventArgs e) {
foreach(string url in urllist) {
Dnld.Dnld(url);
DnldBgWorker.ReportProgress(0, String.Format($"Downloaded: {url}\r\n"));
if (DnldBgWorker.CancellationPending) {
e.Cancel = true;
return;
}
}
}
private void StopBtn_Click(object sender, EventArgs e) {
DnldBgWorker.CancelAsync();
}
}
public class Download {
// Some codes omitted
public WebClient client = new WebClient();
public EditProcess Process;
public Download(string dir, EditProcess Process) {
this.dir = dir;
this.Process = Process;
}
public void Dnld() {
client.DownloadFile(url, dir);
}
}
public class EditProcess {
public TextBox Box;
public EditProcess(TextBox Box) {
this.Box = Box;
}
public void Text(string textToAdd) {
Box.Text += textToAdd;
}
}
There are 2 issues here:
Regarding cancellation - you need to check for cancellation status in the loop that does downloading (thus downloading only part of requested files), not in the later loop which I don't really understand.
As an additional side note you can avoid using BackgroundWorker by using WebClient.DownloadFileAsync and WebClient.CancelAsync combo.
As of reporting progress - make you BackgroundWorker report progress back to the UI thread via ReportProgress and update UI from there.
As for how to cancel a thread. Here is a basic example, for a console application, that I hope you can fit into your more complex code.
void Main()
{
var tokenSource = new CancellationTokenSource();
System.Threading.Tasks.Task.Run(() => BackgroundThread(tokenSource.Token));
Thread.Sleep(5000);
tokenSource.Cancel();
}
private void BackgroundThread(CancellationToken token)
{
while (token.IsCancellationRequested == false) {
Console.Write(".");
Thread.Sleep(1000);
}
Console.WriteLine("\nCancellation Requested Thread Exiting...");
}
The results would be the following.
.....
Cancellation Requested Thread Exiting...
Secondly, as far as how to invoke from your thread to interact with the user interface, hopefully this blog will help you. Updating Windows Form UI elements from another thread
Please let me know if you found this helpful.
To support cancellation you need to set the property
DnldBgWorker.WorkerSupportsCancellation = true;
It is not clear if you set it somewhere else, but you need it to cancel the background worker as you can read on MSDN
Set the WorkerSupportsCancellation property to true if you want the
BackgroundWorker to support cancellation. When this property is true,
you can call the CancelAsync method to interrupt a background
operation.
Also I would change the GoDownload method to
private void GoDownload(Download Dnld, string[] urllist, EventArgs e)
{
foreach(string url in urllist)
{
Dnld.Dnld(url);
// this is just to give more time to test the cancellation
System.Threading.Thread.Sleep(500);
// Check the cancellation after each download
if (DnldBgWorker.CancellationPending)
{
e.Cancel = true;
return;
}
}
}
For the second problem you need to call that method when your code is running on the UI thread and not in the background thread. You could easily achieve this moving the textbox update in the event handler for the ProgressChanged event. To set up the event handler you need another property set to true
DnldBgWorker.WorkerReportsProgress = true;
And set the event handler for the ProgressChanged event
DnldBgWorker.ProgressChanged += DnldBgWorker_ProgressChanged;
private void DnldBgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
EditProcess.Text(String.Format("Downloaded: {0}\r\n", e.ProgressPercentage));
}
and raise this event in the GoDownload with
DnldBgWorker.ReportProgress(i);

Hooking IE Events in C#

I am trying to capture events from an existing IE window. In the code sample below, I am attempting to capture the mouseClick event within the browser document when a user clicks on an element, and then eventually pull back some attributes about the element being clicked.
public partial class frmBrowserElementBuilder : Form
{
InternetExplorer ie;
public frmBrowserElementBuilder()
{
InitializeComponent();
}
private void frmBrowserElementBuilder_Load(object sender, EventArgs e)
{
//create IE
ie = new InternetExplorer();
ie.Visible = true;
//handle document completed
ie.DocumentComplete += new
DWebBrowserEvents2_DocumentCompleteEventHandler(DocumentComplete);
}
public void DocumentComplete(object pDisp, ref object URL)
{
//document was loaded
//MessageBox.Show("DocumentComplete: " + URL);
//create event handler and hook onclick from IE
DHTMLEventHandler onClickHandler = new DHTMLEventHandler(ie.Document);
onClickHandler.assignedEvent += new DHTMLEvent(this.ie_onClick);
ie.Document.onclick = onClickHandler;
}
private void ie_onClick(mshtml.IHTMLEventObj e)
{
//something was clicked
MessageBox.Show(string.Format("Event Hooked {0}, Qualifier {1}", e.type, e.qualifier));
}
public delegate void DHTMLEvent(IHTMLEventObj e);
[ComVisible(true)]
public class DHTMLEventHandler
{
public DHTMLEvent assignedEvent;
private mshtml.HTMLDocument document;
public DHTMLEventHandler(mshtml.HTMLDocument doc)
{
//assign to instance of IE document
this.document = doc;
}
[DispId(0)]
public void Call()
{
//call the event
assignedEvent(this.document.parentWindow.#event); //{System.InvalidCastException: "Specified cast is not valid."}
}
}
}
The code compiles and the void Call() triggers as expected, however, the value of this.document.parentwindow is null and is throws System.InvalidCastException: Specified cast is not valid when stepping into the assignedEvent method.
When I inspect this.document, the value of parentWindow states
The function evaluation requires all threads to run.
after forcing evaluation it states:
'((mshtml.HTMLDocumentClass)this.document).parentWindow' threw an
exception of type 'System.InvalidCastException'.
Any ideas?
This is a threading issue. The Call() call happens on an MTA thread, and you can't access MSHTML from an MTA thread. There are many ways to change this, however, the most simple is to do this:
public void DocumentComplete(object pDisp, ref object URL)
{
var events = (HTMLDocumentEvents2_Event)ie.Document;
events.onclick += (evt) =>
{
MessageBox.Show(string.Format("Event Hooked {0}, Qualifier {1}", evt.type, evt.qualifier));
return false;
};
}

WPF WebBrowser not loading

I'm having a problem with the WebBrowser control. I have added it to one of the windows, but it doesen't load the page that i navigate to. I want to access to the control from the other windows, so i made public methods like Navigate etc. I have tried adding WebBrowser to other forms and it seems to work normally. It worked on this window when it was without any added code. I'm using the AutoResetEvent , so when the site would load it would continue the program. Could anyone tell me where could be the problem in this code?
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
readonly AutoResetEvent thread1Step = new AutoResetEvent(false);
public void EnterForm(string ElementId, string value)
{
HTMLDocument document = (HTMLDocument)TempBrowser.Document;
document.getElementById(ElementId).innerText = value;
}
public void Navigate(string url)
{
TempBrowser.Navigate(url);
thread1Step.WaitOne();
thread1Step.Reset();
}
public void PressButton(string id)
{
HTMLDocument doc = (HTMLDocument)TempBrowser.Document;
IHTMLElement btn = doc.getElementById(id);
if (btn != null)
{
btn.click();
}
}
public void Scroll(int n)
{
HTMLDocument doc = (HTMLDocument)TempBrowser.Document;
doc.parentWindow.scroll(0, n);
}
private void TempBrowser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
thread1Step.Set();
}
public void CallFunction(string Funct)
{
TempBrowser.InvokeScript(Funct);
}
}
I prepared an async code for WPF based on my other answer ...
public static class MyExtensions
{
public static Task NavigateAsync(this WebBrowser browser, Uri uri)
{
var tcs = new TaskCompletionSource<object>();
LoadCompletedEventHandler loadCompleted = null;
loadCompleted = (s, e) =>
{
browser.LoadCompleted -= loadCompleted;
tcs.SetResult(e.WebResponse);
};
browser.LoadCompleted += loadCompleted;
browser.Navigate(uri);
return tcs.Task;
}
}
Now you can remove thread1Step and TempBrowser_LoadCompleted method. Just use
await TempBrowser.NavigateAsync(url);
DoYourWork(); //At this point your page is loaded. Read its content...

How to invoke a control within a class

I have a windows form with a button.
I click the button and it starts a method in a separate class. I start this method in a separate thread.
When this class.method finishes it raises an event back to the windows form class.
When this happens I start another method in that separate class that tells a system.windows.form timer (declared in that class) to be enabled and thus start processing.
But the timer does not start (I did put a break point inside the 'tick' event).
I am assuming that it is because I declared the timer outside of the calling thread right at the start of my code.
Normally, I would use this to invoke a method on the same thread...
this.invoke(mydelegatename, any pars);
But, 'this' cannot be called with an class because unassumingly it is related to the UI thread.
I know this all looks bad architecture and I can easily solve this problem by moving the timer to the UI thread (windows form class).
But, I have forgotten how I did this many years ago and it really is an attempt to encapsulate my code.
Can anyone enlighten me pls?
Thanks
The Code:
[windows class]
_webSync = new WebSync(Shared.ClientID);
_webSync.evBeginSync += new WebSync.delBeginSync(_webSync_evBeginSync);
Thread _thSync = new Thread(_webSync.PreConnect);
_thSync.Start();
private void _webSync_evBeginSync()
{
_webSync.Connect();
}
[WebSync class]
private System.Windows.Forms.Timer _tmrManifestHandler = new System.Windows.Forms.Timer();
public WebSyn()
{
_tmrManifestHandler.Tick += new EventHandler(_tmrManifestHandler_Tick);
_tmrManifestHandler.Interval = 100;
_tmrManifestHandler.Enabled = false;
}
public delegate void delBeginSync();
public event delBeginSync evBeginSync;
public void PreConnect()
{
while (true)
{
if (some condition met)
{
evBeginSync();
return ;
}
}
}
public void Connect()
{
_tmrManifestHandler.Enabled = true;
_tmrManifestHandler.Start();
}
private void _tmrManifestHandler_Tick(object sender, EventArgs e)
{
//NOT BEING 'HIT'
}
You have to call _tmrManifestHandler.Start(); enabling is not enough.
Using a System.Windows.Forms.Timer on another thread will not work.
for more info look here.
Use a System.Timers.Timer instead, be carefull of CrossThreadExceptions if you are using accessing UI elements.
public class WebSync
{
private System.Timers.Timer _tmrManifestHandler = new System.Timers.Timer();
public WebSync(object id)
{
_tmrManifestHandler.Elapsed += new System.Timers.ElapsedEventHandler(_tmrManifestHandler_Tick);
_tmrManifestHandler.Interval = 100;
_tmrManifestHandler.Enabled = false;
}
public delegate void delBeginSync();
public event delBeginSync evBeginSync;
public void PreConnect()
{
while (true)
{
if (true /* just for testing*/)
{
evBeginSync();
return;
}
}
}
public void Connect()
{
_tmrManifestHandler.Enabled = true;
_tmrManifestHandler.Start();
}
private void _tmrManifestHandler_Tick(object sender, EventArgs e)
{
//NOT BEING 'HIT'
}
}

Handling events after receiving a MSMQ message (thread issue?)

I created two separate Windows Forms applications in C# that use MSMQ for communicating. Here's how it works, it looked simple enough though:
App1 sends a details request to App2.
App2 creates an event to open the window.
App2 opens a "details" window.
The only problem I have is that when received the message, the "details" window freezes after appearing.
As I handle MSMQ messages handling in an object that uses threads, I suspect the problem comes from there... But I have no experience in handling MSMQ messages or specific events handling between parts of an application.
Here's part of the code I use for App2:
/*Class declared in the Core namespace*/
public class TaskMessageQueueHandler
{
public TaskMessageQueueHandler()
{
this.Start();
}
private Thread m_thread;
private ManualResetEvent m_signal;
public event System.EventHandler messageReceived;
public void Start()
{
m_signal = new ManualResetEvent(false);
m_thread = new Thread(MSMQReceiveLoop);
m_thread.Start();
}
public void Stop()
{
m_signal.Set();
}
protected virtual void SendEvent(object sender, EventArgs e)
{
if (messageReceived != null)
messageReceived(this.message, e);
}
public string message;
private void MSMQReceiveLoop()
{
bool running = true;
MessageQueue queue = new MessageQueue(#".\Private$\queue1");
while (running)
{
try
{
var message = queue.Receive();
message.Formatter = new XmlMessageFormatter(new String[] { "System.String,mscorlib" });
this.message = message.Body.ToString();
string m = this.message;
SendEvent(m, System.EventArgs.Empty);
if (m_signal.WaitOne(10))
{
running = false;
}
}
catch
{
Console.WriteLine("ERROR");
running = false;
}
}
}
}
/*Main process, in the Program namespace*/
[...]
Core.TaskMessageQueueHandler tmqh = new Core.TaskMessageQueueHandler();
EventListener el = new EventListener();
tmqh.messageReceived += new System.EventHandler(el.ShowDetails);
[...]
/* Class in the Program namespace */
class EventListener
{
public void ShowDetails(object sender, EventArgs e)
{
int numero = int.Parse(sender as string);
Details details = new Details(numero);
details.Show();
}
}
Where did I go wrong? Where did I go right?
Thanks a lot,
Stephane.P
EDIT: if the MSMQ handler is stopped with Stop() anywhere around the event sending, the details window appears then disappears right away...
EDIT2: After the workaround given by Slugart, I managed to make this work:
class EventListener
{
Main control;
public EventListener(Main main)
{
control = main;
}
public void ShowDetails(object sender, EventArgs e)
{
int numero = int.Parse(sender as string);
control.Invoke((Action)(() => ShowDetails(numero)));
}
private void ShowDetails(int numero)
{
Details details = new Details(numero);
details.Show();
}
}
Which is used like:
Core.TaskMessageQueueHandler tmqh = new Core.TaskMessageQueueHandler();
EventListener el = new EventListener(this);
tmqh.messageReceived += new System.EventHandler(el.ShowDetails);
You're creating and displaying a form Details on a thread other than the main GUI thread and not an STA thread at that.
Your EventListener should have a reference to a running form (your main form perhaps) and then call form.Invoke() on it.
class EventListener
{
Control control; // A valid running winforms control/form created on an STA thread.
public void ShowDetails(object sender, string message)
{
int numero = int.Parse(message);
control.Invoke(() => ShowDetails(numero))
}
private void ShowDetails(int numero)
{
Details details = new Details(numero);
details.Show();
}
}
Also sending your event data as the sender is not really following the Event pattern that has been put in front of you. You want to use the EventArgs parameter for this, use the EventHandler delegate (EventHandler in your case).

Categories

Resources