John W. Colby
jwcolby at colbyconsulting.com
Fri Mar 5 21:33:52 CST 2004
Now that I have briefly described a class, lets get down to using them. First of all, every form has or can have a built in class, one instance of which is loaded when the form loads. This class holds events for the form itself such as the Open, Close and AfterUpdate event. It can also be used to set properties of the form itself. For any class, the ME keyword is used to manipulate that instance of that class. In other words, if you need to set the Caption of a form from code running in the form, ME.Caption = "Some text" allows you to do so. Since the "code behind form" resides in the form's class, any Access developer who writes code in the form is already using classes. The fact that the class is part of the form is definitely nice in that you can export that form to another database and any code in the form's module (class) goes with it. Unfortunately it also creates maintenance headaches if you write very complex code in that class and then you need that same code in another form. Many developers simply cut and paste the code into the next form and be done with it. What happens though if the code has a bug? Now you have to open both forms and fix the bug in both forms. What happens if you exported the form to another project? Now you have to open the forms in both projects and edit the code to fix the problem. Recognizing this as an issue, some developers develop libraries and place such code in their own library. This works very well (and is what I do). Now if for example the OnEnter of a text box needs to call a function, the function is in the library and both forms just call the same function in the library. If there's a bug, fix it in the library and everything that uses that function gets fixed at once. Much better. However we still have the issue of the event stub itself. In order for the combo's OnEnter to call a method, that event has to cause code to run somewhere. That somewhere for 99% of Access programmers is in the form's class of the form where the combo exists. There is absolutely nothing wrong with this approach, but it can get terribly messy when a large form has 10 tabs and 80 controls on it, each control firing 3 or 4 different events, plus half a dozen events for the form, plus 30 functions for general processing plus... well I'm sure you get the picture. Now just trying to work in the form becomes a nightmare, paging up and down yards of code trying to find things. This in fact is one of the major problems with any of the Office Applications (Word, Excel, Access, Powerpoint) is that controls do not have their own class like the form does. Therefore the form's class serves as the controls class. Remember I said in the last email that classes should model one object? Already we are running into a class that is modeling a form and also modeling many different types of controls. Hmmm..... The following examples demonstrate how to change the background color of text boxes as the user moves through the controls. Just open the relevent form and start hitting the tab key watching the cursor as you do. For this example see frmPeopleClasseless: Option Compare Database Option Explicit Private Const mclngBackColor As Long = 16777088 'A pretty blue color to set the text box back color to Private mlngBackColorOrigFName As Long Private mlngBackColorOrigLName Private Sub txtFName_Enter() mlngBackColorOrigFName = txtFName.BackColor 'When we enter the text box, save the original back color txtFName.BackColor = mclngBackColor 'Set the back color to our favorite color End Sub Private Sub txtFName_Exit(Cancel As Integer) txtFName.BackColor = mlngBackColorOrigFName 'Set the back color to the original color End Sub Private Sub txtLName_Enter() mlngBackColorOrigLName = txtLName.BackColor 'When we enter the text box, save the original back color txtLName.BackColor = mclngBackColor 'Set the back color to our favorite color End Sub Private Sub txtLName_Exit(Cancel As Integer) txtLName.BackColor = mlngBackColorOrigLName 'Set the back color to the original color End Sub This code when placed in a form with a pair of text boxes called txtFName and txtLName causes the back color of these controls to switch from whatever they currently are top cyan when they get the focus (OnEnter) and back to their original color when they lose the focus (OnExit). That's kind of cool but look at what we are already running into. We need a variable at the top of the form for EACH control that we want to perform this behavior for, to save the old back color. It's ok if we only have one or two controls but what if we have 20 or 40? Can you say PITP (Patuty)? Further, we not only need the variables to store the stuff, but we also need to set up the OnEnter and OnExit subs for each control we want to do this. Multiply this times 20 or 40 and that's a REAL PITP. Fortunately we can build a class that models a specific control, lets say a text box. I am going to keep this simple so that you can see the workings, and add in the nice troubleshooting stuff I mentioned later. This is in dclsCtlTextBox. The following code represents a class for a text box, simple but marginally useful. Explanation IN-LINE. Option Compare Database Option Explicit Notice that we have ONE variable to hold the back color, the constant for the back color of our choice, and a place to store a pointer to a specific Private WithEvents mtxt As TextBox 'Dimension a text box Withevents Private Const mstrEventProcedure = "[Event Procedure]" 'A constant to hold the string [Event Procedure] Private Const mclngBackColor As Long = 16777088 'A pretty blue color to set the text box back color to Private mlngBackColorOrig As Long 'A place to store the original back color Next we have an init function where we are passed in a pointer to a specific text box. We save this pointer to the text box to our private variable in the class header. We also set the OnEnter and ONExit properties of that control to the string [Event Procedure]. 'The init function of every class "initializes" the class Function Init(ltxt As TextBox) 'Pass in a pointer to a specific control Set mtxt = ltxt 'Save that pointer to a private variable here in the class mtxt.OnEnter = mstrEventProcedure 'Set the OnEnter property of the control to [Event Procedure] mtxt.OnExit = mstrEventProcedure 'Do the same for the OnExit End Function We have a term function where we release or cleanup the pointer to the text box control ' 'The term function of every class cleans up all pointers to objects stored in our class ' Function Term() Set mtxt = Nothing 'Set the pointer to the control to nothing End Function And finally we have the same event stubs for OnEnter and OnExit for our specific control ' 'These are the event stubs for the control's OnEnter and OnExit events ' Private Sub mtxt_Enter() mlngBackColorOrig = mtxt.BackColor 'When we enter the text box, save the original back color mtxt.BackColor = mclngBackColor 'Set the back color to our favorite color End Sub Private Sub mtxt_Exit(Cancel As Integer) mtxt.BackColor = mlngBackColorOrig 'When we exit the control, set the back color back to the original color End Sub Having done that we are ready to instantiate this class once for each text box we want to control. I'm going to use the actual code form my demo database which handles FOUR text boxes. This is code in the form frmPeople2. The form's header now looks like: Option Compare Database Option Explicit Notice that we dimension the class four times, each with a distinct name. Private fdclsCtlTextBoxFName As dclsCtlTextBox Private fdclsCtlTextBoxLName As dclsCtlTextBox Private fdclsCtlTextBoxAddr1 As dclsCtlTextBox Private fdclsCtlTextBoxAddr2 As dclsCtlTextBox The form's Open event initializes each of these class instances: Private Sub Form_Open(Cancel As Integer) Set fdclsCtlTextBoxFName = New dclsCtlTextBox fdclsCtlTextBoxFName.Init txtFName Set fdclsCtlTextBoxLName = New dclsCtlTextBox fdclsCtlTextBoxLName.Init txtLName Set fdclsCtlTextBoxAddr1 = New dclsCtlTextBox fdclsCtlTextBoxAddr1.Init txtAddr1 Set fdclsCtlTextBoxAddr2 = New dclsCtlTextBox fdclsCtlTextBoxAddr2.Init txtAddr2 End Sub And the form's Close cleans up behind us: Private Sub Form_Close() fdclsCtlTextBoxFName.Term Set fdclsCtlTextBoxFName = Nothing fdclsCtlTextBoxLName.Term Set fdclsCtlTextBoxLName = Nothing fdclsCtlTextBoxAddr1.Term Set fdclsCtlTextBoxAddr1 = Nothing fdclsCtlTextBoxAddr2.Term Set fdclsCtlTextBoxAddr2 = Nothing End Sub That's it folks. Notice that the individual event stubs for the controls themselves are missing! The reason of course is that the text box class itself is sinking those events right inside of that class so we don't need event stubs here. Now I hear you already saying "yea, but there's still a lot of code just to do the init and cleanup". True, but there are also tricks that we can use. See FrmPeople3. The first is to use a class factory function such as: Private fdclsCtlTextBoxFName As dclsCtlTextBox Private fdclsCtlTextBoxLName As dclsCtlTextBox Private fdclsCtlTextBoxAddr1 As dclsCtlTextBox Private fdclsCtlTextBoxAddr2 As dclsCtlTextBox Private Sub Form_Open(Cancel As Integer) ClassFactory fdclsCtlTextBoxFName, txtFName ClassFactory fdclsCtlTextBoxLName, txtLName ClassFactory fdclsCtlTextBoxAddr1, txtAddr1 ClassFactory fdclsCtlTextBoxAddr2, txtAddr2 End Sub Private Sub Form_Close() ClsDestroy fdclsCtlTextBoxFName ClsDestroy fdclsCtlTextBoxLName ClsDestroy fdclsCtlTextBoxAddr1 ClsDestroy fdclsCtlTextBoxAddr2 End Sub Function ClassFactory(ldclsCtlTextBox As dclsCtlTextBox, txt As TextBox) Set ldclsCtlTextBox = New dclsCtlTextBox ldclsCtlTextBox.Init txt End Function Function ClsDestroy(ldclsCtlTextBox As dclsCtlTextBox) ldclsCtlTextBox.Term Set ldclsCtlTextBox = Nothing End Function Even better we could use a collection to hold the pointers to our control classes which eliminates the "new variable per class" syndrome as well as much of the cleanup code. See frmPeople4 Option Compare Database Option Explicit Private colClasses As Collection Private Sub Form_Open(Cancel As Integer) Set colClasses = New Collection ClassFactory txtFName ClassFactory txtLName ClassFactory txtAddr1 ClassFactory txtAddr2 End Sub Private Sub Form_Close() ClsDestroy End Sub Function ClassFactory(txt As TextBox) Dim ldclsCtlTextBox As dclsCtlTextBox Set ldclsCtlTextBox = New dclsCtlTextBox ldclsCtlTextBox.Init txt colClasses.Add ldclsCtlTextBox, txt.Name End Function Function ClsDestroy() Dim obj As Object For Each obj In colClasses obj.Term Set obj = Nothing Next obj Set colClasses = Nothing End Function Even BETTER... we could build a form class that did all this for us!!! But that is for another day. Go to my site and click on C2DbFW3G to go to a page I have set up for the downloads. John W. Colby www.ColbyConsulting.com