Desaware Home
Products    Purchase    Publishing    Articles   Support    Company    Contact    
Articles
.NET
COM

 

 

bluebar
Contact Desaware and order today

bluebar
Sign up for Desaware's Newsletter for the latest news and tech tips.

The Object that came in from the Code

by Daniel Appleman
Copyright © 1997 by Daniel Appleman. All Rights Reserved.

(The following story is fiction, except for the technical information which is factual. Names and places remain more or less unchanged, but the facts have been distorted to protect the guilty).

For once the long range forecasts were right. El-Nino. Every other day a new storm. With the Silicon Valley types busy filling sandbags to keep their million dollar estates from sliding into the Bay, business was slow. Not that being a high tech private investigator was such great shakes anyway. Now on the other hand, if I was a D.C. special prosecutor.. but I digress.

The Email message arrived at 1:00AM, the middle of my working day. I'd been watching the plausibly live coverage of the Nagano winter Olympics that ended last week. My machine announced "you have mail" in a distorted, yet sultry tone. I could tell from the static that the message was from far away. I gave it a quick read and knew right away that I was in for some overtime.

He wanted to do sub-objects in VB authored controls - and price was no object.

What's a sub-object, you ask?

Simple. Open VB5 and create a blank UserControl. Then use the ActiveX control interface wizard to add a single "Font" property (clear the others). Map that property to the UserControl.  The code will look like this:

Option Explicit 
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MappingInfo=UserControl,UserControl,-1,Font
Public Property Get Font() As Font
   Set Font = UserControl.Font
End Property
Public Property Set Font(ByVal New_Font As Font)
   Set UserControl.Font = New_Font
   PropertyChanged "Font"
End Property
'Initialize Properties for User Control
Private Sub UserControl_InitProperties()
   Set Font = Ambient.Font
End Sub
'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
   Set Font = PropBag.ReadProperty("Font", Ambient.Font)
End Sub
'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
   Call PropBag.WriteProperty("Font", Font, Ambient.Font)
End Sub
      

Now drop this control on a blank form. The Font property for the control will appear in the VB property window and will be associated with a special font property page. Now save the form and open the .FRM file using a text editor. You'll see the following listing:

VERSION 5.00
Begin VB.Form Form1
   Caption         =   "Form1"
   ClientHeight    =   3195
   ClientLeft      =   60
   ClientTop       =   345
   ClientWidth     =   4680
   LinkTopic       =   "Form1"
   ScaleHeight     =   3195
   ScaleWidth      =   4680
   StartUpPosition =   3  'Windows Default
   Begin Project1.UserControl1 UserControl11
      Height          =   645
      Left            =   1170
      TabIndex        =   0
      Top             =   720
      Width           =   915
      _ExtentX        =   1614
      _ExtentY        =   1138
      BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851}
         Name            =   "MS Sans Serif"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
   End
End
Option Explicit

Pretty interesting. The Font object has a whole bunch of fields such as the name of the font, its size and characteristics. But you don't have to save them individually in the SaveProperties event. That's because the Font object is smart - it knows how to save itself in a single operation. When you look at the listing, the Font characteristics appear as their own section bracketed by BeginProperty and EndProperty.

It's one of the coolest things around for controls. Fonts use it. Pictures use it. Constituent controls use it. The only thing is - you can't use it. Sure, you can create objects using class modules both in your control and in a separate DLL. You can rig things so they can be edited with a property page. But you can't make them save themselves into a property bag in a single operation. You can't make a VB object self-persisting. It's a features that Microsoft left out of VB.

I checked Appleman's book on ActiveX development (Dan Appleman's Developing ActiveX Components with Visual Basic 5.0: ISBN 1-56276-510-8) to see if he had any insights into the problem. No joy. He did show a way to pass the property bag to a class and let it save a bunch of properties with the control name attached, but the result wasn't clean. He promised in the book to look into it further, but there was nothing on his web site.

