Terminating a Form while COM DataReceived Event keeps fireing. C# - c#

I'm currently having a problem, which seems to be related to closing a Form, while a scale, which is connected through a Serial Connection keeps sending data (about 3 packages per sek).
I handle new data over the DataReceived-Event (handling itself might be uninteresting for this issue, since I'm just matching data) Keep an eye on the COM_InUse variable and the allowFireDataReceived check.):
private void COMScale_DataReceived(object sender, EventArgs e)
{
if (allowFireDataReceived)
{
//set atomar state
COM_InUse = true;
//new scale:
if (Properties.Settings.Default.ScaleId == 1)
{
strLine = COMScale.ReadTo(((char)0x2).ToString());
//new scale:
Regex reg = new Regex(Constants.regexScale2);
Match m = reg.Match(strLine);
if (m.Success)
{
strGewicht = m.Groups[1].Value + m.Groups[2];
double dblComWeight;
double.TryParse(strGewicht, out dblComWeight);
dblScaleActiveWeight = dblComWeight / 10000;
//add comma separator and remove zeros
strGewicht = strGewicht.Substring(0, 1) + strGewicht.Substring(1, 2).TrimStart('0') + strGewicht.Substring(3);
strGewicht = strGewicht.Insert(strGewicht.Length - 4, ",");
//write to textbox
ThreadSafeSetActiveScaleText(strGewicht);
COMScale.DiscardInBuffer();
//MessageBox.Show(dblScaleActiveWeight.ToString(), "dblScaleActiveWeight");
}
}
//free atomar state
COM_InUse = false;
}
}
The COM_InUse variable is a global bool and "tells" if there is a current process of handling data.
The allowFireDataReceived is also a global bool and if set to false will lead to no extra handling of the data which has been sended.
My problem now is the following:
It seems that Eventhandling is a separate Thread, which leads to a deadlock on klicking the Cancel-Button since the COM_InUse will never turn to false, even if the Event was handled (see end of COMScale_DataReceived, where COM_InUse is set to false).
While setting allowFireDataReceived = false works perfectly (no handling any more), as I said: the while loop will not terminate.
private void bScaleCancel_Click(object sender, EventArgs e)
{
allowFireDataReceived = false;
while (COM_InUse)
{
;
}
if (!COM_InUse)
{
ret = 1;
SaveClose();
}
}
When I comment out the while-block I have to click twice on the button, but it works without a crash. Since this very user unfriendly, I'm searching for an alternative way to safely close the window.
Info:
Simply closing (without checking if the COM-Data was processed) lead to a fatal crash.
So, maybe someone can explain to me what exactly causes this problem or can provide a solution to this. (Maybe one would be to trigger the Cancel-Clicking Event again, but that is very ugly)
Greetings!
I count on you :)
//edit:
Here is the current code of
private void ThreadSafeSetActiveScaleText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.lScaleActive.InvokeRequired)
{
SafeActiveScaleTextCallback d = new SafeActiveScaleTextCallback(ThreadSafeSetActiveScaleText);
this.Invoke(d, new object[] { text });
}
else
{
this.lScaleActive.Text = text;
}
}

ThreadSafeSetActiveScaleText(strGewicht);
Yes, the DataReceived event runs on a threadpool thread. You already knew that, you wouldn't have called it "ThreadSafe" otherwise. What we can't see is what is inside this method. But given the outcome, it is highly likely that you are using Control.Invoke().
Which is going to cause deadlock when you loop on COM_InUse in code that runs on the UI thread. The Control.Invoke() method can only complete when the UI thread has executed the delegate target method. But the UI thread can only do that when it is idle, pumping the message loop and waiting for Windows messages. And invoke requests. It cannot do this while it looping inside the Click event handler. So Invoke() cannot complete. Which leaves the COM_InUse variable for ever set to true. Which leaves the Click event handler forever looping. Deadlock city.
The exact same problem occurs when you call the SerialPort.Close() method, the port can only be closed when all events have been processed.
You will need to fix this by using Control.BeginInvoke() instead. Make sure the data is still valid by the time the delegate target starts executing. Pass it as an argument for example, copying if necessary.
Closing the form while the scale is unrelentingly sending data is in general a problem. You'll get an exception when you invoke on a disposed form. To fix this, you'll need to implement the FormClosing event handler and set e.Cancel to true. And unsubscribe the DataReceived event and start a timer. Make the Interval a couple of seconds. When the timer Ticks, you can close the form again, now being sure that all data was drained and no more invokes can occur.
Also note that calling DiscardInBuffer() is only good to randomly lose data.

