Undocumented Corner

George and Scot look inside OLE control property pages so you can see what's happening behind the scenes. They also present a workaround for adding parameterized properties to an OLE control.


August 01, 1996
URL:http://www.drdobbs.com/web-development/undocumented-corner/184409946

August 1996: Undocumented Corner

Inside OLE Control Property Pages

George Shepherd and Scot Wingo

George is a senior computer scientist with DevelopMentor and can be contacted at [email protected]. Scot is a cofounder of Stingray Software. He can be contacted at [email protected]. They are the coauthors of MFC Internals (Addison- Wesley, 1996).


OLE controls live at the top of the OLE food chain. They implement all the basic OLE technologies including Automation, OLE Document Inplaceness, and OLE connections. If you haven't developed an OLE control yet, give it a try. Thanks to Visual C++ and ClassWizard, the process is relatively painless. Just start a control project, decide what to draw, add some events, and you've got a control. (It's actually a bit more complicated than that, but it's not nearly as tedious as implementing all the interfaces yourself.)

Because OLE controls are Automation objects, they expose properties via IDispatch. That means their clients can access the control's properties. In addition to exposing Automation properties in the normal way, OLE controls let the client change the properties through some tabbed dialog boxes that let the user access the control's properties. OLE property pages are dialog boxes that frame one or more tabbed dialog boxes called "property sheets." Programming an OLE control's property page is fairly easyespecially when using VC++'s ClassWizard. However, if you simply follow Microsoft's instructions, you'll miss some of the beauty of OLE controls and a chance to see OLE in action as a real integration technology.

This month, we'll look inside OLE-control property pages so you can see what's happening behind the scenes. Also, we'll present a work-around for adding parameterized properties to an OLE control.

Property Pages in a Nutshell

OLE controls implement property pages so that clients can modify a control's properties without having to implement any user-interface code. For example, if property pages didn't exist, anyone wanting to write a client to manipulate your control's properties would have to spend a lot of time rigging up dialog boxes and so forth. OLE controls are supposed to be self-contained packages of software. Why should an OLE-control client have to implement a lot of user-interface code to set up a control's properties? The control knows all about its own properties, so it makes sense to let the control be responsible for providing some user interface for accessing properties. OLE controls provide this user interface through a verb.

Because OLE controls are OLE document-content objects, they are able to implement verbsone of them being the properties verb. OLE controls usually react to the properties verb by displaying a dialog box with tabs for manipulating a control's various properties. Figure 1 is Microsoft's Grid control (inserted in the test container provided with VC++). The figure shows how Microsoft's grid control responds to the properties verb by displaying this type of tabbed dialog box.

Before dissecting properties, take a closer look at Figure 1. There are seven separate OLE objects showing. The OLE control container (the test container) implements some OLE interfaces, so it's an OLE object. There's the grid control itself. The property frame (the dialog-box housing the tabbed-dialog boxes) is an OLE object. Finally, each of the four tabs in the dialog box is a separate OLE object. This is a great illustration of OLE as an integration technology.

Notice how the test container, grid control, property frame, and properties dialog box appear to work as a single, cohesive unit. If you didn't know better, you might think this was all happening within a single .EXE file (as in the old, pre-OLE days). But it's not! The test container is one .EXE file. Of course, the grid control resides inside a .DLL. The dialog box tab that's showing (the General tab) also resides inside the control's .DLL. Finally, the objects behind the other three tabs (the Pictures, Fonts, and Colors tabs) come from MFC40.DLL, which comes with VC++. Smooth, seamless, and standard integration of three separate binary files: This is one of the main goals of OLE.

Programming the Property Page

Working with OLE control property pages in VC++ is easy, and involves three steps:

1. Defining the OLE control property.

2. Setting up a dialog box template for the property page.

3. Linking the properties between the dialog box and the control itself.

If you're using VC++, it's no problem to define a property within the OLE controljust use ClassWizard to add the Automation property as you normally would. Make sure you're looking at the automation tab, press the properties button, and add the property. It's important to remember the external name because it's critical for implementing the property page. For example, if you're writing an OLE control for monitoring a real-time data source, you may want to have the control beep whenever the data changes. Furthermore, you may want to provide a way for turning the sound on and off. The most straightforward way to do this is to add an Automation property called "Sound" to the control.

The second step is to set up the dialog-box template for the property page. Each property page is really just another dialog box. In fact, the ControlWizard generates a blank default property page whenever you create the control: You can use this dialog box or create a new one. Add a control to the dialog box for manipulating the OLE control's property. Using the Sound property, you'd probably use a check box in the dialog box. So far, so good.