I stared out my rain streaked window. Where to begin? Then I saw the path to a solution hidden in the Email message itself. Price was no object. I got on the phone. Twenty four hours later I was on my way to Japan.

The Answer lies in the East

Look, it wasn't just a matter of escaping the storms. After all, Japan this time of year can be even colder and wetter than California. And it wasn't just that my client was in Japan. And it wasn't just the unlimited budget. Those factors made up, oh, no more than 95% of the reason for going.

The important reasons have to do with precision. You see, in order to figure out a way to create self persisting objects, I knew I'd have to go deep under cover. I'd have to dig under the pristine surface of Visual Basic and wallow in the gutters of its internal operation. I'd have to deal with the Component Object Model (COM) and interfaces. And interfaces are about precision - about specifying methods and properties and their parameters so precisely that you and everyone else in the world know exactly how they will work - the first time and every time.

Have you ever heard of the Japanese Shinkansen, the "bullet trains"? These trains travel four or five hundred miles per hour, are spaced three minutes apart, and if you buy a ticket for a train leaving at 8:53:04 PM from Tokyo station, you can be sure it will leave within half a second of that time, even if it has to decapitate a person at the door in order to do so.

Any people who can run a train system like that knows about precision.

The morning after I arrived I began my search in the streets of Akhiabara. Akhiabara - the closest place to heaven on earth for a Silicon Valley techie. You have thousands of tiny stores, each of which specializes in a particular high tech product. You want a 14.4 modem with a built in hair dryer? You'll find a store that specializes in it. And there are the department stores - 20 feet wide by 40 feet long by 50 stories high - and each floor specializes in a different technology. I went directly to the 35th floor of the COM store which specialized in storage interfaces. An elderly gentleman in the corner specialized in VB internals. He didn't speak a word of English. We understood each other perfectly.

When it comes to saving a form's properties into a .FRM file, you're actually dealing with three different objects: Visual Basic (the container), the form and any controls on the form and objects associated with properties, and the form file itself. Visual Basic asks the form to save its properties, and passes it a reference to the form file object. The form then saves all of the properties, and requests that each control save its own properties into the form file.

At this point, a VB authored control will have its SaveProperties event triggered. The form object is passed to the control in the form of a property bag object. The control can use the WriteProperties method of the property bag object to save its properties and sub-objects.

I knew how the job was committed. But I needed to look at it in more detail. The objects were my prime suspects - and I needed to understand them thoroughly to see exactly what role each one played in the crime.

The Container:

The Visual Basic form is an OCX container - it has the ability to host ActiveX controls and ask them to save its properties. You don't have to worry right now about communicating with the container - all it's doing is holding references to the form file and your control and telling the control when it should save its properties to the form.

The Form File:

The container needs to store the properties somewhere. When you're saving a form to disk, it goes in a form file. The part of the file with a .frm extension contains a text description of the properties. Any properties that can't be stored as text are saved as binary data in a file with the extension .frx. Other components may use other extensions (such as .ctl and .ctx for controls), but the principles are the same for each. Your VB authored control sees this file as a PropertyBag object, but this is actually a high level object that hides quite a bit of work from you. Underneath the file is represented as an object that implements an interface named IPropertyBag.

The Control:

The control has an event called SaveProperties that is raised when its time to save the control's properties. It's operation is fairly clear except for one mystery. When you call the PropBag.WriteProperty method for an sub-object such as a Font or Picture object, somehow the property bag object knows how to save that object. If you try to call it for an object created with a class module, you get an error. Which brings us to.

The Sub-Objects:

What makes a Font or Picture object different from an object that you create? The trick is both subtle and simple at the same time. You know that the main interface for a Font object contains properties that allow you to access information about a font. What you may not know is that the Font object has additional interfaces. COM components can support as many interfaces as you wish, and the Font object supports at least three other interfaces. The one we're concerned with right now is called IPersistPropertyBag. When an object implements this interface it effectively tells the world that it is able to save its properties into a property bag.

The Caper:

The old man's story began to make sense. I could see everything falling into place as if it were a conversation..