Related

VS 2022 C# Forms: System.InvalidOperationException: 'Invoke or BeginInvoke cannot be called on a control until the window handle has been created.'

// Script Explanation
I'm trying to create a sort of timer which counts using a loop and a counter integer on startup, which saves the amount of seconds to a .txt file and stops when the program stops.
Because I'm using a while loop for the counting I have to do that in a separate thread to not stall the UI from updating and therefore count inside the separate thread and then invoke the text to the UI function.
The script will be at the bottom however thought I'd include this explanation for easier understanding.
// Issue
This all works fine, it updates the text label to show the seconds elapsed just fine, however when trying to close the application it throws an exception which I tried to fix by putting a bool check on when it should be counting and should not be counting inside the while loop instead of just running the while loop when true is true. I activate the bool at the start of the script when defining it and I disable it on the Form1_FormClosing event, but it still throws the following exception:
System.InvalidOperationException: 'Invoke or BeginInvoke cannot be called on a control until the window handle has been created.'
// My Script
using System;
using System.Windows.Forms;
using System.Threading;
using System.Diagnostics;
using System.IO;
using System.Reflection.Emit;
namespace PC_Timer
{
public partial class Form1 : Form
{
private bool Counting = true;
private int count = 0;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
if (File.Exists("count.txt"))
{
string text = File.ReadAllText("count.txt");
count = int.Parse(text);
label1.Text = count.ToString() + " Seconds";
}
else
{
File.WriteAllText("count.txt", "0");
}
new Thread(Loop).Start();
}
private void Loop()
{
while (Counting)
{
count++;
label1.Invoke((MethodInvoker)delegate {
label1.Text = count.ToString() + " Seconds";
});
Thread.Sleep(1000);
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
Counting = false;
File.WriteAllText("count.txt", count.ToString());
}
}
}
// Final Remarks
Maybe there is a better way to go about this, than doing it in a while loop since they are pretty inefficient however this is supposed to be a simple program for solo use purposes, which is why I went about doing it with a while loop because its what I'm most comfortable with.
I also tried to check if IsHandleCreated was true, which did work but I don't think it dealt with the exception, since I could not publish it because of one or multiple errors whchi Visual Studio was unable to determine the cause of.
Anyways what should I be doing different for the Invoke function to not get stuck when exiting the program?
This is a classic race condition. The loop has already checked that Counting is true and is about to call Invoke() when the main UI thread closes the form, and thus the Invoke() fail.
It's actually more difficult to fix that one might think. An obvious idea would be to use a lock to prevent the thread loop from trying to invoke while the form is closing, but that's likely to cause deadlock if the Form1_FormClosing() is blocked waiting for the lock when the loop is trying to call Invoke().
Have you considered using a Timer to update the label instead of a background thread? That would avoid the problem

Timer won't tick until form closed

