An Applied Example: The 'Downloader' Control

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.

The ILoadlate Interface

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.

Coding the Downloader Control

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.

Coding the Test Client

Typically, a client of this object will:

  1. Call ILoadlate::GetLatest() with information for the object to download and install. This call will return immediately while the download and installation occurs asynchronously.

  2. Handle user interface or perform other tasks, while intermittently calling the ILoadlate::PollResult() looking for a status string of “DONE!”. It can also update the user on the progress of the download if desired.

  3. After ILoadlate::PollResult() returns “DONE!” for status, call ILoadlate::GetFactory() to obtain the class factory interface of the object.

  4. Use the class factory to create the object, and release the class factory.

  5. Proceed to use the newly created 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(&current, &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.

Testing the Downloader Control

The following is the procedure for testing the 'downloader' control with the test program.

  1. Unregister the current ATLDept1 object by going to the directory where Atldept.dll resides, and perform a REGSVR32 /U ATLDEPT.DLL.

  2. Use Oleview.exe to ensure that the ATLDept1 object no longer shows up (i.e. removed from the registry).

  3. Copy 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.

  4. Make sure your web server is working properly.

  5. Start the test program, Lotestw.exe.

  6. Press the Install Object Asynchronously button.

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.

  1. Go to the \Windows\Occache directory.

  2. Do a REGSVR32 /U ATLDEPT.DLL to remove the registry entries.

  3. Delete Atldept.dll to remove the downloaded DLL.

  4. Restart the test program and try download/install again.

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.

© 1997 by Wrox Press. All rights reserved.