Visual Basic:

"My developer wants to save a form. I've opened a text file and created an object that implements the IPropertyBag interface. Anyone calling the Write method of that interface for a property will have it converted into text if possible, and the text saved into the form file. Binary data will be sent to the .frx file. But wait! It's Visual Basic and since the IPropertyBag interface is not directly compatible with Visual Basic, I'll create a special PropertyBag object that has a WriteProperty method that can be called from VB. Now you - control - it's time for you to save your properties - or else!  Take this PropertyBag object and write your properties into it or I'll plug you full of lead!"

Control:

"Ok, anything you say. Here's my first property (it's written). And here's a Font object - I don't know how to save its properties. Please don't kill me!"

Visual Basic:

"Hmm - an uncooperative object. Let's check it out. Hey you - Font object. I've got a QueryInterface operation here that wants to know if you can talk to me. Do you support the IPersistPropertyBag interface?"

Font object:

"Yep, here's a pointer to my IPersistPropertyBag interface. Do your worst!"

Visual Basic:

"I'm going to call the Save method on your IPersistPropertyBag interface. And one of the parameters is going to be a pointer to my IPropertyBag interface! How does that grab you."

Font object:

"Well, I'm going to call the Write method on your IPropertyBag interface for each and every one of my properties. So you can save them as a sub-object."

Visual Basic:

"Hey control - you did well. I was able to talk to the Font object and have it save all of its properties. Do you have anything else for me?"

Control:

"No sir - all properties are saved. Now leave me alone".
The old man scrawled out a diagram showing the object hierarchy and the interfaces belonging to each object (see figure 1). I suddenly had this sinking feeling in my stomach. The old man smiled and pointed to the door around the corner. I told him no - it's not that - and pointed at the extra interfaces. IStream? IPersistStream?

He pointed at the form file and I began to get it.

A property bag is designed to save properties in text form so that they can be easily understood by human beings and edited with a text editor. That's great when your saving properties to a form file, but there are many cases when a control has to save properties without saving to a file. For example: every time you run a program in the VB environment, each of your controls is destroyed and recreated - this time in run mode. The design time control has to save its properties before it is destroyed so that they can be loaded by the newly created runtime control. You can see that it would be pretty wasteful to save the properties to a disk file in this case. In fact, it's also wasteful to convert the property values to text. So Visual Basic uses a different approach in this situation. It stores the properties into a block of memory that is formatted internally using OLE Structured Storage - a technology for building very complex documents out of individual streams of data. (Most Visual Basic programmers never use structured storage directly because it's nearly impossible to use it directly from VB, but Desaware's StorageTools product does allow you to easily create and manipulate these types of files from VB).

The block of memory is referenced using an object that implements an interface called IStream that has methods to read and write binary data. Where VB used the IPropertyBag interface to write data to a form file, it uses the IStream interface to write to the memory stream. Just as an object implements the IPersistPropertyBag interface to indicate that it can save properties to a form file, it implements the IPersistStream and IPersistStreamInit interfaces to indicate that it can save properties to a memory stream. This mechanism is hidden from you when you write a VB control by the PropertyBag object. As you can see in figure 2, the PropertyBag object's WriteProperty method can save data to either a property bag or a stream depending on where Visual Basic is storing data.

Getting Loaded

I could now see how the properties could be stashed, but just as a thief plans to recover his loot after the heat dies down, saving properties is useless if VB can't load them back.

At first glance it looked obvious - When VB loads a form it can simply go to each control and sub-object and ask it to load properties from either the form file or the memory stream. But is a catch. How can a form go to each control and sub-object and tell it to load its properties when the controls and sub objects don't exist? A clean form has no controls when it starts loading! I pointed this out to the old man. He waved again at the door on the right. I again declined, and he started laughing madly. I could see it was all a scheme to make me think I had the solution. I turned my back in frustration, feeling the trail grow cold. Just as I left I heard him shout out the word "Bunka Orient". I turned to ask him what he meant, but he had vanished.

