John W. Colby
jwcolby at colbyconsulting.com
Fri Mar 12 23:44:25 CST 2004
The example database and a word document of this email is on my site - www.colbyconsulting.com Click C2DbFW3G, then the hotlink to C2DbFW3G-DemoCtlClassV3.zip. Eventually all of these emails will be cleaned up by DatabaseAdvisors and published in Many-to-Many. The ultimate purpose of a framework is to give you clearly defined places to go to place properties and behaviors for objects that should be available for future projects. Controls are a prime example of this since they have no built-in class and yet we want to extend their functionality. Thus we build classes for each control type so that we can extend the functionality of that control. To this end in this discussion we are going to add some new functionality to the combo class we defined. The new functionality will be dependent object re-query. Dependent object re-query means that we will create a method of selecting objects (classes) that filter their data based on the currently selected data in the control in this case a combo. As the user selects new data in the combo, any dependent objects need to be automatically re-queried so that they display filtered data that depends on this combo. This behavior is generally useful and will demonstrate how we expand a framework to add things that we want our framework to do. Dependent Objects Dependent objects are a common occurrence in Access applications. As an example a form has two combos on it, cboCompany and cboEmployees. As the user selects cboCompany, cboEmployees needs to be filtered to only display the employees for the company just selected. Or perhaps a company is selected and only Addresses for that company are displayed in cboAddress. Dependent objects are usually implemented by referring to the filtering object in a field of the query which pulls data for the dependent object. In other words, cboAddress will have something that looks like Like form!frmMyForm!cboCompany in the criteria of the query for cboAddress or cboEmployees. cboAddress is filtered by or dependent on cboCompany. In order to implement this functionality in our framework we could simply add a dependent object collection directly inside of the control class, then add methods to each control class to add objects to that collection, remove them again when the control class closes, and of course a re-query method to iterate through all of the objects in the collection, calling the re-query method of every object. Notice that this implies that every control that can be dependent on another control must have a public re-query method in its class. The dependent object code The code for this would look something like: In the header of each control class (stripped of all error handling for easy reading): Private mcolDepObj As Collection Collection initialization code in each control class Initialize event Private Sub Class_Initialize() Set mcolDepObj = New Collection End Sub Cleanup code in each control classes Term() function Public Sub Term() Set mcolDepObj = Nothing End Sub And finally public functions (methods) to handle the dependent object specific stuff: Private Function ColEmpty() While mcolDepObj.Count > 0 mcolDepObj.Remove 1 Wend End Function 'Requery every obj in the dependent object collection Public Function Requery() Dim Obj As Variant For Each Obj In mcolDepObj Obj.Requery Next Obj End Function 'Add multiple objects into the collection Public Function DepObjs(ParamArray lDepObjsArr() As Variant) Dim Obj As Variant For Each Obj In lDepObjsArr mcolDepObj.Add Obj, Obj.Name Next Obj End Function 'Add a single object Public Function Add(item As Variant, key As Variant) mcolDepObj.Add item, key Requery End Function 'Remove a single object Public Function Remove(index As Variant) mcolDepObj.Remove index End Function 'get a count of the objects in the collection Public Function Count() Count = mcolDepObj.Count End Function 'get one specific object back out of the collection Public Function item(index As Variant) item = mcolDepObj.item(index) End Function Theres nothing wrong with doing it that way but we are trying to learn to use classes and another way we could implement this is to build a dependent object class. Then we simply add a dependent object class to each control class that needs one. A huge advantage to using a dedicated class for this is that now we put all of that exact same code in one place, in one class, then simply dim a clsDepObj wherever we need it and call the class methods. We dont have to go add this stuff to each and every control class that may have dependent objects. Each control class code Instead of all that code above in each control class, we now have: In each control class header (stripped of all error handling for easy reading): Private mclsDepObj As clsDepObj Initialization in each control class Initialize event. Private Sub Class_Initialize() Set mclsDepObj = New clsDepObj End Sub Cleanup in each control class Term() Public Sub Term() mclsDepObj.Term Set mclsDepObj = Nothing End Sub And a property get to return a handle to the clsDepObj class instance for this control class instance Public Property Get clsDepObj() As clsDepObj Set clsDepObj = mclsDepObj End Property You have to admit adding a few lines of code to each control class is way cleaner and easier than adding all of that dependent object code to each control class. Not to mention if we want to add some other dependent object functionality to the framework we just do it in one place instead in every control class. 'Requery the control and then requery all dependent objects Public Function Requery() mcbo.Requery mclsDepObj.Requery End Function One thing that we have to remember is to add a re-query method to every control class that will have a dependent object class. Obviously if this class control has a data source then we need to re-query this control as well as calling the re-query method of the dependent object class. And finally, when the combo AfterUpdate fires we want to re-query all the objects in our new class dependent object collection. We do that by calling the mclsDepObjs re-query method. Withevents! You gotta love them! Private Sub mcbo_AfterUpdate() mclsDepObj.Requery End Sub So now that we have this wonderful new class and functionality, how do we tell the framework to use it? The answer to that lies in the form class initialization which occurs in the forms Open event. The forms built-in class I have modified frmPeopleV3 to add two combos in the form header section. cboCompany and cboEmployee respectively contain data for companies and employees of those companies. Thus the forms code now looks like: Option Compare Database Option Explicit Our custom form class dclsFrm Public fdclsFrm As dclsFrm And the forms Open event. Private Sub Form_Open(Cancel As Integer) Set fdclsFrm = New dclsFrm fdclsFrm.Init Me, Me With fdclsFrm.colClasses .item("cboCompany").clsDepObj.Add .item("cboEmployee"), cboEmployee.Name .item("cboCompany").clsDepObj.Add fdclsFrm, Me.Name .item("cboCompany").Requery End With End Sub I know this looks very complicated but we will take it one step at a time and show you what we are doing. The set statement and init look very much like before. We are passing in a reference to the forms built in class (Me) and a reference to the form itself (Me). Unfortunately in the case of the form, Me refers both to the class as well as the physical form so that can be a little odd at times. Set fdclsFrm = New dclsFrm fdclsFrm.Init Me, Me Next we use a with statement to do a partial resolution of dclsFrm to speed up getting at things. With fdclsFrm.colClasses dclsFrm has a property fdclsFrm.colClasses that returns a pointer to the classes collection. Using that we can now refer to that collections properties and methods. We use the .item() method to return something from the collection. .item("cboCompany").clsDepObj.Add .item("cboEmployee"), cboEmployee.Name Remember that in dclsFrm we had a control scanner that found all the controls on the form, built a class for each one, and placed a pointer to each controls class instance in colClasses. Remember also that we used the controls name as the key or index into the collection. Thus .item("cboCompany") tells the class to return the item in colClasses that was named cboCompany, in other words the control class for the control cboCompany. Thus: .item("cboCompany") is a pointer to a combo class and is exactly equivalent to the more verbose: dim ldclsCbo as dclsCbo set dclsCbo = .item(cboCompany) Since we are dealing with a pointer to a combo class we can now call any method or property of that class which is what we do with: .item("cboCompany").clsDepObj.Add We are saying for the combo company class, call the clsDepObject method which returns a pointer to the dependent object class, and then call that classes add method. Whew! Just remember that the period denotes a method or property of the class, so you just end up referencing classes with properties that return classes with properties that return classes where you finally use a method of that class. fdclsFrm.colClasses.item(class name).clsDepObj.Add By the way, you can set a break point on that line of code and step through the code watching the methods of the various classes being called, returning pointers to the next object, which steps into a method of that and returns a pointer to the next object etc. until you finally drill down to the final .Add method. This stuff is FUN! OK, back to the big picture. With fdclsFrm.colClasses .item("cboCompany").clsDepObj.Add .item("cboEmployee"), cboEmployee.Name .item("cboCompany").clsDepObj.Add fdclsFrm, Me.Name .item("cboCompany").Requery End With We have now told the dclsFrm to add something to the clsDepObj. The something is the class for the cboEmployee. .item("cboCompany").clsDepObj.Add .item("cboEmployee"), cboEmployee.Name Likewise I have made the form itself dependent on the cboCompany so we also have to tell the class for cboEmployee that dclsFrm is a dependent object. .item("cboCompany").clsDepObj.Add fdclsFrm, Me.Name And finally, we have to re-query cboCompany so that it requeries all of its dependent objects. .item("cboCompany").Requery And that, as they say, is that. Reference the forms colClasses, pulling out the class for cboCompany. Using the clsDepObj method of that class to pull the clsDepObj, use that class Add method to add an object to the dependent object collection. The object added is the class for cboEmployee and the class for the form itself, fdclsFrm. Once we have added these two items to our dependent object class, re-query the whole structure. I know quite well that this stuff can make your eyes cross when you first look at it but believe me it will become second nature once you have worked with classes, methods and properties, and collections for awhile. Summary By adding a new, rather small class with a single collection variable and a handful of public methods, we have built a method of manipulating dependent objects. Once the class exists, it is a simple matter to add a few lines of code to the class of each control that can have dependent objects and by doing so add dependent object handling to any class that needs it. Probably the biggest stretch was decoding the rather obtuse code in the forms Open method that programs the specific class that has dependent objects, dclsCbo for cboEmployee, thus telling it to place pointers to two objects in its dependent object handling system. Those two objects are the class for cboEmployee and the class for the form itself. The end result is that when the form initially opens it displays no records because there is no company selected and the form is filtered by or dependent on cboEmployee. However once we select an employee, the combo now has something that can be used to filter the form and the cboEmployee. Since cboCompany calls its clsDepObjs re-query, the forms class requeries the form, and cboEmployees class requeries cboEmployee. By the way, if cboEmployee had been programmed with dependent objects of its own, cboEmployees class would have requeried all of its dependent objects and the process just ripples down the chain automagically. Classes are powerful tools that allow us to encapsulate functionality into a single location, hold all of the variables and code needed to implement necessary behaviors, and provide a single place to go to add new related behaviors if necessary. You can pull this dependent object class into your own framework and with relatively little effort add dependent object processing to your own combos and other controls. Dependent object processing is a trivial application, but imagine if the class had dozens of properties and methods! This encapsulation and portability becomes a prime reason to know how to use classes. John W. Colby www.ColbyConsulting.com