The
GSystem
Properties
Component
Copyright/Disclaimer
This Document and the software described herein are protected under Copyright by InnoVisioNate Inc.
To the maximum extent permitted by applicable law, InnoVisioNate Inc. shall not be held liable for any damages; whether they be consequential, incidental, indirect, special, or of any other type, whatsoever. These may include but are not limited to; damages for loss of business profits, business interruption, loss of business information, or any other pecuniary loss arising out of the use of, or inability to use, this product.
Contents
The component deployed at the application level
The component deployed at the component level
Part 0 – What is the GSystem Properties Component ?
Provide simple “memory” persistence for your system.
Easily create a record-oriented “database” application -
without the database
Manage contents of windows or controls
Provide properties settings dialogs
Provide run-time configuration and persistence of objects
within your system.
Instantiating a GSystem Properties Component within your
development environment.
An important note about timing
Setting the properties of the GSystem Properties
Component
GSystem Properties Component Summary
Part I – What Are Properties ?
Storing sets of objects within a single property
Providing Direct Access Storage
Using Normal Access to Storage
Combining an Array Property with an Array of Windows
Controls
An important note about the “Set” of properties
Part II – Provide Application Level Persistence
Developing a persistable application – by example
Instantiate a GSystem Properties Control object
Set the properties of the Properties Component
Provide the persistence mechanism to any other objects
Participate in the persistence process
Use the file manipulation capabilities
Summary of the file manipulation methods
Part III – Provide Component Level Persistence
Developing a persistable object – by example
Step 1: Create an instance of the GSystem Properties
component
Step 2: Create the set of properties maintained by the
component
Step 4 (Optional) provide access to your system for the
GSystem Properties Component
Part IV – Intercepting the Save, Load, and InitNew
functionality of the Properties Component
A Clarifying Point Concerning Object Hierarchies
The mechanics of implementing the IGPropertiesClient COM
interface
Providing an interface in a VB project
Providing an interface in a MFC project
Providing an interface in a ATL project
The specifics of the IGPropertiesClient interface
Ensuring multiple objects are persisted
Ensuring actively changing numbers and types of objects
are persisted
Part V - Using the Property Page implementation
An Introduction to Property Pages
The Mechanics of a Property Page
Providing a property page window handle to the Properties
Component
Providing the easier properties windows – Summary
Enable the use of IGPropertyPageClient and custom
property page windows
The Property Page Creation and Manipulation Processes
Highly optional implementation of the IPropertyNotifySink
interface
Showing the property pages of other objects
Adding property pages of any object
Editing properties of any object
Part VI – The relationship between properties and common
windows controls
IGProperty/IGProperties Window/Control Support methods
Part VII – Interfaces reference information
Miscellaneous IGProperty methods
The window relationship methods
Miscellaneous methods in IGProperties
File handling methods on IGProperties
Property management methods on IGProperites
Persistence methods in IGProperties
Property page methods in IGProperties
Initiate the edit properties process
Property Stack and Comparison Methods
More Extensive Property Page Support Methods
The window relationship methods
The IGPropertiesClient Interface
Persistence IGPropertiesClient Methods
The IGPropertyPageClient interface
|
ActiveX |
A Microsoft trademark. Sometimes used to indicate a COM component that can be used as a control on an application window. Within this document, the term ActiveX is interchangeable with “COM Object”. |
|
COM |
Component Object Model – Two complimentary sets of rules and regulations. Providers of functionality can follow one set in such a way that developers needing that functionality can follow the other set to utilize that functionality seamlessly and without trouble. |
|
Client |
The user, or consumer, of an object. When a COM object is placed on a Visual Basic form, the Visual Basic code accessing that COM object in the form is the client. The COM object itself and the binary code that implements the object is called the server. |
|
Interface |
A set of functionality. Strictly, a precise description of a group of functions, including the arguments and return values, is an interface definition. An interface implementation is a physical instance of this set of functions. This existence is as a table of functions which is the same as a C++ Virtual Function Table. Any software environment that can create and/or manipulate a table of functions like this can participate in COM. |
|
Persistence |
The act of saving a state at one point in time and then, at a later point in time, returning to exactly the same state. |
|
Server |
An external binary software resource accessed through COM that a client application, or any other software artifact, is able to create and use. |
|
Storage |
The place where properties are saved too and restored from. Technically a file but in an operating system format specifically geared towards the persistence of COM objects. Directly accessed only through operating system provided functionality. |
The GSystem family of components is a suite of powerful tools with which software developers can produce superior quality systems.
This particular component is one of the core tools in the GSystem family. It evolved as the need for persistence across all of the other components within this suite was recognized.
As a user of software, the author of this tool feels that one of the obvious indicators of poor quality in software, is when the developers have not taken the opportunity to make it a personal experience for users. In other words, while using software, a user will “configure” their experience, for example, by sizing and placing a window of the system. That user is expressing a preference by doing so; for the system to recognize these preferences, and to respect the user enough to remember them the next time the software is run, is a clear display of quality in software.
There is a great deal of complexity in the COM persistence model. It is not this complexity, however, that drove the need to develop this product. It was that, after having gone through a lot of effort in implementing the stock COM persistence model, it turns out to be weak, inflexible, and a lot of work to repeat. There needed to be a way to provide a capable persistence and property pages mechanism that provides access to each of the steps in the processes involved yet which would limit the amount of software the developer would have to provide to accomplish the task.
Thus, the GSystem Properties Component embodies the right mix of ease of use while still providing access to the entire process. This offers the power and flexibility to allow developers to meet their initial needs quickly and easily while giving them the flexibility to evolve and improve the systems they develop as their own users needs become more complex.
It is true that certain “COM enabled” tools, such as Microsoft MFC (Microsoft Foundation Classes), implement persistence using wizards to generate code. However, as happens with all real projects, it doesn’t take long before you find these solutions are insufficient. Perhaps the first place you notice this is when your object utilizes other COM objects and you need to allow these objects to persist as well. No generic implementation of persistence can possibly “know” about your system’s object/ownership hierarchy, either as it exists at code generation time, or as it might (and probably will) evolve over time.
Part of the philosophy behind every component in the GSystem family is that there is no knowledge, nor should there be, of the context in which each component is used. Other than that the general need is one that the component is designed to fulfill. Specifically, the goal is that these components will not impact you in any way with respect to your programming model or habits nor will you find that they are “in the way “ as you grow and maintain your system.
The GSystem Properties component is designed to allow your application or object’s code to participate thoroughly in the entire process such that you can direct this process to work on behalf of your system how you see fit, not how the authors of MFC or ATL (ActiveX Template Library) presumed you would use it.
As with all of the components in the GSystem toolset, there is no use of any Microsoft MFC software or component. No ‘.dlls, other than the system ‘.dlls, are needed for implementation of any of the GSystem components. Indeed, all of the GSystem sub-systems are stand-alone ‘.ocx files that will have no reliance on any other ‘.dll except amongst themselves and then only through COM. The latter statement means that there will never be any reliance by GSystem binaries on anything other than the standard system level ‘.dlls (kernl32.dll, user32.dll, common dialogs, etc.), and amongst themselves, for example, when one GSystem component creates and uses another GSystem component.
At the software application level, the GSystem Properties Component is the tool that can make the system present itself to the user, when started, just exactly as that user had last utilized the system. Of course, this is more than just the size and placement of key windows in the application, it can extend to any part of an application or system which varies based on user action.
The major parts of this mechanism are;
An additional and powerful feature of the GSystem Properties component is that you can provide to your users the properties settings dialogs of any ActiveX or COM object while at the same time, the settings that the user chooses through these dialogs will be persisted right along with the other parts of your system.
Thus, for example, you can let users configure, at runtime, complex windows components, such as the list view control, as they prefer in the same way that you configure such objects in the development environment.
When you utilize the GSystem Properties Component in the development of your own components, you are generally providing persistence “services” that clients of your components will call upon to persist within their (the client’s) own context. In other words, you typically won’t deal with the “application” level persistence issues, such as prompting for file names, creating a storage and stream upon which to store the objects, and the saving and restoring of the all object’s properties.
Instead, you are providing the services which enable your client’s application to seamlessly provide persistence, at least for your objects, through these services.
These two “arenas” where you utilize the GSystem Properties Component do have overlapping boundaries. If it seems confusing why the distinction has been made, perhaps it will become clearer once you utilize the component in both of these kinds of development activities.
In any case, one reason for the distinction is that in this document there are places where the discussion or example is more relevant to one type of environment than to the other.
InnoVisioNate Inc. is focused on the development of high quality and reliable software components that provide customers solid power, flexibility, and extensibility.
We know that the systems in which our components are used are built by professional developers with a keen desire to produce the best software they possibly can.
It is therefore an honor for us to be a part of these development teams.
Our goal is to continually earn our customer’s trust by producing software components of the utmost quality and utility and by providing strong support for our products.
Do you have an idea for a great component? InnoVisioNate Inc. is more than willing to partner with other interests to see that the best quality components get developed and placed on the market.
This document will describe how to effectively use the GSystem Properties Component in your development efforts.
The style of this document is intended to get you started as quickly as possible while providing efficient access to the information once you understand the general concept of using the component. We feel that typical software documents are either at such a low level that they are insulting for professionals to use, or, that they are so unconnected with the actual process of using the tool that they are worthless in relation to every day, real world, situations.
Describes the GSystem Properties Component as the “Master of Properties” and as a component that can provide a wide range of services to your application or object.
Describes “Properties” in the context of the GSystem Properties Component as a development tool. The term “properties” is used commonly in the component development arena to indicate characteristics, or attributes, of objects which are typically accessible through certain methods on the object’s default IDispatch interface. These methods generally set or retrieve the value of some variable within the implementing object’s variable space.
However, the properties discussed in this document are actually very powerful objects in their own right. The knowledge of the windows API embodied within these objects makes them much more completely integrated with the windows and controls of your application than mere variables are. For example, as objects, GSystem Properties can associate with the entries in a Combo-Box naturally without your having to write any code whatsoever, other than a single call to the appropriate method on the property object.
Focuses on the use of the component in the development of applications. This covers the implementation of persistence for applications that you develop as opposed to within objects that are part of applications.
The basic framework for this discussion will be around the development of a system using Visual Basic. The general concepts will apply to any application development environment, though there would be differences in the syntax, etc., in other environments, such as PowerBuilder and Delphi. If you find a problem or have a question about implementation in any other environments, please don’t hesitate to contact us at<EMAIL_ADDRESS>Deals with the development of COM/ActiveX objects which are intended for use by inclusion in some other software system built by a different developer. Here the persistence is somewhat different in that it is being “driven” by something outside of the code you are working in. For example, some development environment may be asking your object to persist, perhaps within a development environment. However, it could be that you are participating in run-time persistence, if, for example, your client’s client (the end-user) is running the application built by your client. One would hope that this developer would have used the GSystem Properties Component and followed the techniques described in Part I, though, of course, this is not necessary for the success of your object in persistence.
The discussion in Part II will be centered on the use of C/C++ in the construction of a COM object. Further, except for clarifying points, the discussion will make no reliance on any particular COM technique in C/C++, such as the MFC COleObject class or on ATL.
Even though our discussion does not center around MFC or ATL, there have been extensive examples provided with this system on how to use this tool effectively in each context. This document will reference these examples where clarification needs to be made.
To the extent that you are developing your COM objects in other environments, such as Visual Basic, it is hoped that the discussion in Part I will be able to answer any questions arising out of the difference in environment that are not answered in Part II.
In both arenas, we stress that it is not our intent to foster, or recommend, any one environment over another. One of the admirable goals of the COM model is that development ought to be language independent and people should be encouraged to work with the tools they know best while the fruits of their labors are available to all regardless of implementation language or environment.
The GSystems family of components is fully committed to this goal and through such commitment we ask you to please help out by letting us know when the component does not live up to the nature of the objective. For example, should you be developing in some environment and find that our components do not work, or work inconsistently within that environment, please let us know.
Comments are welcome at<EMAIL_ADDRESS>This section offers a thorough discussion of the methods you supply in the IGPropertiesClient interface. Since these methods are critical in the process of saving and restoring properties and managing the state of your application or object’s internal data with respect to the properties, they have been given their own section in this document.
PartIV will cover the rich set of capabilities the GSystem Properties Component presents which allow you to quickly present to the user, and importantly, manage the process of, property settings dialogs. These are the common user interface component that presents the typical settings dialogs that we all recognize as members of a tab-control. Where each tab in the control refers to the settings of a particular object, or class of properties.
That section will also cover the details of providing the properties dialogs of other objects used in your system to the user.
This section will deal with the association between the properties and the window controls that you may be using in your application or object. There is a family of methods that you can use to quickly swap the value of a property between window objects, such as edit fields.
A discussion of the array properties and array of controls is also found here.
Finally, for reference purposes, and for those who prefer to simply soak up all the relevant information without any supporting information; there is Part VII.
This section is pure reference and describes each interface and method without context, and hopefully, without reliance on any other part of the system.
Any time a method (function) on one of the interfaces of the component is discussed, it will appear in a fixed font, such as method(). Note that the presence of the “()” is simply a further indication that it is a method. However, even though their may be nothing between the “(“ and “)”, this does not always mean that the method has no arguments. The () are simply there to reinforce that the indicated name is a method. There will generally be arguments required for most methods.
The GSystem Properties Component is a COM enabled object that you can use to provide a robust persistence mechanism within your own application or object. This tool also provides several tools that also support aspects of persistence, such as the manipulation of property values by users, the inclusion of arbitrary object’s properties into the persistence mechanism, and managing the relationships between windows objects and property values.
When an application runs, and the user makes changes to the application, to the interface, to the data embodied by the application, or to anything about that system that can change in any way; it is very nice, from the user’s perspective, if the application retains anything of value from the perspective of the user.
Stated another way, lets say a user of your application has already answered some questions, or has arranged your application the way that is most pleasing to herself. Now, when she runs that application again and the system either re-asks the same questions (!) or does not remember the way that it was set up previously as clearly desired, then this user is not going to be very impressed with the system.
At a simple level, the GSystem Properties Component could be thought of as an automated implementation of preferences. Many programmers are familiar with this and know how to read and write entries into some form of persistence file, it is tedious but works. Use of the GSystem Properties component achieves this result easily and naturally.
However, that is the component at its simplest level. Perhaps this is all you need. Certainly, this goes a long way to providing the kind of “memory” an application should have as described above.
The GSystem Properties Component is able to provide much more far-reaching capabilities in the management of features within your application. Should you choose to use these features, you can achieve the following levels of persistence in your system.
As stated previously, the GSystem Properties Component in its simplest form will allow you to “remember” how your user last used the system. You simply need to make “Properties” with the Properties Component and then use these properties just as if they were variables in your system. When the user exits the application, simply call Save() on the GSystem Properties Component control. When your system restarts, you call OpenFile() on that control and each of the properties will have the values they had when at the end of the last run, in any case, they will have the value that was last saved to the file.
The Properties Control creates properties that you can use exactly like any other variable in Visual Basic. In other words, your entire variable space can be saved and restored through runs of your system. To achieve this, all you have to do is to provide the startup code that creates the properties from the Properties Control, one call for each property (variable), and that makes the single calls to open the file on program startup and to save the file on program exit.
Persistence on behalf of a specific window in an application can be thought of as representing a “record” with that window. Specifically, you can use the GSystem Properties Component to remember all of the contents of a window, and the properties and contents of objects upon that window. Then, you can provide a simple mechanism whereby the user selects one of many persistence files[1] and you use the Properties Component to load the appropriate screen with the data from that file. Saving and restoring the contents of a window just as if these were records in an actual database.
The property object has methods that allow you to manipulate the contents of controls within your application and to associate the property with the control.
For example, you can call methods on properties that you have associated with drop-down list boxes, probably a string array property, to manage the relationship with the value of the property and the list box, for example, the current selection in the list box.
It is customary to provide a dialog that the user can raise to edit and/or browse the properties in the system. The GSystem Properties Component provides the framework that you can use to accomplish this task easily.
This framework includes presentation and manipulation of the dialog, using a window that you create, and it includes the “process” of communicating the user’s actions during this activity with your application. So you can, for example, correctly react to the “Ok”, “Cancel”, and “Apply” buttons as the user is working with the property settings.
Also, you can direct the GSystem Properties Component to display the properties settings of any of the objects or components in your system (that support property pages), either individually, or all together in one properties settings dialog[2]. Thus, your application will appear to be providing the functionality of the COM objects you use, presenting an ever more seamless face to the end-user.
Many windows objects contain properties that you set in your application development environment that control how these objects look and act. You can allow your users to set these same properties and these objects will look and act as your users desire. All of your end user’s preferences could not have been known when you initially placed these controls within the application.
This is very important when you are integrating complex objects within your application. Consider that these objects have properties (sometimes a large number of them) that govern much more than just the visual representation of the object. Also, consider that these objects may be a big factor in your entire application.  However, at the same time, you can imagine that your users may very well appreciate the rich functionality of these controls. Now, you can present all of the functionality of a given object directly to your users at run-time.
If ease of development is not reason enough, consider that, using the GSystem Properties Component, you don’t even have to know what objects are going to be used by your system at run-time! For example, imagine a configurable “host” system that allows the user to select, at run-, or configuration-time, the actual object to use for a particular purpose. This is one of the truly powerful features of COM. To achieve goals like this, you must avoid having knowledge of specific COM objects built into your application code, to the extent possible, of course.
Because the GSystem Properties Component is an ActiveX Control, you can create and use the object within your environment just as you would any other control. In Visual Basic, for example, you can double click the icon in the Palette of controls, and an instance of the control will be placed on your form.
In a C/C++ environment, you can declare an instance to the GProperties interface, and then create this instance using a simple call to COM[3], for example:
CoCreateInstance(
CLSID_GSystemProperties,
NULL,
CLSCTX_INPROC_SERVER,
IID_IGProperties,
(void**)&iProperties);
.
.
.
iProperties -> Release();
In the example, the vertical ellipsis is used to mean “sometime later”. In practice, you simply release the interface when you are done with it, perhaps in the OnFinalRelease() method in an MFC generated ActiveX control. Note that the above C code needs to have the file Properties_i.h “included” for it to compile. Note also that the system that uses this code also needs to have the file Properties_i.c file “included” in one and only one source file in the system. Both of these files are provided with the GSystem Properties Component, you may simply need to manage the location of these files such that your development environment can properly find them at compile time.
You may need to be aware of the sequence of events that will occur in your software while the process of persistence is underway. If you are creating an application, it is likely that this process is entirely under your control so you may not encounter this issue.
The first important thing to remember is that the GSystem Properties are objects that you must create within code when your object starts up, perhaps in your main Form_Load() method for an application, or in the constructor of your object if you are building a control or component.
In any case, remember that the property objects must exist as early on in the process as follows. Of course, since you generally use the GSystem Properties Component as the creator of properties, your instance of that object must also exist as early as possible.
To clarify the reasons for this, consider the timing issues involved if you are creating a component. Some client of your component will obtain an interface to your component through a call to COM, which will result in your object getting created through a mechanism provided either by you or the framework within which you built your object, ATL, for example. In any case, the only thing guaranteed to have happened before every client that will use your object is this “create instance” code. If you, within this code, create your instance of the GSystem Properties Control, and, through that, create all of your properties[4], you will be ready to persist whenever your client requests you to do so.
Without knowing all of the clients which may use your object, you can’t know what those clients will do in terms of persisting your object, however, you can be prepared to provide persistence support irrespective of how any client asks for it.
The COleControl class is the base class of objects generated with the ActiveX Control wizard. Since this base class (actually, one of it’s parent classes) provides persistence support, it is necessary to take a few minor steps to ensure the GSystem Properties Component can be used to it’s full potential in this case.
First, you must “re-direct” interface queries for the persistence interfaces on to the instance of the GSystem Properties Component. This is discussed in detail on page ____.
Next, you must be aware of the timing issues involved. A lot of clients will attempt to persist your object very early on (even just after creation). This has an impact on your instance of the GSystem Properties Component. Primarily, if you want to use the GSystem Properties Control as an ActiveX control integrated into your system through the resource editor, i.e., as a visual component. You must ensure that the window that contains this instance is created in the code in the constructor of your main object!
However, with COleControl, you can’t simply create your main window in the constructor without other problems. Typically, your window is not valid until sometime after the OnCreate() method is called. Nevertheless, you can create a window, for example, the one containing the GSystem Properties Control, in this constructor if you create a parent for it first. Then, in OnCreate() you can switch that window’s parent back to the originally intended parent. This is less difficult than it may seem. A complete example is provided in the FunctionContainer example.
Note that it is not necessary to have a visual component representation of the GSystem Properties Control and there is no real advantage to it; there are only a few properties on this component that you would set in a development environment anyway. This means that you can create your instance of the component direct through COM and the above issue will not exist.
Still, if you do have ActiveX controls on some window in your application (or object) that you want to persist, those controls must also exist when the system will try to “load” your object. In this case, you may again find it necessary to create the parent windows of these objects as early as possible.
The component does have a small set of properties that it will use primarily for file management when it is used in an application. Especially when its services regarding the opening and saving of files are used. Again, this is typically in an application rather than when the component is used in support of persistence in an object.
These properties are shown in the following table.
Table 1 GSystem Properties Control Properties
|
debuggingEnabled |
True/False |
Causes messages concerning invalid usage of the component to appear during run time. Intended for the developer while the application using the component is under construction |
|
FileAllowedExtensions |
string |
A semi-colon delimited list of extensions that represent the “types” of files originally listed in the File-Open dialog box. |
|
Example: *.dat;*.exe;*.txt |
||
|
FileName |
String |
The current active file |
|
FileSaveOpenText |
String |
The text in the title-bar of the file open dialog |
|
FileType |
String |
Descriptive text appearing in the “File Type” drop-down box of the File Open dialog box. |
In an application development environment, such as Visual Basic, clicking on the visual representation of the Properties Component should provide an opportunity to set the above properties.
In an environment or situation where the GSystem Properties Component is created at run time, you can set or retrieve these properties at run-time. For example, in a VB application
GProperties.FileName = “d:\temp\settings”
or, in a C environment,
iProperties -> put_FileName(L”d:\\temp\\settings”);
where iProperties is a variable such as that retrieved on page 4. Note the use of the L”” macro in the C example, which causes the argument to be passed as an OLE safe string. Even if the GSystem Properties Component is placed on a form at design time, you can still override these properties with calls as in the above example.
This section has described how to instantiate a GSystem Properties Component and use it to load a set of properties that the component will manage.
The true power of the Properties Component is more than just the simple storage of the values using the persistence mechanism. There are two other key areas where the GSystem Properties Component is intended to be useful. These are:
The first topic is the subject of Part IV next. Part V will deal with the second topic, property pages
Each “property” you create with the GSystem Properties Component is a full featured object in it’s own right.
The fact that these properties can contain data, just like variables do, and that this data is persistent, that is saved and restored across instances of an application or object, is but one of the important aspects of properties.
Properties have other features, however, that make them integrate well with important parts of your software system. This feature set easily maintains the “connection” between the property’s underlying data, and the current state of the system in such a way that frees your software from many of the details of managing the relationship between windows and the data within your system.
As an example, you can create an array property and provide it with the set of strings you’d like in a list box control. Then, you simply tell the property to set the contents of a list box control and the list box is populated. You no longer need to clear the control and loop over your array of strings inserting them one-by-one into the control. You don’t even have to maintain an array of strings, the Property object will be doing that for you, maintaining any additions or changes through user action along the way to boot.
Another way to use an array property is to associate it with an array of windows controls! This way, you can have a single property represent all of the data on an entire window or dialog. For example, if the array property contained an array of VARIANTs, and if you gather up the window handles of all of the controls on a dialog into another array, then, you can make a single call, passing the array of window handles, to the array property in order to update the entire window. Similarly, you make a single call to update the array property with the current window values. Add to this the powerful persistence features and process integration that the master GSystem Properties Component provides and you can persist an entire window with 1 property and 2 simple calls to its methods.
Thus, properties contain a rich set of features that you can use to make your connection between system data and windows easier to manage. Before discussing these, however, we will cover the details about how to make properties and what sort of “properties” properties have, such as type, contents, etc.
The GSystem Properties Component can be thought of as the Mother of all properties. Generally, you will “Create” a property object by making a call to the Add() method on the GSystem Properties Component:
IGProperty* pIGProperty_interface;
GProperties.Add(“property name”,&pIGProperty_interface);
Where GProperties is an instance of the GSystem Properties Component (strictly, a reference to it’s default interface). The second argument is the returned interface pointer to the created property object[5]. Both arguments to this call are optional; the name of the property can be left unspecified – in which case the system will generate one, while you don’t have to obtain the reference to the created property (the second argument) unless you want to. For example, if you are going to call methods on this property in the future from within your application.
In any case, the master Properties Control offers a method to retrieve one of the “named” properties if you need a reference to it on an occasional basis.
Note that you do not need to AddRef() and/or Release() the property interface returned because the GSystem Properties Components owns this object and will manage it’s lifetime. However, the interface does represent a COM object, so if you QueryInterface() through the property to retrieve any other interface from it, you must, of course, Release() that interface.
Because a property is a pure COM object in it’s own right, you can also create them through COM, for example, with CoCreateInstance() from C/C++, or, CreateObject(‘GSystem.GProperty”) from VB, among other techniques. If you do this, you would then call the Include() method on the GSystem Properties Component. Further, you must manage the lifetime of the object, i.e., Release() the reference to it when you are finished with it.
There are several different types of properties that may be maintained by the system. These are represented by the PropertyType enumeration defined in the type library embedded in Properties.ocx and in the Properties_i.h header file.
Table 2 Property Types
|
Property Type |
Value |
Purpose |
|
|
TYPE_LONG |
0 |
Any long
value. 32 bits |
|
|
TYPE_DOUBLE |
1 |
A double
value. |
|
|
TYPE_SZSTRING |
2 |
A typical
“C” null-terminated array of characters, the last being a ‘\0’ |
|
|
TYPE_STRING |
3 |
A BSTR type,
the new OLE safe wide character string. |
|
|
TYPE_BINARY |
4 |
Any binary
value, can be used to store any structure, such as the SIZEL or RECT
structures. |
|
|
TYPE_BOOL |
5 |
True or
False |
|
|
TYPE_VARIANT |
6 |
Any VARIANT
value. Cannot be VT_DISPATCH or VT_UNKNOWN |
|
|
TYPE_ARRAY |
7 |
An array
type, strictly a SAFEARRAY, can be an array of any type, i.e., an array of
VT_VARIANT or an array of VT_I4, for example |
|
|
TYPE_OBJECT_STORAGE_ARRAY |
8 |
This type is
explained on page ____ |
|
Though there is a “type” property (the method is put_type() for C clients or just “type = ___” for other environments) for each property object, you don’t typically have to set this value. When you set the value of a property, it will learn of its intended type. Depending on the development environment you are using, the argument passed to put_value[6]()for the property object may be different than what you are expecting. For example, since this argument is a VARIANT, one development environment might pass a literal integer as a VT_I2, while another might create a VT_I4 argument to pass.
Only in the case of problems in these interpretations would you find it absolutely necessary to set the type of the property.
There is one type of property, the TYPE_OBJECT_STORAGE_ARRAY that is for a very powerful special usage described next.
It is important that you create properties in the same order all of the time so that the system can save and retrieve the properties without mixing up which part of the storage belongs to which property.
There is a special case where this is an inconvenient restriction, when you have an arbitrary number of COM objects that your system is using at any given time and when you are asked to persist your property space. This is an inconvenience because you don’t know up front how many of these objects you’d like to have stored on the storage, yet the system wants you to not “create” properties after the saving and loading processes have completed. Strictly speaking, you could add properties to the list of the properties after loading but before saving if you are sure to create the same number of objects, in the same order, the next time your application or object starts up. Though this latter case offers a solution to the problem, it is difficult to setup and manage.
A much better approach is to use the special TYPE_OBJECT_STORAGE_ARRAY property type provided for situations like these (though, you will see that this type of property has a great deal of potential in being able to store anything you want).
This special property type is used to indicate that you have 0 or more “sub” objects that you would like to store within your properties’ storage space, but at any given point in time, you don’t know exactly how many of these you will be storing. Since you create one instance of this property (or more if you need to, the point is you always create the same number of them) when the application or object starts, there will be no confusion when the system encounters this property in the stream of storage where the properties’ values are stored. Then, when you are notified by the system that it is a good time to prepare for saving, you “add” objects to this array of objects, all stored within this one property, so that the system can include those “sub” objects’ storage information all embedded within this one property.
Here is the call to “add” an object to the property:
pProperty.addStorageObject(IUnknown* pObject);
This call notifies the property of type = TYPE_OBJECT_STORAGE_ARRAY in the pProperty object to add some object to its list of objects that it will maintain the next time the property is saved.
Be aware that the property will attempt to retrieve an IPersistStream interface from the passed in IUnknown interface and, if successful, obtaining that interface will cause an AddRef() on the property.
For the above reason, you should call removeStorageObject() (or clearStorageObjects()) as soon as the following step is completed.
When you are ready to have the property update its internal value based on the state of the objects it is maintaining, you call
pProperty.writeStorageObjects()
and the property will represent all of the persistence data of all of the objects previously added to the list of objects this property maintains.
Then, because the objects will have been AddRef()ed as stated previously, you should call removeStorageObjects() or clearStorageObjects():
pProperty.clearStorageObjects()
which will cause the previously obtained pointer to the objects’ IPersistStream pointers to be released. Typically, you add all of the objects, write the storage, and clear the list all at the same time when you want the property to “contain” the current state of all of the sub-objects. Thus, you normally do this in the SavePrep() method on your IGPropertiesClient interface.
One thing to keep in mind is that when you attempt to “restore” these properties from the loaded storage, you have to know how many there are. You can retrieve this information using the storageObjectCount() property:
from C/C++:
long cntObjects;
pProperty.get_storedObjectCount(long* cntObjects);
or, VB
Dim k as long
k = property.storedObjectCount
Using this count of objects, you can create these sub objects at load time (using COM or whatever technique you used to create them before), add them to the property using addStorageObject(), and then, have the system restore them to their previous state:
pProperty.readStorageObjects();
Note again that these sub-objects are simply COM objects. There is no restriction on the size of their saved data, nor is a there any requirement that these objects be built using the GSystem Properties Component. The only requirement is that they support the COM interface IPersistStream (which any object of merit will do). When the GSystem Properties Component saves these objects, it provides the objects with an implementation of IStream upon which they in turn write their persistence data. The GSystem Persistence Object then saves this data as the persistence data for those objects (the objects that have been “added” to the property).
As you can see, the use of this type of property has extensive possibilities, not the least of which is to use the technique to store any single object’s information within your own object’s data. However, for that purpose, i.e., just one object when you know there will always be 1, the GSystem Properties Component provides another technique. This is the AddObject()method which tells the master component to include a foreign object’s persistence data anytime your own software’s data is saved or loaded. This is the preferred method of saving “known” objects, that is, objects that exist in your development environment and for which there will be no more or less of at any time during the running of your software.
Note, in this discussion, “storage” is used to denote your application or object’s memory space.
One of the tasks you undertake when using Properties is to ensure that the value of the property is appropriately “in-synch” with the data[7] in your application so that when your properties are saved to the storage, they have the correct value. The necessity of performing this task is the reason why there is a SavePrep() call on the IGPropertiesClient interface, so that you can concentrate all of the code that would be required to do this in one place.
However, there is an automated way to ensure this association is always valid by simply giving the property physical access to the data’s memory. This technique is not available in all environments but where it is available it is easy and effective to use.
To use this technique, you pass the address of a variable, and the number of bytes it occupies in memory to the property which you want to associate that variable with.
propertyObject.directAccess(
propertyType,&memberVariable,sizeof(memberVariable));
Here, the property type is provided from the above table, the &memberVariable is the address of some variable in your system, and of course, the sizeof() operator provides the system with the extent of this space.
Behind the scenes, this causes the property to use the data in that memory space every time it is asked to provide and/or set it’s underlying value. Though this technique is very efficient, you must be aware of the issues involved with using it.
1) The property object must have the “rights” to access that memory space. It must be able to both read and write into that space as long as the property object is using it.
2) Manipulation of that space must have no impact to the development environment in use. That is, the memory will be manipulated external to the development environment and that must not impact the environment’s own manipulation of that memory. Keep in mind this has nothing to do with the ultimate “container” application that will be using an object developed using this technique.
In general, if the development environment
will give you access to this memory, i.e., through a reference to it, then it
is at least worth attempting whether you can pass that reference on to the
property object to see whether it will work in that environment.
To clarify, if you are building an ActiveX control using MFC, then you can
certainly pass the address of a variable to the property component and the
property component can utilize that space. However, in an environment like
PowerBuilder, it is unclear whether you
can access underlying memory for variables and therefore whether you could then
use this technique.
3) The “lifetime” of the variable whose memory you are providing must be at least as long as the property object.
This simply means that the memory must not “revert” to use by some other part of the system after giving it to the property object. For example, by passing the address of a variable created on the stack will cause immediate and severe problems when the property object tries to write to that address in the future.
However, the address of a member variable of some C++ object, for example, the object that is an ActiveX control you are developing, may be legally provided to the properties object.
4) This technique will not work in a DCOM environment or across different processes or anywhere where marshalling is required.
The GSystem Properties Component was not developed as a distributed component. It is a development tool that is tightly integrated into your own code. Even if you do set the registration information correctly to use the component in such an environment, this technique will not work properly under these conditions.
In lieu of the above technique of providing the connection between your data and the property’s data, you need to ensure that the value of the property is valid at certain times.
Most of this discussion is held in the description of the IGPropertiesClient interface where this activity happens at the level of the GSystem Properties Component rather than at the level of each property.
However, what is important from that discussion is that at certain times, your code must set the value of the property so that the correct value will be saved on the storage. In particular, your software is notified just before this is going to occur.
One other technique, however, that you can use is to always keep the property updated whenever the underlying window object, or system variable, changes. For example, if you want to save the contents of an Edit control in a property, then, in the OnChanged() event of for that control, simply set the property there and the property will be valid when it is stored. Similarly, in your application’s code that runs after the properties are loaded, simply set the contents of the edit control from the value of the property.
There is an entire class of functionality provided by the property object that can be used for this purpose, i.e., for exchanging the property’s value between a windows object and the property object itself. These methods are discussed in detail in Part ____. However, the following will serve as a general introduction.
When you want to set the value of some window object, a control or other child window on a dialog or window, you can call the “setWindow…” methods on the property object.
The purpose of these methods is to allow the property to set it’s value into the appropriate window control in a single call rather than retrieving the property’s value and then using the windows API or other development environment technique to get that value into the control.
For example, the setWindowListBoxList() method will set the contents of a windows list box based on the current array of strings in the property[8]. You no longer need to manage the actual list box using this type of property. All you have to do is to be able to get the “handle” to the list box window, this is the hWnd property of any control on a Visual Basic form. Another class of these methods takes a child window identifier as well as the parent window handle. The above method name becomes setWindowItemListBoxList(), and the hWnd passed is the handle of the form while the numeric ID of the particular child window involved is also passed[9].
Following the example, when you want to retrieve the value of a list box control, you call the getWindowListBoxList() method on the array property’s interface. Again, gone is the special code you have to write to loop through the list box to retrieve the values into your object or application. You don’t even have to manipulate the array entries in the array property, all of that is handled by the property itself.
In general, these methods are straight forward and self explanatory. For detailed reference level information for the rest of the methods in this class, please see section ___ on page ___.
However, there is one type of property and window access method combination that is particularly powerful in a given type of use. That combination is described next.
As mentioned before, an array property may contain any number of elements holding any type of data in each element.
If you have defined a property of this type, and if you then create an array of longs representing each of the controls on a given window, you can save and restore the entire window using just this one property.
To get an array of controls, you simply need to create an array of long values with the same number of values as there are controls.[10] Then, you fill up the values of this array with the window “handles” of each of the controls. Further, in some environments like Visual Basic, the system may provide convenient access to this “array” of windows by the use of an internal mechanism, like, in VB’s case, the “Control Array”.
In any case, once you have your control array populated[11] and you have an existing array property with the same number of values in it, you simply call:
pArrayProperty.setWindowArrayValues(controlArray);
which will populate each of the windows represented by controlArray with the corresponding value in the property pArrayProperty.
Note that the two arrays do not have to be the same in terms of dimensions, only that they have the same total number of elements. In fact, it is perfectly legal for the array dimensions to be different.
It is important to understand, however, the order that the entries in arrays are accessed by the GSystem Properties Component. For reference, the order of arrays processed is as follows:
a. For 1 dimensional arrays, from the lower bound to the higher bound.
b. For multi-dimensional arrays, the right-most index moves fastest, i.e., if the array is dimensioned like
[0-2][1-3][0-4]
the first element considered is (0,1,0), the second is (0,1,1), then, (0,1,2) and so on.
This order will always be followed. Therefore, you must populate the array values into the array property in such a way that the control you want to be associated with each value is “under” that value when the arrays are traversed side by side. The best way to understand this issue is to see it in action by trying it out in your own code.
When it becomes time to perform the opposite action and update the value of the properties from the current state of the window, perhaps just before saving the properties to storage, you make a similar one line call:
pArrayProperty.getWindowArrayValues(controlArray);
which takes the current contents of each window and puts it in the associated element in the array property. This property is now ready to be saved on the storage. When it is reloaded, the first action is taken and the entire window takes on the exact appearance it had when it was last saved. Using this technique, you can create “record oriented” windows in your system that swap entire records into and out of the window simply and efficiently. Indeed, saving the single property as one instance of the record.
Again, your software system will create a group of properties and will have the values pertinent for each property maintained on a storage in such a way that the properties can have the same value they had when previously written to that storage.
Keep in mind that the order that these property objects will be restored from this storage is exactly the same order that they were saved to it, which is the order that the properties were Add()ed to the GSystem Properties Component.
Additionally, you may have instructed the GSystem Properties Component to save the “contents” of entire sub-components on the same storage that is used to maintain your own properties – persisting other objects such as common windows controls and other ActiveX controls.
Because of all of the data that is being maintained on this storage, it is important that you “create” your properties in the same order every time your application starts or your component is created. This is, of course, very natural because the order of execution of this code is highly predictable and consistent from run to run. Except, perhaps, in rare cases would you be creating a different “set“ of properties, or creating them in a different order.
By using the debugging property on the GSystem Properties Component, you will be notified if the system encounters data that has seemed to “shift” from run to run. If, for example, you add a property to your system, recompile it, and when running it, open up a file containing the persisted data, the system will see that the data no longer fits the “set” of properties defined and will pop up a message indicating this. Typically, when that file is re-saved (or a component is saved to it’s client supplied storage), then this problem will not occur the next time that file is opened.
A sample Visual Basic application has been included with your package which will demonstrate how to achieve results from the GSystem Properties Component in the development of an application.
This sample is the VB project PropertiesSample.vbp. If you cannot find the sample, please feel free to download it from InnoVisioNate.com.
If you do need to re-download the sample, it might also necessary to reinstall your copy of the GSystem Properties Component.
In any case, please ensure that, when you open the project in Visual Basic, the “G System Properties” component is referenced, press Ctrl-T to raise the components dialog and confirm this. The component should also be listed in the dialog accessed through the “Projects – references….” menu item. A sample ActiveX control, built, by the way, using the MFC Control example project, FunctionContainer.dsp, is used in the VB sample. This control’s component is registered as “FunctionContainer ActiveX Control module” which, as a component, should have been registered when the GSystem Properties Component was installed.
This sample application has many comments that are intended to take you step by step through the requirements of achieving persistence. We try to match up the comments with the flow of this section of the document. To the extent that the sample may get updated, however, it will of course drift slightly from that goal.
Here is the step-by-step procedure for using the GSystem Properties Component. If you prefer a less procedure oriented description, you may use Part ___, the non-procedural description of each aspect of the component.
Put the control on your main form by dragging it off the palette. The icon on the palette looks like this .
You can reference the control throughout your system. In source files (or modules) other than the main form. You may need to qualify the object with the name of the form where you have placed it. For example, if it is on Form1, and you have called it GProperties, reference it with
Form1.GProperties
This provides the convenience of being able
to manipulate the control within the development environment.
However, keep in mind that you might have a
situation where you want to have several of GSystem Properties Controls at
run-time. Perhaps your application or system is divided into several “logical”
areas where it makes sense to have a persistence mechanism for each. This
allows you, for example, to save settings in one area of the system while
saving different settings in another area. This is entirely conceivable, for
example, when you have a “record” oriented screen and you might provide the
selection of which record the screen represents in some other area of the
application. In such a case, you would have persisted each record for the
screen and would load the appropriate persistence file as appropriate as the
user chooses to view a particular record.
Such a use makes the GSystems Properties
Component viable as a simple and convenient “database” tool.
In cases where you will have more than a few,
or even an unknown number of, instances of the Properties Control, you can
instantiate the controls at run-time. Here we instantiate an
nvGSystemProperties (non-visual) object:
Dim
nvProperties As nvGSystemProperties
Set
nvProperties = New nvGSystemProperties
Then, we can use the variable nvProperties
wherever we need as if it were a GSystem Properties Component placed on a form;
the interfaces and capabilities are identical to each other, you just have to
set the properties of the instantiated object at run-time
Regardless of how
you created your component. You may need to set the properties of it.
For example, you can
provide a file name and/or file path to open or save the persisted data to.
There are only a few such properties in the Properties Component and setting
them is optional; a default action will occur if the properties are not set.
For example, if your application attempts to open a persistence file, but you
have not set the filename property, the system will raise a “select file”
dialog box to the user.
The GSystem
Properties Component is the master of all properties. You use the control to
create the actual property objects. These objects are instances of the GSystem
Property object, which has a rich set of capabilities in its own right (please
see Part 0 on page 1).
At this point, you
create the properties by calling the Add()method on your Properties Component[12]. For example
GProperties.Add(“property
name”)
creates a named
property that will be used to hold any value, when this property gets a value
assigned to it, it will take on one of the special property “types” previously
described.
The property name is
provided so that you can retrieve and/or access this property later on, for
example, by calling the Property() method on the GProperties component
GProperties.Property(“property name”) =
“string value”
or,
Dim prop as GSystemProperty
Set prop = GProperties.Property(“property
name”)
prop.getWindowText Form1.someControl.hWnd
The first method
allows you to use the property object directly without putting it into an
intermediate variable. If you need to retain that property object, however, you
can do so by holding onto the property object returned from the Property()
method in an appropriate object variable. In such a case you might as well keep
the property as soon as it is created using the Add()
method on the GProperties object, note that in this case, an actual
name is not necessary[13]
because you never have to go back to the GProperties object and search for the
property of the given name, since you already kept a reference to it.
Dim formWidth as GSystemProperty
Set formWidth = GProperties.Add(“”)
will create a
GSystemProperty object and provide a reference to it in the variable formWidth.
Now you can use this
variable formWidth just like any other variable in your system[14].
When you reload the persistence file for the system, the value of the property formWidth
will automatically contain the same value as when you last “saved” the
persistence file.
Here’s how to
persist on the width of your main window.
In the resize event
of the form:
Private
Sub Form_Resize()
formWidth
= Width
formHeight
= Height
End Sub
This code will keep
the associated properties “up to date” with respect to the current window
state. Now, when the persistence file is saved and subsequently reloaded, the
values of the properties will be consistent with the last time (in the previous
run!) that the window was saved.
GProperties.OpenFile(“persistence
file name”)
Form1.width
= formWidth
Form1.height
= formHeight
Just after opening
the file, you can be assured that the properties have the correct value.
Since other objects
are likely to be a part of your application, you can use the GSystem Properties
Component to manage the run-time persistence of these objects too.
Note that these may
be other components as well as some of the enhanced windows controls such as
tree and list views. What is important is that the particular component must
implement one of the COM persistence interfaces, which will usually be the
case, otherwise, the control would be of little use as in real environments.
You allow the
GSystem Properties Component to handle persistence for these objects with the AddObject()
method.
GProperties.AddObject
listView.Object
Here, the properties
component is being passed the underlying “object” of the control on the form
that has been named “listView”. Note that the “.Object”
qualification is necessary as without it, you will be referring to an object
that Visual Basic creates in support of hosting the actual object in the VB
environment.
Once you have
provided this information to the GSystem Properties Component, it will handle
every aspect of persistence from that point forward. In particular, any time
the storage is either saved or loaded, the Properties Component will instruct
each associated object to save or load itself as appropriate. Additionally, if
supported, objects will also have the opportunity to initialize their defaults
whenever appropriate.
It is important to
understand that when the GSystem Properties Component has saved and/or restored
the properties from the storage, you as developer are responsible for updating
your application with whatever state reflects the new values of these
properties.
Whereas this may not
be extremely complex, it is typically desirable to have all of the software
that achieves these goals in one and only one place. It is also desirable to
allow the GSystem Properties control access to this code at times of its choosing. This subtle point is
because it is, after all, the GSystem Properties Component that knows precisely
when the properties have changed, at least from having been loaded from a
storage, or when the properties need to be current, as, for example, just
before saving the properties to a storage.
For these reasons, a
client of the GSystem Properties Control, that is the software you are developing,
is able to provide just such a set of functionality and can notify the GSystem
Properties of where this functionality is and therefore let the Properties
Component access these services as the needs arise.
This functionality
is embodied in an interface called the IGPropertiesClient interface. Note the
use of the suffix “Client” in the GSystem family of components means that the
user of the GSystem component, the client, is going to implement the interface.
In the Visual Basic
environment, you must create a “Class” by adding a new “Class Module” to your
project. Note that the first statement in the new file is:
Implements
IGPropertiesClient
This statement tells
Visual Basic that you agree to provide all of the functions or subroutines that
are defined as being in that interface. To see what these are, you can browse
the object that declares the
interface, press F2 for this function. If you look at the GSystemPropertiesCtl
library, you will see the interface IGPropertiesClient. Selecting this interface
will show you all the functions that you must implement in order for the class
module to compile.
You can cut and past
the entire PropertiesClient.cls file from the PropertiesSample application and
simply delete any irrelevant code internal to each function/End Function or
Sub/End Sub pair.
In fact, in general
you may only need to implement three[15]
of these subroutines, the SavePrep(),
InitNew(), and the Load()
subroutines.
Here is the very
simple intent of each of these functions.
SavePrep()is called when it is time to update the value
of your properties because they are about to be written to a storage. Usually
you have one line of code per property. This line of code will assign the value
of the property based on whatever that property is associated with in your
application.
Of course, if you
had been using the property as if it were a variable, the value will be up to
date at all times and this step would not be necessary for that property. Often
times, however, you may need to take the value in a control, for example, an
edit field, and set the value of the property with this value. There are
several “direct” ways to accomplish this, for example, to update the property’s
value based on a particular control’s current contents,
theProperty.getWindowValue
Form1.editControl.hWnd
where theProperty
is a property object variable (or use the master Properties component’s Property(“property name”) method) and editControl is the name of an entry field on, for
example, Form1. The hWnd property of any window is the window “handle”. You
need to pass this to many of the Property methods when you want to associate a
window control with the property value.
Note that this
example retrieves the value showing in the edit control into the value of the
property without having to format that value based on the “type” of the
property. Specifically, if the property is of TYPE_DOUBLE, then the value of
the property will be correctly formatted from the text in the edit control; you
do not have to deal with that at this point. Part ____ deals exclusively with
these tools.
InitNew() is generally only called when the GSystem
Properties Component is used in support of a COM object that you may be
developing. Specifically, when a COM object comes to life for the first time,
i.e., it is not “loaded” from a storage, then the InitNew()
method is called. In our case, since we are writing an application, it is
likely that InitNew() will never be called.
However, it is
convenient to use this method as a place for all the code that will be the
“default” for your application. Then, at that point where the user asks for a
“new” state of the application, your code can call[16]
InitNew() to set all of the properties to the default values, whatever they may
be. Perhaps the defaults are stored in yet another storage managed by a
different GSystem Properties Component elsewhere in the application.
In keeping with this
strategy, then, first set all of your property’s default values in this method,
again, usually one line of code per property. Then, at the end of this, make a
call to the Loaded() method, described next, because this latter method is where you update
the visual state of the application based on the current value of all of the
properties.
Loaded()
is called by the
GSystem Properties Component just after having read all of the properties from
the storage. It is here that you make the opposite association than you did in SavePrep().
Generally, any updating of your windows or other content in your system that
reflects the current properties is done here. Any aspect of your system that
depends on the value of the saved (and now restored) property should be set in
this subroutine.
Saved() is called by the GSystem Properties
Component after the properties you
are managing have been saved to the storage. It is important not to confuse
this subroutine with the SavePrep() subroutine.
IsDirty() is called by the GSystem Properties
Component to determine if any of your properties are “out of date”, or have changed, since the last time the
properties were saved. This helps to
determine if it is necessary for you to prompt the user for a file name (which
you can use the GSystem Properties Component to do for you), or if you should
provide similar logic to prevent the current state of the system from being
overwritten. Your implementation must return a zero value for “Yes the
properties have changed”, or a non-zero value for “No”.
Since you are
writing an application, your system must initiate the persistence process so
that all of the properties maintained by the GSystem Properties Component
ultimately get saved to the storage.
Remember that this
is not the actual manipulation of the binary data representing the storage,
rather, it is a set of higher level tools that allow you to provide
functionality the user will use (or which you use entirely within code) to
manage the set of properties as a file.
In order to use
these features, you generally implement menu events that will initiate dialogs
with the user as appropriate.
However, it is
constructive to consider an elementary case first that will be used to describe
the basic functionality.
Assuming there is an
existing file on your user’s system that had been previously saved using the
GSystem Properties Component, you can load that file using code such as:
Dim
fileLoaded As Boolean
GProperties.filename
= filePathAndName
If
Dir(GProperties.filename) <> “” Then
FileLoaded = GProperties.LoadFile
Else
GProperties.New
GProperties.Save
End If
Which will initiate
the process of restoring the persistence data within the file with the given
name. Perhaps the filePathAndName value is one of the arguments passed to the
application’s executable, thus naming a “startup” file, for example. If the
file does not exist, the New() method is called and the persistence data is
immediately saved, thus creating the file. Note that you may ship such a file
with your application if you’d like to specify the startup state of the
application the first time it is run by your users.
At any point in your
application, you can start the saving process by instructing the GSystem
Properties Component to save the properties to the file[17]
using the following code
GProperties.Save()
This will cause the
system to store all of the properties to the file whose name is the value in
the FileName property of the GSystem Properties Component.
For a fully
automated system, you can have this method called as your application is
shutting down, for example in the Unload() event of your main form. You must be sure,
however, that any of the windows needed by the SavePrep()
method you write are available to that method whenever Save() is
called.
You can have the
GSystem Properties Component initiate dialogs that allow the user to specify
where to save the current properties file and/or which file to open. Note that
most of the “properties” on the GSystem Properties Component are provided in
support of this activity.
In contexts where it
makes sense for your users to save the properties you have defined for your
application, you provide the menu items or command buttons that initiate these
calls to the GSystem Properties Component:
New() will cause the properties to take on the
“default” values specified through your implementation of IGPropertiesClient
(if any). This method will result in the creation of a temporary file so the Save()
(see below) method will work if called subsequent to this.
LoadFile[18]() is used to read the file associated with the
current values of FileName. As described in the previous section.
OpenFile(string
fileName) causes
the named file to be opened and the “restore” process will begin. Note that
this means the Loaded() method on your implementation of IGPropertiesClient will
get. With this method, the passed in parameter represents a file that already
exists, the method will fail if it does not – successful operation is indicated
by a 0 return value.
Open
() will cause the Properties Component to
raise a system dialog that will prompt the user for the name of a file to open.
The method will return the name of the file chosen by the user, or an empty
string if the user cancels the selection dialog.
If the user does not cancel, however,
the persistence data from the file will be Loaded()
as in the previous method.
Save()
will cause the system
to save all of the current properties in the file whose name is the value of
the FileName property.
SaveAs() will cause the system to prompt the user for
a file within which to save all of the properties. As with the OpenFile()
method, this will return the name of the file actually chosen by the user, or
an empty string if the user canceled the operation.
SaveTo(string
fileName) causes the
named file to be the current file and all of the properties’ values are saved
to it.
The other major area
of functionality the GSystem Properties Component can provide for you is with
Property Pages and settings dialogs. The discussion of these powerful
techniques is in Part V.
A sample object is
included with your package. This object is a sample version of the GSystem
Function Generator. It is equivalent to the full version of the control except
that it does not fire some of the events necessary to achieve complete
functionality with the control.
However, code
relevant to this discussion is included where necessary to make the
illustration. Further, this object is developed using raw COM, i.e., it does
not use any MFC or ATL technique associated with COM. Therefore, the samples
shown will not be reliant on any ActiveX development framework. However, two
samples using each of these popular frameworks are provided with this system
and certain sections of this part of the document will highlight differences in
the approach where necessary because of
the use of these other development tools.
If you are developing an object or component,
one easy client, or container, you might find is the ActiveX Control test container
accessible from Microsoft Visual Studio™. If you create an instance of an
ActiveX/COM object in the test container, the container will persist the object
when you save the it in the test container’s file format; ‘.tcs files. For the
purposes of testing persistence, this tool can be more than adequate.
Note that Part II of this document was built around a sample
Visual Basic application. This application uses the sample object[19]
under discussion in this section. Therefore, you might find that the Visual
Basic sample is also a suitable client within which to test your object as you
learn about the topics in this section since using the VB sample will allow you
to see the use of the GSystem Properties Component from its other main
perspective – persistence at the application level.
The goal of this exercise will be to show
precisely how a COM object should implement the GSystem Properties Component in
such a way to achieve persistence and property page support for the object.
This will be accomplished by demonstrating the specific C++ code in the object
that is related to the use of the GSystem Properties component.
If you haven’t already done so, you need to
install the GSystem Properties Component for this exercise. The GSystem
components are shipped with an installation utility. However, the only
important accomplishment of installation will be to “register” each component.
All of the GSystem components are built such that this is also the only required
step to make each component functional on any host system.
Specifically, all you have to do is to “open”
each relevant ‘.ocx file with the program regsvr32.exe; make sure the ‘.ocx
files are where you want to permanently store them and either use explorer to
left-click and “open” the file with regsvr32.exe (regsvr.exe on Win 95), or
type regsvr32 filename.ocx at a DOS prompt. Please make sure that the files
Properties.ocx and FunctionSample.ocx are registered for this exercise.
Note: If you want to implement the Properties component using COM in a C or
C++ environment, you may want to use the interface definition files provided
for this purpose. The installation utility will place the files Properties_i.h,
Properties_i.c, and Properties.tlb in the same location as the Properties.ocx
file. You may need to manage the location of these files, i.e., put them where
your development environment can find them.
In the ActiveX Test Container, create a new
GSystem Function Sample using “Edit/Insert New Control...”
The above figure shows the GSystem
Function Generator Sample. To see persistence in action for this object, change
the expression and/or click on either of the “variable” tabs (x or y) and
change any of the settings for the variables. Then, you can select ‘File/Save”
from the Test Container’s menu, save the “object” to a file and later open that
file, you should see the settings you made for the ‘x’ and/or ‘y’ variable have
been retained.
Note that the properties that were changed,
and subsequently saved, did not have to be set by the user using a “properties”
dialog. While such functionality is a big part of the GSystem Properties
Component, it is also easy and natural to connect the properties with running
code in your object. For example, if you had written the function control, you
might have the following code in your OnChanged() method for the expression entry field:
pIGProperties
-> GetWindowItemValue(
“expression”,hwnd,IDDI_EXPRESSION);
In the above,
pIGProperties is your pointer to the GSystem Properties Component object’s
IGProperties interface, the “expression” argument is the name of the specific
property, the hwnd argument is the window handle of the dialog containing the
edit control and the IDDI_EXPRESSION value is the id of the edit control in the
dialog (hwnd).
The next section discusses what the GSystem
Function component did to achieve persistence.
The GSystem Function
object obtains an instance of the IGProperties (a GSystem custom interface)
interface by creating an instance of the GSystem Properties Component when the
function is created. In particular, the function object’s class has a member
variable that is a pointer to type IGProperties, i.e.,
IGProperties*
pIGProperties;
The definition of IGProperties (and
other important information) is obtained by the inclusion of the header file:
Properties_i.h.
Note: Anytime you use a specific definition such as provided by one of the
interface definition header files, the compiler will be referencing a variable
of type CLSID. This means that if you specifically include one of these
definition files, then, in one and only one of the source files for your
project, you must “include” the corresponding ‘.c file, (Properties_i.c), which
will instantiate the CLSID variable the compiler references in other files.
Note II: Some development environments have wizards and generated code, among
other techniques, to make using COM objects easy. We choose not to center our
discussion of the GSystem components within the context of these techniques for
2 reasons; 1) because not all users
will have these environments and of those who do, not all are likely to utilize
the techniques, and 2) for a minimal amount of increased complexity, the
material covered is more in line with the reality of what is actually
happening, a knowledge of which can make the technology much more accessible
and understandable in the long run.
Note III:
However, if you are using MFC, you can use the Class Wizard to create a
COleDispatch wrapper class for the GSystem Properties Control. Simply invoke
Class Wizard, select the “Add Class … From a type library” option, navigate to
the Properties.ocx file, and select all of the available classes that appear in
the subsequent dialog, or, as many as you want to use. Note that this will
creates a header and implementation file that get included in your project.
Here is the code, in the constructor of the function object, which
creates an instance of the GSystem Properties Component and obtains an
interface pointer on it:
CoCreateInstance(CLSID_GSystemProperties,
NULL,
CLSCTX_INPROC_SERVER |
CLSCTX_INPROC_HANDLER,
IID_IGProperties,
&pIGProperties);
The GSystem Properties Component is not
written for, nor expected to be used in, a distributed COM environment. Thus,
the CLSCTX_… constant should be one that ensures COM doesn’t end up providing
an instance of the server either out-of-process or from another machine on a
network.
As
with all COM objects, you must call
pIGProperties ->
Release()
when
you are finished with the GSystem Properties Component to allow the object to
free itself.
In
an MFC project, you might create a reference to a COleDispatch derived wrapper
for the object:
DGProperties&
nvProperties = *(new DGProperties());
The
DGProperties class is defined as per the note concerning MFC on page 34. This technique requires you to “connect” to the underlying object at
an early point in the creation of your object, preferably, in the constructor.
nvProperties.CreateDispatch("GSystem.GProperties");
which
allows you to begin using the nvProperties object.
An
MFC project may also utilize the technique described next for ATL projects.
Properties are created and accessed through a name you give them. Here
is code that creates a property for the GSystem Properties Component to
maintain:
IGProperty* pIGPropertyExpression;
pIGProperties ->
Add("expression",TYPE_SZSTRING,
&pIGPropertyExpression);
Note that this
method takes the address of an IGProperty pointer variable so that you can keep
a reference to the property later if you want. You can omit this last argument
if you don’t need this type of access to the property. You will see later ways
in which you can use this interface.
The GSystem Properties Component generally utilizes storage that it
allocates for each property. Our example property definition will create a
property of type “string” (TYPE_SZSTRING) (these constants are defined in
Properties_i.h).
You may want to have the Properties component use storage provided by
its client, i.e., your own software. This is a more convenient way to manage
the relationship between the property and the storage (variable) that the
property represents in your software. You do this by calling the DirectAccess() method on the IGProperties interface.
static
char szExpression[256];
.
.
.
IGProperties
->
DirectAccess(“expression”,szExpression,256);
This way, whenever the char array szExpression is changed, you can be
assured that the appropriate value of the property itself is updated.
It is very important to review the notes concerning the use of this
technique found on page 15.
Without the direct “connection” so established, you would need to
“update” the value of the property just before the object is to be saved.
However, by virtue of this connection, the relationship between your data and
the property you want to represent that data is always up to date, which can
lead to a much easier and natural integration of the GSystem Properties
Component within your system.
The GSystem Properties Component will provide the services that manage
the storage and retrieval of all of the properties you have defined. However,
when you are creating a COM object, you typically need to provide access to these services, where the
client of your object will use these services through the access you provide. This section will describe how you
provide this access.
The container application, i.e., Visual Basic, PowerBuilder, etc.,
will, at times, ask your object for an interface pointer to one of the standard
persistence COM interfaces. It needs this interface pointer in order to tell
your object when and where to store its persistence data. If you are building
an application, you need to know the event or method in your application that
will be called by the system just before or during persistence. Or, as
described in Part II of this document, you can take control of the
process and initiate the saving and restoring of properties yourself.
If you are building
a COM component, you must return the value of QueryInterface()on your saved pIGProperties interface pointer any time your object is
asked for one of the persistence interfaces. In other, words, you forward a
request for an interface having to do with persistence on to the GSystem
Properties Component that you are using.
Here is the relevant
code from the Function Object’s implementation of the QueryInterface()method of IUnknown:
.
.
if ( riid ==
IID_IGProperties )
return pIGProperties ->
QueryInterface(riid,ppv);
else
if ( riid ==
IID_ISpecifyPropertyPages )
return pIGProperties ->
QueryInterface(riid,ppv);
else
if ( riid ==
IID_IPersistStorage )
return pIGProperties ->
QueryInterface(riid,ppv);
else
if ( riid ==
IID_IPersistPropertyBag )
return pIGProperties ->
QueryInterface(riid,ppv);
else
if ( riid ==
IID_IpersistPropertyBag2 )
return pIGProperties ->
QueryInterface(riid,ppv);
else
if ( riid ==
IID_IPersistStreamInit )
return pIGProperties ->
QueryInterface(riid,ppv);
else
if ( riid ==
IID_IPersistStream )
return pIGProperties ->
QueryInterface(riid,ppv);
else
.
.
Note that if you are
building your object using ATL or MFC, you need to take special steps to ensure
that the external (from a client) call to QueryInterface()gets routed appropriately. These techniques are discussed next.
MFC COM objects
typically provide interfaces through the INTERFACE_MAP related macros. In the
MFC internals, a table of programmer supplied “nested” interfaces is searched
for the desired interface when QueryInterface() is called.
This technique does
not work with the persistence interfaces implemented by the GSystem Properties
Component because MFC requires the interface to be nested, i.e., physically
part of the client object’s class, when using the INTERFACE_PART macro.
Further, the INTERFACE_AGGREGATE macro will not work because this is searched
for interfaces after MFC resolves
which interfaces the base class (usually COleControl) supports, thus not
finding it necessary to search the aggregates[20].
The solution to all
of this is to override the base class implementation of GetInterfaceHook(). MFC will call this hook before searching for any interface as a
result of a call to QueryInterface().
Here is the declaration
and implementation of this method that will serve the purpose:
//
Declaration in header file
IUnknown*
GetInterfaceHook(const void*);
//
Implementation
IUnknown*
Function::GetInterfaceHook(const void* piid)
{
REFIID riid = *((IID*)piid);
if ( riid == IID_ISpecifyPropertyPages ||
riid == IID_IPersistStreamInit ||
riid == IID_IPersistStorage ||
riid == IID_IPersistStream ||
riid == IID_IPersistPropertyBag ||
riid == IID_IPersistPropertyBag2)
{
IUnknown* p = CCmdTarget::GetInterfaceHook(piid);
pIGProperties ->
QueryInterface(riid,&p);
if ( p )
ExternalRelease();
return p;
}
return CCmdTarget::GetInterfaceHook(piid);
}
This method will
help MFC to find the interfaces (again, through the object’s cached standard
interface pointer on the GSystem Properties Component). The call to ExternalRelease() is necessary to ensure that the reference count will go to zero at the
appropriate time; MFC will call AddRef() on the object (your COM component) after
getting the interface pointer, so you must call Release()
to balance out that call to AddRef().
Note: The redirection of the
IID_ISpecifyPropertyPages interface is optional. You can simply use the MFC
implementation of property pages automatically generated for you. If you use
the GSystem Properties Component’s implementation of property pages, you will
have much tighter control over the processes involved in setting properties and
you will need to include this interface among those that are “redirected”.
ATL
has a similar mechanism for managing interfaces; it uses a “map” of interfaces
and a looping mechanism over this map to find the appropriate interface at
run-time.
However,
ATL’s implementaion is much more powerful, for example, there are several
different “types” of entries in the interface map. One of these instructs the
looping mechanism to call a certain function when a given interface is
requested.
Here
is a sample from a header file for an object that will redirect persistence
interfaces to the GSystem Properties Component.
BEGIN_COM_MAP(CATLPropertiesSample)
.
.
.
/* Overiding persistence and property pages interfaces
COM_INTERFACE_ENTRY(IPersistStreamInit)
COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
COM_INTERFACE_ENTRY(IPersistStorage)
*/
// Code Added for GSystem Properties Component Support:
COM_INTERFACE_ENTRY_FUNC(IID_IPersistStream,0,QIHook)
COM_INTERFACE_ENTRY_FUNC(IID_IPersistStreamInit,0,QIHook)
COM_INTERFACE_ENTRY_FUNC(IID_IPersistStorage,0,QIHook)
COM_INTERFACE_ENTRY_FUNC(IID_IPersistPropertyBag,0,QIHook)
COM_INTERFACE_ENTRY_FUNC(IID_IPersistPropertyBag2,0,QIHook)
COM_INTERFACE_ENTRY_FUNC(IID_ISpecifyPropertyPages,0,QIHook)
.
.
.
END_COM_MAP()
.
.
.
static HRESULT WINAPI
QIHook(void* pv,REFIID riid, LPVOID* ppv,
DWORD dw);
Note that the wizard generated entries in
this map have been commented out. This is because the GSystem Properties
Component will provide these services. Note also that the classes dealing with
persistence have also been removed from the object’s inheritance structure,
since they are no longer needed.
These
interface map entries tell the ATL interface searching mechanism to call the
function QIHook() any time an interface of the type indicated
is requested. The declaration of QIHook() (it is a static member function of the
class) is as shown above, its very simple implementation is:
HRESULT
WINAPI
CATLPropertiesSample::QIHook(
void* pv,REFIID riid,void** ppv,DWORD dw)
{
CATLPropertiesSample*
pThis = (CATLPropertiesSample*)pv;
if ( ! pThis -> pIGPropertiesClient )
{
pThis -> QueryInterface(
IID_IGPropertiesClient,
(void**)&pThis ->
pIGPropertiesClient);
pThis -> pIGProperties -> Advise(
pThis ->
pIGPropertiesClient);
pThis -> pIGPropertiesClient ->
Release();
pThis -> pIGPropertiesClient ->
Release();
}
return
pThis -> pIGProperties ->
QueryInterface(riid,ppv);
}
The
argument “dw” is the “0” declared in the interface map entry macro.
In
the above example, the interface request is simply forwarded on to the GSystem
Properties Component; the pIGProperties variable is a pointer to the default
interface on that component.
Note
that the example also takes the opportunity to notify the GSystem Properties
Component it will participate in the persistence process. It does this by
getting a pointer to it’s own implementation of IGPropertiesClient and calling
the Advise() method on the GSystem Properties Component.
This
needs to be done before any persistence occurs and you know that no persistence
can occur until the persisting agent (a client of your object) asks for a
persistence interface. Therefore, the first time a client asks for one of these
interfaces is a good time to setup the advise mechanism. Note the two Release()s are required to ensure the calling object
will get deleted at the appropriate time. The object knows it’s the first time
because its member variable pIGPropertiesClient is set to NULL in the constructor and will
be NULL the first time QIHook() is called.
Persistence
of your object or application’s properties is a process that will occur as the
user runs or interacts with his or her application (which would be your client
when you are developing an object).
The
GSystem Properties Component provides two ways that you can hook your own
object into this process. The easier way involves the use of the windows
messaging mechanism; you simply provide the Properties Component with a handle
to a window that the component can then send messages to, using message ID
values that you also provide.
The
more involved method is to provide the Properties Component with an interface
that includes methods that the Properties Component will call at certain points
within the process.
The
easier method is described first.
You
call the PutHWNDPersistence() method on your copy of the IGProperties
interface to use this simpler technique. You can call this method as many
times, that is, for as many windows, as you’d like. For example, if you have implemented multiple windows, perhaps
each depicting a separate object or part of an object, you can pass the handle
to the Properties Component for each of them.
Calling
this method will cause the GSystem Properties Component to send a particular
message to your code (the associated window’s message handler) at key points in
the process. To help describe this, here is a sample call:
pIGProperties -> PutHWNDPersistence(
hwndPersistence,
hwndInit,
hwndLoaded,
hwndSavePrep);
This
call passes the handle of the window in which several commands are available.
Each “command” is associated with the windows command event on each of the 3
sub-windows (controls), typically command buttons, passed to the method.
Provide a NULL window handle for functionality you do not implement.
Each
of the control window handles is associated with the following actions:
When
you use this technique, you put code in your window’s message handler that
intercepts the WM_COMMAND messages for the above command button controls[21]. Note you passed a handle to a window to PutHWNDPersistence() and that this window’s message handler will
receive the messages. You may need to investigate the mechanics of windows
messaging for your particular environment if your find that your code is not
getting called in the expected places and times from the GSystem Properties
Control.
Note: With this form of the persistence process,
you need to make sure that your window (that handles the commands) has been
created by the time the container or client application asks your object to
load from the storage. In general, you can’t know exactly when (after creation)
this will occur, so you may need to create the associated window early in your
object creation. Of course, you can’t call the PutHWNDPersistence() method until after the window has been created. Note also that the window
handles passed to the method must also exist throughout the duration when the
persistence mechanism may be used, which may in turn be as long as your object
exists.
The GSystem
Properties Component is able to communicate with your application or object
using an interface called IGPropertiesClient. This interface is defined in the
file Properties_i.h.
The “Client” in the
name of this interface refers to the GSystem Properties Component. In other
words, the GSystem Properties Component is going to turn around and get
services from your software by calling methods on this interface. While
implementing this interface is more work on your part, the functionality thus
enabled allows your software to participate completely within the process of
saving your application or object to system storage.
In order to take advantage of the functionality described in this
section, you must tell your instance of the GSystem Properties Component that
you support the IGPropertiesClient interface. To do this, you must first obtain
a pointer to your implementation of IGPropertiesClient, usually by calling QueryInterface() for the interface IID_IGPropertiesClient (defined in Properties_i.h).
MFC Users can call InternalQueryInterface(). Or, if you call GetControlUnknown(), this will supply an IUnknown pointer to
your own object. You can use that IUnknown Pointer to obtain your
implementation of IGPropertiesClient:
IGPropertiesClient* pIGPropertiesClient;
pIUnknown ->
QueryInterface(IID_IGPropertiesClient,
(void**)&pIGPropertiesClient);
Carefully note, however, that you must “implement” this interface. This
means you must write the code for the methods in the interface and “connect”
those methods to your object or application. Typically, this means using the
INTERFACE_PART macro, which is one approach in MFC.
Once you have the interface pointer, provide it to the GSystem
Properties Component:
pIGProperties
-> Advise(pIGPropertiesClient);
The GSystem
Properties Component will AddRef() the interface.
The
IGPropertiesClient interface provides a rich set of methods that connect your
software with the GSystem Properties Component. This section has just been an
introduction to this topic. Following is an entire Part of this manual covering
the details of using the IGPropertiesClient interface. Also, you can refer to Page
___ to see the full descriptions of these methods in a reference format.
The COM persistence interfaces offer a
standard mechanism for reading and writing binary data onto a storage space.
Typically, to enable persistence, you need to implement one of the persistence
interfaces and subsequently process the calls clients of your object, or, other
parts of your system, will make on that interface to give you the opportunity
to save and restore.
Because you are using the GSystem Properties
Component, all of the details with regard to the reading and writing of your
property data are handled for you. To make the system as flexible as possible,
the GSystem Properties Control uses a certain process which happens when this
activity (saving or loading data) occurs. The best way to explain the use of
the methods involved is by following this process and encountering each method
as it is used.
It is very likely that you will utilize other
objects in the development of your software. One of the powerful features of
the GSystem Properties Controls is that it can completely cover the persistence
needs of these “other” controls – seamlessly managing the properties of these
controls just as if they had also
used the GSystem Properties Control. In general, all you need to do is to
provide the GSystem Properties Control with access to each of these objects and
the control will store the objects’ data along with your own object’s data.
There will be much more detail on the
techniques used to integrate other software artifacts into your system later.
Depending upon your development environment,
there may be several ways that you “implement” an interface.
Implementing an interface generally involves
two general concepts:
1)
Writing the
code for each of the methods in the interface. If you want to implement an
interface, it is necessary that you write code, probably all within one source
code file, for each method that is in that interface. These methods must have
exactly the signature[22]
specified in the publicly accessible interface definition[23].
The required methods will probably be known to your development environment so
that the environment may be able to let you know if you have successfully
provided all that is necessary.
2)
Publishing the
interface so that clients external to your software will be able obtain that
interface from your object or application. It may sometimes be only necessary
that your application or object provide a
client with the appropriate interface through some “Advise” mechanism. Usually,
in such a case, the “Client” defines the interface and the Server provides an
appropriate instance of the interface to the client.
There are several ways to accomplish these
steps. However, this document will cover only a few that can be used in popular
development environments. If you are using an environment that is not able to
use any of these techniques, you are welcome to check the InnoVisioNate.com web
site for additional information that will become available in the future.
Visual Basic provides the “Implements”
keyword that tells the compiler that this file (an additional class file) will
form the interface (step 1 above). The 2nd step, publishing the
interface is handled entirely behind the scenes within VB.
As you type in the methods (called Functions
and Subs in VB) of the interface, Visual Basic will alert you if you have not
gotten the “signature” of any of the methods correct, that is, if the arguments
or return type are not as specified in the interface definition. Also, in the
VB environment, note that the interface definition is accessible by pressing
the F2 key and looking at the referenced objects’ type library information.
When you create functions or subs in your
implementation file, note that the name of the file is pre-pended with the name
of the interface. Thus, for the IGPropertiesClient interface’s Saved()
interface, the declaration is:
Private Sub IGPropertiesClient_Saved()
End Sub
Until you correctly specify the stub of each
of the required functions or subs, VB will not successfully compile the source
file. Remember that the definition of these functions may also include the ByRef and
ByVal keywords and that the return value of any
functions must also be exactly as specified in the interface definition.
ActiveX Objects using an MFC generated
project are based on the COleControl base class. This class also has as one of
its ancestors, the CCmdTarget base class, which,
in turn, implements a table driven approach to finding and supplying
interfaces to clients.
This table of interfaces is called the
“Interface Map” and you are able to add entries into this map so that
interfaces that you implement will be found there, by the underlying mechanism,
and will be available to external clients.
The step by step procedure to implement the
IGPropertiesClient interface in an MFC project is as follows:
a.
Include the
"Properties_i.h" file in the header file. Usually the control you are
building's main header file. This file is shipped with the GSystem Properties
Component.
b.
Put the
DECLARE_INTERFACE_MAP macro in the declaration of your control's class.
c.
Declare all the
methods of the IGPropertiesClient interface between the BEGIN_INTERFACE_PART
and END_INTERFACE_PART macros, also in the declaration of your control's class.
Here is the relevant
code from the object’s header file:
DECLARE_INTERFACE_MAP()
BEGIN_INTERFACE_PART(GPClient,IGPropertiesClient)
STDMETHOD(SavePrep)();
STDMETHOD(InitNew)();
STDMETHOD(Loaded)();
STDMETHOD(Saved)();
STDMETHOD(IsDirty)();
STDMETHOD(GetClassID)(VARIANT *pCLSID);
END_INTERFACE_PART(GPClient)
d.
Put the
BEGIN_INTERFACE_MAP and END_INTERFACE_MAP macros in your control's
implementation file.
e.
Between the
above two macros, put the INTERFACE_PART macro that places the entry for the
IGPropertiesClient interface into MFC's mechanism for finding interfaces.
Here is the relevant
code from the object’s implementation file:
BEGIN_INTERFACE_MAP(CFunctionContainerCtrl, \
COleControl)
INTERFACE_PART(CFunctionContainerCtrl, \
IID_IGPropertiesClient,GPClient)
END_INTERFACE_MAP()
Note that the example is
for building the object whose main class is CFunctionContainerCtrl,
directly inherited from COleControl and that the GPClient
entry is the same entry in the first position of the BEGIN_INTERFACE_PART macro
used in the header file.
f.
Finally,
implement the code for the interface. The MFC sample included, the
FunctionContainer sample, contains the necessary source in the
IGPropertiesClient.cpp file, which can easily be copied to your own project as
a stub if desired.
An additional note is that you must implement
the IUnknown members of this interface (QueryInterface(), AddRef(),
and Release()). In general, these methods simply
“delegate” to the main object. The above referenced sample provides an example
of this.
Implementing an interface in an ATL project
is somewhat easier than it is with MFC. In ATL, you right click on your
object’s main class in the “Class View” tab of the work prompter, and select
“Implement interface”.
This will present a tabbed dialog that shows
you certain Type Libraries which are known to the project and which describe
interfaces. If the GSystemProperties tab is not present here, click “Add TypeLib…”
button, select “G System Properties”, and click OK.
From the GSystemProperties tab, select the
IGPropertiesClient (and IGPropertyPageClient for later use if you like)
interface and click OK.
This will add the methods defined in the
interface to your object’s main header file, You may want to create a new
source file and move these methods over to it, leaving the declarations in the
header file, of course. Your object will now have the capability to provide the
interface to any external client asking for it, in this case, a GSystem
Properties Component. Note that ATL uses interface inheritance as its mechanism
for implementing interfaces. Therefore, you will see that your object now has
IGPropertiesClient in its inheritance structure.
A new object will be created either by your
object, or by the user initiating the creation of the object through some COM
mechanism, for example, through Insert-Object on a menu. What is important is
that you have “redirected” the QueryInterface() call so that the
GSystem Properties Component provides the persistence services for you, see
page 36.
 Note that the Properties Component will be receiving this call. In order
