Pete Warden left a comment on my blog that he's porting a Firefox extension to IE and that he appreciates the articles I wrote about IE add-on development. Thanks Pete, good to know they're good for something. :)
Anyway, I was reading his blog and came across this post. He indicates he isn't going to use MSXML's XMLHTTPRequest object, because using the onreadystatechange event from C++ is too complicated. While I agree that it's poorly documented and hampered by the fact that almost all samples that talk about it use ATL, it's not actually that hard to use.
The documentation for onreadystatechange suggests you need to use connection points to get the event, but that isn't true (it doesn't even appear possible as querying an XMLHTTPRequest object for either IConnectionPoint or IConnectionPointContainer fails).
In fact, all you need to do is create a simple IDispatch implementation, and pass this to the onreadystatechange property. It will call Invoke with a dispIdMember of zero every time the onreadystatechange event is raised. The class that receives the event is pretty vanilla:
class XMLHttpEventSink : public IDispatch { public: XMLHttpEventSink(IXMLHTTPRequest *request) : _refCount(1), _request(request) { // Don't increase the reference count to the request object; // doing so would create a circular reference and thus a memory leak. } virtual ~XMLHttpEventSink() { } // IUnknown STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject); STDMETHODIMP_(ULONG) AddRef(); STDMETHODIMP_(ULONG) Release(); // IDispatch STDMETHODIMP GetTypeInfoCount(UINT *pctinfo); STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo); STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId); STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr); private: ULONG _refCount; IXMLHTTPRequest *_request; };
As you can see, it's a simple class that implements the IDispatch interface. For this example, I'm also storing the IXMLHTTPRequest object itself in a member so we can use it later. The implementations of the IUnknown methods are bog-standard COM, and all IDispatch members except Invoke never get called so they can just return E_NOTIMPL. I won't post that code here, but if you're really interested in it, you can see it in the full sample. An example Invoke implementation that checks the state and prints part of the response when completed is shown below:
STDMETHODIMP XMLHttpEventSink::Invoke(DISPID dispIdMember, const IID &riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) { // Since this class isn't used for anything else, Invoke will get called // only for onreadystatechange, and dispIdMember will always be 0. long state; // Retrieve the state _request->get_readyState(&state); std::wcout << L"State: " << state << std::endl; if( state == 4 ) { // The request has completed. // Get the request status. long status; _request->get_status(&status); std::wcout << L"Status: " << status << std::endl; if( status == 200 ) { // Get the response body if we were successful. _bstr_t body; _request->get_responseText(body.GetAddress()); std::wstring bodyString = body; std::wcout << L"First part of response: " << std::endl; if( bodyString.length() > 200 ) bodyString = bodyString.substr(0, 200); std::wcout << bodyString << std::endl; } } return S_OK; }
Note that this code has no error handling for reasons of readability. In a real application, you will want to add that.
The one thing that remains is the question of how we use this with IXMLHTTPRequest itself. As I indicated, this is much simpler than the documentation makes it out to be. We simply instantiate the event sink object, and pass it to IXMLHTTPRequest::put_onreadystatechange:
// Create XMLHTTPRequest object. IXMLHTTPRequest *request; CoCreateInstance(CLSID_XMLHTTP30, NULL, CLSCTX_INPROC, IID_IXMLHTTPRequest, reinterpret_cast<void**>(&request)); // Open the request _bstr_t method = L"GET"; _bstr_t url = L"http://www.ookii.org/rss.ashx"; _variant_t async = true; request->open(method, url, async, _variant_t(), _variant_t()); // Hook up the onreadystatechange event handler IDispatch *sink = new XMLHttpEventSink(request); request->put_onreadystatechange(sink); // Send the request request->send(_variant_t());
Note again that this code has no error handling. You may also note that I use MSXML 3; this version of MSXML is supported, and was included with IE6 so almost everybody has it.
And there you have it. Not quite as difficult as it may seem at first glance.
2007-12-10 23:29 UTC
Wow, thanks! As someone struggling through an IE extension, this saved me days of work. Question for you, though: is it possible to get IXMLHTTPRequest to notify you when the URL you're requesting returns a 302 redirect? My Invoke() code only seems to get called after the redirect is made (with the results of the redirect). I'd like to stop the original request and handle the redirect myself. Any suggestions?
2009-10-22 08:51 UTC
Thanks for your code. I can run it under windows XP plantform, but the same code seemed not to be run under windows CE; There's only state == 1 to be returned to XMLHttpEventSink::Invoke. Please, help~~
2010-09-01 18:13 UTC
Hi!
Many thanks for this great example. It works nicely, but I have one question: where does your XmlHttpEventSink get released from memory? You don't seem to destroy it anywhere ... I think this code produces leaks. Perhaps it should call Release() on itself inside Invoke() ?
2010-09-02 04:07 UTC
Hi Kurt,
If you check the full sample in the download, it releases the sink object at the end of the main() method.
However, I have found that this can still cause a memory leak. Unless you call request->put_onreadystatechange(NULL) before calling request->Release(), the sink's reference count doesn't reach zero until CoUninitialize() is called.
2013-07-22 15:52 UTC
Hello
Thanks a thousand times for this code!!
Great !
If I would not have found your article I would have given up with the useless MSDN that leads to nowhere.
_____________________________
IMPORTANT:
It is EXTEMELY important to note that you MUST call
CoInitializeEx(NULL, COINIT_MULTITHREADED);
If you call
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
instead the XmlHttpRequest will hang with READYSTATE_LOADING (1) forever !!
Comments are closed for this post. Sorry.