Once there's a check box in the dialog box, you need to link the state of the check box (on or off) to the OLE control's property. Again, ClassWizard makes this almost trivial. Use ClassWizard to add to the dialog box a member variable representing the check box. Do this by clicking on the Member Variables tab and pressing the Add Variable button. ClassWizard then asks you for some information about the variable using the dialog box shown in Figure 2.

This adds a member variable representing the check box to your property page dialog. In addition, ClassWizard sets up MFC's dialog data exchange mechanism (DDX/DDV), which exchanges the state of the check box (checked or unchecked) and the member variable (m_bSound).

Also notice the Optional OLE property-name field, which contains the string "Sound." The OLE control's Sound property (the regular Automation property) is called "Sound." If you guessed that the OLE control's Sound property and the state of the check box in the property page are linked via Automation, you're right. Remember that the external name of the control's Sound property is Sound. In effect, the property page becomes an Automation client to the control, driving the control's IDispatch interface. Here's how the properties verb works.

Inside the Properties Verb

When an OLE control is embedded within some sort of container, the container usually has some means of invoking the control's verbs. For example, if you've added a control to Microsoft's test container, you can get to the verbs menu by moving the mouse cursor over the control's border and pressing the right-mouse button. This gives you one of those tracking menus. If the control supports property pages, you'll see the properties verb in that menu. Selecting the properties verb tells the inplace item to invoke its properties verb. (The OLE control is an embedded item that exposes the IOleObject interface. The container uses IOleObject to invoke verbs by calling IOleObject::DoVerb()). Inside the control, the framework routes the container's IOleObject::DoVerb() call to COleControl::OnDoVerb(), which ends up in COleControl::OnProperties().

COleControl::OnProperties() works in the following way. The control's properties are linked via IDispatch. Each property page is a separate OLE object that will communicate with the OLE control through the control's main IDispatch pointer. That means the control has to hand its IDispatch pointer over to the property pages so the property pages can use it to manipulate the control's properties. COleControl::OnProperties() retrieves the OLE control's main IDispatch pointer (in this case, the one through which the client can access the Sound property). Because COleControl is ultimately derived from CCmdTarget, the control can extract its IDispatch pointer using CCmdTarget::GetIDispatch(). The OLE control will use this dispatch pointer to create the property pages.

Since these property pages are separate OLE objects, they will be created via the normal OLE object-creation method, by calling CoCreateInstance(), which uses an OLE object's GUID to create the OLE object. Of course, this means an OLE control needs to know the GUIDs of all the property pages it wants to show.

In keeping with tradition, MFC uses macros to manage the control's list of GUIDs representing property pages. These macros are called DECLARE_PROPPAGEIDS, BEGIN_PROPPAGEIDS, END_ PROPPAGEIDS, and PROPPAGEID. You may have seen them in some OLE-control source code. For the property page list of a control, DECLARE_PROPPAGEIDS, see Example 1(a), goes inside the control's header file. BEGIN_PROPPAGEIDS, PROPPAGEID, and END_PROPPAGEIDS go inside the control's source-code file; see Example 1(b).

The macros simply expand to generate a static array of GUIDs. DECLARE_PROPPAGEIDS declares an array of GUIDS (representing property pages) and a member function so the framework can retrieve the list of GUIDs when necessary. BEGIN_

PROPPAGEIDS, PROPPAGEID, and END_

PROPPAGEIDS expand to implement the array of GUIDs.

After retrieving the OLE control's IDispatch, COleControl::OnProperties() calls the OLE API function OleCreatePropertyFrame() to create the dialog box that holds the property pages. COleControl::OnProperties() uses the OLE control's IDispatch pointer and list of GUIDs to create the dialog frame. OleCreatePropertyFrame() creates a modal dialog box that houses the tabbed property pages. Once you call it, OLE takes over and creates the property pages. Basically, OleCreatePropertyFrame() just calls CoCreateInstance() for each GUID in the list, setting up each property page in turn.

To see the rest of the property-page story, let's jump to the other side of the fence and see how MFC implements OLE property pages. The class is COlePropertyPage.

Inside COlePropertyPage

In MFC, COlePropertyPage is the class used for handling OLE property pages. It's derived from CDialog, so it does everything a CDialog object does. In addition to regular dialog-box support, COlePropertyPage implements an interface called IPropertyPage2. When you pull up an OLE control's property pages, the tabbed dialogs are placed within a dialog frame. The dialog frame uses each property page's IPropertyPage2 interface to communicate with the property page.