I rode the escalators down to ground level. It was getting dusk. I went to the nearest subway station and learned firsthand how the Japanese fit 400 people onto a subway car designed for 50. It has something to do with opening an interdimensional portal to another universe. but I digress.

I got out at Tokyo station, not clear what my next step would be. Suddenly I saw a trail of stickers that were being put up by a group of giggling high school girls. I looked at them and said "Bunka Orient?". They giggled louder and sang out "Sendai". It was the clue I was looking for.

I got on the Shinkhansen heading towards Sendai, a city about 2 hours North of Tokyo. It was getting late and I hadn't eaten, so I grabbed a box lunch. Actually, it was a lunch of boxes - thirty five bite size courses, each in its own perfectly wrapped box. I didn't know what most of them were, and didn't care. The secret to eating well in Japan is to never ask what it is you're eating. It tastes good and the survival rate of American tourists is at least 50% - more than that you don't need to know.

I got off the train and found myself right in the middle of the strangest snow storm I'd ever seen. Snow clumps the size of 10,000 yen melons were pummeling me from all sides. I hadn't gone more then ten feet when one of them knocked me out.

I woke up and found myself surrounded by a dozen little kids from the Meisen Friends Academy who presented me with valentine cards. I couldn't figure out if I was awake or hallucinating. I opened one of the cards. It showed the following:

Begin Project1.UserControl1 UserControl11
BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851}
And have a Happy Valentines Day

It was the answer I was looking for. When Visual Basic reads a form file, it sees two types of statements that indicate the start of a set of properties for an object. The Begin statement is followed by the programmatic name of an object such as an ActiveX control. The BeginProperty statement is followed by the name of the object along with a GUID (globally unique identifier) for the object. Visual Basic can use the programmatic name of a control or the GUID of an object to search the registry for information on which executable program or dynamic link library supports that control or object. It can then load the program or DLL and ask it to create an instance of the specified object. Once created, VB can perform any necessary initialization including asking the control or object for its IPersistPropertyBag or IPersistStream interfaces. It can then have the control or object load its properties from the property bag or memory stream!

All of the pieces finally fell into place. I could say how a control or sub-object could be responsible for saving and loading its own properties or internal data. The only thing I couldn't see is how you could create a sub-object like the Font object using Visual Basic.
Near as I could tell, such an object would have to do the following:

  • Implement the IPersistPropertyBag interface.
  • Implement the IPersistStream interface.
  • Implement the IPersistStreamInit interface.
  • Be able to call the methods of the IPropertyBag interface on an object provided by VB.
  • Be able to call the methods of the IStream interface on an object provided by VB.
  • Be implemented in its own DLL as a public object.

Why the last? Because when a control loads a sub-object, all it has to start with is the GUID of the object. This GUID must be stored in the system registry in order for VB the be able to find the EXE or DLL that knows how to create and support the object. This means that the object must be public - object created using private class modules are not recorded in the system registry. Since ActiveX control projects in Visual Basic cannot support public objects, the object must clearly be stored in a separate DLL project.

But what about the first five problems? All of the interfaces in question are either hidden, or are not compatible with Visual Basic. It was theoretically possible that I could create some type libraries or modify my system registry to let me use them from VB, but I hate tampering with my registry - there's too great a risk of screwing things up.

The kids had remained quiet as I thought this out. They now looked at me expectantly. On a lark I asked them: "you wouldn't happen to know how to implement and call arbitrary interfaces using VB"? I was blown over when they chanted in unison "Ask Mr. Powerman", and pointed at a long steep hill.

Climb Every Mountain

It was a long climb to the top. I found myself in front of a classic Japanese temple surrounded by a mote full of Koi fish. Naturally I fed the fish - like any tourist I found it nearly irresistible. I had to shake myself back to attention. I entered the sanctuary and saw him. Mr. Powerman himself. He sat there with his Burmese harp, a picture of oriental serenity. I tried asking him about the sub-object dilemma, but he just waved me over to sit down beside him. For the next three hours he patiently taught me to play the harp, as I impatiently tried to get an answer to my problem (See figure 3). It was especially trying because two dozen supplicants were standing around us shooting literally thousands of pictures of me struggling to play the awkward instrument.

