Design-Time ActiveX Controls Made Easy with Visual C++ 5.0

Like many other programming addicts, I've spent a great deal of my free time these past few months tinkering around with the hot new features in Microsoft® Visual Studio™ 97-not getting any real work done, mind you, but certainly having a lot of fun. While I'm really excited about these tools-five CDs worth!-I'll admit I'm a bit overwhelmed. By the time I learn all there is to know about this plethora of development tools, the next version of Visual Studio will be released and there will be even more new things to learn. That's not to say I haven't made any progress. I've written an ActiveX™ control with Visual Basic® 5.0, created a Web site using Visual InterDev™, used the Active Template Library (ATL) just enough to wonder how anyone ever developed COM objects without it, and even taken a few timid sips of Java™ using Visual J++™. But even so, I've still just scratched the surface. There are a seemingly unlimited number of Internet development APIs to learn and a limited amount of time in which to do it.

Since you may feel as overwhelmed as I do, I've decided to try to make it really easy to master at least one facet of Internet development: design-time ActiveX controls. I'm not the first person to discuss this topic. In fact, Jay Massena gave an overview of design-time controls in this magazine several months ago (see the March 1997 Preview column). As a result, I won't spend much time telling you what a design-time control is. Instead, I'll give you all the information you need to build and host one using Visual C++® 5.0. First, I'll discuss the IActiveDesigner interface as I walk you through the steps required to develop a design-time control. To make it easy, I'll provide you with a custom Design-Time ActiveX ControlWizard. Second, I'll show you how to create a simple design-time control container-not a fully functional container like Visual InterDev or the ActiveX Control Pad, but one that works nonetheless. Finally, I'll show you a nifty sample I've developed called the Web Page Signature control.

Background

In case you haven't heard, a design-time control is simply an ActiveX control that exposes an additional Component Object Model (COM) interface named IActiveDesigner that generates text and makes it available to the control's container. Unlike regular ActiveX controls, design-time controls are used only during development. You don't download a design-time control and run it from a Web page or place it inside
an application. Instead, you use a design-time control in much the same way you would use a sophisticated macro in a text editor.

For example, when you place a design-time control on a Web page using an authoring tool like Visual InterDev, what ends up getting displayed to the user is the text generated by the control, not the control itself. If you've used Microsoft FrontPage™, you'll recognize this as the idea behind the WebBot®. The key difference is that since design-time controls adhere to the ActiveX open standard, they are much easier to build, share, and host. As a result, while design-time controls currently are used only by Visual InterDev, you'll soon find them supported by many different applications.

IActiveDesigner

As I mentioned previously, the key element of design-time ActiveX controls is the IActiveDesigner interface. As shown in Figure 1, IActiveDesigner has a total of eight methods, including the three inherited from IUnknown. Obviously, to support IActiveDesigner in a control, you must supply the code for each of those functions. If you're not a COM expert, don't worry, the implementation of this interface is straightforward. You'll find that for all but the most complicated design-time controls, you can simply copy boilerplate code from one project to another. In fact, I've developed a custom ControlWizard that does it automatically. I'll show you how easy it is to create a fully functional design-time ActiveX control using Visual C++ 5.0.

Figure 1: IActiveDesigner Methods