There's some strange mistake with timer and forms.
I am making editor for game. Editor has two forms - MainForm and PreviewForm. PreviewForm contains only control for OpenGL output (Custom control based on GLControl from OpenTK), named glSurface.
glSurface has two inline timers (Windows.Forms.Timer) - one for rendering, and one for updating game state. Timers fires in glSurface method Run(double updateRate, double frameRate).
So, I want to show PreviewForm and run updating and rendering from MainForm.
My code is:
PreviewForm = new PreviewForm();
PreviewForm.glSurface.Run(60d, 60d);
PreviewForm.Show(this); //Form is "modal"
Body of Run method:
if (Running)
throw new Exception("Already run");
_updateRate = updateRate;
_renderRate = frameRate;
var renderFrames = Convert.ToInt32(1000/frameRate);
var updateFrames = Convert.ToInt32(1000/updateRate);
RenderTimer.Interval = renderFrames;
UpdateTimer.Interval = updateFrames;
RenderTimer.Start();
UpdateTimer.Start();
Running = true;
Timers is being initialized in OnVisibleChanged event:
protected override void OnVisibleChanged(EventArgs e)
{
...
RenderTimer = new Timer();
UpdateTimer = new Timer();
RenderTimer.Tick += RenderTick;
UpdateTimer.Tick += UpdateTick;
...
}
Weird things start here.
When PreviewForm is showing, nothing happens. BUT when I close that form, both timers fire their events! I have tested for possible cross-thread interaction, but PreviewForm.InvokeRequired and glSurface.InvokeRequired are both false.
Please help me find out what the hell is going on.
In this case declare and initialise and start your timers all within the one code block:
{
.../...
RenderTimer = new Timer();
UpdateTimer = new Timer();
RenderTimer.Tick += RenderTick;
UpdateTimer.Tick += UpdateTick;
var renderFrames = Convert.ToInt32(1000/frameRate);
var updateFrames = Convert.ToInt32(1000/updateRate);
RenderTimer.Interval = renderFrames;
UpdateTimer.Interval = updateFrames;
RenderTimer.Start();
UpdateTimer.Start();
.../...
}
Without seeing the program flow this is the safest option. It appears the variables are local to the OnVisibleChanged event, so I'm not sure how you're not getting a null refernce exception when you're calling them from your if (Running).
The other thing you could do is make them class variables and ensure they are initialised before you use them. Then call start within the if statement.
As for the issue of them starting when the form closes, it's impossible to determine from the code you've shown.
Edit: There's a deeper problem.
You really shouldn't be using system timers to drive your game updates and rendering.
System timers on most platforms have low accuracy that's inadequate for high performance multimedia such as audio and most games. On Windows System.Windows.Forms.Timer uses Win32 timers which have particularly low accuracy, typically resulting in intervals at least 15ms off(see this answer). See this technical breakdown and this overview for more information. Basically, even if your code worked correctly your frames would stutter.
Most games "tick" by running an infinite loop in the main thread, doing the following each time(not necessarily in this order):
Call back into the framework to handle pending OS events.
Track the time difference since the last "tick" so frame-independent timing works.
Update the state of the game(such as physics and game logic, possibly in another thread).
Render the scene based on a previous update(possibly in another thread).
As noted by commenters, the main problem in your timer code is that initialization is split between Run and OnVisibleChanged. I was unable to reproduce the case where the timer fires after a sub form is closed. I suspect some other code you haven't posted is the cause. You'll save yourself a great deal of trouble if you use OpenTK.GameWindow. It handles the loop plumping for you, similar to XNA. This is an example of one way to integrate it with WinForms. See the manual for more information.
In Run, you set Interval and start each timer. No Tick callbacks are set. In OnVisibleChanged, you recreate the timers and assign Tick callbacks. No intervals are set, and the timer's haven't been started.
The timer initialization code in Run is essentially ignored because no tick callbacks are set and OnVisibleChanged recreates the timers. OnVisibleChanged triggers almost immediately after Run, shortly after you call PreviewForm.Show(this).
If you're dead set on using system timers, this should work:
// somewhere before Run(ideally in the initialization of the main form).
RenderTimer.Interval = Convert.ToInt32(1000 / frameRate);
RenderTimer.Tick += RenderTick;
UpdateTimer.Interval = Convert.ToInt32(1000 / updateRate);
UpdateTimer.Tick += UpdateTick;
void Run(double frameRate, double updateRate)
{
// ...
RenderTimer.Start();
UpdateTimer.Start();
// ...
Running = true;
}
// ...
protected override void OnVisibleChanged(EventArgs e)
{
// ...
// Don't initialize timers here.
// ...
}

Terminating Thread Running an Event

