|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Hello again, While evaluating this webbrowser product we have noticed strange behavior, when we open a new form from inside the webbrowser it will be under the current form that has the webbrowser control on it. What we expect is that the new form will be on top of the current form. With a lot of effort we have managed to reproduce this problem in a standalone example, I'll clarify below. In both situations we start a new thread that will create a new form and show it without owner. In our old working situation (we are migrating from the IE WebBrowser control and are also trying out the CefSharp open source control) and situation A in the example I have the new form is displayed on top, example as we expected. In situation B in the example the new form is displayed under the current form, which is not what we expect. The example has the following structure. It has a form with the WebBrowser control and a TextBox on it, the TextBox has a TextChanged handler that will show the new form, this is situation A and works as expected. The WebBrowser control has a MouseDoubleClick handler that will also show the new form, this is sutation B, and does not work as expected. The code is as follows, I have not included the default Form1.cs that Visual Studio generates when creating a new Windows Forms project. Quote: using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms;
using EO.WebBrowser; using EO.WinForm;
namespace EOTest { static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false);
Application.Run(CreateTestForm(true)); }
private static Form CreateTestForm(bool displayBrowser) { var form = new Form1();
if (displayBrowser) { var webControl = new WebControl { Dock = DockStyle.Fill, TabStop = false };
var webView = new WebView { Url = "https://www.google.com" }; webView.MouseDoubleClick += (sender, e) => ShowFormInNewUIThread(); webControl.WebView = webView;
form.Controls.Add(webControl); } else { var textArea = new TextBox { Dock = DockStyle.Fill, Multiline = true }; form.Controls.Add(textArea); }
var textBox = new TextBox { Dock = DockStyle.Bottom }; textBox.TextChanged += (sender, e) => ShowFormInNewUIThread(); form.Controls.Add(textBox);
return form; }
private static void ShowFormInNewUIThread() { new Thread(() => { var newForm = CreateTestForm(false); var nativeWindow = new NativeWindow(); nativeWindow.AssignHandle(IntPtr.Zero); newForm.ShowDialog(nativeWindow); }).Start(); } } }
We hope that this issue can be resolved, as it blocks the successful evaluation of your product.
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
Hi,
You should avoid using a second thread in this case. The fundamental difference between other solutions such as MS WebControl or cefcharp and us that we run the browser engine in a separate process outside of your application's process while they run inside your process. Running the browser engine outside of your process has many benefits. For example, you do not need separate binaries for 32 bit and 64 bit systems; A crash in the browser engine won't crash your application, etc.
This multiple process architecture does have a few implications --- mostly related to how Windows manages input and window handles. The basic guideline for input/window handle manage is Windows will try to "group" window handle based on the process/thread in which they are created and parent/child relationship. At any time, there is a single foregroup group that receives the input from the user. Within this foregroup group, everything works as expected. For example, if you call ShowModal the window will be shown on top. However if the window fails to qualify foregroup group, then programmatically switching the window to top may not work and user may need to intervene explicitly (such as switching from task bar) to bring the window to foreground. This is most designed to avoid user losing their focus unexpectedly --- for example, a sudden window switching while user is typing feverishly would be extremely annoying to the user.
Windows tries to group the window based on a couple of relationship factors (let's call it "links") between the new window and the current foreground group. The scenario you run into is when your double click is triggered: 1. The current focus is inside the WebControl, because the browser engine runs outside of your process, this means the current focused window is outside of your process. So the "active process link" is not there; 2. The new window is created in a different thread. So the "thread link" is not there; 3. The new window's parent window is IntPtr.Zero, so the "parent link" is not there;
Typically one of the above three link would exist and you will not run into any problems. However in your case all three links are broken and caused the new window to be put into a different group rather than the foregroup group.
The easiest way to resolve this problem is to fix issue #2. Issue #1 can not be changed because the browser engine runs outside of your process. Issue #3 in theory can be changed but Windows Forms will check the thread affinity of the parent window. That leaves avoiding issue #2 as the best solution.
I am not sure why you need a worker thread for a new Form. This is not necessary with EO.WebBrowser because EO.WebBrowser runs outside of your process anyway (thus it already has its own separate threads). So creating a separate thread for it does not have much benefits.
Hope this helps. Please feel free to let us know if you have any more questions.
Thanks!
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Thank you for your quick reply!
Actually the reproducible example is somewhat dumbed down compared to our real application, but I hoped this would be enough to reproduce it. And it is, but it misses one important nuance, we do ensure that all GUI-related callbacks from the browser thread are executed on the GUI thread. In our application I have verified this by logging the thread id and the callbacks are really executed on the GUI thread (which is the same thread as where the browser control got instantiated, aka the underlying form).
I have also changed this in the example, I have replaced the following lines: webView.MouseDoubleClick += (sender, e) => ShowFormInNewUIThread(); textBox.TextChanged += (sender, e) => ShowFormInNewUIThread();
by: webView.MouseDoubleClick += (sender, e) => form.Invoke((MethodInvoker)delegate () { ShowFormInNewUIThread(); }); textBox.TextChanged += (sender, e) => form.Invoke((MethodInvoker)delegate() { ShowFormInNewUIThread(); });
While we use a different mechanism in our application, form.Invoke should also ensure that the callback is executed on the GUI thread.
When testing with the above code the issue still reproduces, and now the action of creating a new thread should be coming from the original GUI thread, hence the link between the active thread should still be there. I myself have mainly verified this also with CefSharp (from which we actually used the WPF variant to embed in an ElementHost as the WinForms variant was suboptimal), and there this issue is not present as long as you ensure that all callbacks (like starting a new thread) are posted back to the GUI thread.
I hope we'll be able to resolve this issue.
Update: Also, in our application I have found that this issue appears in the BeforeNavigate callback of the WebView, that might be another hint towards solving the actual issue.
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
Hi,
Whether you use form.Invoke is not the issue. The issue is inside your ShowFormInNewUIThread you created a new thread and then create the new Form in that thread. During our test if we remove that thread it works fine.
Thanks!
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
As of this moment that does not seem to be a solution for us, shouldn't the process link still be there though? With Invoke we create the new thread from the same process as the form is, hence we expect that it stacks.
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
The process link is the "active process link". The active process by the time you double click is the browser engine process (because your focus is inside the web page at that moment), not your application process. Invoke does not make any difference here. If you really do not want to get rid of your extra thread, you can try to change your code to something like the below. Note that the code posted here has all the pieces but is not grammatically correct (for example, functions are declared without enclosing class), so you will need to understand it in order to piece them together into your code. Definitions:
Code: C#
//This line is needed for DllImport attribute
using System.Runtime.InteropServices;
//This function is used to set the owner window
[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
private static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr hWnd2);
You will need to change your ShowFormInNewUIThread to something like this:
Code: C#
private static void ShowFormInNewUIThread(IntPtr owner)
{
new Thread(() =>
{
var newForm = CreateTestForm(false);
//Make sure the newForm's window handle is created because we will need it in the next line
newForm.CreateControl();
//Set the newForm's window's owner to the main form
SetWindowLong(newForm.Handle, -8, owner);
//Run the newForm
Application.Run(newForm);
}).Start();
}
Note the changes: 1. It added a owner window parameter. This is for establishing the owner/parent link (link #3 in our initial reply); 2. It calls SetWindowLong API to establish the link; 3. It uses Application.Run instead of Form.ShowModal. Form.ShowModal will complain in this case since the owner window is from a different thread. Linking windows from different thread this way is dangerous that's why Form.ShowModal won't let you do it; You will need to change your code that calls ShowFormInNewUIThread and pass the main form's handle as the owner window.
Code: C#
//Pass the main form's Handle to ShowFormInNewUIThread
webView.MouseDoubleClick += (sender, e) => ShowFormInNewUIThread(form.Handle);
The above code should get your new Form to be displayed on top. However because now the owner/parent link has been established, it won't allow you to switch back to your main form as the new form is in a modal loop. To avoid this problem, you can handle the new Form's Activate event (at which point the new form is already on top) and remove the owner/parent link:
Code: C#
private void Form1_Activated(object sender, EventArgs e)
{
//Pass IntPtr.Zero as the third argument to break the owner/parent link
SetWindowLong(Handle, -8, IntPtr.Zero);
}
We would still recommend removing your worker thread though. Generally when you use Windows Forms you should try to avoid creating window/controls in a worker thread. Windows putting a lot of hidden mechanisms to try to make such case work transparently but there are danger zones (such as this issue you run into) and its hard to troubleshoot if you run into those. So Windows.Forms put a lot of blocks (such as inside form.ShowModal) to prevent you from doing so. As a result, in order to make it work well you need to know both how to avoid the hidden danger zones from Windows' implementation and how to avoid hitting the safety blocks from Windows.Form's implementation. So we definitely think it's cleaner if you can avoid this threading mess all together. Hope this makes sense to you. Thanks!
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Thank you for your detailed reply! I agree that following your proposed workaround/solution definitely puts us in shady territory, but it might indeed work.
I have been thinking about it for a while though and I think that I see a different solution, what I do in the ShowFormInNewUIThread callback is the following: 1. I give the original form the focus, so effectively it switches from the web control to the form it is contained within. 2. I start my new thread and show the new form, this is the piece of code: webView.MouseDoubleClick += (sender, e) => form.Invoke((MethodInvoker)delegate () { ShowFormInNewUIThread(); }); 3. Then I give the focus back to the control (in the textbox case) respectively the web control (in the browser case).
I have verified this in the reproducible example and it works.
Next I have implemented this in the actual program and it didn't work, it only works when I remove the 3rd part where I give the focus back, and it's actually understandable because giving the focus will also bring the window to the front.
My goal with this solution was to restore the active process link, and that part seems to have worked, however in the actual program I do lose the focus of the webbrowser control and that doesn't seem to be ideal even though I cannot judge the impact yet. Do you think that this solution would be acceptable too?
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
That does look like a very interesting and promising solution. I agree with you that it makes sense that you can not give the focus back to the original WebControl since doing so implies that you want to bring the window to the front. So I think just keeping the first two steps could actually be a good solution --- of course the final jury has to be the user and at any given time we are at Microsoft's mercy because they set the rules.
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Actually, I think I got to an even better solution. I still give the WebControl the focus just before opening the new form, as described above. But now I also add an event handler to the original form's Activate event, which will focus the WebView once again and then remove itself (the event handler).
This seems to work exactly as I expect it to work, the only thing I need to take care of is that I do this for every new form that can be opened, but luckily that is not on too many places.
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
That will work too. :)
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
So we trialed this solution a bit as we are still evaluating this product and came to the conclusion that the above described focus fix does introduce several issues in our application and therefore we are going to look for a generic solution again.
After research we found out that there is an AllowSetForegroundWindow call available in the user32.dll, we think that this would fix our issue, possibly in combination with calling SetForegroundWindow.
Our plan is as follows: 1. Ensure that the browser process grants permission to our application to set foreground windows, this will be done by calling AllowSetForegroundWindow (our application process id) in the browser engine process. 2. Open a form as usual. 3. If needed, call SetForegroundWindow explicitely on the new form we have just created.
One issue arises though, how do I call AllowSetForegroundWindow in the browser process? It seems like most if not all events are executed inside the hosting process (our application) and no option seems available yet to make a call like this. Are you willing to work with us to obtain a solution? If this can be solved then we are a lot closer to being ready to purchase this component.
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
Hi, This is an interesting discovery. However why do you need to call AllowSetForegroundWindow in browser process? I believe the idea of this function is for someone who already have foreground privilege to share this privilege with someone else. The current active process (the one user explicitly switched to front, which is your application) is the one that has this privilege. So if you wish to share this privilege with other process (browser process), you must call it in your process, not in the browser process. You can get the browser process's process ID through this method: https://www.essentialobjects.com/doc/eo.webengine.engine.getchildprocessids.aspxThe first element in the returned array is the PID of the browser process. Thanks!
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Hello, Thank you for your quick response! We trialed your suggestion but it does not work, and I think I understand why. Reading the documentation of AllowSetForegroundWindow we see that it "Enables the specified process to set the foreground window using the SetForegroundWindow function.". In our case the currently active process is the browser engine as the user has clicked a link in a webpage and subsequently our own application has lost focus. We want our own application to call AllowSetForegroundWindow to bring the new window onto the top. So that's why we think that the browser engine needs to give our application permission to call SetForegroundWindow in order to put a window on top of the browser process, which normally is not allowed because none of the three links (active process link, thread link and parent link) mentioned earlier in this thread are present.
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
I see. We will check this. Like the other issue (keyboard integration) you have reported, this may defer to the 2019 cycle. However we should be able to add this code in the initial 2019 build if it does work and we can provide you pre-release builds if you are interested. You can contact us by the end of December to see if we have a pre-release build available at that time.
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Good evening,
Is a pre-release build available at this time?
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
Not yet. We are done with updating Chromium but we are still running the test. After that we will add this, so it may take another 2 weeks before a test build is available.
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Good morning,
Is there any news available?
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
We should have something by the end of this week.
|
|
Rank: Advanced Member Groups: Member
Joined: 10/24/2018 Posts: 97
|
Is there any news? :)
|
|
Rank: Administration Groups: Administration
Joined: 5/27/2007 Posts: 24,218
|
Unfortunately only bad news --- testing revealed some problems and we are still working on it.
|
|