After I finally stumbled through a perfect harp rendition of Rachmaninoff's Third, he stood up, bowed, and indicated that the session was over. I shook my head as I left, but before I stepped outside he whistled. I turned and he silently mouthed one word. The insight shattered my mind like a flash bulb (or was it yet another flash bulb shattering my mind like an insight?). Either way, I had my answer.

I made my way back to Tokyo and met up with some friends from Shoeisha. I presented my results to the client over an authentic Mexican dinner. I don't know what pleased them more, the answer or the meal.

The Mystery Solved

The word Mr. Powerman had shared with me was SpyWorks. I should have known. The latest version (5.1) includes the ability to both implement and call any interface regardless of whether it's compatible with Visual Basic or not. The complete sample code can be found at ftp.desaware.com/SampleCode/Articles/subobject.zip. It should prove interesting to anyone, though only people with SpyWorks professional will actually be able to run it. The same principles can be applied by VC++ programmers using either ATL or MFC.

The following partial listing of the ControlSubObjects.vbp control shows how the control can handle the new "ControlSubObject" object as easily as a font object.

' Control that demonstrates use of sub-objects
' Copyright © 1998 by Desaware Inc. All Rights Reserved
Option Explicit
'Property Variables:
Dim m_SubObject As ControlSubObject
'WARNING! DO NOT REMOVE OR MODIFY THE FOLLOWING COMMENTED LINES!
'MappingInfo=UserControl,UserControl,-1,Font
Public Property Get Font() As Font
   Set Font = UserControl.Font
End Property

Public Property Set Font(ByVal New_Font As Font)
   Set UserControl.Font = New_Font
   PropertyChanged "Font"
End Property

' The SubObject property uses a trick to make it
' appear in the VB property page. At design time it's a string,
' at runtime it's an object reference.
' We use the Procedure Attributes 'Map Property To Page'
' to set this property to the object's property page.
' A more sophisticated implementation would use Desaware's SpyWorks
' ActiveX extension DLL to implement the IPerPropertyBrowsing interface,
' forcing the VB property window to always display a user 
' specified message at design time even if the user tries to edit it.
Public Property Get SubObject() As Variant
   If Ambient.UserMode Then
      Set SubObject = m_SubObject
   Else
      SubObject = "Sub object"
   End If
End Property

Public Property Set SubObject(ByVal New_SubObject As ControlSubObject)
   Set m_SubObject = New_SubObject
   PropertyChanged "SubObject"
End Property

Public Property Let SubObject(ByVal New_Object As Variant)
   If Ambient.UserMode Then
      Err.Raise 382
   End If
End Property

'Initialize Properties for User Control
Private Sub UserControl_InitProperties()
   Set Font = Ambient.Font
   Set m_SubObject = New ControlSubObject
   m_SubObject.Text = "Default Value"
   PropertyChanged "SubObject"
End Sub

' The control displays the contents of the sub-object
Private Sub UserControl_Paint()
   CurrentY = 0
   If Not (m_SubObject Is Nothing) Then
      With m_SubObject
         Print .X
         Print .Y
         Print .Text
      End With
   Else
      Print "Empty"
   End If
  
End Sub

'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
   Set Font = PropBag.ReadProperty("Font", Ambient.Font)
   Set m_SubObject = PropBag.ReadProperty("SubObject")

End Sub

'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
   Call PropBag.WriteProperty("Font", Font, Ambient.Font)
   Call PropBag.WriteProperty("SubObject", m_SubObject)
End Sub

As you can see, the new ControlSubObject object is completely self persisting. The real magic is in the object implementation which is in the SubObjectComponent project. I'll focus here on the Property Bag implementation - the memory stream implementation is nearly identical.