I wrote an API that automates a certain website. However, on the testing stage, I noticed that (not very sure), my thread is not being terminated correctly.
I am using the WebBrowser object to navigate inside a thread, so that it works synchronously with my program:
private void NavigateThroughTread(string url)
{
Console.WriteLine("Defining thread...");
var th = new Thread(() =>
{
_wb = new WebBrowser();
_wb.DocumentCompleted += PageLoaded;
_wb.Visible = true;
_wb.Navigate(url);
Console.WriteLine("Web browser navigated.");
Application.Run();
});
Console.WriteLine("Thread defined.");
th.SetApartmentState(ApartmentState.STA);
Console.WriteLine("Before thread start...");
th.Start();
Console.WriteLine("Thread started.");
while (th.IsAlive) { }
Console.WriteLine("Journey ends.");
}
private void PageLoaded(object sender, WebBrowserDocumentCompletedEventArgs e)
{
Console.WriteLine("Pages loads...");
.
.
.
switch (_action)
{
.
.
.
case ENUM.FarmActions.Idle:
_wb.Navigate(new Uri("about:blank"));
_action = ENUM.FarmActions.Exit;
return;
case ENUM.FarmActions.Exit:
Console.WriteLine("Disposing wb...");
_wb.DocumentCompleted -= PageLoaded;
_wb.Dispose();
break;
}
Application.ExitThread(); // Stops the thread
}
Here is how I call this function:
public int Attack(int x, int y, ArmyBuilder army)
{
// instruct to attack the village
_action = ENUM.FarmActions.Attack;
//get the army and coordinates
_army = army;
_enemyCoordinates[X] = x;
_enemyCoordinates[Y] = y;
//Place the attack command
_errorFlag = true; // the action is not complated, the flag will set as false once action is complete
_attackFlag = false; // attack is not made yet
Console.WriteLine("Journey starts");
NavigateThroughTread(_url.GetUrl(ENUM.Screens.RallyPoint));
return _errorFlag ? -1 : CalculateDistance();
}
So the problem is, when I call the Attack function, couple times like this:
_command.Attack(509, 355, new ArmyBuilder(testArmy_lc));
_command.Attack(509, 354, new ArmyBuilder(testArmy_lc));
_command.Attack(505, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 359, new ArmyBuilder(testArmy_lc));
_command.Attack(505, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 359, new ArmyBuilder(testArmy_lc));
My application most of the times, gets stuck in one of these function (usually happens after the 4th or 5th). When it gets stuck the last log that I see is
Web browser navigated.
I assume it is something to do with termination of my thread. Can someone show me how I can run a thread which runs the DocumentCompleted event ?
I don't see any obvious reason for deadlock, nor did it reproduce at all when testing the code. There are a number of flaws in the code but nothing that yells "here!" loudly. I can only make recommendations:
Consider that you do not need a thread at all. The while (th.IsAlive) { } hot loop blocks your main thread while you wait for the browser code to finish the job. That is not a useful way to use a thread, you might as well use your main thread. This instantly eliminates a large number of potential hang causes.
The state logic in PageLoaded is risky. We cannot see all of it but one glaring issue is that you dispose the WebBrowser twice. If you have a case where you use return without a Navigate() call then you'll hang as described. No need to unsubscribe the event but same story, if you do unsubscribe but don't all Application.Exit() then you'll hang as described. State machines can be hard to debug, thorough logging is necessary. Minimize the risk by moving the Dispose() call and unsubscribing the event out of the logic, it doesn't belong there. And you need to test what happens when any Navigate() call ends up in failure, redirecting to a page you did not expect.
The _wb.Dispose() call is risky. Note that you destroy the WebBrowser while its DocumentCompleted event is in flight. Technically that can return code execution to code that is no longer alive or present. That can trip a race condition in the browser. As well as in the debugger, there is a dedicated MDA that checks for this problem. It is trivially avoided by moving the Dispose() call after the Application.Run() call where it belongs.
The while-loop burns 100% core, potentially starving the worker thread. Not a good enough reason to explain deadlock, but certainly unnecessary. Use Thread.Join() instead.
You create a lot of WebBrowser objects in this code. It is a very heavy object, as you can imagine, you need to keep an eye on memory usage in your program. Especially the unmanaged kind. If the browser leaks, like they so often do, you could technically create a scenario where the WB initializes okay but does not have enough memory left to load the page. Strongly favor using only one WB.
You need to consider that this might well be an environmental problem. On the top of that list is forever anti-malware and firewall, they always have a very good reason to treat a browser specially since that is the most common malware injection vector. You'll need to run your test with anti-malware and firewall disabled to ensure that it is not the cause of the hang.
Another environmental problem is one I noticed while testing this code, Google got sulky about me hitting it so often and started to throttle the requests, greatly slowing down the code. Talk to the web site owner and ask if he's got similar blocking or throttling counter-measures in place, most do. You need to test your state logic to verify that it still works properly when the browser redirects to an error page.
Yet another environmental issue is the WB will display a dialog itself in certain cases. This can deadlock in 3rd party code, very hard to diagnose. You should at least set the WebBrower.ScriptErrorsSuppressed to true but beware of Javascript code in the web page you load that itself creates new windows or displays alert dialogs. Using one WB is the workaround.
Keep in mind that your program can only be as reliable as your Internet connection and the web page server. That's not a terribly good place to be of course, both are quite out of your reach and you don't get nice exceptions to help you diagnose such a failure. And consider that you probably have not yet tested your program well enough yet to check if it can survive such a failure, it doesn't happen enough.
Quite a laundry list, focus first on eliminating the unnecessary thread and temporarily suppressing anti-malware. That's quick, focus next on using only one WebBrowser.
Hans thank you, I was able to fix this issue with one of your ideas. As you spent your time giving me a long answer, I wanted respond in same manner.
2 - I built the state machine structure carefully and with a lot logs (you can see it from my git account) also did a lot of debugs. I am sure that after I'm done navigating, I use Application.ExitThread() and wb.Dispose() only once.
3 - I tried placing the wb.Dispose() outside the event, however I couldn't find any other place where the Thread is still alive. If I try disposing WebBrowser outside the thread which is created inside the thread, the application gives me an error.
4 - I changed the code while (th.IsAlive) { } with th.Join(2000) this is absolutely a better idea but did not change anything. It optimized the code and as you mentioned, it prevented burning 100% core of my CPU.
5 - I tried using a single WebBrowser object which is instantiated in the constructor. However when I tried to navigate inside the thread, the application wouldnt even fire the events anymore. For some reason, I couldn't make it running whit a single WB object.
6,7 - I tested my application with different PC's and diffrent networks(with firewall and non-firewall protection). I changed windows firewall options as well but no travail. On my original code I do have _wb.ScriptErrorsSuppressed = true; so this shouldn't also be the issue.
8,9 - If these are the reasons, I can't do anything about it. But I doubt the real problem is caused because of them.
1 - This one was a good suggestion. I tried implementing my code without using a thread and it is now working fine. Here is how it looks like (still needs a lot optimization)
// Constructer
public FarmActions(string token)
{
// set the urls using the token
_url = new URL(token);
// define web browser properties
_wb = new WebBrowser();
_wb.DocumentCompleted += PageLoaded;
_wb.Visible = true;
_wb.AllowNavigation = true;
_wb.ScriptErrorsSuppressed = true;
}
public int Attack(int x, int y, ArmyBuilder army)
{
// instruct to attack the village
_action = ENUM.FarmActions.Attack;
//get the army and coordinates
_army = army;
_enemyCoordinates[X] = x;
_enemyCoordinates[Y] = y;
//Place the attack command
_errorFlag = true; // the action is not complated, the flag will set as false once action is complete
_attackFlag = false; // attack is not made yet
_isAlive = true;
Console.WriteLine("-------------------------");
Console.WriteLine("Journey starts");
NavigateThroughTread(_url.GetUrl(ENUM.Screens.RallyPoint));
return _errorFlag ? -1 : CalculateDistance();
}
private void NavigateThroughTread(string url)
{
Console.WriteLine("Defining thread...");
_wb.Navigate(url);
while (_isAlive) Application.DoEvents();
}
private void PageLoaded(object sender, WebBrowserDocumentCompletedEventArgs e)
{
Console.WriteLine("Pages loads...");
.
.
.
switch (_action)
{
.
.
.
case ENUM.FarmActions.Idle:
_wb.Navigate(new Uri("about:blank"));
_action = ENUM.FarmActions.Exit;
return;
case ENUM.FarmActions.Exit:
break;
}
_isAlive = false;
}
This is how I was able to wait without using a thread.
The main problem was probably as you mentioned in number 3 or 5. But I wasn't able to fix the problem as I spent couple of hours.
Anyway thanks for your help it works.

