IWebBrowser2: how to force links to open in new window?
Asked Answered
S

5

5

The MSDN documentation on WebBrowser Customization explains how to prevent new windows from being opened and how to cancel navigation. In my case, my application is hosting an IWebBrowser2 but I don't want the user to navigate to new pages within my app. Instead, I'd like to open all links in a new IE window. The desired behavior is: user clicks a link, and a new window opens with that URL.

A similar question was asked and answered here and rather than pollute that answered post, it was suggested I open a new discussion.

The members on the related post suggested I should be able to do this by trapping DISPID_BEFORENAVIGATE2, setting the cancel flag, and writing code to open a new window, but I've found out that the browser control gets lots of BeforeNavigate2 events that seem to be initiated by scripts on the main page. For example, amazon.com fires BeforeNavigate2 events like crazy, and they are not a result of link invocation.

Replies appreciated!

Stafford answered 27/5, 2010 at 21:54 Comment(2)
Note that i wasn't suggesting to keep the question there static, but rather to ask a question about the specific problem with reliably identifying the context for BeforeNavigate2() et al.Govern
Georg, I understand, but my goal is simply to solve the problem stated above, and I'll be happy with any solution, whether it involves BeforeNavigate2 or something else.Stafford
S
4

What I ended up doing was using IHTMLDocument directly rather than IWebBrowser. IWebBrowser is a superset of IHTMLDocument, and the navigation model implemented by IWebBrowser isn't customizable to the degree I wanted.

I actually got MS Developer Support involved and this approach was their recommendation. They say this is what Outlook uses for HTML-based email, which is the user experience I wanted to emulate. They also confirmed that there's no reliable way to filter the OnBeforeNavigate events that result from user action from those that result from script activity.

Hope this helps anybody facing the same issues. It wasn't too hard to port the code to use IHTMLDocument. If you end up doing this, you may also find yourself looking for a way to figure out when the document is done loading. To do that, hook HTMLDocumentEvents instead of DWebBrowserEvents, and look for the DISPID_HTMLDOCUMENTEVENTS_ONREADYSTATECHANGE event. It doesn't tell you what the ready state is; you need to call IHTMLDocument::get_readyState and parse the resulting string. Goofy, but there you go.

Stafford answered 17/6, 2010 at 23:11 Comment(3)
Wow, good work. It's nice to find out there is a definitive answer to this one.Kenaz
Do you have an article on how to use IHTMLDocument, I found many but all were using IWebBrowserSuper
Hi Madhur- I had the same problem when I posted this question originally. All the documentation pointed to IWebBrowser. I honestly don't remember what led me to look at IHTMLDocument anymore (this was nearly two years ago) but I don't think I had an article per se. I just used the API documentation on the MSDN site and a lot of experimentation to figure out what I needed. Good luck.Stafford
S
3

You can bind to onclick event before document is complete while creating browser in OnCreate() using IHTMLDocument2::put_onclick():

#include <comutil.h>

ClickEvents<RootFrame> clickEvents;
_variant_t clickDispatch;
clickDispatch.vt = VT_DISPATCH;
clickDispatch.pdispVal = &clickEvents;

CComQIPtr<IDispatch> dispatch;
hr = webBrowser2->get_Document(&dispatch);
ASSERT_EXIT(SUCCEEDED(hr), "webBrowser->get_Document(&dispatch)");

CComQIPtr<IHTMLDocument2> htmlDocument2;
hr = dispatch->QueryInterface(IID_IHTMLDocument2, (void**) &htmlDocument2);
ASSERT_EXIT(SUCCEEDED(hr), "dispatch->QueryInterface(&htmlDocument2)");

htmlDocument2->put_onclick(clickDispatch);

ClickEvents class implements IDispatch, you only need to implement Invoke method, in rest return E_NOTIMPL:

HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
    DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
    HRESULT hr;

    CComQIPtr<IWebBrowser2> webBrowser2;
    hr = rootFrame->GetDlgControl(rootFrame->rootview.GetDlgCtrlID(), IID_IWebBrowser2, (void**) &webBrowser2);
    ASSERT_EXIT(SUCCEEDED(hr), "rootframe->GetDlgControl(IID_IWebBrowser2) failed");

    CComQIPtr<IDispatch> dispatch;
    hr = webBrowser2->get_Document(&dispatch);
    ASSERT_EXIT(SUCCEEDED(hr), "webBrowser2->get_Document(&dispatch)");

    CComQIPtr<IHTMLDocument2> htmlDocument2;
    hr = dispatch->QueryInterface(IID_IHTMLDocument2, (void**) &htmlDocument2);
    ASSERT_EXIT(SUCCEEDED(hr), "dispatch->QueryInterface(&htmlDocument2)");

    CComQIPtr<IHTMLWindow2> htmlWindow2;
    hr = htmlDocument2->get_parentWindow((IHTMLWindow2**) &htmlWindow2);
    ASSERT_EXIT(SUCCEEDED(hr), "htmlDocument2->get_parentWindow(&htmlWindow2)");

    CComQIPtr<IHTMLEventObj> htmlEvent;
    hr = htmlWindow2->get_event(&htmlEvent);
    ASSERT_EXIT(SUCCEEDED(hr), "htmlWindow2->get_event(&htmlEvent)");

    CComQIPtr<IHTMLElement> htmlElement;
    hr = htmlEvent->get_srcElement(&htmlElement);
    ASSERT_EXIT(SUCCEEDED(hr), "htmlEvent->get_srcElement(&htmlElement)");

    CComBSTR hrefAttr(L"href");
    VARIANT attrValue;
    VariantInit(&attrValue);
    hr = htmlElement->getAttribute(hrefAttr, 0 | 2, &attrValue); // 0 = case insensitive, 2 = return BSTR
    ASSERT_EXIT(SUCCEEDED(hr), "htmlElement->getAttribute()");

    wchar_t href[2084]; // maximum url length in IE, http://support.microsoft.com/kb/208427
    wcsncpy_s(href, _countof(href), attrValue.bstrVal, _TRUNCATE);

    if (!rootFrame->IsURLAllowed(href)) {

        VARIANT variant;
        variant.vt = VT_BOOL;
        variant.boolVal = VARIANT_FALSE;
        htmlEvent->put_returnValue(variant);

        ShellExecute(0, L"open", href, 0, 0, SW_SHOWNORMAL);
    }

    return S_OK;
}