The project has a class called ControlSubObject. The first step is to get the class to implement the IPersistPropertyBag interface. This is done by adding a reference to the Desaware ActiveX extension library that is part of SpyWorks. It allows you to create an object called a dwControlHook object, and to implement an interface called IdwCustomOleHook. The dwControlHook object is called ControlHook, and is initialized in the class initialization routine. An array named Interfaces is defined to hold the IID values for each interface. You see, each standard interface has a unique interface identifier. You can find the values for standard interfaces by searching the registry or the .IDL files that come with C++.

' SubObject Example Component
' Copyright © 1998 by Desaware Inc. All Rights Reserved

Option Explicit

Dim ControlHook As dwControlHook
Implements IdwCustomOleHook
' Space for 3 interface IIDs
Dim Interfaces(15, 2) As Byte

Private Sub Class_Initialize()
   Set ControlHook = New dwControlHook
   ControlHook.Initialize Me
End Sub

The IdwCustomOleHook interface that was implemented using the VB implements statement is used by the ActiveX extensions to initialize the list of interfaces that you wish to implement. In this example, we specify the IID for each standard interface by setting it into to the InterfaceName parameter when the IdwCustomOleHook_GetInterfaceName method is called. You actually could simply specify InterfaceName = "IPersistPropertyBag" and it would search the registry for you, but I prefer to use the actual IID string just in case the registry is missing one of the standard interface identifiers. This approach is also faster.

Private Sub IdwCustomOleHook_GetInterfaceCount(iCount As Long)
   ' Implement 3 custom interfaces
   iCount = 3
End Sub

Private Sub IdwCustomOleHook_GetInterfaceName(ByVal _
InterfaceNumber As Long, InterfaceName As String)
   ' GUID's are from C IDL files.
   Select Case InterfaceNumber
      Case 0
         'IPersistPropertyBag
         InterfaceName = "{37D84F60-42CB-11CE-8135-00AA004BB851}"
      Case 1
         'IPersistStreamInit
         InterfaceName = "{7FD52380-4E07-101B-AE2D-08002B2EC713}"
      Case 2
         'IPersistStream
         InterfaceName = "{00000109-0000-0000-C000-000000000046}"
   End Select
End Sub

You also need to let the ActiveX extensions know which functions you are using to implement the methods of the interface. These functions must be in a standard module, and their addresses are obtained using the AddressOf operator.

Private Sub IdwCustomOleHook_GetInterfaceVtbl(ByVal _
InterfaceNumber As Long, FunctionAddresses() As Long)
   Select Case InterfaceNumber
      Case 0
         ' IPersistPropertyBag
         ReDim FunctionAddresses(3)
         FunctionAddresses(0) = GetAddress(AddressOf PropBagGetClsid)
         FunctionAddresses(1) = GetAddress(AddressOf PropBagInitNew)
         FunctionAddresses(2) = GetAddress(AddressOf PropBagLoad)
         FunctionAddresses(3) = GetAddress(AddressOf PropBagSave)
      Case 1, 2
         ' IPersistStream and IPersistStreamInit only
         ' differ in last function (IPersistStream doesn't have InitNew)
         ' No harm in including it though - 
         ' the extra pointer will just be ignored.
         ReDim FunctionAddresses(5)
         FunctionAddresses(0) = GetAddress(AddressOf PropBagGetClsid)
         FunctionAddresses(1) = GetAddress(AddressOf PSIsDirty)
         FunctionAddresses(2) = GetAddress(AddressOf PSLoad)
         FunctionAddresses(3) = GetAddress(AddressOf PSSave)
         FunctionAddresses(4) = GetAddress(AddressOf PSGetMaxSize)
         FunctionAddresses(5) = GetAddress(AddressOf PSInitNew)
   End Select
End Sub