Writing to textbox inside an unselected tab

I have a program that can read and write on serial, GPIB, USB, and Ethernet. It has a tab for each method of communication with a textbox inside that displays communication on the port. One of the tabs is listed as All Comms and that text box has data from all communication methods. I am currently working on the serial port portion of the code and my program keeps freezing. Half the time I run my code it functions without issue writing to both tabs. The other half it freezes up when it tries to write to the text box inside the tab that is not selected(found by stepping through the code a line at a time).
I pulled the text boxes outside the tab control and this fixes the freezing issue. When the program freezes it does not display an error message and does not crash so no crash report(left it running over the weekend and it never finished crashing).
I would think that I need to select the other tab and then write to it, but why would the code work correctly half the time I run it?
Image of the program
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
rxString = rxString + serialPort1.ReadExisting();
if (rxString == "\b")
{
//Removes 1 character when backspace is the key pressed
rxSerialTextBox.ReadOnly = false;
rxSerialTextBox.Select(rxSerialTextBox.TextLength - 1, rxSerialTextBox.TextLength);
rxSerialTextBox.SelectedText = String.Empty;
rxSerialTextBox.ReadOnly = true;
rxString = "";
}
while (rxString.Length != 0)
{
try
{
while (rxString.Length != 0)
{
if (rxString.IndexOf("\r\n") == 0)
{
//Adds a newline when newline is the next characters in the string
rxString = rxString.Substring(rxString.IndexOf("\r\n") + 2);
rxAllCommsTextBox.AppendText(Environment.NewLine);
rxSerialTextBox.AppendText(Environment.NewLine);
}
//Adds a new character to the text box
rxAllCommsTextBox.AppendText(rxString.Substring(0, 1));
rxSerialTextBox.AppendText(rxString.Substring(0, 1));
rxString = rxString.Substring(1, rxString.Length - 1);
}
}
catch
{
//rxString = "";
}
}
}
A quck look at the SerialPort.DataReceived event documentation brings into attention the following Remarks section paragraph:
The DataReceived event is raised on a secondary thread when data is received from the SerialPort object. Because this event is raised on a secondary thread, and not the main thread, attempting to modify some elements in the main thread, such as UI elements, could raise a threading exception. If it is necessary to modify elements in the main Form or Control, post change requests back using Invoke, which will do the work on the proper thread.
According to this, your code that touches UI elements (text boxes) inside that event handler is incorrect. What the documentation doesn't say is that when you do so, the behavior is undefined - sometimes it may work, another time hang, yet another time throw exception.
So, instead of asking why your incorrect code sometimes work, you'd better concentrate on making it correct, and only then if something is not working, ask why and seek for a solution.
P.S. I'm not going to address how the concrete issue can be solved - there are a tons of posts, explanations and examples of how to marshal the calls to the UI thread, and in that regard there is nothing special in your case.
It was a little confusing your question ...
you can try, it is understood correctly, try to keep the value you want to assign the memory context, and assign the value based on another type of iteration.
At the moment the application freezes, which shows the breakpoint? The expected behavior does it work?
I would try a configuration object, such as a list, with various configurations, and certain state, the amount you need to pass the list to the tabcontrol. Obviously, it needs to check what's not working and why is not ...

