I have a control that allows the user to perform some heavy duty image processing on a specific part of an image and they have arrow buttons to move this area around the image.
as the process is very heavy duty (avg 800ms per run) I have used a repeat button which turns this are into a "Ghost" and only executes the process upon the mouse up event.
This works really well and solves most performance issues relating to this function
HOWEVER
A certain group of users are refusing to learn this method of holding and releasing and persist in tapping the button to move it rather than holding and releasing.
This means that the heavy duty method is being called every time the they tap and as it only moves a small increment each time the method fires, so they end up with a application hang whilst it tries to do > 100 of these 800ms + processes
MY QUESTION
How can I handle this tapping behaviour in the same way as holding and releasing?
I thought about a timer but cant work out how I would detect the difference between a normal tap and the last tap.
Quick, dirty solution: use a Timer.
Each time the user taps the button, stop the timer, increase the number of taps, start the timer. If the timer elapses before the user taps again, then it should do your big work method.
Is this prone to threading issues? Probably. I'm not a threading expert. I would love if a threading expert can come comment on it.
Is this the best solution? Hardly. But it will get you by for a while (until the threading issues come up).
private int _totalTaps = 0;
private const int _tapSequenceThreshold = 250; // Milliseconds
private Timer _tapTimer = new Timer(_tapSequenceThreshold);
private void InitializeTimer()
{
_tapTimer.Elapsed += OnTapTimerElapsed;
}
private void OnTapTimerElapsed(object source, System.Timers.ElapsedEventArgs e)
{
_tapTimer.Stop();
// The `DoBigLogic` method should take the number of taps and
// then do *something* based on that number, calculate how far
// to move it, for example.
DoBigLogic(_totalTaps);
_totalTaps = 0;
}
// Call this each time the user taps the button
private void Tap()
{
_tapTimer.Stop();
_totalTaps++;
_tapTimer.Start();
}
Best solution: this method plus moving this work off the GUI thread. Then you don't have to worry about taps or click-and-hold, you won't block the GUI thread.
If you have to do work that doesn't update the UI (redraw the image, for example) then send the image to the UI, you can make a new thread, then you'll hit an error about 'accessing a UI element from a non-UI thread', just drop the UI code in a Marshal for it.
await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(
Windows.UI.Core.CoreDispatcherPriority.Normal,
() => { UpdateDisplay(); }
);
Consider monitoring mouse activity and start your heavy duty process after a short period of inactivity.
Consider running the process on a separate thread - this might mean cloning (part of) the image in memory.
Consider preventing the process from being ran multiple times concurrently (if that is possible ie. the process is async).
Consider one of these options:
Disable the button until the process completes, causing that 800ms delay. Users will soon learn to use the hold down method. This would involve the smallest amount of code and put the onus on humans. It also ensures you are not holding up the app with clicks in the buffer or over-using resources.
Put a timer in your button click event:
'Ghost area'
Timer Start ( or reset to zero)
Then the code to call your main work is in the timer elapsed event which will be set to whatever pause you wish. (ie If a user has not clicked again within a second or so)
Then stop the timer
Execute code
You could try using reactive extensions which is available on nuget.
using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.Linq;
using System.Reactive.Linq;
namespace SmoothOutButtonTapping
{
public static class Filters
{
// First we need to combine the pressed events and the released
// events into a single unfiltered stream.
private static IObservable<MouseButtonState> GetUnfilteredStream(Button button)
{
var pressedUnfiltered = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
x => button.PreviewMouseLeftButtonDown += x,
x => button.PreviewMouseLeftButtonDown -= x);
var releasedUnfiltered = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
x => button.PreviewMouseLeftButtonUp += x,
x => button.PreviewMouseLeftButtonUp -= x);
return pressedUnfiltered
.Merge(releasedUnfiltered)
.Select(x => x.EventArgs.ButtonState);
}
// Now we need to apply some filters to the stream of events.
public static IObservable<MouseButtonState> FilterMouseStream(
Button button, TimeSpan slidingTimeoutWindow)
{
var unfiltered = GetUnfilteredStream(button);
// Ironically, we have to separate the pressed and released events, even
// though we just combined them.
// This is because we need to apply a filter to throttle the released events,
// but we don't need to apply any filters to the pressed events.
var released = unfiltered
// Here we throttle the events so that we don't get a released event
// unless the button has been released for a bit.
.Throttle(slidingTimeoutWindow)
.Where(x => x == MouseButtonState.Released);
var pressed = unfiltered
.Where(x => x == MouseButtonState.Pressed);
// Now we combine the throttled stream of released events with the unmodified
// stream of pressed events.
return released.Merge(pressed);
}
}
}
Now we have a stream that will respond immediately whenever a user presses, but will not fire a released event unless the button is released for long enough.
Here is an example of how you could consume the above method. This example simply changes the color of the control while the button is in the Pressed state, but you could easily do whatever you wanted.
using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Reactive.Linq;
using System.Threading;
namespace SmoothOutButtonTapping
{
public partial class SmoothTapButtonControl : UserControl
{
public SmoothTapButtonControl()
{
InitializeComponent();
_pressed = new SolidColorBrush(Colors.Lime);
_released = Background;
// Don't forget to call ObserveOn() to ensure your UI controls
// only get accessed from the UI thread.
Filters.FilterMouseStream(button, SlidingTimeoutWindow)
.ObserveOn(SynchronizationContext.Current)
.Subscribe(HandleClicked);
}
// This property indicates how long the button must wait before
// responding to being released.
// If the button is pressed again before this timeout window
// expires, it resets.
// This is handled for us automatically by Reactive Extensions.
public TimeSpan SlidingTimeoutWindow { get; set; } = TimeSpan.FromSeconds(.4);
private void HandleClicked(MouseButtonState state)
{
if (state == MouseButtonState.Pressed)
Background = _pressed;
else
Background = _released;
}
private Brush _pressed;
private Brush _released;
}
}
You can find the complete version of the above examples (project files and xaml included) on my github.
You can prevent program from executing your 800 ms function by setting simple flags(Preferably bool type).
You need to create three events. One Mouse Down, one Mouse Up event and other is Mouse Move event of the button. Set your flag false at declaration. When you click the button, make your flag true in Mouse Down event. And when you lift your mouse click i.e., Mouse Up event make your flag false.
Simple illustration of code.
bool click = false,run_process = true;
mouseDown_event()
{
click = true;
}
mouseUp_event()
{
click = false;
}
mouseMove_event()
{
if(click == true && run_process == true)
{
click = false;
run_process = false;
//call your function
}
}
How can I handle this tapping behaviour in the same way as holding and releasing?
Create a formula which calculates a weight value based last tap frequency, current operation and time between last actual operation; with any other factors I may not be aware of. With the right formula it should be representational to the person who uses the system correctly verses someone who sends multiple clicks.
The weighted value should be passed to an alternate thread which is handling the actual operation to the screen and can handle a blizzard of taps or a single tap without missing a beat, per se.
Related
I'm running into an issue that I'm not sure is solvable in the way I want to solve it. I have a problem with a race condition.
I have one project running as a C++ dll (the main engine).
Then I have a second C# process that uses C++/CLI to communicate with the main engine (the editor).
The editor is hosting the engine window as a child window. The result of this is that the child window receives input messages async (see RiProcessMouseMessage()). Normally this only happens when I call window->PollEvents();.
main engine loop {
RiProcessMouseMessage(); // <- Called by the default windows message poll function from the child window
foreach(inputDevice)
inputDevice->UpdateState();
otherCode->UseCurrentInput();
}
The main editor loop is the WPF loop which I don't control. Basically it does this:
main editor loop {
RiProcessMouseMessage(); // <- This one is called by the editor (parent) window, but is using the message loop of the (child) engine window
}
The RawInput processor which is called sync by the engine and async by the editor:
void Win32RawInput::RiProcessMouseMessage(const RAWMOUSE& rmouse, HWND hWnd) {
MouseState& state = Input::mouse._GetGatherState();
// Check Mouse Position Relative Motion
if (rmouse.usFlags == MOUSE_MOVE_RELATIVE) {
vec2f delta((float)rmouse.lLastX, (float)rmouse.lLastY);
delta *= MOUSE_SCALE;
state.movement += delta;
POINT p;
GetCursorPos(&p);
state.cursorPosGlobal = vec2i(p.x, p.y);
ScreenToClient(hWnd, &p);
state.cursorPos = vec2i(p.x, p.y);
}
// Check Mouse Wheel Relative Motion
if (rmouse.usButtonFlags & RI_MOUSE_WHEEL)
state.scrollMovement.y += ((float)(short)rmouse.usButtonData) / WHEEL_DELTA;
if (rmouse.usButtonFlags & RI_MOUSE_HWHEEL)
state.scrollMovement.x += ((float)(short)rmouse.usButtonData) / WHEEL_DELTA;
// Store Mouse Button States
for (int i = 0; i < 5; i++) {
if (rmouse.usButtonFlags & maskDown_[i]) {
state.mouseButtonState[i].pressed = true;
state.mouseButtonState[i].changedThisFrame = true;
} else if (rmouse.usButtonFlags & maskUp_[i]) {
state.mouseButtonState[i].pressed = false;
state.mouseButtonState[i].changedThisFrame = true;
}
}
}
UpdateState() is called only by the engine. It basically swaps the RawInput to the currently used input. This is to prevent input updating in the middle of a frame loop (aka. during otherCode->UseCurrentInput();)
void UpdateState() {
currentState = gatherState; // Copy gather state to current
Reset(gatherState); // Reset the old buffer so the next time the buffer it's used it's all good
// Use current state to check stuff
// For the rest of this frame currentState should be used
}
MouseState& _GetGatherState() { return gatherState; }
void Reset(MouseState& state) { // Might need a lock around gatherState :(
state.movement = vec2f::zero;
state.scrollMovement = vec2f::zero;
for (int i = 0; i < 5; ++i)
state.mouseButtonState[i].changedThisFrame = false;
}
So as you can see the race condition happens when RiProcessMouseMessage() is called while Reset() was called in the main engine loop. If it wasn't clear: The Reset() function is required to reset state back to it's frames default data so that the data is read correctly every frame.
Now I'm very much aware I can fix this easily by adding a mutex around the gatherState updates but I would like to avoid this if possible. Basically I'm asking is it possible to redesign this code to be lock free?
You are asking lock-free which is not quite possible if both ends alter the buffer. But if you ask lock that is optimized and almost instantaneous then you can use FIFO logic. You can use the .net's ConcurrentQueue "https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentqueue-1?view=net-5.0" to write updates and poll updates from this queue.
If you really get rid of the lock then you may check lock-free circular arrays aka lock-free ring-buffer,
If you want to dig deeper into hardware level to understand the logic behind this then you can check https://electronics.stackexchange.com/questions/317415/how-to-allow-thread-and-interrupt-safe-writing-of-incoming-usart-data-on-freerto so you will have an idea about concurrency at the low-level as well; With limitations, a lock-free ring buffer can work when one end only writes and the other end only reads within known intervals/boundaries can check similar questions asked:
Circular lock-free buffer
Boost has well-known implementations for lock-free: https://www.boost.org/doc/libs/1_65_1/doc/html/lockfree.html
I am creating a simple application which scrapes some XML relating to the status of some machine tools which are outputting live sensor data and uses the X/Y coordinates of the device to make a little rat dance around the screen.
The rat is placed in the correct location the first time the machine is polled but doesn't move each time the draw function is called by subsequent timer driven events.
I assumed this was just due to machine being on standby and the only coordinate changes being little jitters of the servo but just to check I created a random number generator and had the system use the randomly generated coordinates instead of the scaled X/Y data coming in.
I then found that the rat doesn't move!
This is the function where I am drawing the rat(s) (There are 2 systems but we are only worrying about 'bakugo' right now) We are looking particularly at if (dekuWake == false) and (bauwake == true);
Here I have had the values printed to the console (Driven by a timer) and the "system.drawing.point(s)" are shown to be valid (in range and changing).
The timer is initiated by a button in form1.
Timer event calls polling function which scrapes the XY variables from the site (See my question here for that function - What is wrong with my use of XPath in C#?)
At this point it ascertains whether the status was 'AVAILABLE' (which it is) and sets the 'rat's' 'awake' bool to true (determines which images are drawn, if a machine is offline the 'rat' stays in its box)
It then scales the coordinates to the resolution of the program window
(Normally, right now it is stepping through 2 arrays of integers generated when the polling first begins. The update coordinate function sets the X,Y coords of ImageRat.Bakugo) and calls drawRats().
Why does changing the location of my images not actually relocate the pictureboxes?
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Reflection;
using System.Drawing;
using System.Windows.Forms;
namespace XMLRats3
{
public class Drawing
{
private PictureBox HouseImage;
private PictureBox DekuImage;
private PictureBox BakuImage;
public Drawing(PictureBox house, PictureBox deku, PictureBox baku)
{
HouseImage = house;
DekuImage = deku;
BakuImage = baku;
}
public void ClearRats()
{
HouseImage.Hide();
DekuImage.Hide();
BakuImage.Hide();
}
public void DrawRats(bool DekuWake, bool BakuWake) // Call this function using active status of 2 machines
{
ClearRats();
/*// This shows that the generated coordinates are reaching this point successfully
Console.WriteLine("BAKU X: " + ImageRat.Bakugo.PosX);
Console.WriteLine("BAKU Y: " + ImageRat.Bakugo.PosY);
*/
System.Drawing.Point DekuCoord = new System.Drawing.Point(ImageRat.Deku.PosX, ImageRat.Deku.PosY); // Create a 'System Point' for Deku
System.Drawing.Point BakuCoord = new System.Drawing.Point(ImageRat.Bakugo.PosX, ImageRat.Bakugo.PosY); // Create a 'System Point' for Bakugo
if (DekuWake == false)
{
DekuImage.Hide();
if (BakuWake == false)
{
BakuImage.Hide();
HouseImage.Image = DesktopApp1.Properties.Resources.bothsleep;// set HouseImage to both sleep
}
else
{
BakuImage.Location = BakuCoord;
Console.WriteLine("Point:" + BakuCoord);
//Console.WriteLine("Reaching Relocation condition"); // Ensure we are getting here as animation not working
BakuImage.Show();
//BakuImage.
HouseImage.Image = DesktopApp1.Properties.Resources.dekuSleep; //Set HouseImage to DekuSleep
}
}
else //DekuWake == true
{
DekuImage.Show();
if (BakuWake == true)
{
HouseImage.Image = DesktopApp1.Properties.Resources.nosleep;//Set House image to nosleep
BakuImage.Location = DekuCoord;
DekuImage.Show();
BakuImage.Location = BakuCoord;
BakuImage.Show();
}
else
{
BakuImage.Hide();
HouseImage.Image = DesktopApp1.Properties.Resources.bakusleep;// Set house image to bakusleep
DekuImage.Location = DekuCoord;
DekuImage.Show();
}
}
HouseImage.Show(); // Out here as it should always happen
}
}
}
Ok so I don't have an exact answer on how to resolve this but I can tell you why it occurs and point you in the direction of some knowledge that will help you.
At the time I wrote this code I was (and still am) very new to C# and the concept of multithreaded applications in general.
It's a matter of poor software architecture.
The problem here is that the UI can only be updated from a single thread in c#, and since the timer runs in another thread, anything called from the timer is not allowed to update the UI.
I think it's possible to dip into the UI thread using delegates, though I haven't read into this enough yet to give you exact information on how this is done.
Hopefully this will help the people who have starred my question!
How do I update the GUI from another thread?
Assuming that you are using a timer form a different class, that occupies a different thread than the main one, for a timer to use an object, you should add the object as one of "Synchronizing Objects". So assuming that your timer is called timer1, in a method where you set the properties of the timer, you should write the following line of code
timer1.SynchronizingObject = yourPictureBox;
I hope that solves your problem
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.
// ...
}
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.
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.