To begin, download the sample code accompanying this article (available at http://www.microsoft.com/mind) and build the WebDC project found in the Wizard Source directory. This project creates the custom Design-Time ActiveX ControlWizard and places it in your DevStudio\SharedIDE\
Template directory. You're now ready to build design-time controls the easy way!

Create a new project and choose the MFC Design-Time ActiveX ControlWizard from the list of project options (see Figure 2). Since it's a bare-bones sample, this ControlWizard has no steps-it simply gives your control the same name as the one you chose for your project. For example, if you give your project the name Sample, the ControlWizard generates a project containing classes named CSampleApp, CSampleCtrl, and CSamplePropPage, among others. Furthermore, that control would be registered with the name Sample Control.

Figure 2: Project Options

That's it! You've just created a fully functional design-time ActiveX control! Obviously, you'll need to enhance the control to meet your needs, but you've got a working template from which to start.

Preliminaries

You'll probably want to test the control to make sure it works, but before I explain how to do that, let me point out a few things:

virtual HRESULT GetRuntimeText(CString& strText);

// Interface maps
DECLARE_INTERFACE_MAP();

public:
  BEGIN_INTERFACE_PART(ActiveDesigner, IActiveDesigner)
    STDMETHOD(GetRuntimeClassID)(CLSID *pclsid);
    STDMETHOD(GetRuntimeMiscStatusFlags)(DWORD *dwMiscFlags);
    STDMETHOD(QueryPersistenceInterface)(REFIID riidPersist);
    STDMETHOD(SaveRuntimeState)(REFIID riidPersist,
        REFIID riidObjStgMed, void *pObjStgMed);
    STDMETHOD(GetExtensibilityObject)(IDispatch **ppvObjOut);
  END_INTERFACE_PART(ActiveDesigner)

The way the code is structured, you don't need to understand how the IActiveDesigner interface is implemented. You simply override the GetRuntimeText function shown above, as I'll describe shortly. However, if you're new to MFC or COM and would like to learn what the DECLARE_INTERFACE_MAP, BEGIN_INTERFACE_PART, and STDMETHOD macros do, you should refer to the MFC help topic entitled "TN038: MFC/OLE IUnknown Implementation."

·When you place your design-time ActiveX control inside a control container such as Visual InterDev, the container calls the control's XActiveDesigner::SaveRuntimeState interface method to get the runtime text generated by that control. If you inspect the code (see Figure 3), you'll notice that SaveRuntimeState is passed a pointer to an interface named IStream rather than simply being passed a pointer to a buffer. The control calls its own GetRuntimeText virtual function and then calls IStream::Write to store the text in the data stream managed by the container. Again, you don't really need to understand these details; you can simply override the GetRuntimeText function and let the boilerplate code do the rest.

Figure 3: Sample Control Implementation


/////////////////////////////////////////////////////////////////////////////
// CSampleCtrl::GetRuntimeText

HRESULT CSampleCtrl::GetRuntimeText(CString& strText)
{
        // TODO: Replace the code below with your own

        strText = _T("<p><i>Your Sample Control text here!</i>");
        return S_OK;
}

/////////////////////////////////////////////////////////////////////////////
// CSampleCtrl::XActiveDesigner - IActiveDesigner interface
//
// The following functions represent the implementation of the IActiveDesigner
// interface. For the most part, these functions can be left as is. The
// SaveRuntimeState() function will call the CSampleCtrl::GetRuntimeText()
// function when it needs to generate run-time text.

STDMETHODIMP_(ULONG) CSampleCtrl::XActiveDesigner::AddRef(void)
{
        METHOD_MANAGE_STATE(CSampleCtrl, ActiveDesigner)
        return (ULONG)pThis->ExternalAddRef();
}

STDMETHODIMP_(ULONG) CSampleCtrl::XActiveDesigner::Release(void)
{
        METHOD_MANAGE_STATE(CSampleCtrl, ActiveDesigner)
        return (ULONG)pThis->ExternalRelease();
}

STDMETHODIMP CSampleCtrl::XActiveDesigner::QueryInterface(
        REFIID iid,        LPVOID far* ppvObj)
{
        METHOD_MANAGE_STATE(CSampleCtrl, ActiveDesigner)
        return (HRESULT)pThis->ExternalQueryInterface(&iid, ppvObj);
}

STDMETHODIMP CSampleCtrl::XActiveDesigner::GetRuntimeClassID(CLSID *pclsid)
{
    *pclsid = CLSID_NULL;
    return S_FALSE;
}

STDMETHODIMP CSampleCtrl::XActiveDesigner::GetRuntimeMiscStatusFlags(DWORD *pdwMiscFlags)
{
        if (!pdwMiscFlags)
                return E_INVALIDARG;
        *pdwMiscFlags = NULL;
        return E_UNEXPECTED;
}

STDMETHODIMP CSampleCtrl::XActiveDesigner::QueryPersistenceInterface(REFIID riid)
{
        if (riid == IID_IPersistTextStream)
                return S_OK;
        return S_FALSE;
}

STDMETHODIMP CSampleCtrl::XActiveDesigner::SaveRuntimeState(
        REFIID riidPersist,        REFIID riidObjStgMed, void  *pObjStgMed)
{   
        HRESULT  hr;
        CString  cstrText;
        BSTR     bstrText;

        METHOD_MANAGE_STATE(CSampleCtrl, ActiveDesigner)

        if (riidPersist != IID_IPersistTextStream)
                return E_NOINTERFACE;

        if (riidObjStgMed != IID_IStream)
                return E_NOINTERFACE;

        hr = pThis->GetRuntimeText(cstrText);
        if (SUCCEEDED(hr))
        {
                bstrText = cstrText.AllocSysString();
                if (bstrText)
                {
                        hr = ((IStream *)pObjStgMed)->Write(bstrText,
                              SysStringByteLen(bstrText) + sizeof(OLECHAR), NULL);
                        SysFreeString(bstrText);
                }
                else
                        hr = E_OUTOFMEMORY;
        }

        return hr;
}

STDMETHODIMP CSampleCtrl::XActiveDesigner::GetExtensibilityObject(IDispatch **ppvObjOut)
{
        if (!ppvObjOut)
                return E_INVALIDARG;
        return this->QueryInterface(IID_IDispatch, (void **)ppvObjOut);
}

Testing the Control

As I mentioned earlier, at the time of this writing Visual InterDev is the only commercial product that supports design-time ActiveX controls. Of course, many more products should support them in the future, but for now, if you don't own a copy of InterDev you'll need to find another way to test your controls. There are two ways to do this: you can use the bare-bones test container I've provided, or you can download the Design-Time ActiveX Control Pad provided free by Microsoft (http://www.microsoft.com/intdev/sdk/dtctrl). While my container, aptly named SimplEdit, is quite pitiful when compared to Visual InterDev or the Control Pad, it is sufficient for testing most controls. To use it, you'll first need to build the SimplEdit project found in the Simple Container folder of the source code that accompanies this article. When you run SimplEdit, you'll see that it is little more than a simple text editor (hence, its name).

Assuming you've already built a sample control using the MFC ActiveX ControlWizard, click the Insert Control button on the SimplEdit toolbar to insert your control. A dialog box appears containing a list of the design-time controls registered on your machine. For example, Figure 4 shows the design-time controls registered on my machine. Choose your control from the list and click OK. The property page for
the control appears on the screen, allowing you to manipulate the properties of the control. Since your control has
no properties-at least not yet-just click OK. As you can see in Figure 5, the test container inserts the text generated
by the control into the document, but does not insert the
control itself.

Figure 4: Design-Time Controls

Figure 5: Sample Control Text

At this point, you're probably wondering what all the fuss is about. Why not simply type in the HTML by hand and save yourself the trouble of creating a design-time ActiveX control? Admittedly, if design-time controls could only generate static text-meaning the text could not change based on a Web author's preferences-there would be no point to using them. But as I'll describe later, you can add preference settings to your control's property page (see Figure 6) and alter the text sent to the container based on those preferences. With this flexibility, design-time controls become a powerful Web authoring tool.

Figure 6: Web Page Signature COntrol Property Page

SimplEdit

I just showed you how to use the SimplEdit application to test your design-time controls. Without providing an exhaustive explanation of how it works, I will touch on a few points of interest.

I built the SimplEdit project as an SDI application using the MFC AppWizard. I chose the default wizard settings for everything but the view, which I inherited from CEditView.

Then I created a CDialog-derived class named CControlPicker that uses the IEnumCLSID and ICatInformation COM interfaces to enumerate the design-time controls registered on the machine. You simply choose the control you want from a list box, as shown in Figure 4.

Next I created a CWnd-derived class, named CDesignWnd, that acts as a wrapper around the design-time ActiveX control. This class creates an instance of the control, sends the control a message telling it to display its property page, and calls the control's SaveRuntimeState interface method to get its runtime text.

To invoke the control's Properties verb-which is the COM way of saying "to display its property page"-I used the MFC COleControlSite class. As a result,
I had to copy four private MFC header files into the project: occimpl.h, ocdb.h, ocdbid.h, and olebind.h. This was necessary because those files are not
found in the mfc\include directory; they're located in mfc\source, instead, which means that they're not part
of the standard include path. Since your copy of Visual C++ probably
is installed in a different directory than mine, the SimplEdit project would not compile on your machine if I had simply included a full-path reference to those header files in the code. By making project-local copies of those files, I've been able to
prevent potential compilation problems, but future versions of MFC
may cause unexpected results if you don't keep those four files in sync.

I added an Insert Design-Time menu item-complete with a toolbar button, of course-and created an
associated message handler, CSimplEditView::OnInsertDesignTime:

void CSimplEditView::OnInsertDesignTime() 
{
    CLSID clsid;
    CControlPicker pickerDlg;
    if (pickerDlg.DoModal() != IDOK ||
        !pickerDlg.GetSelectedClass(clsid))
        return;

    CRect rect;
    CString strText;
    CDesignWnd designCtrl(this, clsid);
    if (designCtrl.GetRuntimeText(strText))
        GetEditCtrl().ReplaceSel(strText);
    else
        AfxMessageBox("Could not insert control.");
    designCtrl.DestroyWindow();
}

As you can see, the CControlPicker and CDesignWnd classes do the work of displaying the control's property page and retrieving its runtime text, which is inserted into the document at the location of the cursor.

The Design-Time ActiveX Control Pad

While the SimplEdit control container is functional, it certainly isn't very robust. Once you've inserted a design-time control into a document, there's no way to edit the control's properties because only its text is stored-no record is kept of the control itself. It would be nice if there were a way to save the state of the control within the HTML document without actually displaying that information to the user. Ideally, you'd be able to simply click a button to open the property page for the control when you needed to make subtle changes to its properties, rather than having to delete the generated HTML and re-insert the control each time.

As it turns out, sophisticated control containers like the Design-Time ActiveX Control Pad and Visual InterDev do just that. They store the control's context-its class identifier, size, and parameters-along with the runtime text in an HTML tag called METADATA (see Figure 7). This is far different from what happens when you insert a regular ActiveX control. While all of the state information is saved as part of the Web page, only the runtime text is visible from the Web browser, and as I mentioned earlier, the design-time control itself is not downloaded. Thus, the container is able to store information about the state of the control without affecting the user.

Figure 7: HTML Generated by Design-TIme ActiveX Control Pad

Let's look at how it works. If you're following along at home, you should first download and install the Design-Time ActiveX Control Pad (http://www.microsoft.com/intdev/sdk/dtctrl). Run the Control Pad, then click Edit | Insert ActiveX Control. You'll see a dialog box that lists the ActiveX controls registered on your machine, similar to the one displayed by the SimplEdit application. The only difference is that the Control Pad lets you insert all types of ActiveX controls, not just design-time controls.

Click the Design-time tab, then select the control you built using the wizard. When you click OK, the Control Pad displays two windows, one containing the visual representation of the control and the other displaying the control's properties. Since you haven't added any properties yet, the properties window is blank. When you close those windows, the METADATA tag is inserted into the HTML document, as shown in Figure 7.

To edit the control once it's been added, simply click the Edit icon that appears at the left of its HTML representation. To display the control's property page, click on the control with the right mouse button, and then choose Properties from the context menu.

Enhancing the Control

Now that I've explained how to create and test design-time ActiveX controls, let me show you one that actually does something useful-my Web Page Signature control. This control makes it really easy to add any of the following items to the bottom of your Web page: a horizontal line, the last-
modified date, a copyright message, and the Microsoft Internet Explorer logo. (Displaying the Internet Explorer logo on you page is especially useful if you're interested in a free membership to the Microsoft Site Builder Network. Check out http://www.microsoft.com/sitebuilder for details.) Figure 6 shows the control's property page and Figure 8 shows how its output looks in the Web browser.

Figure 8: Web page Signature Output

To create the Web Page Signature control, I started with the code generated by the Design-Time ActiveX Control Wizard and then added several IDispatch properties. Based on the state of those properties, I decide what text to generate in the control's GetRuntimeText function. Fortunately, this process is fairly easy even if you're not an OLE expert because the ClassWizard takes care of most of the work for you. For example, to add a CompanyName property like the one I added to my control, you click the Automation tab in the ClassWizard, select the control class-not the property page class-from the class name combo box, and click the Add Property button.

In the Add Property dialog box, type CompanyName in the External name edit box, set the property type to BSTR, and choose Get/Set methods for the implementation. The ClassWizard adds two member methods to your class, GetCompanyName and SetCompanyName, and adds the following entry to the control's dispatch map:

DISP_PROPERTY_EX(CSignatureCtrl, "CompanyName", GetCompanyName,
                 SetCompanyName, VT_BSTR)

Next, add a protected member variable named m_strCompany to the control class, then edit the GetCompanyName and SetCompanyName methods so they look like this:

BSTR CSignatureCtrl::GetCompanyName() 
{
    return m_strCompany.AllocSysString();
}

void CSignatureCtrl::SetCompanyName(LPCTSTR lpszNewValue) 
{
    m_strCompany = lpszNewValue;
    BoundPropertyChanged(dispidCompanyName);
    SetModifiedFlag();
}

Add a property exchange macro to your control's DoPropExchange method to associate the m_strCompany variable with the CompanyName property.

void CSignatureCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);
    PX_String(pPX, _T("CompanyName"), m_strCompany);
}

In the resource editor, add a company name edit box to the property page. Set the resource ID of the edit box to IDC_
EDIT_COMPANY. To associate the edit box with the CompanyName property, open the ClassWizard, click the Member Variables tab, select the property page class, highlight IDC_EDIT_COMPANY, and click the Add Variable button.
Fill out the dialog box as shown in Figure 9.

Figure 9: Add Memeber Variable Dialog Box

Host Services

A design-time control container can make certain functions, called host services, available to the controls it hosts. For example, Visual InterDev exposes an interface called IBuilderWizard Manager that gives your design-time controls access to several built-in dialog boxes such as the URL Picker and the Query Builder. It's beyond the scope of the article to explain these services in detail, but you can find out more information about them in the Design-Time Control SDK (available for download at http://www.microsoft.com/workshop/prog/sdk/dtctrl).

To give you an example, I added a Build URL button to the property page of the Web Page Signature control (see Figure 6) and created a simple class, CURLBuilder, that encapsulates the handling of the IBuilderWizardManager interface, making it really easy to employ the URL Picker dialog box provided by Visual InterDev (shown in Figure 10). However, when you insert the control using SimplEdit or the Design-Time ActiveX Control Pad, the Build URL button is disabled because those containers provide no host services.

Figure 10: URL Picker Dialog Box

Conclusion

Because of the simplicity and power of design-time ActiveX controls, I expect that they will soon become an integral part of many Web authoring tools. I hope this article has given you the information you need to get a head start. While there are many more Internet technologies to master, at least you have one less to worry about!