John W. Colby
jwcolby at colbyconsulting.com
Wed Mar 3 21:04:45 CST 2004
Folks, I have thrown out suggestions about how I handle things "using my framework". Robert has asked me what a framework is and how to start one. Unfortunately the word Framework is used by different people for different things, so I will state my own definitions with the warning that I have no intention of arguing with anyone who doesn't like my definition. For my purposes, a framework is a skeleton. Look at a skyscraper being built on the horizon. The framework is all that steel, the elevators, the air conditioning, electrical service, water, sewage etc. ALL of that stuff is necessary regardless of whether you intend to rent one thousand square feet or one million. And once all of that stuff is in place, the rest is just sheetrock, aluminum framing, doors and windows. My framework starts with a class named dclsFW, the framework class. It is instantiated ONE time (a single instance), but inside of that class is the foundation of the rest of the system. It has "class global" variables for other "service" classes. By "class global" I mean private to the class (can only be directly manipulated by the class) but global to the class (can be seen from anywhere in THAT class). By Service classes I mean classes such as my SysVars, Zip/unzip, Encrypt/Decrypt and so forth. These are really "standalone classes", they do not require my framework at all in order to function, but by placing then in my framework class I provide them to any other part of my class. dclsFW instantiates all these service classes when dclsFW initializes, and tears them down when dclsFW terminates. dclsFW also provides property gets to allow other code to access these service classes directly. ALL classes, EVERY SINGLE ONE, have a set of common stuff at the top, a handful of private constants and variables, and init/term events. This stuff is SO common that you can literally cut and paste it from a "template class" into a new class and save that and have a new working class. The framework class dclsFW is just the foundation of the framework, it is NOT the skeleton itself. Because Access is so Form-centric I have an entire skeleton for forms and controls. Thus I have a form class named dclsFrm. This class is instantiated by any form that wants to use my framework (90% or more in my databases) in the form's OnOpen. Each form has a "form global" (dimensioned PUBLIC) variable for the dclsFrm, and instantiates it in OnOpen, then calls the init of dclsFrm passing in a pointer to itself. dclsFrm then stores that pointer to the form in a private variable in it's header. dclsFrm is the foundation of the FORM skeleton if you will, but it uses services provided by dclsFW (the framework foundation class). dclsFrm also SINKS EVERY form event. The private form variable in dclsFrm's header is dimensioned WithEvents and I then built event stubs for every single form event. The ONLY one that doesn't actually function is OnOpen and that is because the class is instantiated in the form's built-in class in OnOpen and therefore by the time dclsFrm loads OnOpen has come and gone. One implication of this is that NO FORM is lightweight since it must have its built-in class to store the pointer to my dclsFrm, and of course an OnOpen to set and initialize dclsFrm. Just as we have a class for the form, EVERY data aware control has a class which I name dclsCtlCbo, dclsCtlTxt, dclsCtlGrp etc. mostly so that all of the control classes will group together in the module window, but also because it makes it obvious that these classes are control classes. dclsFrm has a private function called from its Init() which I call FindControls (very descriptive I know). This function iterates the form's Control Collection. Remember that dclsFrm was passed a pointer to the form by the form itself as it initialized dclsFrm. As I iterate the control collection I have a large case statement that basically says: for each ctl in frm.controls select case ctl.ControlType case "textbox" 'instantiate the text box control class case "combo" 'instantiate the combo class etc end select next ctl Thus as each control is examined I discover the type of the control, I load an instance of the class for that type of control and pass in a pointer to the control. I save all of these control classes into a collection. By the time FindControls is finished I have loaded a class instance for EVERY control on the form (more or less), and each of those control class instances has a pointer to it's control. As I do in the form, I dim the control variable in each control class Withevents and build event sinks for the control events. In this case I am a little more lenient and only build event sinks for the events I actually use. I did this partly because I don't use many of the key events and mouse events (in every control) and didn't want the overhead of those event stubs being called all the time. Now this sounds like a LOT of work, and a LOT of overhead. It is a lot of work, but in fact very little overhead. It turns out that classes load the entire class ONE TIME, then only a new header section (global variables) for each additional instance of that class type. Thus if I load 10 combo class instances, only one loads completely, then just the header of the other 9. All of the code is shared... unless there are static variables in the functions which is handled appropriately such that each class instance has it's own static variables. I ran some timing awhile back on a VERY complex form with dozens of controls. What I discovered is that on an old 100 mhz Pentium of the day, the overhead was one half of one millisecond per class instance, to load each instance. Folks, that is NOTHING compared to the time to load the data for example. And of course that was a sloooooowwwwww computer compared to what we have now. So there you have it. By the time dclsFrm loads, it also loads a class for each data aware class on the form. The form's skeleton is built and loaded. Now that I have classes for each control and the form, and these classes sinking all the necessary events, I can add functionality to each class as desired. All of the various things you have heard me discuss in the "in my framework I do..." emails are nothing more than discovering the code required to do this stuff, then putting it out in the classes in the framework. Let's take a working example. Every data aware control may be referenced by a combo, list or subform in the SQL statement or query that loads the data into these objects. Thus a combo can be "filtered" by another combo, or by a check box, or by a text box etc. I call the object being filtered a "dependent object" because its dataset depends on some other control (or controls). In ALL of my classes for data aware controls I have a collection which I call colDepObjs. So every combo, list, textbox, checkbox etc. class has this collection. It also has a function which allows me to pass in to the class a list of controls that are dependent on that control, i.e. whose data is filtered by that control. A pointer to these controls (or their class actually) are stored in colDepObjs. Each class also has a public RequeryDepObjs method which can be called. This method... you guessed it... iterates the dependent object collection and calls the requery method of every class in the collection. Thus is 3 combos are dependent on ComboA, calling ComboA.RequeryDependentObjects causes requery ,method of the class for comboB, ComboC, and ComboD. The requery method requeries the actual control (combo or list etc) but also calls its own RequeryDependentObjects method which ... calls the Requery method of any classes in its colDepObj. In order to use this functionality, all I have to do is call a function of a class passing in pointers to the controls that are dependent on this control. Now, when ComboA AfterUpdate fires (remember I sink the events in the control classes) the AfterUpdate calls it's RequeryDependentObjects which starts the ball rolling requerying all dependent objects down the chain. One of the things that has been critical to efficiently handling all this stuff is my framework SysVar table. In my SysVar table I can turn on/off functionality for the entire framework (all forms for example) or for a specific service. As an example I have a sysvar that says "turn on the ZIP/Unzip service classes. I leave them turned off under normal circumstances. However if a specific application needs zip/unzip functionality, I can OVERRIDE the Sysvar by reading framework sysvars out of a table in the FE. Thus for that FE I can turn on/off the zip/unzip service classes, and having done so, I can now just call a property of the framework to get the zip class, call a method and zip up a file. Likewise I can turn on / off a form behavior for a specific application. I can also override form behaviors on a form by form basis so that one form has the behavior while the next does not. Doing things this way allows me to tailor the framework for a specific application, even down to tailoring it for specific forms. I hope this email has started you thinking about frameworks, how you would use them and what you would do with them. If you ever take the time to build one you will never look back. Frameworks are an awesome tool that takes an already RAD environment (Access) and allows you to plop down a skeleton on which you build your app. Imagine being able to tell the client "I can build your skyscraper in 1/10th the time because I already have the skeleton done". Just add walls and windows and move in next week. (Ok, next month). We all know that the data design is a critical piece which I have not addressed here at all, but once that part is done, building forms should be much more standardized than the way many developers do it. I am going to stop here to allow anyone to ask questions, or other developers who have their own frameworks to pipe in with "this is what I do". John W. Colby www.ColbyConsulting.com