September 1996
Download MSJSEP.exe (46KB)
Don Box is a co-founder of DevelopMentor where he manages the COM curriculum. Don is currently breathing deep sighs of relief as his new book, Essential COM (Addison-Wesley), is finally complete. Don can be reached at http://www.develop.com/dbox/default.asp. |
Q
When users edit my OLE control's properties via its property page, the applied changes are reflected in the visual state of my control but not in the Visual Basic® property inspector. This leaves the state of my control inconsistent with the property inspector until I reselect it, which seems to force Visual Basic to re-read my properties. Is there a way to ensure that the property inspector is always consistent with my control?
A
The fact that your control's visual state is consistent with its property values indicates that you got the basics of state synchronization correct. Figure 1 shows the overall notification and request diagram for controls. As you can see, a control's state can be modeled in terms of internal and external states. The transient values of the control's data members are the control's internal state. The persistent image of the control on disk and the visual image on the user's display can be viewed as the external state of the control. The control implementor is typically focused on the internal state of the control, since this is its data member. It's manipulated via the control's methods, where control implementors spend most of their time adding features, bugs, and Easter eggs. It is important, however, to ensure that the internal and external states of the control are always in synch. Fortunately, the COleControl class makes this simple.
|
|
COleControl::InvalidateControl is logically equivalent to CWnd::InvalidateRect. However, unlike a simple CWnd, a control is drawn in its own window when the control is active, and is drawn by the control's container when the control is displayed in design mode. In addition to invalidating the control's window when it is active, the control must also notify any external clients that have established advisory connections with the control's rendering interfaces (IDataObject and IViewObject2). This is exactly what the COleControl's implementation of InvalidateControl does.
To indicate that the persistent state of the control is out of synch, call COleControl::SetModifiedFlag: |
|
COleControl::SetModifiedFlag is logically equivalent to CDocument::SetModifiedFlag. COleControl's implementation simply caches the indicated dirty state in a data member (m_bModified). This member is used to implement the various OLE persistence interfaces (IPersistStorage, IPersistStreamInit, and IPersistPropertyBag) that are exposed to the container, allowing it to save the control's state. No OLE notifications need to be sent because the act of synchronizing the control's persistent state only occurs when some external action takes place in the container (for example, selecting Save from the File menu).
The second operation required to maintain external state synchronization is to correctly implement the virtual function that resynchronizes the outside world's image of the control. To resynchronize the visual state of the control, the COleControl framework calls its virtual function OnDraw, giving the derived class a chance to render: |
|
OnDraw is called when either the control's HWND receives a WM_PAINT message or the OLE cache or out-of-process clients need metafile renderings. The control framework automatically routes all three of these requests through OnDraw.
To resynchronize the persistent state of the control, the COleControl framework calls its virtual function DoPropExchange, giving the derived class an opportunity to serialize its properties. |
|
This virtual function is called when the client calls Save or Load on any of the control's persistence interfaces. The control framework maps the requests onto a call to DoPropExchange automatically (often via the control's Serialize member).
The previous discussion (as well as your control implementation) ignored one other external state of the control: property pages and inspectors. At various times, a control may have one or more external agents displaying and possibly modifying the control's IDispatch-based properties. As you might expect, this external state also requires explicit notification of dirtiness, and the control framework maps requests for resynchronization onto a virtual member function. To notify all property viewers that a property has changed, call COleControl::BoundPropertyChanged. |
|
COleControl's implementation of BoundPropertyChanged will walk the list of connected property viewers and call the OnChanged method on each viewer's IPropertyNotifySink interface. When property viewers need to display a control's properties, they attach themselves to the control via its connection point for the IPropertyNotifySink interface. The COleControl base class implements this connection point by default.
When a property viewer receives the notification, it will call back into the control to refresh its state (see Figure 1). It does this via the control's primary IDispatch interface. As with any MFC-based IDispatch implementation, the control routes this request via the dispatch map to the appropriate data member or accessor function. Dispatch maps are usually manipulated by the Visual C++ ClassWizard or TextWizard. |
Figure 1 Control Notifications and Requests |
Given this explanation, should you call BoundPropertyChanged each time you modify a property? Well, maybe. You are only obliged to call BoundPropertyChanged for properties you have marked as bindable in your type library (using the bindable keyword in IDL or ODL). However, if you want the Visual Basic inspector to remain in sync when your control's property page applies its changes, you need to do this at least when the control is in design mode. You can detect this by calling COleControl::
AmbientUserMode.
|
|
This will yield exactly the behavior you are looking for in both design and user modes.
Be aware that while you will not be sending extraneous notifications at run time when the control is active, you will be calling the control site's IDispatch interface each time the property is set, which will not always be faster than sending the unneeded property notifications. A somewhat less elegant techniquethat relies on the fact that most containers run your control in-place activeis considerably faster: |
|
The m_bInPlaceActive data member is part of the undocumented protected interface to COleControl and, unlike the AmbientUserMode method, is not guaranteed to be supported in future versions of MFC.
|
Q
How do I get Visual Basic to use those drop-down lists and popup dialogs when editing my control via its built-in property editor?
A
The Visual Basic property editor is a generic browser that fills a subclassed list box with one entry per property. For an OLE control, it can determine the DISPIDs and names of the control's exposed properties by enumerating the type information associated with the control. By default, when a user selects a property for editing, Visual Basic allows the user to type raw text into an edit control. Once the user changes input focus or hits the Enter key, Visual Basic uses IDispatch::Invoke to communicate the new value to the selected object.
|
Figure 3 Browsing and Predefined Strings |
The MFC COleControl class implements the IPerPropertyBrowsing interface and maps each of the member functions to virtual functions with default implementations. To provide an array of strings to the Visual Basic browser, you can implement your own version of OnGetPredefinedStrings (see Figure 4).
Once the user selects an item from the list, the browser must map the selected item to a VARIANT that can update the property's value. It calls the control's GetPredefinedValue method, asking the control to map from the DWORD associated with the selected string onto the actual value represented as a VARIANT. The COleControl implementation simply routes this to a virtual member function that derived classes can override (see Figure 5). Implementing these two member functions forces Visual Basic to use a drop-down list for your control's property. Predefined strings are extremely useful for editing properties that are enumerations or restricted integral types and not string-based. For example, in the Bob control excerpted in Figure 3, you could represent the FirstName property as an integral type and expose an enumeration in your type library. |
|
This allows you to set the property in Visual Basic by using symbolic constants instead of strings.
|
|
Assuming that the FirstName property is of the enumeration type FIRSTNAMES, the implementation of OnGetPredefinedValue must return the correct enumeration value, not the string that it represents (see Figure 6).
While our implementation is in control of the cookies it receives (we will only see values that we had given out in OnGetPredefinedStrings), it is still necessary to guard against range errors for the property values passed in by programmers. Like the C programming languge, Visual Basic regards enumerations as suggested values, not mandatory ones. That means passing any integral value where an enumerated type is expected is completely legal in Visual Basic. To protect against illegal values, the following range check needs to take place in the mutator function that IDispatch uses to implement DISPATCH _PROPERTYPUT: |
|
Throwing a dispatch exception is the easiest way to indicate that an error occurred. You could also make an argument for indicating that a type error occurred by returning DISP_E_TYPEMISMATCH from the Invoke method. This would require intercepting the MFC implementation of Invoke, which is beyond what I'm willing to do for so little gain.
While you can implement just GetPredefinedValue and GetPredefinedStrings, you must implement GetDisplayString as well to achieve the correct look and feel. VisualBasic calls GetDisplayString to map the current value of a property to a string displayed on the right half of the list-box entry. COleControl implements this function by calling the virtual function OnGetDisplayString, giving derived classes a chance to map their custom properties. For the Bob control, the implementation shown in Figure 7 would suffice. In the absence of this function, Visual Basic displays the integer value returned from the control's PROPERTYGET implementation. For properties that are too complex to display in a simple drop-down list, the OLE control specification allows controls to map arbitrarily complex property pages onto individual properties. Browsers are expected to display these pages whenever the user attempts to edit the property. The Visual Basic browser displays an ellipsis button to indicate that the property has a specific property page (see Figure 8). To discover whether or not a property has a specific property page associated with it, browsers call the control's MapPropertyToPage method: |
|
If the control returns S_OK from the call, then the CLSID parameter indicates which property page can optionally edit this property. If the control returns S_FALSE, then the CLSID parameter indicates which property page must edit the property. All other results indicate that the control has no property-specific page to edit the requested property. |
Figure 8 Browsing with Per-Property Pages |
The COleControl class implements this interface method by calling the virtual function OnMapPropertyToPage, allowing derived classes to specify their own property pages. The default im-
plementation of this virtual function maps the stock font and color properties to the built-in
font and color property pages. To map a control's property to a specific page, just override
OnMapPropertyToPage (see Figure 9).
Based on this implementation, when the user selects the LastName property in the Visual Basic property browser, the ellipsis button will be shown. When clicked, the button invokes a property sheet with only the page indicated in the code above. This is somewhat different from the behavior of the Visual C++ generic property browser (see Figure 10), which is implemented as one property page in the dialog editor's property sheet. This also contains the dialog's property page in addition to all property pages in the selected control's property page map. When attempting to edit a property with a custom property page, Visual C++ simply jumps to the indicated property page within the same sheet. This implies that the custom property page must be part of the control's property page map, which is not required by Visual Basic. |
Figure 10 Property Browsing in Visual C++ |
So, as always, just clicking some buttons and selecting a few options in AppWizard and ClassWizard produces some code that compiles, links, and even works, but the resulting binary is less than satisfying for the user. To achieve a professional-quality fit and finish on the control, you have to scrape under the surface of MFC's COleControl class to hook into the underlying COM-based infrastructure. Fortunately, the IPerPropertyBrowsing interface is sufficiently hookable and requires that only a few virtual functions be implemented in the derived class.
|
Have a question about programming with ActiveX or COM? Send your questions via email to Don Box at dbox@develop.com or http://www.develop.com/dbox/default.asp |
From the September 1996 issue of Microsoft Systems Journal.