After the property-page client (in this case, the OLE control) calls OleCreatePropertyPage(), OLE starts calling CoCreateInstance() for each property sheet being managed by the OLE control. Provided CoCreateInstance() succeeds, control is handed over to the COlePropertyPage's constructor. After the COlePropertyPage is constructed, OleCreatePropertyFrame() calls QueryInterface() on the property page's IUnknown to get its IPropertyPage2 interface. Once OLE has the IPropertyPage2 interface, OLE uses the interface to setup the property page.

Remember that the OLE control implements IDispatch so it can expose its properties. That means any client code trying to access the OLE control's properties has to have a copy of the OLE control's IDispatch property. In other words, the OLE property page needs the OLE control's IDispatch pointer. The property page gets the control's IDispatch pointer in the following manner: IPropertyPage2 has a function named SetObjects(). IPropertyPage2::SetObjects() takes an array of IUnknown pointers. In the case of a regular, MFC-based OLE control, OleCreatePropertyFrame() passes the control's main IDispatch pointer to the property page. The OLE property page saves the IDispatch pointer for later use (for example, when the property page wants to access the OLE control's property).

Notice that the property frame and IPropertyPage2::SetObjects() both deal with IUnknown pointers. That means that there's quite a bit of flexibility here. While IDispatch is the most logical way for a control to deal with properties, you're not limited to using IDispatch. Those IUnknown pointers could represent your own custom interface. While MFC uses IDispatch, there's no reason you couldn't write your own property-page class. However, note that the control and the property page both have to agree on the type of interface to use.

Accessing the Properties

Once the OLE property page has the OLE control's IDispatch pointer, the property page can easily access the control's properties. MFC extends the DDX mechanism to handle OLE control property pages.

Using the previous example, the default property page performs this magic in the DoDataExchange() function. Example 2 is the code that the Wizard inserts. The two lines of code between the comments perform the data exchange between the dialog box's control and the dialog box's member variable. MFC provides a number of property-exchange functions. Table 1 lists MFC's property-page exchange functions.

COlePropertyPage implements several member functions for performing the actual work: GetPropText()/SetPropText(), GetPropCheck()/SetPropCheck(), GetPropRadio()/SetPropRadio(), and GetPropIndex()/SetPropIndex(). They all work more or less the same way, except they exchange different types of data. As an example, take a look at how a COlePropertyPage exchanges data between a check box and an Automation property.

Inside GetPropCheck() and SetPropCheck()

To get the data from the property page to the OLE control, GetPropCheck() simply has to use the OLE control's main IDispatch pointer to set the OLE control's property. COlePropertyPage maintains an array of IDispatch pointers. This array was filled with pointers when the property page was created (when the OLE control called OleCreatePropertyFrame(), which used IPropertyPage2::SetObjects()). As the first order of business, GetPropCheck() declares a COleDispatchDriver object on the stack. COleDispatchDriver is MFC's wrapper for IDispatch and it has helper functions for using an IDispatch interface.

GetPropCheck() then goes through each IDispatch pointer in the array, trying to access the Automation property by name. GetPropCheck() does this by calling IDispatch::GetIDsOfNames() to get the property's DISPID. Once GetPropCheck() has the DISPID, GetPropCheck() attaches the IDispatch pointer to the COleDispatchDriver object on the stack and calls COleDispatchDriver::GetProperty() to retrieve the control's Automation properties.

Inside, SetPropCheck() works almost identically, except that the function sets the property in the OLE control instead of just reading the property.

Implementing Parameterized Properties

For the most part, you can just go on blissfully using ClassWizard to do all your OLE control property sheets. However, there may be one or two times that you want to bend the rules a bit. MFC's property-exchange mechanism is well integrated into VC++'s wizard technology, making it awfully convenient to develop property pages. However, ClassWizard's support falls apart when the properties are parameterized.

When adding an Automation property using ClassWizard, you can include parameters with that property. Imagine writing an OLE control that manages several data channels. Each channel might have properties like protocol and baud rate associated with it. In such a case, you would probably like the user to be able to examine all the properties for one channel at a time. One way to accomplish this is to parameterize the properties. For example, to add a "protocol" property parameterized by channel, just bring up ClassWizard, add the property as a Get/Set property, and add channel to the list of parameters. Figure 3 illustrates this.

That's simple enough. Now the OLE control has a protocol property that can be selected for a certain channel. The trouble starts when you try to link the protocol property to the OLE control's property sheet. ClassWizard only lets you hook up a single variable to the protocol property in the property sheet. In other words, ClassWizard doesn't leave any room for that channel parameter, and adding the channel parameter to the C++ code by hand breaks ClassWizard. Fortunately, now that you know that the MFC-based property sheets are really just IDispatch clients, you can add the parameterized property to the property sheet.