If you look in the standard module named "ControlMethods.bas", you'll find the following functions that implement the Load and Save methods of the IPersistPropertyBag interface. The first parameter of each function is always the object itself, so it's easy to know exactly which object is being referenced when the function is called. In this case we assign the obj parameter to a parameter of the correct object type, which allows us to call friend functions on the object. That way the internal workings of this interface mechanism remains private to the component. The second parameter 'p' is a reference to the object into which the properties are going to be saved (typically the form file). This object reference is a pointer to an IPropertyBag interface, which is passed back to the class method that is going to actually save or load the properties.

' IPersistPropertyBag Load method
Public Function PropBagLoad(ByVal obj As IUnknown, _
ByVal p As IUnknown, ByVal IErrorLog As IUnknown) As Long
   Dim myobj As ControlSubObject
   Set myobj = obj
   Call myobj.intPropBagLoad(p, IErrorLog)
End Function

' IPersistPropertyBag Save method
Public Function PropBagSave(ByVal obj As IUnknown, _
ByVal p As IUnknown, ByVal fClearDirty As Long, _
ByVal fSaveAllProperties As Long) As Long
   Dim myobj As ControlSubObject
   Set myobj = obj
   Call myobj.intPropBagSave(p)
End Function

Looking back at the class module, both the Load and Save methods receive the IPropertyBag reference 'p' and an error log parameter that we pretty much ignore. How do we call methods on the IPropertyBag interface, which is actually not compatible with Visual Basic? This is done using the SpyWorks generic calling scheme. Two entry points are defined for the functions using the standard VB declare statement as follows:

Private Declare Function PropertyBagRead Lib _
"dwaxextn.dll" Alias "dwGenericCall" (ByVal ObjectReference _
As Long, ByVal s As Long, v As Variant, ByVal IErrorLog As IUnknown) _
As Long  ' 3
Private Declare Function PropertyBagWrite Lib _
"dwaxextn.dll" Alias "dwGenericCall" (ByVal ObjectReference _
As Long, ByVal s As Long, v As Variant) As Long ' 4

Now this might look terribly confusing. Every experienced VB programmer knows that the Declare statement is used to call exported functions - it has nothing to do with calling methods belonging to an object! Also, you'll notice that thanks to the Alias statement both of these Declare statements actually end up calling the exact same exported function!  Also, they both have different numbers and types of parameters - who ever heard of an exported function that can have multiple parameter numbers and types?

No, it's not a mystical far Eastern philosophy - and it's not magic. It's just a really elegant solution to the problem of calling any interface method while taking advantage of the huge flexibility provided by the VB declare statement. The trick is to create an object called a dwGenericCall object. You then use the SetInterfaceInfo method to tell it which object you are using and which interface you want to use to access the object. In both of these cases we use the 'p' parameter which references the object in which the data will be stored, and specify the IPropertyBag interface using the standard IID string format. We then use the PropertyBagRead and PropertyBagWrite functions declared earlier. The only trick is the first parameter, which receives the result of a GenericCallReference function call on the gencall object. The parameter for this function is simply the position of the method that you are calling. For example: The Load method of the IPropertyBag interface is the fourth method in the interface (the first three methods of every interface are the three methods belonging to the IUnknown interface: AddRef, Release and QueryInterface). When you call the function, the ActiveX extension library looks at the first parameter, and automatically calls the correct method using the remaining parameters that you passed to the function.

Because you defined the parameter types using a Declare statement, you have complete control over what you are passing as a parameter to the method. In this example, the IPropertyBag Load method requires a pointer to a null terminated Unicode string containing the name of the property. We could take the string and convert it into a Unicode byte array and pass a reference to the first byte in the array. But in this case it was easier to just create a null terminated string and pass a pointer to the internal string as it is stored by Visual Basic (VB always stores strings internally as Unicode).

