Let's put some of these interfaces and APIs to work. We'll create an ActiveX control that you can use to download and install components over the intranet. We'll be encapsulating in an ATL based component all of the APIs and interfaces we've described above. Through COM based code reuse, we can avoid the required elaborate coding each time we need the download and install functionality. The implementation of the control is skeletal in nature, in order to keep things simple. The reader can easily modify and enhance the control for specific situations.
Our completed component will support an ILoadlate
interface. This is a dual interface that we've defined. The methods for this interface are:
ILoadlate Method Name |
Description |
QueryInterface() |
Standard IUnknown requirement. |
AddRef() |
Standard IUnknown requirement. |
Release() |
Standard IUnknown requirement. |
GetFactory([out, retval] LPUNKNOWN* itf) |
Called after the download and installation is completed in order to obtain the IClassFactory interface from the newly downloaded (and instantiated) object. |
GetLatest([in] BSTR bstrCLSID, [in] BSTR bstrURL) |
Called to start the code download, install, and object creation process. bstrCLSID should contain the string representation of the CLSID of the object; bstrURL should contain the URL to download the object from. |
PollResult([out] long * current, [out] long * max, [out,retval] BSTR * bstrStatus) |
Called periodically during download to get the current status of the download. bstrStatus is a status message indicating the current progress. current may contain the number of bytes downloaded and max may contain the number of bytes of the object being downloaded (this depends on the server and the URL used). |
This is the only interface that our ActiveX control will provide. Essentially, it encapsulates all the calls necessary for code download and installation, and it also changes the 'callback' model that the CoGetClassObjectFromURL()
calls to an easier-to-use polling model for the client.
Use the Visual C++5 ATL COM AppWizard to create a new DLL based project. In the project, use the ATL Object Wizard to put a Simple COM object into the project. Call this object CLoadlate
, call the interface ILoadlate
, select Apartment model threading and create dual interfaces.
When the basic code generation is completed, add the three methods according to the ILoadlate
interface description earlier in this section.
Next, add the implementation for these methods:
STDMETHODIMP CLoadlate::PollResult( long * current, long * max, BSTR * bstrStatus)
{
*bstrStatus = SysAllocString(m_bstrStatus);
*current = m_lLoaded;
*max = m_lTotal;
return S_OK;
}
The implementation of ILoadlate::PollResult()
simply copies the value of a set of member variables to the return values. We'll see how these variables get updated later.
The ILoadlate::GetFactory()
implementation is:
STDMETHODIMP CLoadlate::GetFactory(LPUNKNOWN * ift)
{
*ift = m_myFactory;
return S_OK;
}
Again, we assume that the caller will be calling after the download/binding is completed. We simply copy the member variable's value to the return value.
Most of the work is done in the ILoadlate::GetLatest()
implementation:
STDMETHODIMP CLoadlate::GetLatest(BSTR bstrCLSID, BSTR bstrURL)
{
HRESULT hr;
hr = CLSIDFromString(bstrCLSID, &m_clsid);
if (FAILED(hr))
return S_FALSE;
Here, we recover the CLSID from its string representation. This will be used later in the CoGetClassObjectFromURL()
call. We'll store it in the m_clsid
member variable for now.
m_bstrUrl = bstrURL;
m_pbsc = new CBindStatusCallback(this);
if (m_pbsc == NULL)
return S_FALSE;
We then save the value of the URL in the m_bstrUrl
variable, and create a CBindStatusCallback()
object (which implements the IBindStatusCallback
interface), storing a pointer to it in the m_pbsc
member variable.
hr = CreateAsyncBindCtx(0, m_pbsc,NULL, &m_pbc);
if (FAILED(hr))
return S_FALSE;
Next, we create a bind context object by calling CreateAsyncBindCtx()
using the CBindStatusCallback
object we created earlier. The resulting bind context is assigned to a m_pbc
member variable (of the point-to-IBindCtx
type). Finally, we're ready to call CoGetClassObjectFromURL()
.
LPUNKNOWN tpInf;
hr = CoGetClassObjectFromURL(m_clsid,
m_bstrUrl,
0xffffffff,
0xffffffff,
NULL,
m_pbc,
CLSCTX_INPROC_SERVER,
NULL,IID_IClassFactory,
(void * *)&tpInf);
if (!FAILED(hr))
m_myFactory = tpInf;
return hr;
}
Note that we used 0xfffffff
for both version numbers to ensure that the control will download the latest version of the requested object. The reader may customize this for actual deployment. When called successfully, this call will return MK_S_ASYNCHRONOUS
which indicates that the download and install will be taking place asynchronously.
We used a few member variables in the code above without declaring them. Now we must add these variables to the CLoadlate
class. Add the following variables and make them all public.
WCHAR m_bstrStatus[100];
CComBSTR m_bstrUrl;
IUnknown* m_myFactory;
long m_lLoaded;
long m_lTotal;
uuid_t m_clsid;
IBindCtx* m_pbc;
IBindStatusCallback* m_pbsc;
This completes our CLoadlate
class. However, we see above that we must also define the CBindStatusCallback
class. This class is defined in the .H
file as:
class CLoadlate;
class CBindStatusCallback : public IBindStatusCallback
{
public:
// IUnknown methods
STDMETHODIMP QueryInterface(REFIID riid,void ** ppv);
STDMETHODIMP_(ULONG) AddRef() { return m_cRef++; }
STDMETHODIMP_(ULONG) Release() { if (--m_cRef == 0) { delete this; return 0; } return m_cRef; }
// IBindStatusCallback methods
STDMETHODIMP OnStartBinding(DWORD dwReserved, IBinding* pbinding);
STDMETHODIMP GetPriority(LONG* pnPriority);
STDMETHODIMP OnLowResource(DWORD dwReserved);
STDMETHODIMP OnProgress(ULONG ulProgress, ULONG ulProgressMax, ULONG ulStatusCode,
LPCWSTR pwzStatusText);
STDMETHODIMP OnStopBinding(HRESULT hrResult, LPCWSTR szError);
STDMETHODIMP GetBindInfo(DWORD* pgrfBINDF, BINDINFO* pbindinfo);
STDMETHODIMP OnDataAvailable(DWORD grfBSCF, DWORD dwSize, FORMATETC *pfmtetc,
STGMEDIUM* pstgmed);
STDMETHODIMP OnObjectAvailable(REFIID riid, IUnknown* punk);
// constructors/destructors
CBindStatusCallback(CLoadlate * pLate);
~CBindStatusCallback();
// data members
CLoadlate * m_ptrLate;
DWORD m_cRef;
IBinding* m_pbinding;
CCodeInstall * m_CodeInstall;
};
Note: If you use the New Class... menu option to add this and the following class, then you need to add a generic class derived from the interface. Then in the .CPP
file you'll find a #define
for new
to DEBUG_NEW
. This needs to be removed as it's for MFC projects only.
We can see that this class implements the IBindStatusCallback
interface through its member functions. It also has a member which holds a pointer to the CLoadlate
class which instantiates it (in order to update the member variables of the CLoadlate
class during status call back). It also has the m_pbinding
member to hold an IBinding
interface during the binding process. Since we need to support installation, there's also a CCodeInstall
object reference (we'll describe this class later).
The implementation of this class (in the .CPP
file) is simple. The constructor initializes member variables, and the destructor does nothing.
CBindStatusCallback::CBindStatusCallback(CLoadlate * pLate):m_ptrLate(pLate)
{
m_ptrLate->m_lLoaded = 0;
m_ptrLate->m_lTotal = 0;
m_ptrLate->m_myFactory = NULL;
m_cRef = 1;
m_pbinding = NULL;
} // CBindStatusCallback
CBindStatusCallback::~CBindStatusCallback()
{
} // ~CBindSt
Several functions return trivially with E_NOTIMPL
:
STDMETHODIMP CBindStatusCallback::GetPriority(LONG* pnPriority)
{
return E_NOTIMPL;
}
STDMETHODIMP CBindStatusCallback::OnLowResource(DWORD dwReserved)
{
return E_NOTIMPL;
}
STDMETHODIMP CBindStatusCallback::OnDataAvailable(DWORD grfBSCF, DWORD dwSize, FORMATETC* pfmtetc, STGMEDIUM* pstgmed)
{
return E_NOTIMPL;
}
In QueryInterface()
, we handle request for our own IBindStatusCallback
, as well as ICodeInstall
since we support installation.
STDMETHODIMP CBindStatusCallback::QueryInterface(REFIID riid, void** ppv)
{
*ppv = NULL;
if (riid==IID_ICodeInstall)
{
m_CodeInstall = new CCodeInstall();
*ppv = (ICodeInstall *) m_CodeInstall;
m_CodeInstall->AddRef();
return S_OK;
}
if (riid==IID_IUnknown || riid==IID_IBindStatusCallback)
{
*ppv = this;
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
Notice that above we create a new CCodeInstall
object if the query interface for ICodeInstall
is called.
Before download (binding) begins, the GetBin()
member will be called by the system to determine how to bind. We set the flags to indicate an asynchronous bind/download, and that the newest version should be fetched.
STDMETHODIMP CBindStatusCallback::GetBindInfo(DWORD* pgrfBINDF, BINDINFO* pbindInfo)
{
*pgrfBINDF = BINDF_ASYNCHRONOUS
| BINDF_ASYNCSTORAGE |BINDF_GETNEWESTVERSION ;
return S_OK;
}
When download (binding) begins, the OnStartBinding()
member will be called by the system. We're obliged to grab hold of a binding object at this time:
STDMETHODIMP CBindStatusCallback::OnStartBinding(DWORD dwReserved, IBinding* pbinding)
{
if (m_pbinding != NULL)
m_pbinding->Release();
m_pbinding = pbinding;
if (m_pbinding != NULL)
{
m_pbinding->AddRef();
}
return S_OK;
}
During the binding/download, the OnProgress()
member will be called regularly. We take this opportunity to update the status variables of the CLoadlate
class.
STDMETHODIMP CBindStatusCallback::OnProgress(ULONG ulProgress, ULONG ulProgressMax, ULONG ulStatusCode, LPCWSTR szStatusText)
{
wcscpy(m_ptrLate->m_bstrStatus, szStatusText);
m_ptrLate->m_lLoaded = ulProgress;
m_ptrLate->m_lTotal = ulProgressMax;
return S_OK;
}
When the binding/download is completed, the OnStopBinding()
method is called. Here we release the binding object we seized earlier in OnStartBinding()
. We also signify to our client that the binding is completed by setting the m_bstrStatus
of the CLateload
member to “DONE!”
.
STDMETHODIMP CBindStatusCallback::OnStopBinding(HRESULT hrStatus, LPCWSTR pszError)
{
wcscpy(m_ptrLate->m_bstrStatus, L"DONE!");
if (m_pbinding)
{
m_pbinding->Release();
m_pbinding = NULL;
}
return S_OK;
}
When installation is completed and the requested IClassFactory
interface is available, the OnObjectAvailable()
member will be called. Here, we simply assign the pointer to our member variable, ready for the client to fetch.
STDMETHODIMP CBindStatusCallback::OnObjectAvailable(REFIID riid, IUnknown* punk)
{
m_ptrLate->m_myFactory = punk;
return S_OK;
}
This is all that's required for the CBindStatusCallback
class to implement the IBindStatusCallback
. Next, we'll examine the ICodeInstall
interface (and the CCodeInstall
class) that we'll need. The CCodeInstall
class is defined in the .H
file as:
class CCodeInstall:public ICodeInstall
{
public:
// IUnknown methods
STDMETHODIMP QueryInterface(REFIID riid,void ** ppv);
STDMETHODIMP_(ULONG) AddRef() { return m_cRef++; }
STDMETHODIMP_(ULONG) Release() { if (--m_cRef == 0) { delete this; return 0; } return m_cRef; }
STDMETHODIMP GetWindow(REFGUID ab, HWND *pwnd);
STDMETHODIMP OnCodeInstallProblem(ULONG ulStatus, LPCWSTR szDest, LPCWSTR szSrc, DWORD dwRes);
CCodeInstall();
~CCodeInstall();
protected:
DWORD m_cRef;
};
ICodeInstall
only has two new member functions. In the .CPP
file, constructor and destructor are implemented trivially.
CCodeInstall::CCodeInstall():m_cRef(1)
{
}
CCodeInstall::~CCodeInstall()
{
}
ICodeInstall::QueryInterface()
is also implemented in a standard fashion.
STDMETHODIMP CCodeInstall::QueryInterface(REFIID riid, void** ppv)
{
*ppv = NULL;
if (riid==IID_IUnknown || riid==IID_ICodeInstall)
{
*ppv = this;
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
ICodeInstall::GetWindow()
asks for a parent window to display some user interface. We return NULL
which indicates that the desktop window should be used.
STDMETHODIMP CCodeInstall::GetWindow(REFGUID ab, HWND *pwnd)
{
*pwnd = NULL;
return S_OK;
}
To implement ICodeInstall::OnCodeInstallProblem()
, we return S_OK
for all the reported problems which we would ignore, and return E_ABORT
for all other problems (e.g. out of disk space).
STDMETHODIMP CCodeInstall::OnCodeInstallProblem(ULONG ulStatus, LPCWSTR szDest, LPCWSTR szSrc, DWORD dwRes)
{
switch(ulStatus)
{
case CIP_OLDER_VERSION_EXISTS:
case CIP_NAME_CONFLICT:
case CIP_TRUST_VERIFICATION_COMPONENT_MISSING:
case CIP_NEWER_VERSION_EXISTS:
return S_OK;
default:
return E_ABORT;
}
}
Finally, we need to include the definitions for the two interfaces we're implementing as well as linking in the relevant library. The best place to add the required header is in StdAfx.h
:
...
#include <atlbase.h>
//You may derive a class from CComModule and use it if you want to override
//something, but do not change the name of _Module
extern CComModule _Module;
#include <atlcom.h>
#include <urlmon.h>
...
In the Link tab of the Project Setting dialog, you need to add Urlmon.lib
to the list of library modules:
This completes our implementation of the 'downloader' control. You can now compile, link, and register the control.
Typically, a client of this object will:
ILoadlate::GetLatest()
with information for the object to download and install. This call will return immediately while the download and installation occurs asynchronously.ILoadlate::PollResult()
looking for a status string of “DONE!”
. It can also update the user on the progress of the download if desired.ILoadlate::PollResult()
returns “DONE!”
for status, call ILoadlate::GetFactory()
to obtain the class factory interface of the object.Let's follow these five steps and quickly code our test client. First, create a new Win32 application project in Visual C++ 5. We called ours 'lotestw'. Create a blank C++ source file in the project and start entering the code:
#include <assert.h>
#include <comdef.h>
#include "resource.h"
#import "codeload.tlb" no_namespace
#import "atldept.tlb" no_namespace
Here, we're using the native COM support of the Visual C++ 5 compiler again. In the two #import
directives, we obtain the type information (and smart pointer definitions) for our Atldept.dll
, as well as the type information from our new 'downloader' ActiveX control. Next, we have a few constant declarations.
const char szURLofObject[] = "http://PENTIUM1/ATLDEPT.DLL";
const unsigned TIMER_DURATION = 200; // refresh every 200 ms
const int TIMER_ID = 1001;
Since the ILoadlate
interface supports polling, we'll be creating a Windows timer to activate our polling during the download. TIMER_DURATION
is the interval time between each pool, the TIMER_ID
is used to identify the timer in Win32 API calls. Next, we define a custom Windows message. This message is posted by the timer handler after receiving “DONE!”
from the control; the handler for this message will get the IClassFactory
interface and create an object from it.
#define WM_CUSTOM1 WM_USER+100
The next section is the COM exception handling required by the native COM support. We aren't going to implement the details here, but you're free to do so.
void dump_com_error(_com_error &e)
{
// handle error intelligently in here
}
The WinMain()
function is quite straightforward:
int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR szCmdLine, int nCmdShow)
{
HRESULT hr;
hr = CoInitialize(NULL);
if (FAILED(hr))
return hr;
DialogBox(hinst, MAKEINTRESOURCE(IDD_DIALOG1), HWND_DESKTOP, (FARPROC)&DialogProc);
CoUninitialize();
return 0;
}
In the above code, we simply call CoInitialize()
, create a dialog box, then call CoUninitialize()
and quit. Making the desktop window the parent for the dialog box means we don't need to create any window in WinMain()
(however, CoInitialize()
will actually create a hidden window).
Let's take a look at the dialog box; create this using the resource editor in Visual C++ 5 and name it IDD_DIALOG1
(the default).
Use the default dialog generated by the resource editor, and change the text of the OK button to Install Object Asynchronously and the Cancel button to Exit. Create a read-only edit and name it IDC_EDIT1
.
Now, we're ready to examine our dialog procedure, where all of the testing logic resides. If you're keying in the code, make sure this goes before the WinMain()
in the file.
BOOL CALLBACK DialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
static ILoadlatePtr pIC;
long current, max;
BSTR bstrStatus;
We declare a smart pointer to our ILoadlate
interface as static, so we would not recreate the object on every message. Current
, max
, and bstrStatus
are to hold the results from ILoadlate::PollResult()
for reporting to the end user. Next, we're into the message decoding loop portion:
switch(message)
{
case WM_INITDIALOG:
try
{
pIC.CreateInstance("Loadlate.Loadlate.1");
}
catch (_com_error &e)
{
dump_com_error(e);
}
break;
In the WM_INITDIALOG
message (sent only once after the creation of dialog box), we instantiate a 'downloader' control, and obtain the ILoadlate
interface through the smart pointer using the CreateInstance()
member.
case WM_TIMER:
{
bstrStatus = pIC->PollResult(¤t, &max);
char sz[255];
if(bstrStatus!=NULL)
WideCharToMultiByte(CP_ACP, 0, bstrStatus,-1, sz, 255, 0,0);
char msg[100];
wsprintf(msg,"Loading: %s %d of %d ", sz, current, max);
SetWindowText(GetDlgItem(hwndDlg,IDC_EDIT1), msg);
if (wcscmp(bstrStatus, L"DONE!") == 0)
{
KillTimer(hwndDlg, TIMER_ID);
PostMessage(hwndDlg, WM_CUSTOM1, 0, 0);
}
}
break;
The next message handled here is the timer message. We call the ILoadlate::PollResult()
method through the smart pointer pIC
and then fill the edit box in the dialog with the status. We convert the string from wide character BSTR
to MBCS in order to format the output using wsprintf()
. If the download is completed, we stop the timer messages and then post our custom message WM_CUSTOM1
.
case WM_CUSTOM1:
{
HRESULT hr;
IClassFactory * tpF = (IClassFactory *) pIC->GetFactory();
IUnknown * tp2;
tpF->CreateInstance(NULL, IID_IUnknown, (void **) &tp2);
if (!FAILED(hr))
SetWindowText(GetDlgItem(hwndDlg, IDC_EDIT1), "Object Created!");
else
SetWindowText(GetDlgItem(hwndDlg, IDC_EDIT1), "Cannot create obj!");
tpF->Release();
tp2->Release();
}
break;
Handling of the WM_CUSTOM1
message is straightforward. At this time, the download has been completed. We obtain the IClassFactory
interface by calling ILoadlate::GetFactory()
through the smart pointer. We then use IClassFactory::CreateInstance()
to create an instance of the ATLDept object. If this is successful, we print the message Object Created! in the dialog box. In any case, we release the class factory and the object before returning.
We come now to the button handling messages:
case WM_COMMAND:
{
switch (LOWORD(wParam))
{
case IDOK:
{
_bstr_t bstrUrl(szURLofObject);
_bstr_t bstrStatus;
try
{
LPOLESTR ab;
CLSID myID = __uuidof(ATLDept1);
StringFromCLSID(myID, &ab);
_bstr_t bstrProgID(ab);
pIC->GetLatest(bstrProgID, bstrUrl);
}
catch (_com_error &e)
{
dump_com_error(e);
};
SetTimer(hwndDlg, TIMER_ID, TIMER_DURATION, NULL);
}
break;
When the Install Object Asynchronously button (OK button) is pressed, we call ILoadlate::GetLatest()
. This is done by calling StringFromCLSID()
on the CLSID of the ATLDept object that we want to create to get a BSTR
parameter. We also start the timer for refreshing the status edit box during the download.
case IDCANCEL:
pIC.Release();
EndDialog(hwndDlg,0);
return 1;
}
break;
}
}
return 0;
}
Finally, if the user presses the Exit button, we release the interface encapsulated within our smart pointer and exit from the application.
This is all the code required to download and install the ATLDept object. Thanks to the 'downloader' control and its ILoadlate
interface, the code for this client is very simple and uncluttered.
You can now compile the test program. Make sure that you have updated the URL embedded in the test program so that it points to your own web server.
The following is the procedure for testing the 'downloader' control with the test program.
Atldept.dll
resides, and perform a REGSVR32 /U ATLDEPT.DLL
.Oleview.exe
to ensure that the ATLDept1 object no longer shows up (i.e. removed from the registry).Atldept.dll
to the location of the URL. In our case, we have it at C:\Webshare\Wwwroot\Atldept.dll
. The Personal Web Server for Windows 95 is set to point to the directory \Webshare\Wwwroot
as the root directory. Our URL is http://PENTIUM1/ATLDEPT.DLL
.Lotestw.exe
.
If everything is working properly, you should start to see the status appearing in the read-only edit of the dialog box. You should also see or hear some disk activity. It will report the download progress (but this probably won't be for too long since Atldept.dll
is so small in size). After the download, the following security dialog will pop up asking if it's all right to install a component:
If you click Yes, the installation will complete, and you should see the Object Created! message in the dialog. This indicates a successful object creation using CoGetClassObjectFromURL()
call.
To convince yourself, use the Oleview.exe
program to see that now the ATLDept1 class is registered again. Click on the class entry to create an instance and notice that it is now working.
If you like to repeat the test, you must first remove the newly registered DLL.
\Windows\Occache
directory.REGSVR32 /U ATLDEPT.DLL
to remove the registry entries. Atldept.dll
to remove the downloaded DLL.The 'downloader' ActiveX control substantially simplifies the calls to perform automated code download and install. It can also be called from C++ or other clients (i.e. Visual Basic). While the Internet Explorer itself provides automation capability similar to the 'downloader' control, the control provides the function with a significantly smaller code and runtime memory footprint.