HTML - How do I know when all frames are loaded?

I'm using .NET WebBrowser control.
How do I know when a web page is fully loaded?
I want to know when the browser is not fetching any more data. (The moment when IE writes 'Done' in its status bar...).
Notes:
The DocumentComplete/NavigateComplete events might occur multiple times for a web site containing multiple frames.
The browser ready state doesn't solve the problem either.
I have tried checking the number of frames in the frame collection and then count the number of times I get DocumentComplete event but this doesn't work either.
this.WebBrowser.IsBusy doesn't work either. It is always 'false' when checking it in the Document Complete handler.
Here's how I solved the problem in my application:
private void wbPost_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
if (e.Url != wbPost.Url)
return;
/* Document now loaded */
}
My approach to doing something when page is completely loaded (including frames) is something like this:
using System.Windows.Forms;
protected delegate void Procedure();
private void executeAfterLoadingComplete(Procedure doNext) {
WebBrowserDocumentCompletedEventHandler handler = null;
handler = delegate(object o, WebBrowserDocumentCompletedEventArgs e)
{
ie.DocumentCompleted -= handler;
Timer timer = new Timer();
EventHandler checker = delegate(object o1, EventArgs e1)
{
if (WebBrowserReadyState.Complete == ie.ReadyState)
{
timer.Dispose();
doNext();
}
};
timer.Tick += checker;
timer.Interval = 200;
timer.Start();
};
ie.DocumentCompleted += handler;
}
From my other approaches I learned some "don't"-s:
don't try to bend the spoon ... ;-)
don't try to build elaborate construct using DocumentComplete, Frames, HtmlWindow.Load events. Your solution will be fragile if working at all.
don't use System.Timers.Timer instead of Windows.Forms.Timer, strange errors will begin to occur in strange places if you do, due to timer running on different thread that the rest of your app.
don't use just Timer without DocumentComplete because it may fire before your page even begins to load and will execute your code prematurely.
Here's my tested version. Just make this your DocumentCompleted Event Handler and place the code that you only want be called once into the method OnWebpageReallyLoaded(). Effectively, this approach determines when the page has been stable for 200ms and then does its thing.
// event handler for when a document (or frame) has completed its download
Timer m_pageHasntChangedTimer = null;
private void webBrowser_DocumentCompleted( object sender, WebBrowserDocumentCompletedEventArgs e ) {
// dynamic pages will often be loaded in parts e.g. multiple frames
// need to check the page has remained static for a while before safely saying it is 'loaded'
// use a timer to do this
// destroy the old timer if it exists
if ( m_pageHasntChangedTimer != null ) {
m_pageHasntChangedTimer.Dispose();
}
// create a new timer which calls the 'OnWebpageReallyLoaded' method after 200ms
// if additional frame or content is downloads in the meantime, this timer will be destroyed
// and the process repeated
m_pageHasntChangedTimer = new Timer();
EventHandler checker = delegate( object o1, EventArgs e1 ) {
// only if the page has been stable for 200ms already
// check the official browser state flag, (euphemistically called) 'Ready'
// and call our 'OnWebpageReallyLoaded' method
if ( WebBrowserReadyState.Complete == webBrowser.ReadyState ) {
m_pageHasntChangedTimer.Dispose();
OnWebpageReallyLoaded();
}
};
m_pageHasntChangedTimer.Tick += checker;
m_pageHasntChangedTimer.Interval = 200;
m_pageHasntChangedTimer.Start();
}
OnWebpageReallyLoaded() {
/* place your harvester code here */
}
How about using javascript in each frame to set a flag when the frame is complete, and then have C# look at the flags?
I'm not sure it'll work but try to add a JavaScript "onload" event on your frameset like that :
function everythingIsLoaded() { alert("everything is loaded"); }
var frameset = document.getElementById("idOfYourFrameset");
if (frameset.addEventListener)
frameset.addEventListener('load',everythingIsLoaded,false);
else
frameset.attachEvent('onload',everythingIsLoaded);
Can you use jQuery? Then you could easily bind frame ready events on the target frames. See this answer for directions. This blog post also has a discussion about it. Finally there is a plug-in that you could use.
The idea is that you count the number of frames in the web page using:
$("iframe").size()
and then you count how many times the iframe ready event has been fired.
You will get a BeforeNavigate and DocumentComplete event for the outer web page, as well as each frame. You know you're done when you get the DocumentComplete event for the outer webpage. You should be able to use the managed equivilent of IWebBrowser2::TopLevelContainer() to determine this.
Beware, however, the website itself can trigger more frame navigations anytime it wants, so you never know if a page is truly done forever. The best you can do is keep a count of all the BeforeNavigates you see and decrement the count when you get a DocumentComplete.
Edit: Here's the managed docs: TopLevelContainer.
Here's what finally worked for me:
public bool WebPageLoaded
{
get
{
if (this.WebBrowser.ReadyState != System.Windows.Forms.WebBrowserReadyState.Complete)
return false;
if (this.HtmlDomDocument == null)
return false;
// iterate over all the Html elements. Find all frame elements and check their ready state
foreach (IHTMLDOMNode node in this.HtmlDomDocument.all)
{
IHTMLFrameBase2 frame = node as IHTMLFrameBase2;
if (frame != null)
{
if (!frame.readyState.Equals("complete", StringComparison.OrdinalIgnoreCase))
return false;
}
}
Debug.Print(this.Name + " - I think it's loaded");
return true;
}
}
On each document complete event I run over all the html element and check all frames available (I know it can be optimized). For each frame I check its ready state.
It's pretty reliable but just like jeffamaphone said I have already seen sites that triggered some internal refreshes.
But the above code satisfies my needs.
Edit: every frame can contain frames within it so I think this code should be updated to recursively check the state of every frame.
I just use the webBrowser.StatusText method. When it says "Done" everything is loaded!
Or am I missing something?
Checking for IE.readyState = READYSTATE_COMPLETE should work, but if that's not proving reliable for you and you literally want to know "the moment when IE writes 'Done' in its status bar", then you can do a loop until IE.StatusText contains "Done".
Have you tried WebBrowser.IsBusy property?
I don't have an alternative for you, but I wonder if the IsBusy property being true during the Document Complete handler is because the handler is still running and therefore the WebBrowser control is technically still 'busy'.
The simplest solution would be to have a loop that executes every 100 ms or so until the IsBusy flag is reset (with a max execution time in case of errors). That of course assumes that IsBusy will not be set to false at any point during page loading.
If the Document Complete handler executes on another thread, you could use a lock to send your main thread to sleep and wake it up from the Document Complete thread. Then check the IsBusy flag, re-locking the main thread is its still true.

Categories

Resources