Remember that a standard OLE control is really just another .COM object that happens to implement IDispatch. That is, it has its own set of Automation methods and properties. In addition, VC++ generates an ODL file that compiles to a type library as part of the control project. You can use the type library to generate the special IDispatch client code for the property page.

Once your dispinterface is set (that is, you have settled on all your methods and properties), you can use ClassWizard to generate a class from the control's type library. Doing this yields a class derived from COleDispatchDriver, that has member functions that map directly to the methods and properties of the control's dispatch interface. Once you have the IDispatch client class, you can use the Get/Set methods for the property to access the control's Automation properties.

For instance, Listing One retrieves the hypothetical control's protocol property on a specific channel. This code is actually inspired by MFC's COlePropertyPage code used during property exchanges. It's pretty straightforward. PropDispDriver is the class generated by ClassWizardit maps to the control's main dispatch interface. The ppDisp variable represents the array of IDispatch pointers held by the property frame. There's usually only one; however, it's possible for a property frame to hold several IDispatch pointers. The function simply goes through the list of IDispatch pointers, attaching each to the PropDispDriver in turn, and executing the property accessor.

Of course, there would also be a complementary SetProtocol() function. You can use these functions whenever you need to access the control's property (perhaps during DoDataExchange()). This is basically the same thing that the regular MFC property-exchange mechanism does anyway. Using this technique, you can bypass the normal OLE control property-exchange mechanism if you ever have to bend the MFC rules (implementing parameterized properties, for example).

Property Page Wrap Up

Property pages are a convenient way for users to access various attributes of an OLE object. Property pages are standard fare for OLE controls and come more or less for free if you use the ControlWizard and MFC to create your controls.

OLE controls expose their properties via IDispatch. Whenever an OLE control client wants to use a control's property page, the client just invokes the properties verb. As a result, the OLE control uses the OLE API function OleCreatePropertyFrame() to create a dialog box that holds a set of tabbed dialog boxes representing the control's various properties. The OLE control passes its main IDispatch pointer when calling OleCreatePropertyFrame(), which passes the IDispatch pointer on to the property page. Each property page caches the OLE control's IDispatch pointer and uses the pointer to access the OLE control's properties.

Property pages are a great example of how MFC works to smooth out the edges between .EXE and .DLL modules. As you can see, OLE property pages can be a bit complex in some places. Thankfully, MFC takes care of most of the details. However, you can always bend the rules a bit, as long as you're careful and know where to look.

Example 1: (a) Property-page list for a control; (b) going inside the control's source-code file.

(a)

DECLARE_PROPPAGEIDS(CStocksCtrl)

(b)

BEGIN_PROPPAGEIDS(CStocksCtrl, 1)
   PROPPAGEID(CStocksCtrlPropPage::guid)
END_PROPPAGEIDS(CStocksCtrl)

Example 2: Code inserted by the Wizard.

void CStocksCtrlPropPage::DoDataExchange
(CDataExchange* pDX)
{
   //{{AFX_DATA_MAP(CStocksCtrlPropPage)
   DDP_Check(pDX, IDC_SOUND, m_bSound, _T("Sound") );
   DDX_Check(pDX, IDC_SOUND, m_bSound);
   //}}AFX_DATA_MAP
   DDP_PostProcessing(pDX);
}

Figure 1: Microsoft's Grid control's properties.

Figure 2: Adding a member variable to the property page dialog box.

Figure 3: Adding a parameterized Automation property using ClassWizard.

Table 1: MFC's property-page exchange functions.

Function Name         Links this Kind of Dialog Control to a Property

DDP_CBIndex           The selected string's index in a combo box.

DDP_CBString          The selected string in a combo box (without an exact match).

DDP_CBStringExact     The selected string in a combo box (exact match).

DDP_Check             A check box.

DDP_LBIndex           The selected string's index in a list box.

DDP_LBString          The selected string in a list box (without an exact match).

DDP_LBStringExact     The selected string in a list box (with an exact match).

DDP_Radio             A radio button.

DDP_Text              Text.

Listing One



BOOL CParmpropPropPage::GetProtocol(int nChannel)

{
   _DParmprop PropDispDriver;
   BOOL bSuccess = FALSE;

   LPDISPATCH* ppDisp;
   ULONG nObjects;

   ppDisp = GetObjectArray(&nObjects);

   for (ULONG i = 0; i < nObjects; i++) {
      PropDispDriver.AttachDispatch(ppDisp[i], FALSE);
      m_strProtocols[nChannel] =
         PropDispDriver.GetProtocol(nChannel);
      PropDispDriver.DetachDispatch();
      bSuccess = TRUE;
   }
   return bSuccess;
}

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.