As you can see after querying some interfaces I finally have the element that got clicked, then I call IsURLAllowed() defined in my root frame to check whether to allow opening url in current webbrowser window or whether to open it using default browser on user's computer.

This handles all links even if they were appended to document using javascript.

The same should be done with "onsubmit" events for forms.

I also think I have a solution for "window.location" redirects in javascript, I haven't tested it yet, but I will soon test it and I will update this answer then. You could use a combination of "onunload" and "onbeforeunload" events along with DWebBrowserEvents2::BeforeNavigate2(), after onunload/onbeforeunload are called you will know that user is leaving current page so now in BeforeNavigate2() you can cancel it. You can attach unload events using IHTMLWindow2::put_onunload() and IHTMLWindow2::put_onbeforeunload().

See sources of a complete solution for the "onclick" below.

AttachClickEvents in BrowserFrame:

http://code.google.com/p/phpdesktop/source/browse/phpdesktop-msie/msie/browser_frame.h?r=709d00b991b5#125

Invoke in ClickEvents(IDispatch):

http://code.google.com/p/phpdesktop/source/browse/phpdesktop-msie/msie/click_events.h?r=a5b0b350c933#132

Scrawny answered 23/2, 2012 at 6:5 Comment(0)
K
2

I'm hypothesising here but yet another approach could be to maintain a count of navigation events, incrementing the counter on DISPID_BEFORENAVIGATE2 and decrementing it on occurrences of DISPID_NAVIGATECOMPLETE2 and DISPID_NAVIGATEERROR. With that in place, you could speculate that whenever you get DISPID_BEFORENAVIGATE2 and your counter is at zero, it is actual user navigation / link invocation.

I have no idea whether this approach would work, or whether those are the right events you'd need to make it work, but it could be worth investigating.

Kenaz answered 1/6, 2010 at 20:55 Comment(0)
K
1

You could try a different approach instead and physically add the attribute target="_blank" to all <a> tags in the rendered document.

This approach would involve waiting for DISPID_DOCUMENTCOMPLETE and then using IHTMLDocument3::getElementsByTagName() to fetch all of the anchor elements. You would then use IHTMLElement::setAttribute() to set target="_blank" on each of them.

Kenaz answered 28/5, 2010 at 8:27 Comment(4)
Phil, I'll give this a try. I tried something along these lines before posting the question (I set the <base target="_blank"> attribute in the <head> element) but I ran into problems: 1, the user can click before DocumentComplete arrives, and 2, I was getting an HRESULT that indicated write failure. The write failure may have been a symptom of something I was doing wrong, but the fact that the user could click links before the document was complete confounded things. I'll post more after digging into this again...Stafford
Update on this idea: setting the <target> attribute of all the <a> elements on the page works for those elements, but I still have the basic problem of scripts that initiate navigation. Amazon is a good example of a site that seems to use a lot of scripts to navigate rather than <a> elements.Stafford
Something to keep in mind - what happens with sites where scripts dynamically update the DOM?Govern
Georg, you're right, any approach that alters the DOM is going to have problems with scripts. Hmmm...Stafford
A
0

It seems to me, that it you want "to open all links in a new IE window", it means that you want that the opening of new windows must be done in another process. The easiest way to do so: using CreateObject("InternetExplorer.Application") way (see another question which solve a problem, which is opposite to your question: InternetExplorer.Application object and cookie container). With this way you will receive the best isolation from your application and the user who clicks on the link receive all possibilities which exist in IE. You should of cause continue usage of BeforeNavigate2 events to find out the moment when "a new IE window" should be opened.

Atomism answered 27/5, 2010 at 22:23 Comment(5)
Oleg, you wrote: "continue usage of BeforeNavigate2 events to find out the moment when "a new IE window" should be opened" which is exactly the question I'm asking. AFAIK there is no way to differentiate between script-initiated BeforeNavigate2 events, and user-initiated BeforeNavigate2 events. Only the user-initiated events should result in a new window. (Whether that window can or should be opened in a new process or not, is not my question).Stafford
OK, Thanks! Now I understand. I should think about it. You are searching for some way of hooking some script functions? Could you describe a practical situation when you need such behavior? You can have close problems with using ActiveX components also.Atomism
Oleg, I simply want links to open in new windows. The practical situation is that I'm writing an app that requires this behavior. I'm open to any solution. Trapping BeforeNavigate2 has been recommended many times, but I've found it to be insufficient since you can't tell which BeforeNavigate2 events were triggered by scripts and which were triggered by the user.Stafford
You can do control a lot of things by BeforeNavigate2. First of all you control the start page of browsing. Then you receive full html code of any page. You can scan all links, download and analyse the content. You can modify any page dynamically before displaying it to user. If you want full control you can realize it. I see no problem to force opening a link in a new windows, but denying some scripts is much more job. It can be realized, but I really not understand in what situation it is really needed. If it is confidential information, then no problem. I am curious. But why downgrading?Atomism
Oleg, this doesn't answer the question as posted.Govern

© 2022 - 2024 — McMap. All rights reserved.