' Implementation of IPersistPropertyBag.Load method
Friend Function intPropBagLoad(p As IUnknown, elog As IUnknown) As Long
   Dim gencall As New dwGenericCall
   Dim v As Variant
   Dim pname$
   Dim hres As Long
   ' Set gencall to use IPropertyBag
   Call gencall.SetInterfaceInfo( _
   "{55272A00-42CB-11CE-8135-00AA004BB851}", p)
   ' For each sub-property, we pass:
   ' 1: A null terminated Unicode string for the property name.
   ' 2: A variant set to the correct type of data to load
   ' 3: The IErrorInfo object for error logging provided by the Load call.
   pname = "X" & Chr$(0)
   hres = PropertyBagRead(gencall.GenericCallReference(3), _
   StrPtr(pname), v, elog)
   m_x = v
   pname = "Y" & Chr$(0)
   hres = PropertyBagRead(gencall.GenericCallReference(3), _
   StrPtr(pname), v, elog)
   m_y = v
   pname = "Text" & Chr$(0)
   v = ""   ' Set to string type
   hres = PropertyBagRead(gencall.GenericCallReference(3), _
   StrPtr(pname), v, elog)
   m_text = v
End Function

' Implementation of IPersistPropertyBag.Save method
Friend Function intPropBagSave(p As IUnknown) As Long
   Dim gencall As New dwGenericCall
   Dim v As Variant
   Dim s As String
   ' Set gencall to use IPropertyBag
   Call gencall.SetInterfaceInfo( _
   "{55272A00-42CB-11CE-8135-00AA004BB851}", p)
   ' For each sub-property
   ' Load the property into a variant for saving.
   ' Pass it with a null terminated Unicode property name
   v = m_x
   s = "X" & Chr$(0)
   Call PropertyBagWrite(gencall.GenericCallReference(4), StrPtr(s), v)
   v = m_y
   s = "Y" & Chr$(0)
   Call PropertyBagWrite(gencall.GenericCallReference(4), StrPtr(s), v)
   v = m_text
   s = "Text" & Chr$(0)
   Call PropertyBagWrite(gencall.GenericCallReference(4), StrPtr(s), v)
End Function

The proof that this works can be seen in the following listing from a form file containing the control that uses the sub-object. As you can see, not only did the object save its internal properties, but it appears exactly as you would expect, bracketed by a BeginProperty and EndProperty statement.

VERSION 5.00
Object = "*\AControlSubObjects.vbp"
Begin VB.Form Form1
   Caption         =   "Form1"
   ClientHeight    =   3195
   ClientLeft      =   60
   ClientTop       =   345
   ClientWidth     =   4680
   LinkTopic       =   "Form1"
   ScaleHeight     =   3195
   ScaleWidth      =   4680
   StartUpPosition =   3  'Windows Default
   Begin ControlSubObjects.ControlContainsObjects ControlContainsObjects1
      Height          =   1365
      Left            =   900
      TabIndex        =   0
      Top             =   360
      Width           =   1275
      _ExtentX        =   2249
      _ExtentY        =   2408
      BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851}
         Name            =   "MS Sans Serif"
         Size            =   8.25
         Charset         =   0
         Weight          =   400
         Underline       =   0   'False
         Italic          =   0   'False
         Strikethrough   =   0   'False
      EndProperty
      BeginProperty SubObject {7A98FA31-AC29-11D1-B78F-00001C1AD1F8}
         X               =   6
         Y               =   0
         Text            =   "New value2"
      EndProperty
   End
End

Back to El Nino

The code shown above is only a small part of the complete project. The complete sample projects include implementations of IPersistStream and IPersistStreamInit, and demonstrate how to call the IStream interface. They also demonstrate how to implement a property page that allows you to edit sub-objects just as you would edit a font or picture object. More information on creative use of property pages can be found in Appleman's "Developing ActiveX Components with Visual Basic 5.0" book.

It took me weeks to recover from the jet-lag, mostly because two days after returning home I headed to Washington D.C. I wish I could say that the trip was profitable, but I barely broke even. You see, I visited Akhiabara on the way home. And they have the coolest high tech gadgets their that you ever did see...

For notification when new articles are available, sign up for Desaware's Newsletter.

articles
Related Products:
 
Products    Purchase    Articles    Support    Company    Contact
Copyright© 2012 Desaware, Inc. All Rights Reserved.    Privacy Policy