John W. Colby
jwcolby at colbyconsulting.com
Sun Apr 4 20:30:37 CDT 2004
Framework Moving to a Library For the purposes of this lecture, the framework is approaching a useable state. We now have: A framework class that initializes the framework service classes. SysVars that can be used to modify program flow and turn on / off behaviors or modify how behaviors work. A form class that has a functioning control scanner finding specific control types and loading class instances for these controls. Control classes that are loaded by the forms control scanner. A troubleshooting class that logs (or can log) all classes loading and unloading to assist troubleshooting unloading problems. Service classes such as Zip which can be loaded as the application requires. >From this point forward, the lectures will concentrate on how to add functionality to the framework, adding in more service classes, more control classes, and adding behaviors to the form and control classes as we run across useful things that we want available to all of our applications. However a framework is not very useful if it has to be inside the Application FE. What we need to do next is create the typical FE/BE system and reference the framework as a library in an MDA / MDB. This presents a few challenges, but it also allows us to easily exchange the library for a new version without damaging the applications data. The process of moving our demo framework to a FE/BE is reasonably easy. All we do is: Create an FE and export the forms / queries / reports to that, leaving all the classes here in the original container. Create a BE and export the tables to that. Link the FE to the tables in the BE Change the name of the framework to an MDA extension Reference the framework library from the FE. Delete the forms / queries / reports / tables from the framework library. Expose dclsFrm The library is Referenced by selecting from the menu while in any module, Tools / References, click Browse and navigate to the directory where the copy of the library is stored, then selecting it. Having done these things we are ready to use the framework as a referenced lib. There is a small downside to working this way, namely the requirement to relink the tables as you get the demos. I have moved the entire directory from my normal development drive to the c: drive, c:\ C2DbFW3G so that if you place it there the table links will always work when you get the latest version. Using a class factory One process required when moving to a library is to make the classes that you need to reference from inside the FE can be found by your code. There are at least two ways to do this. The simplest is to build a wrapper class inside the library that looks something like this: Public function cFrm() as dclsFrm Set cfrm = new dclsFrm End function This is a class factory that gets a new instance of that class and returns a pointer to it. This method works well and like most things has pluses and minuses to it. The plus is that you dont have to expose the class itself which we shall see in a minute is a little more work. The minus is that there is no intellisense available, and when we are dealing with classes, that is a pretty big minus. Exposing a class directly Another way to do this is to expose the class directly using an undocumented feature in Access. In order to do this we need to export the class module to a text file, edit the text file, delete the original class module in the library, and import the modified class text file back into the library. Export the class To export a class to a text file we need to: Go to the database window. Click the modules tab Click on a class module to select it Click File / Export A File Find dialog box will open. Navigate to the directory for this project. Create a Classes directory if none exists already, then navigate to that directory Select save as type: Text files in the bottom combo of the File Find dialog Add a .txt to the file name Click the Save button. Next we need to edit the text file in Wordpad. Using Windows Explorer, navigate to the file we just created. If you followed the instructions above it should be in a class subdirectory under the project directory for this framework. Open the file just exported The top of the file should look similar to: VERSION 1.0 CLASS BEGIN MultiUse = -1 'True END Attribute VB_Name = "clsFramework" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = False Option Compare Database Option Explicit We can then remove the VERSION line and the BEGIN/END section plus the two Option lines so that all that remains (of that section) is: Attribute VB_Name = "clsFramework" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = False Attribute VB_PredeclaredId = True Attribute VB_Exposed = False Next we need to edit this section, specifically we need to change VB_Creatable to True, and VB_Exposed to True Attribute VB_Name = "clsFramework" Attribute VB_GlobalNameSpace = False Attribute VB_Creatable = True Attribute VB_PredeclaredId = True Attribute VB_Exposed = True Save the file and exit Wordpad. At this point the class is edited and ready to import back into the library. In the library, delete the class that we are working on. Now from the menu Click Insert / Class module, then Insert / File. Use the file find dialog to navigate to the directory where the class txt file is located and select that file. The contents will load into the empty class module we just created. On the menu click Debug/Compile just to check that everything is working properly. If not fix the compile errors. There should be no errors but if you failed to edit the header of the class in the text file you could get some stuff up there (like duplicate Option statements). Once it compiles, on the menu click File / Save. It will ask if you want to save the class and will offer you a name. Just click yes. You have finished the process of making a class visible from outside of the library. Standard Disclaimer: This is an undocumented procedure which means that Microsoft may at any moment pull some stunt that makes this not work. However this method works from Access97 up through AccessXP and probably A2003 (I havent personally tested that last). Using a library Using a library is more complex than having all of your code inside the FE, however the pluses far outweigh the minuses. Some issues in no particular order: When you are running code in the library from a FE, you can edit the code in the library but those edits are not permanent. The library is read only and you are just making changes to the code loaded in memory (more or less). A result of the above is that in order to make changes permanent you must work in a completely different copy than the one referenced by the project. In other words, even if you were to open the library from another instance of Access, any attempt to make edits will cause Access to inform you that the changes will not be saved. In fact, if I remember correctly, this is not true for A97, it could indeed make changes to the lib from another instance of Access and make them stick, but from A2K onwards you cannot do that. Thus you need to always have a referenced library and a working library in different directories. The referenced library is of course referenced by the application and in use when the App is open. The working library is the copy that you modify with changes that you wish to become permanent. Libraries can be shared, i.e. multiple users can reference the lib. However it is more often the case that (in a multi-user environment) we will download both the FE and the BE to a local directory on the users hard drive. This brings up another issue, if the reference is broken, i.e. the library cannot be found in the location that the reference says it should be, then Access will automatically search a path in an attempt to find the file. I cant find information about the exact file path at the moment but I know that Access will search the Windows directory, System32 and the current application directory, I believe in that order. This means that you can drop your framework in any of those locations and even if the application has the reference to some other place, the lib will be found and the broken reference will be fixed automatically. If the referenced library is not found, then a broken reference will be visible in the Reference wizard and you will need to fix it manually. You also have to initialize the framework before initializing anything else so that the framework is up and running by the time the application and its forms try and use it. In addition the framework has to be unloaded when the application closes. It is possible for objects loaded by the framework to prevent Access closing so we have to have a failsafe method of unloading every time. I do this by loading a form usysfrmFWCleanup in the frameworks Init() function and calls the frameworks Term() function from its Close event. This form resides in the framework library. Since Access cannot close without closing all forms, regardless of how it occurs the application closing will close this form which cleans up the framework. Because closing this form unloads the framework, I also call Application.Quit acQuitSaveNone. Users are not supposed to be closing this form (or even seeing it). If the form is closed for any reason, the application will not function correctly so I just close the Application. Ordinarily this form closes because the application is already closing, but if it is closed accidentally, the application will be closed as well. These are some of the issues that must be considered when using a library. However the alternative of placing all such code directly in the FE causes immense maintenance issues if you have more than one project. A library is used precisely because a fix to a single library and a redistribution of that library will fix a bug or problem for every project that uses that lib. Likewise adding a behavior to a lib will provide that behavior automatically to every project that uses the library. Sysvars Using SysVars in a framework / Library is probably not familiar to most developers so I will take a few moments to discuss how we implement framework overrides. As we have seen in past lectures, the SysVars are used to turn on/off or otherwise modify behaviors that the framework makes available to the developer / application. In order to really be useful however we have to be able to change the default behaviors from inside of the application. The way we do this is to copy the framework SysVar table into the FE. We will leave a copy of those SysVars that we want this specific application to be able to override. Now, by setting the SysVars to whatever state we desire for this Application and merging in the SysVar table in the application, we can perform the Override. I have done this in the demo for this lecture. I have also added two more SysVar systems to the Framework, one for the Application control (code) and the other for the Application Data. These two SysVars are so common that I just include them right in the framework so that all the developer has to do is run the Init method and start using them. The Applications control SysVar table is usually embedded in the FE so that the SysVars controlling the Application travel with the FE container. The Applications data SysVar table is generally placed in the BE so that things like Company information etc. do not get corrupted when a new Framework or FE is distributed. The additional code for consist of two additional variables in the framework class header: Private mclsSVAppCont As clsSysVars Private mclsSVAppData As clsSysVars Then in the frameworks Initialize we SET these variables: Set mclsSVAppCont = New clsSysVars Set mclsSVAppData = New clsSysVars We add a new set of functions to initialize the framework instance, return a pointer to the class instance and return a SysVar from each Instance. One set for the Application Control: ' 'Initialize the SysVars for the Application Control ' Public Function SVAPPContInit(strTblName As String) mclsSVAppCont.Init Nothing, gcnn, strTblName End Function ' 'Get a pointer to the Application Control SV class instance ' Public Function cSVAppCont() As clsSysVars Set cSVAppCont = mclsSVAppCont End Function ' 'Return SysVars from the Application Control SysVar class ' Public Function SVAppCont(strSVName As String) As Variant SVAppCont = mclsSVAppCont.SV(strSVName) End Function And one set for the Application Data: ' 'Initialize the SysVars for the Application Data ' Public Function SVAPPDataInit(strTblName As String) mclsSVAppData.Init Nothing, gcnn, strTblName End Function ' 'Get a pointer to the Application Data SV class instance ' Public Function cSVAppData() As clsSysVars Set cSVAppData = mclsSVAppData End Function ' 'Return SysVars from the Application Data SysVar class ' Public Function SVAppData(strSVName As String) As Variant SVAppData = mclsSVAppData.SV(strSVName) End Function I have exposed the SysVars class so that you can easily instantiate your own SysVars if you need more than the Application Control and Application Data SysVars provided by the framework. In order to demonstrate using these two SysVar sets, I have created a label in the switchboard that displays a version number in the format YYYY.MM.DD. The visible property of this label is set to false. In the FE I created the table usystblAppCont where I set a DisplayVersion SysVar = True. In the applications init() I called the SVAPPDataInit method of the framework: Function AppInit() If Not blnAppInitialized Then 'initialize the framework fwinit blnAppInitialized = True fw.SVAPPContInit "usystblAppCont" fw.SVAPPDataInit "usystblAppData" End If End Function This has the effect of loading all the SysVars in usystblAppCont (theres only one at the moment). Then in the switchboards Open event I set lblVersion.Visible to the value in the SysVar table. Private Sub Form_Open(Cancel As Integer) ' Minimize the database window and initialize the form. AppInit ' Move to the switchboard page that is marked as the default. Me.Filter = "[ItemNumber] = 0 AND [Argument] = 'Default' " Me.FilterOn = True lblVersion.Visible = fw.SVAppCont("DisplayVersion") txtDeveloper = fw.SVAppData("Developer") & vbCrLf & fw.SVAppData("DevEmail") End Sub Since I have the DisplayVersion set to True in the table, the label will be visible. This demonstrates the usage of SysVars to control Application behaviors. Likewise I added a table to the BE called usystblAppData and linked that into the FE. In that table I added two SysVars, Developer and DevEMail. I then created a txtDeveloper text box on the switchboard that displays this information. Notice that this one pulls its data from fw.SVAppData. Now as the switchboard opens it pulls these developer SysVars out of the BE. Perhaps Developer data wasnt the best thing to use for Application data but I didnt particularly want to put my TaxID in there. As you can see the end user of the system can load their data into this table and have it displayed in the switchboard, report footers etc. SysVars can be used for many purposes, but the Framework control, Application control and Application data are so common that I just include these three SysVar tables right in the framework for immediate use. Modifications The modifications required when we move to an FE consist of building an AppInit() and calling it from somewhere. Private blnAppInitialized As Boolean Function AppInit() If Not blnAppInitialized Then 'initialize the framework fwinit blnAppInitialized = True End If End Function Notice that we set a Boolean so that if the function is called multiple times it will only execute once. You can then place any code that you need to initialize in this function as well. To run this code the options are an Autoexec macro or a splash screen or switchboard. To keep things simple I just built a very simple switchboard and then call this function from the Open of that switchboard. Private Sub Form_Open(Cancel As Integer) AppInit End Sub We then open usysFrmFWCleanup from the FWInit() ' 'The init function for the framework ' Public Function FWInit() If mblnFWInitialized = False Then ' mblnFWInitialized = True DoCmd.OpenForm "usysFrmFWCleanup", , , , , acHidden Set mfwcnn = CodeProject.Connection Set mcnn = CurrentProject.Connection Randomize Set mclsInstanceStack = New clsInstanceStack mclsInstanceStack.Init Nothing Set mclsFramework = New clsFramework mclsFramework.Init Nothing End If End Function I built a simple form called usysFrmFWCleanup with code in that forms Close event which cleans up and forces the app closed if that form is closed. Private Sub Form_Close() FWTerm On Error Resume Next Application.Quit acQuitSaveNone End Sub Opening the switchboard calls APPInit() which calls FWInit() which initializes the framework and loads usysFrmFWCleanup. Closing the app in any manner forces usysFrmFWCleanup to close. usysFrmFWCleanup closing calls FWTerm() cleaning up the framework and forcing Access to close if it wasnt already.. Summary In order to be useful the framework has to be split off into a library. Doing this causes a handful of issues that you have to be aware of. We may need to expose classes using the method shown above I have already exposed dclsFrm so that the forms would work correctly. It also requires setting up init and cleanup code so that the framework is shutdown reliably as the app closes. Additionally we need to add a new pair of SysVar instances for the developer to use for control of the Application and data for the Application. >From this point forward, the lectures will focus on adding functionality to the framework, generally to the form class or control class or both in order to implement some behavior that the developer has found useful, has found or developed code for, and now wishes to make available to all his projects by moving that behavior to the framework. In order to use the library we need to take the steps to break out the FE / BE and library modules into separate containers. I have outlined the process in the top of this document and have actually performed this process and provided a set of three mdb containers the FE/BE and Library. I do not intend to leave the demo broken into the three pieces however; I only did so to demonstrate the process and what goes where. For the purposes of writing a demo and allowing me and the readers following this lecture series to easily work with the demos it is much more helpful in a single file. I dont want to lose any readers because they cant deal yet with the nitty gritty of actually doing development in three parts, getting references working, getting table links fixed etc. Therefore this document and associated files actually consist of the three MDBs but the next document will have them merged back into a single file. The framework is ready however, and can be split out again at a moments notice. John W. Colby www.ColbyConsulting.com