Products Purchase Publishing Articles Support Company Contact |
Articles > COM > The Object that came in from the Code |
|||||
.NET COM
|
The Object that came in from the Codeby Daniel Appleman (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 EastLook, 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". 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 LoadedI 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.
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 MountainIt 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 SolvedThe 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 NinoThe 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. |
|
|||
Products Purchase Articles Support Company Contact Copyright© 2012 Desaware, Inc. All Rights Reserved. Privacy Policy |
|||||