ArticleDirectory
- Introduction
- The problems
- A sanity check
- Implementing property pages in. net
- The derived classes
- InterOP blues
- Reborn In managed C ++
- Usercontrols, events, & hosting environments
- From usercontrol to form
- COM and. Net InterOP gotchas
- The ActiveX control test container and. net
- What's left
- The source code
Introduction
I 've written a lot of COM code over the years. one of things I used quite liberally were Ole property pages. it was a handy way to configure an otherwise invisible COM component in a safe, reliable way. I know, most people associate property pages with ActiveX controls, but property pages are based on interfaces, and those interfaces are completely independent of the ActiveX control architecture.
One of the best things about Ole property pages is that you don't have to know anything about the object you 've got just instantiated to use them. you can just query for the supported interface (ispecifypropertypages) and if it exists, call the getpages () method to retrieve the CLSID's of the property pages for the object. from there, it's just a little jump to olecreatepropertyframe () and voil à, you 've just given that COM component the ability to configure itself rather than you having to do all the work.
Ole property pages were painfully absent from. net, and I immediately began trying to figure out how to get them back. What I found was that this was no simple task.
The problems
The first obvious problem was porting the various interfaces and structs. net. there are three primary interfaces required to implement this fully. they are ispecifypropertypages, ipropertypage, and ipropertypagesite. when scanning all the methods of those interfaces, I quickly narrowed down what structs I wowould have to implement. fortunately for me ,. net already knows about rect (rectangle), MSG (Message), size (size), and point (point) structs and how to convert al them, so all we really needed to implement was the proppageinfo struct and the cauuid struct.
Both structures allocate and free com memory in ways that are counter-intuitive for straight conversion to strings and guid arrays. I opted for the intptr method for the three strings in the proppageinfo struct and for the guid array in the cauuid structure. from there, I just implemented a few helpers within the struct to assign and extract the data out of the intptr's.
A sanity check
After I had successfully mapped the structs and interfaces, it was time to test this baby out! Quickly, I hashed together a few lines of code:
Type typ;ObjectOBJ; guid [] G; typ = type. gettypefromprogid ("mscomdlg. commondialog "); OBJ = activator. createinstance (typ); activexmessageformatter. initstreamedobject (OBJ); ispecifypropertypages PAG = (ispecifypropertypages) OBJ; cauuid cau =NewCauuid (0); PAG. getpages (RefCAU); G = CAU. getpages ();// The method below was added in a base class mentioned later in// The articlePropertypage. createpropertyframe (intptr. Zero, 100,100, "Hello world ",New Object[] {OBJ}, G );
I ran it, And voil à! The common dialog control's property pages popped up on the screen. encouraged, it was time to get down to implementing my own property pages in my existing. Net apps.
Implementing property pages in. net
The first thing I needed was a base class to handle the varous aspects of implementing a property page. it wowould originally derive from usercontrol, but for reasons I will explain later, I instead inherited from form. this base class wocould implement ipropertypage and handle the management of the underlying objects whos properties are being exposed along with providing a few virtual methods for a derived class to receive events on. this base class also had to do a little bit of magic to get the usercontrol to display inside the requested container (ipropertypage: Activate passes a parent window inside which this control needs to reside ).
The derived classes
I created two pages, derived from my propertypage base class. I threw a couple controls on them, gave them both [GUID ("")] attributes, and made them public. I also opted to register the class library for com InterOP because we're re writing essential tially a com-callable set of classes. I chose also to give them the [classinterfacetype (classinterfacetype. none)] attribute.
I then created a class that wocould expose the ispecifypropertypages interface. The single method, getpages (), is really simple:
Public VoidGetpages (RefCauuid ppages) {guid [] G =NewGuid [2]; G [0] =Typeof(Mypropertypage1). guid; G [1] =Typeof(Mypropertypage2). guid; ppages. setpages (g );}
I didn't think it merited any kind of base class. Okay! So, after giving this class a guid () Attribute and setting its classinterfacetype to none, it was time to get cracking!
InterOP blues
I re-ran my test application, replacingMscomdlg. commondialogProgid with the progid of my new class (the one that exposed ispecifypropertypages). to my horror,It failed to cast the resultant object to ispecifypropertypages. It seems that because I had compiled both the class library and the test application using the common source containing the interfaces and structs, each of those declarations became specifically unique to each assembly. therefore, I couldn't do a straight cast to the interface even though they had the same guid. this was totally understandable, of course, so I quickly regrouped.
Because I was using the activator class to activate the object, what wocould happen if I instead used cocreateinstance () directly to make a COM object? Wocould the. NET implements ALER realize I was creating a. Net object in spite of my using the API directly? Turns out the answer to that question is yes. On the successful return of cocreateinstance,. Net helpfully unwrapped my class and handed me a. Net object back.
It turns out that, no matter how hard I tried, or how indirectly I attempted to marshal this object ,. net wowould always give me back the base. net class object. I tried getting an intptr of the object's iunknown, calling marshal. queryInterface on it to get the interface and putting it back into object form. I tried every conceivable method I cocould think of, and none of them worked. this was a major stumbling block.
There was no way I cocould guarantee that any arbitrary. net class wocould use a common assembly that implemented a "common" version of the interfaces I wanted. furthermore, there was no way I cocould guarantee that one person's implementation of the interfaces wocould be identical to mine. I simplyHadTo use. Net's login aling code to work with these objects through COM-COM was the only commonality I had, so I had to use it regardless of what language the actual object was written in.
Because. net wocould insist on giving me. net object no matter how I cast, cajoled, or cocreateinstance 'd. net class, I had to resort to drastic measures. I 'd have to do something unmanaged.
Reborn In managed C ++
C ++ is the only place I can mix managed and unmanaged code, so this is the only place where my unorthodox requirements cocould be met. I quickly threw the propertypage base class, interfaces, and structs into C ++ (when I say "quickly, "I mean I hacked at it endlessly until I managed to get the clean C # code into clunky C ++ managed format ), and created an unmanaged global function to retrieve the ispecifypropertypages interface given a CLSID and retrieve the pages from it.
After all that, I revisited my original C # class library. A quick reference to my c ++ managed DLL, A recompile later, and my test application was working perfectly! I'm home free! Wait! Uh oh...
Usercontrols, events, & hosting environments
Once my property page showed up on the screen, I was convinced my work was done. however, as soon as I began typing keys, I realized I was far from finished. the tab key didn't work properly, the usercontrol on the page wasn't grouping events properly, and things were not at all right.
I thought the tab key was a quick fix. I ran the spy ++ program and analyzed the windows of the property pages. I noticed that the usercontrol upon which my propertypage base class was derived wasn't given the ws_ex_controlparent extended window style. I modified the base class to give the property page this style when I show the page.
After making that change, the tab key started working, albeit badly. I noticed that none of the controls stored ed an onfocus event, and the tab key seemed to randomly go to the controls on the page, ignoring my tab order completely. it wasn' t so random, actually. it was using the Z-Order of the controls on the page rather than the tab order. it seemed to me that windows was authentication ing through the controls rather than using the form's inner logic.
I started looking for ways to properly apply the translateaccelerator () messages I was loading ing from the page's site. in the end, I had to rip out the code that set ws_ex_controlparent (it was interfering. net's tabbing internals) and write my own tabbing logic in the base class.
From usercontrol to form
Once I got tabbing right, I again thought that I was home free! It wasn' t until I started adding mnemonics to controls that things once again went south. in spite of my improved translateaccelerator () handling code, I was still unable to Use mnemonics. in desperation, I decided to change my inheritance from usercontrol to form. to transition to form, I had to radically alter the window styles using the API before showing the window, set the form's parent, and pray.
I wasn't confident this wocould work. I'm delving deep into the "unsupported" section of windows. form, so who knows what kind of mess I was creating? Fortunately for me, however, things started looking up. I recompiled the class library and re-ran my test application. To my delight, all was working beautifully! The tab key was tabbing properly, mnemonics were working, and I sat down to start writing this article.
COM and. Net InterOP gotchas
Because OLE property pages are a com animal, I decided to see just how well native com coshould deal with these property pages. I wrote a usercontrol in C # for use as an ActiveX control to see what wowould happen if I added property page support to it. activeX controls in. net aren't officially supported, and you're about to find out one of the reasons why!
The ActiveX control test container and. net
After plugging in all the code I 'd need, it was time to break open the test container and see what happens. I successfully added my control to the container, and it did show properly. I right-clicked the control, and lo and behold,PropertiesMenu option was there and enabled! I clicked it, and was quite pleased to find my property pages flashing up on the screen before me. Success!
Not. although the test container cballs the standard olecreatepropertyframe API just like my. net test application did, the objects it passed in as parameters were to my horror, inherited from _ comobject-which meant that. net for some reason wasn' t able to unwrap my objects to their native. net form. as a result, when a page tried to access or modify any member of the underlying object, it failed.
It was time to dig out the source for the test container and start scraping. I quickly found out that the test containerAggregatesThe ActiveX control if it can. meaning, when it creates the ActiveX control, it creates it with an outer unknown. this outer unknown intercepts iunknown, idispatch, and the container's own extended interface, but delegates the remaining queries to the inner object.
Because the control container (and your other ActiveX Control hosting environments) Create the controls as aggregates, it means that. net has no way of properly stored aling And unwrapping the object before giving it to native. net code! This meant that none of my property pages cocould talk to the underlying control properly. I triedA lotOf different hacks to make this work, and finally came into SS an easy fix.
I added a new interface, called iprovideobjecthandle, to my base C ++ class library. It has a single property, objecthandle, that returns (what else) and objecthandle! Because objecthandle is a stored albyref object that wraps up other objects for remoting, I thought this wocould be the perfect medium to pass my base. Net object to my property pages.
I found that even though my property page inclued _ comobject's from the control test container, I cocould still query for public interfaces and receive pointers (still _ comobject-based, but valid pointers nonetheless ). therefore, by implementing iprovideobjecthandle in my control, my property pages cocould query for it and then call the property objecthandle. here's how my control implemented it:
PublicObjecthandle {get {Return NewObjecthandle (This);}}
if, during the com->. net InterOP, the original aggregated version of the control cocould not be passed properly, the solution is to pass an object that cocould be stored aled and translated to a native object properly! Objecthandle wocould take care of our InterOP transition for us, and once we were in all-native land, provide its unwrap () method to safely provide us with a native object.
There was one other issue, left unresolved regarding the ActiveX control test container-it never released the ActiveX control entirely. there was one open reference on the control itself. this seemed to be the case even if I created an empty control and instantiated it in the test container. the refcount on the control never reached zero, and I assume this is because of the aggregation occurring and the references being passed into and out of the control. it was beyond the scope of this author and this article to try and resolve and is just one of the specified reasons authoring ActiveX controls are not officially supported in. net.
What's left
There are a few things I left out of this implementation-persistance being a major one. i'm leaving that alone because persisting objects in. net is a pretty well-covered topic. you can decide for yourself how to persist the objects you instantiate and configure via property pages.
The other thing I left out was implementing custom property page containers. if the standard olecreatepropertyframe () function isn' t your cup of tea and you want to roll your own, there are a few gotcha's in store for you along the way-all having to do. net native objects talking to other. net native objects and how they pass interfaces around. most of the hoops I had to jump through had to do with that subject.
The source code
The source code associated with this article has three projects:
- The propertypages Project, which is a managed C ++ project complete with a strong name. It defines the interfaces and the base class the other projects use.
- The controlwithpropsheet Project is a C # class library that exposes three public classes, registered for com InterOP. the myusercontrol control implements the ispecifypropertypages and iprovideobjecthandle interfaces, both of which only have a single method and are trivial to implement yourself. the other two classes are the propertypage-Derived classes that provide property pages to the control. they are intentionally simple so you can get an understanding of what's been done.
- The final project is displaypropsheets, a C # console application that displays the property sheets of any COM object you give it, provided the COM Object implements ispecifypropertypages.
Downloads
Propertypages.zip-Vs. NET 2003 project and files
Turn: http://www.codeguru.com/Cpp/Cpp/cpp_managed/nfc/article.php/c8545/#more