to give your object the opportunity to perform its own processing at this time,
the Properties Component will then call the InitNew[24]() method
on the IGPropertiesClient interface that you have implemented.
Essentially, the InitNew()
method can be thought of as a “Reset State” function. It is perfectly legal and
appropriate for your code to call this method from any place within your system
when this kind of action is required, for example, a “reset defaults”
implementation.
When the user attempts to save your object or
application, the system will call Save() on some implementation of IPersistStorage
(or IPersistStream as appropriate). Since you have “redirected” this method to
the GSystem Properties Component, it will handle the actual saving of all of
your binary (properties) data to the provided storage.
However, before writing the data, the GSystem
Properties component will call the SavePrep[25]()
method on your implementation of IGPropertiesClient to give you the opportunity
to ensure that all of your properties have the appropriate values and/or to
allow you to do some prior processing. If you do not keep your properties up to
date with the data within your object, you will need to set the values of the
properties here so that they will have the appropriate values before saving. A
simpler method of keeping your data and the property in synch is to use the DirectAccess()
method described on page 35.
That is all that is really necessary in SavePrep().
After SavePrep() has
been called on your object, by the Properties Component, it will actually save
the data using the standard COM mechanism of writing the binary data to the
storage provided by the system. When this is complete, then the Properties Component will call your implementation of Saved() on
your IGPropertiesClient interface.
When your application or object uses other
COM enabled objects, such as ActiveX controls, you can instruct the GSystem
Properties Component to persist their properties as well.
The AddObject() method on the Properties
Component tells that component that you would like to persist that object’s
properties among your own objects properties.
GProperties.AddObject(listView.object)
The above is a typical VB call to instruct
the Properties Component (named GProperties when it was placed on the form) to include
the properties of another object (in this case, one named listView
which might be an instance of the ListView common objects). The ‘.object
qualifier ensures VB passes the appropriate object; without it, VB will pass a
special “container” object it uses to host ActiveX (or other COM) objects.
Note that you use this when there is a known
number and type of “other” objects you are using in your system. If you are
using an arbitrary number of objects, i.e., you can’t know the number of them,
and at least the type of them, use the technique described next.
There are certain situations where you may
not know how many, or even what type of, other objects may exist in your
control or application at any given time.
A clear example of this would be a development environment that must
manage multiple different objects such as those populated on a form in Visual
Basic.
There is a special type of property provided
for just this situation; when you need to have one property that can manage the
persistence information for any number and any type of objects[26].
The full description of this technique is
provided on page 12.
When the GSystem Properties Component gets
the call to load the data, it will call your implementation of Loaded[27]()when
this processing is finished. Typically,
in this method, you have the opportunity to update the contents of your windows
controls and/or fields because you can be sure that the properties have freshly
received their values that existed just before the prior save.
Once you have performed the necessary
processing in the Loaded() method, your object or application’s state
will be entirely up to date with respect to how it was last saved. If
convenient at this time, you can draw your object(s) so that this state is
visually presented to the user.
When your user wants
to alter the properties in the system, he or she will select something in the
application interface that initiates the process. If your software is an object
within another application or container, then that application would be responsible for providing the capability
to initiate the process.
In any case, there
are two different ways that a client may launch the edit properties process.
When the first
method is used by a client, note that you need do nothing else, the edit
properties process is already under way and your property pages (as described
in the next section) will be shown.
However, in the
second case, you need to start the process off yourself. This is simply
accomplished by calling the EditProperties()method on your cached pointer to the
IGProperties interface.
pIGProperties
-> EditProperties(
hwndParent,
L”tab Text”,pIUnknownThis);
This method is
called with what will be the parent window of the resulting property settings
dialog box, the text to put in the title bar of the dialog, and a pointer to
the IUnknown of the object for which the properties are to be maintained. If
you’ve used MFC, you can call the GetControlUnknown() method to retrieve the pointer for the
IUnknown interface to your object.
Note that the parent
window handle is likely to be the window of the container if your software is
an object. In fact, method DoVerb()receives a window handle that is suitable for
this purpose.
Note also that the
IUnkown pointer can be for any object
that you currently have instantiated within your object or system, not just an
object you are creating. This is true even if the object does not support the
GSystem Properties Component property pages as described next. Significantly,
this allows you to initiate the edit properties process for any object for which you have an
IUnknown pointer (which you can retrieve for all valid COM objects on the
currently running system)[28]
At the heart of this
topic is the implementation of the IPropertyPage interface that is defined by
the Microsoft COM standard documentation[29].
If you have
previously used the above interface, you may have discovered some vagaries with
the definition and use of the interface through the typical property page
implementation, i.e., OleCreatePropertyFrame(). If so, you may appreciate that the GSystem
Properties Component solves some of the problems with that implementation.
If, for example, you
believe that cancel means cancel, you will find the GSystem Properties
Component allows cancel in a way that truly makes sense. This is in reference
to the typical implementations of apply that (correctly) apply property changes
to the system context, for example, updating the window with the proposed changes in effect, but which do
not then allow the rollback of those changes. In particular, when the user
presses cancel, the state should
revert to the state of the system when the user first selected “change
properties”, however, this is typically not the case.
The GSystem
Properties Component fully supports both types of implementations.
The GSystem
Properties Component is able to display a window containing child windows and
controls that contain the values and provide edit opportunities for the
properties. In all objects or applications that support the display of property
values, some window has to be created to host these controls and entry fields.
Users familiar with MFC know that the property page generated for an MFC
component is essentially just a dialog.
The GSystem
Properties Component shares the work of creating the user interface necessary
for properties manipulation with you, the developer of the application or
object that will be using the Properties Component.
Specifically, you
are responsible for creating the dialog window and placing a set of desired
controls on that window, using whatever tool you have to do so. In Microsoft
MFC, you can create a simple dialog based on the CDialog MFC class. In Visual
Basic, you can simply create another form, etc. What is important is that all
of the events that happen to that window, for example, when the user is
manipulating a control or entering text, are under your software’s full
control. The GSystem Properties Component does nothing to interfere with what
your window wants to do in its own implementation. The Properties Component
simply provides the mechanism to display the window at the appropriate time.
The important thing
for you to do, then, when using the GSystem Properties Component is to tell the
component where this window is. There is an easy way (and perhaps entirely
sufficient, depending on your circumstances), and there is a more complex, yet
feature rich, method you can use to accomplish this.
The easier method is
described first.
If your application
creates a simple properties settings dialog, you can provide the GSystem
Properties Component with a handle to that window to display when the user
manipulates the properties. You accomplish this by calling the PutHWNDPropertyPage() method provided by the Properties Component.
pIGProperties
-> PutHWNDPropertyPage(
BSTR displayName,
long hwndProperties,
long hwndStart,
long hwndOK,
long hwndApply,
long hwndCancel);
Note that in the
above syntax, the BSTR variable provides the “displayed” name of the window in
a group of windows, for example, the text appearing on a tab when there is more
than one window available. This is a string variable, your particular
environment probably understands this. In Microsoft C++, the value can be
passed as L”the Text”, as an example.
Note also that you
may call this method with as many windows as you want, which allows you to
“categorize” your properties in a way that makes sense with your application or
users. You can call the associated RemoveHWNDPropertyPage() method to take any property page window back
out of the group.
In the method call,
the HWND variable is the “window handle”, in PowerBuilder, there is a function
called Handle() that provides this value. In an object derived from an MFC window
class, you would use the GetSafeHWND() method to return this value, and in Visual
Basic, this is the hWnd property of a form.
The values for hwndStart,
hwndOK, hwndApply, and hwndCancel, are the handles of the command buttons your
window would implement if the window were to be shown by your own application
or object (which it can do if you’d like). For example, the hwndOK
control (probably a push button control) is one of these that an MFC generated
dialog would use for the “OK” button. What is important is that your code does have to have the event code
implemented. In MFC you must have an event “registered” and implemented for the
dialog whether or not you have actually placed the actual control on the dialog
(which is not needed or desired for the GSystem Properties Component’s use).
You can simply put a
command button on the dialog and pass that button’s handle to the PutHWNDPropertyPage() method. It is your implementation of the command event for that
particular button that is called when the user presses a command button on the
properties dialog[30].
Remember, the apply button on the properties is supplied by the system, not the one you created on your
properties dialog. In fact, you should make that button invisible when the
properties dialog becomes visible. There will be more about this later.
The steps in the
process that occurs when you have taken this simpler approach are outlined to
provide another frame of reference for using this information.
Note that the
windows that you use for these purposes should exist at any time that the user
(or system) is likely to start the activity of setting properties. In MFC, for
example, you might create your dialog and keep it around as long as your object
is in use. You couldn’t just “create” the dialog when it is needed because the
GSystem Properties Component needs to be given a handle to a window (PutHWNDPropertyPage()) that will be used at some arbitrary point in the future. In other
words, there is no way (using this simpler method) for the system to notify
your object that it is going to need a properties setting window soon and for
you to make one, you essentially need to always have one around for this
purpose.
Using the
IGProperties interface to your instance of the GSystem Properties Component,
call the AddPropertyPage() method. This method requires that you pass a
pointer to the IUnknown interface on an object that implements the
IGPropertyPageClient[32]
interface.
Note: The IUnknown interface is for your component, the object you are
developing. The Properties Component will query this interface for an instance
of the IGPropertyPageClient interface which your
object implements and which the Properties Component will use to
communicate with your object during the properties setting process the user is
performing. The Properties Component will not
AddRef() the IUnknown interface, it will, however QueryInterface() through the IUnknown for the IGPropertiesClient interface, which will
cause an AddRef() for that interface.
You don’t have to
provide the handle of the window to use in the interface as you did in the
prior section because the GSystem Properties Component will retrieve this value
later on through the IGPropertyPageClient interface.
The GetPropertyPagesInfo() method (that you implement) is used by the Properties Component to get
the collection of property pages you
want to show in the set properties dialog box. When the Properties Component
calls this method, you need to provide a count of the number of windows
involved, an array of strings to display as the page names, an array of strings
specifying the help files associated with each page, and array of window sizes.
This last array will contain 2 entries (longs) for each window you are
providing; the width and the height of the 1st window are in the
first 2 values, for example.
Note: The GetPropertyPagesInfo() method fills in several SafeArrays[33]
that are passed to the method. These SafeArrays contain space for one set of information upon entry. If
there will be more items returned, it is required that the implementation
“Re-Dim” the provided arrays. Here is the code for an object that implements 8
property pages on behalf of the object G:
HRESULT
G::GetPropertyPagesInfo(long* pCntPages,
SAFEARRAY** thePageNames,
SAFEARRAY** theHelpFiles,
SAFEARRAY** thePageSizes)
{
if ( ! pCntPages ) return E_POINTER;
*pCntPages = 8;
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].cElements = *pCntPages;
rgsabound[0].lLbound = 0;
rgsabound[0].cElements = *pCntPages;
rgsabound[0].lLbound = 0;
// Resize the array to pass back the titles
and help files
SafeArrayRedim(*thePageNames,rgsabound);
SafeArrayRedim(*theHelpDirs,rgsabound);
// Resize the array to pass back the window
sizes
rgsabound[0].cElements = 2 * ( *pCntPages );
SafeArrayRedim(*thePageSizes,rgsabound);
long index = 0;
BSTR bstrPageName = SysAllocString(L"Pos-Size”);
SafeArrayPutElement(*thePageNames,&index++,bstrPageName);
SysFreeString(bstrPageName);
bstrPageName = SysAllocString(L"Style”);
SafeArrayPutElement(*thePageNames,&index++,bstrPageName);
SysFreeString(bstrPageName);
.
.
.
long w = 100;
long h = 100;
index = 0;
SafeArrayPutElement(*thePageSizes,&index++,&w);
SafeArrayPutElement(*thePageSizes,&index++,&h);
.
.
return S_OK;
}
The important point here is that the callee may need to re-dimension
the SafeArrays to pass back to the GSystem Properties
Component.
Because the GSystem
Properties Component is built to strongly support the process of manipulating
property pages, The rest of the property page support is described within that
process.
The process starts
when the user elects to edit properties in your object or application. At this
point, your application will receive a QueryInterface() call for the ISpecifyPropertyPages
interface. Your implementation of IUnknown will forward delegate this call to
your cached instance of the IGProperties interface (see page 36). This starts the chain of events.
The GSystem
Properties Component’s implementation of ISpecifyPropertyPages will call the GetPropertyPages() method on your application’s implementation of IGPropertyPageClient[34].
This method, which you write, returns the window handles and the textual
description of each property page you want to display. Remember, you create
SafeArrays to pass the strings and window handles back to the Properties
Component.
After all of the property pages have been defined, the BeforeAllPropertyPages() method is called on each object that is supporting
IGPropertyPageClient. This call is an opportunity to do whatever your
application needs to in preparation for the property pages. It could, for
example, load values into the controls on the property settings window.
Note that it is possible to “mix” the simpler windows messaging
technique with the technique implementing the IGPropertyPageClient. In such a
case, both these processes are
occurring together. Specifically, your windows involved in the simpler method
will also receive their command messages at the appropriate time. See page 53 for more information.
One very good thing
to handle at this point is to setup the ability to detect changes in the
properties. In particular, this is a good time to “Push” the properties so that
you can later “Compare” the pushed set of values with the current set. Table 3 shows the methods on the IGProperties
interface pertinent to this activity.
|
IGProperties
Method |
Performs
the following |
|
Push() |
Puts the
current set of properties on a “stack”, essentially saving a copy of the
entire property set that can later be restored (Popped). |
|
Pop() |
Pops the
topmost (or last pushed) set of properties off the stack, overwriting the
“current” set of properties. |
|
Discard() |
Pops the
topmost (or last pushed) set of properties off the stack but does not
overwrite the current set of properties. |
|
Compare() |
Returns S_OK
if all properties in the current set of properties equals all properties in
the topmost (or last pushed) set of properties. Returns S_FALSE otherwise.
Returns E_FAIL if the stack is empty. |
Table 3 Property Set Manipulation Methods on IGProperties
In order to have the
ability to both “Apply” and “Cancel” a set of properties, you Push() the
current properties twice at the start of the process. When the user presses
“Apply”, you Discard() the top of the stack, then re-Push() the
current values. The stack remains 2 deep no matter how many times the user
presses “Apply”. Now, if the user presses “Ok”, you can simply Discard()
both of these 2, the current set of properties and values are what the user
wants. If, however, the user presses “Cancel”, you simply Discard()
once, then you Pop() the stack, returning the values of all properties to the values they
had when the entire process started.
Now we will continue
with the events that occur as the user is manipulating the properties.
Remember that your
code created the windows that are showing in the properties dialogs that are
being presented to the user. In particular, these properties dialogs are
implemented using the OleCreatePropertyFrame() system call (which the GSystem Properties
Component sets up and calls on your behalf). This is a “Tabbed” dialog with
each tab constituting one of the windows that you provided to the Properties
Component.
During
the user’s manipulation of controls in the dialog, the system will be making
calls to the IsPageDirty() method on your implementation of
IGPropertyPageClient. You should provide the value false if the current set of
properties is exactly the same as the “saved” set of properties. In other
words, if the user has changed any property (through his/her interaction with
your window and subsequently due to processing that is happening in your window
procedures), return true, the properties are “dirty”.
Note that this method is called with page number[35]
of the particular window that is currently active. In other words, if you were
providing 3 windows to the system you could use the page number provided to
ascertain, at run-time, what particular “sub set” of the properties are being