I used to think that spending time to create your own add-in was pretty silly. However, that has changed, and I'm starting to believe that it would be very cool to create add-ins to perform a number of tasks. Hence, this chapter. Don't get me wrong—add-ins have always been cool, but I thought they belonged in the realm of third-party vendors such as FMS, who creates numerous add-ins for Access and Visual Basic. (You can see some FMS product demos on the CD-ROM in the 3rdPartyFMS folder.)
My thinking that creating add-ins is only for third-party vendors has changed because I've come up with a tool of my own that I find pretty useful, and I want a quick way to put it on most of my forms. I want it placed on a form when the form is created, without the hassle of my having to remember to paste the code behind the forms.
The tool created by the wizard in this chapter is the Bookmark Tracker created in the last chapter. The wizard that you will create in the following sections will place a Bookmark Tracker combo on the form you happen to be in. It will also copy all the objects used by the combo for managing the bookmarks into the current database.
Before going into detail on what the wizard does, let's review the Bookmark Tracker itself. The Bookmark Tracker enables users to manage multiple bookmarks on an open form (see Figure 17.3).
Here are some of the features of the Bookmark Tracker:
You can add bookmarks just by moving to the record you want to bookmark and then selecting << Add a New Bookmark >>.
To delete bookmarks, select << Remove Bookmark(s) >> from the combo box. You are then given the dialog to remove bookmarks.
You can move to a stored bookmark by selecting one from the list.
The Bookmark Tracker consists of one form (not counting the form that the combo is placed on) and two class modules. These objects are as follows:
Object Name | Purpose |
---|---|
bmtRemoveBookmarks (Form type) | Allows for removing one or all bookmarks from the collection |
clsBookmarkItems (Class Module type) | Tracks information for each bookmark |
clsBookmarkManagement (Class Module type) | Performs maintenance routines for the Bookmark Tracker |
These objects, along with the code in Listing 17.1, are necessary for the Bookmark Tracker to do its job.
Option Compare Database '-- Declare a clsBookmarkManagement variable. Private bmtForm As clsBookmarkManagement Private Sub cboBookmarkTracker_AfterUpdate() '-- Call the BookmarkAction method, for all actions. bmtForm.BookmarkAction End Sub Private Sub Form_Load() '-- Create an instance of the clsBookmarkManagement class Set bmtForm = New clsBookmarkManagement '-- Call the custom initialization routine, passing the '-- necessary items. bmtForm.InitBookmarks Me!cboBookmarkTracker, "LastName", _ "FirstName" End Sub |
Note
Most of the code in Listing 17.1 is created by the Bookmark Tracker Wizard. However, the Option statement will already be on the form, not created by the wizard. It is included in this listing to show that the line below the Option statement is in the module's Declarations section.
That's about it for the review of the Bookmark Tracker. If you need more information on Listing 17.1 or the Bookmark Tracker in general, reread Chapter 16, “Extending Your VBA Library Power with Class Modules and Collections.”
It's time now to look at the Bookmark Tracker Wizard and what it enables you to do. The Bookmark Tracker Wizard looks a lot like other wizards in Access 2000 in that it consists of multiple dialogs that take you logically from one step to another. You will see the wizard's dialogs in the next section.
Look at what the Bookmark Tracker Wizard accomplishes:
It lets users select the target form, as well as the section of the form, on which to place the Bookmark Tracker combo box (see Figure 17.4).
It combines up to two fields to display in the combo box from those available based on the target form's record source (see Figure 17.5).
It creates a combo box with a user-specified name (see Figure 17.6).
In addition to the items displayed on the user interface portion of the Bookmark Tracker Wizard, the wizard generates code behind the target form, as displayed in Listing 17.1. It also does the following:
It checks to see whether the objects listed in Table 17.1 are already in the target database.
If objects don't exist in the target database, it copies them into the database and then marks them as hidden.
As you should be able to see by now, the wizard created here is full of features and will get you off and running to create add-ins of your own. But before pulling apart the wizard, look at what it takes to install the new add-in.
You can install your add-ins in Access in numerous ways. Here are two ways to install, the first of which will be discussed in greater detail in the next two sections:
Manually register the add-in by using the Add-In Manager.
Use the techniques discussed in Chapter 18, “Manipulating the Registry with VBA,” to set the values.
Regardless of which method you decide to use to install the add-in, Access provides a means for you to specify the settings necessary to update the Windows Registry. These values are set within the add-in database itself in the USysRegInfo table.
Figure 17.7 shows the USysRegInfo table for the add-in BMTWiz.mda, created for the Bookmark Tracker Wizard in this chapter. This add-in database can be found in the folder ExamplesChap17 on the CD-ROM.
Note
To see this table, you must be able to see system objects. To see system objects in the database explorer, choose Options from the Tools menu. On the View page, check the System Objects in the Show category. While you are there, also check the Hidden Objects so that you can see some objects that will be hidden later in the chapter.
Note
Although the .mda (Microsoft database add-in) extension was used, it's still just an Access database. You use the .mda extension so that the file won't show up when you're opening databases, but will show when using the Add-In Manager.
Tip
You also could create an .mde, which strips out the source code, if you don't want other developers looking at the code. This is how the wizards are shipped with Access. In past versions, you could get the source code for Access wizards from Microsoft's Web site. However, the company decided that because of the support issues and code changes from version to version, the wizard code would not be published. That said, you should check Microsoft's site just in case it received a rash of calls and decided to publish the source code after all.
The fields necessary for Access to register the add-in are as follows:
Field Name | Purpose |
---|---|
Subkey | The key listed under the menu add-ins subkey. The full key in this case will be HKEY_LOCAL_MACHINESoftwareMicrosoftOffice9.0AccessMenu Add-Ins&Bookmark Tracker Wizard. |
Type | Denotes the first entry (0) or the data type of entry: 1 for String, 4 for DWORD. |
ValName | Value names placed under the subkey. The value names used here specify the filename (library) and calling routine used to invoke the add-in (expression). |
Value | The actual values used for the library name and expression. |
Note
The Add-In Manager will replace the Subkey field text HKEY_CURRENT_ACCESS_PROFILE with the actual Registry path at the time of registering the add-in. You can use the standard HKEY_LOCAL_MACHINE if you want to supply the full path.
The same goes for the AccDir text in the Value field. This is replaced by the add-ins' location used by Access. At the time of this writing, Access stores the add-ins for users in WindowsApplicationDataMicrosoftAddIns.
Access add-ins are stored (as MDE files) in the OfficeLCID directory. (LCID is 1033 for English.)
To use the Add-In Manager, follow these steps while in the Chap17.mdb database:
1. |
Choose Add-Ins and then Add-In Manager from the Tools menu. (Add-Ins might be the only submenu item at this point.) |
2. |
In the Add-In Manager, the Available Add-Ins list might be empty. Click the Add New button to display the Open File dialog, which will point to the location in which Access stores add-ins. |
Tip
Note the add-ins' location by clicking the Look In drop-down list. By doing this, after you point to the actual location where the add-in database is presently located, Access copies and stores the database in the official add-ins' location. It's a good idea to know where the add-ins are ultimately located and take care that, if you change the original copy, you don't update the copy in the add-ins' location (unless you stored the original there to begin with). I don't recommend storing your add-in originals in the Add-Ins folder for obvious reasons of versioning.
3. |
Locate your add-in database—in this case, BMTWiz.mda, originally in the ExamplesChap17 folder on the CD-ROM. When it's selected, click Open. |
You've installed the Bookmark Tracker Wizard and can see it in the list of available add-ins, with a × next to it (see Figure 17.8). The Add-In Manager has also added the necessary entries into the Registry as per the USysRegInfo table (see Figure 17.9).
Note
Although you can create a routine to install your add-in programmatically, you then have to know the locations for Menu Add-Ins in the Registry and AccDir for the add-in folder, both of which can change from version to version.
By examining the Bookmark Tracking Wizard, you will see a number of different VBA commands:
Populating combos based off current form sections and record source fields
Creating controls
Copying objects from the add-in database to an application database
Locating lines of code from modules
Adding lines of code to modules
You will see these techniques used as each page of the wizard is discussed. To start, look at the overall form itself in Design mode (see Figure 17.10).
The form used for the wizard, frmBookmarkTrackerWizard, is in the BMTWiz.mda database, located in the ExamplesChap17 folder on the CD-ROM. This form uses Access's tab control with no tabs visible. Note the pages listed in the Object drop-down list (names starting with pg) in Figure 17.10.
Tip
Although you can name your pages 1, 2, 3, and so on, I like to give mine meaningful names that I can switch to in code. If I do this, I don't have to rename pages when I move them around.
You can set the tabs to not be visible by setting the Style property on the tab control to None.
Tip
Until you are ready to use your wizard for production, you may want to leave your tabs visible (Style = Tabs). It's more convenient to click the tabs when in Design view than it is to use the Object drop-down list.
Note
You may be saying to yourself, “How can I open the database if I have it installed as an add-in?” Remember that you're using the original BMTWiz.mda, not the one in the Add-Ins folder. If you were to use that database, you would get an error telling you it's already opened as an add-in.
It's time to look at some routines connected to the main wizard form itself. The first subroutine is the form's Open event (shown in Listing 17.2).
Private Sub Form_Open(Cancel As Integer) Dim strFormList As String Dim strCurrDoc As String Dim aobjForm As AccessObject '-- Look through the forms in the application database and '-- grab those that aren't hidden and not the same name as '-- this form. For Each aobjForm In CurrentProject.AllForms strCurrDoc = aobjForm.Name If Not GetHiddenAttribute(acForm, strCurrDoc) _ And strCurrDoc <> Me.Name Then strFormList = strFormList & strCurrDoc & "; " End If Next aobjForm '-- Trim off the last semi-colon and assign the string to '-- the forms listbox. strFormList = Left$(strFormList, Len(strFormList) - 2) Me!lstForms.RowSource = strFormList End Sub |
The Open event routine first populates the lstForms list box with the available forms that aren't hidden and aren't named the same name as the wizard form. (The reason this form's name is important is that when you start creating your own wizard, you will probably be running the wizard from within itself.)
The last task is to create a row source for the cboSection combo box by going through the Sections array of the target form.
The Form_Open event makes good use of the new CurrentProject object and AllForms collection, both introduced in Access 2000. To read more about these objects, see Chapter 4, “Looking at the Access Collections.”
The tasks of the cmdCancel, cmdPrevious, and cmdNext command buttons are fairly straightforward (see Listing 17.3).
Private Sub cmdCancel_Click() DoCmd.Close acForm, Me.Name End Sub Private Sub cmdNext_Click() '-- Increment the tab page If CheckFilledFields() Then Me!tabWizard = Me!tabWizard + 1 End If End Sub Private Sub cmdBack_Click() '-- Decrement the tab page If CheckFilledFields() Then Me!tabWizard = Me!tabWizard - 1 End If End Sub |
The cmdCancel_Click routine closes the wizard form if the user clicks the button. The cmdPrevious and cmdNext buttons call the CheckFilledFields function to make sure that the required fields are filled in, and then they increment and decrement the page value accordingly.
Tip
One cool thing about these buttons is that they can be used generically on another wizard without your having to change any code. The only code change would be inside CheckFilledFields (shown in Listing 17.4) because the required fields will change from wizard to wizard.
Function CheckFilledFields() As Boolean CheckFilledFields = True '-- Check to make sure the needed fields are supplied. Select Case Me!tabWizard Case 0 If IsNull(Me!lstForms) Then Me!lstForms.SetFocus MsgBox "A form must be selected to continue.", _ vbInformation, Me.Caption CheckFilledFields = False End If Case 1 If IsNull(Me!cboField1) Then Me!cboField1.SetFocus MsgBox "You must select at least one field to include " & _ "in the description of the bookmark.", _ vbInformation, Me.Caption CheckFilledFields = False End If End Select End Function |
The code in CheckFilledFields checks for two fields to be filled in on the wizard. First, if leaving the first page of the wizard, a form must be picked from the lstForms listbox. Otherwise, if leaving the second page, the first combo box of the description (called cboField1) must be filled in.
The next thing to examine is what occurs when you switch pages of the tab control, tabWizard, by clicking the cmdNext and cmdPrevious command buttons. When the tabWizard pages are switched, the Change event occurs (see Listing 17.5).
Private Sub tabWizard_Change() Me!tabWizard.Pages(Me!tabWizard).SetFocus '-- If the combo box page, assign the chosen form's rowsource '-- to the fields combo boxes. If Me!tabWizard = 1 Then Me!cboField1.RowSource = Forms(Me!lstForms).RecordSource Me!cboField2.RowSource = Forms(Me!lstForms).RecordSource End If '-- Handle the cmdNext button based on the current page If Me!tabWizard = Me!tabWizard.Pages.Count - 1 Then Me!cmdNext.Enabled = False ElseIf Not Me!cmdNext.Enabled Then Me!cmdNext.Enabled = True End If '-- Handle the cmdBack button based on the current page If Me!tabWizard = 0 Then Me!cmdBack.Enabled = False ElseIf Not Me!cmdBack.Enabled Then Me!cmdBack.Enabled = True End If End Sub |
This event procedure performs the following steps:
1. |
If the tab control value is one (second page), it fills in the row source of each Description field (cboField1 and cboField2) with the field list located in the RecordSource property of the chosen form in lstForms. |
Tip
Like some other objects and collections in Access, the tab control value is zero-based. Therefore, page 1 has a value of 0, page 2 a value of 1, and so on.
2. |
The next task performed in Listing 17.5 is checking whether the tab control is showing the last page. If this control is on the last page, the code disables the cmdNext button; otherwise, the code enables the cmdNext button. |
3. |
The last command button to look at is the cmdFinish button. However, because the bulk of the work is performed by the Click event for this button, I want to save it for last. For now, look at the validation that goes on for each field on the wizard, because the validation performed will be things you need to watch out for creating your own wizards.
The first control to look at, lstForms, has two events programmed: BeforeUpdate and AfterUpdate. You can see the BeforeUpdate event here:
Private Sub lstForms_BeforeUpdate(Cancel As Integer) Dim mdlForm As Module DoCmd.OpenForm Me!lstForms, acDesign, WindowMode:=acHidden Set mdlForm = Forms(Me!lstForms).Module If CheckBMTAlreadyExists(mdlForm) Then MsgBox "This form already has a bookmark tracker combo on it!", _ vbInformation, Me.Name Cancel = True Exit Sub End If End Sub
This code performs three interesting tasks:
It opens the target form hidden in Design view with the line of code that reads
DoCmd.OpenForm Me!lstForms, acDesign, WindowMode:=acHidden
The form's module is referenced by setting it to the variable mdlForm in the line that reads
Set mdlForm = Forms(Me!lstForms).Module
The module is then passed to the CheckBMTAlreadyExists() function. This routine checks to see whether the Bookmark Tracker already exists on the form. The section of code that uses this is as follows:
If CheckBMTAlreadyExists(mdlForm) Then MsgBox "This form already has a bookmark tracker combo on it!", _ vbInformation, Me.Name Cancel = True Exit Sub End If
Listing 17.6 shows the code for the CheckBMTAlreadyExists function.
Function CheckBMTAlreadyExists(mdlForm As Module) As Boolean Dim lngStartLine As Long, lngStartCol As Long Dim lngEndLine As Long, lngEndCol As Long mdlForm.Find "Private bmtForm As clsBookmarkManagement", _ lngStartLine, lngStartCol, lngEndLine, lngEndCol If lngStartLine <> 0 Then CheckBMTAlreadyExists = True End If End Function |
The routine finds whether the Bookmark Tracker already exists on the form by looking through the module's VBA code. By using the Find method of the Module object, it searches for the existence of the following statement:
"Private bmtForm as clsBookmarkManagement"
Finding this statement in the module code tells the wizard that a copy of the Bookmark Tracker exists. At this point, users can select another form to place the Bookmark Tracker on or cancel the wizard.
The module Find method is a cool command that uses the following syntax:
module.Find strTextToFind, lngStartLine, lngStartCol, lngEndLine, _ lngEndCol
If the text is found in the module, the line number where the text is located will be placed in lngStartLine.
Note
The Find method also returns a Boolean that can be used to see whether the text was found.
In the AfterUpdate event of the lstForms list box control, the code goes through the Sections array and builds the row source for the cboSection combo box. Listing 17.7 shows the code for lstForms_AfterUpdate.
Private Sub lstForms_AfterUpdate() Dim intCurrSec As Integer Dim strSecName As String Dim frmTarget As Form Dim strSource As String On Error Resume Next '-- Build a list of possible sections if they exist. For intCurrSec = 0 To 4 strSecName = Forms(Me!lstForms).Section(intCurrSec).Name If Err.Number = 0 Then strSource = strSource & "; " & intCurrSec & _ "; '" & strSecName & "'" Else Err.Clear End If Next strSource = Mid$(strSource, 2) Me!cboSection.RowSource = strSource End Sub |
The last event procedure to look at is the Enter event for the cboSection combo box. This event checks to make sure that a form is selected in the lstForms list box. Otherwise, a message is given to the effect that a form must be selected. Here's the code:
Private Sub cboSection_Enter() If IsNull(Me!lstForms) Then Me!lstForms.SetFocus MsgBox "A form must be selected to continue.", _ vbInformation, Me.Caption End If End Sub
That's it for the wizard's first page.
Page two of the wizard consists of two combo boxes: cboField1 and cboField2. Figure 17.11 shows page two of the wizard in Design view.
Only two event procedures are used on the second page of the wizard:
Private Sub cboField1_AfterUpdate() Me!cmdFinish.Enabled = Not IsNull(Me!cboField1) End Sub Private Sub cboField2_Enter() If IsNull(Me!cboField1) Then MsgBox "You need to fill in the field for the first part of " & _ "description before filling in part two.", _ vbInformation, Me.Caption Me!cboField1.SetFocus End If End Sub
The first procedure, cboField1_AfterUpdate, enables the cmdFinished button if the cboField1 has been filled in with data.
Tip
You always want to determine the minimal number of controls required for completing the wizard's task before you enable your cmdFinished button. But it's worth the effort to do so because it makes your wizard that much more convenient for you or someone else to use.
The next procedure, cboField2_Enter, makes sure that you've filled in cboField1 before attempting to enter data in cboField2.
Page three, the final page of the wizard, consists of the txtBMTName text box and chkDesign check box. Figure 17.12 shows page three of the wizard in Design view.
The chkDesign control enables users to specify whether they want to keep the target form, including the VBE, open in Design view when the wizard is complete. If this is not specified, the wizard closes the form and the VBE. (You'll see more about this in Listing 17.10 in the next section.)
The txtBMTName text box enables users to pick their own name for the combo box control to be placed on the target form. txtBMTName has two events programmed: BeforeUpdate and AfterUpdate. Listing 17.8 shows both events.
Private Sub txtBMTName_BeforeUpdate(Cancel As Integer) If IsNull(Me!txtBMTName) Then MsgBox "You must supply a name for the combo box control." & _ vbCrLf & vbCrLf & "Please enter a name.", vbInformation, _ Me.Caption Cancel = True End If End Sub Private Sub txtBMTName_AfterUpdate() If CheckBMTNameDupe() Then Exit Sub Else mfNameChecked = True End If End Sub |
The first routine, txtBMTName_BeforeUpdate, checks to make sure that the name of the combo box is blank.
TxtBMTName_AfterUpdates calls the CheckBMTNameDupe() function, which checks whether a control already exists on the target form with the name specified in txtBMTName. Listing 17.9 shows the code for CheckBMTNameDupe().
Function CheckBMTNameDupe() As Boolean Dim ctlCurrent As Control For Each ctlCurrent In Forms(Me!lstForms).Controls If ctlCurrent.Name = Me!txtBMTName Then MsgBox "The control already exists with the name " & _ "specified for the combo box." & vbCrLf & vbCrLf & _ "Please enter a new name.", vbInformation, Me.Caption Me!txtBMTName.SetFocus CheckBMTNameDupe = True Exit Function End If Next ctlCurrent End Function |
This code goes through each control on the target form and compares the name to that entered in txtBMTName. If a match is found, the focus is moved to the txtBMTName text box and True is passed back.
It's now time to look at the meat and potatoes of this whole wizard—the cmdFinished_Click event.
After the user enters all necessary data and clicks the cmdFinished button, the cmdFinished_Click event procedure is executed. This event procedure basically
All these tasks, including creating the VBA code behind the target form, are done with VBA commands. Listing 17.10 shows the cmdFinished_Click event procedure.
The routine cmdFinished_After does a lot, so I will go through it a step at a time:
1. |
This routine checks, if it has not checked in txtBMTName_AfterUpdate, to see whether a control already exists on the target form. That way, if the user clicks the cmdFinished button and never goes to txtBMTName, the default name is still checked. If Not mfNameChecked Then If CheckBMTNameDupe() Then Exit Sub End If End If |
2. |
It tests to see whether the form already has a module. If the form doesn't have a module, it creates one by setting the HasModule property of the form to True: If Not frmTarget.HasModule Then frmTarget.HasModule = True End If |
3. |
By using the CreateControl() function, the cmdFinished_Click routine creates the Bookmark Tracker combo box and the combo box's label. The CreateControl() function does just that—enables you to create controls on a form, specifying all the necessary properties. '-- Set up the combo box to handle manipulating the bookmarks Set ctlCombo = CreateControl(frmTarget.Name, acComboBox, _ Me!cboSection, , , 1710, 100, 2880) With ctlCombo .Name = Me!txtBMTName .ColumnCount = 2 .ColumnWidths = "0;2" .RowSourceType = "Value list" .RowSource = "-1; '<< Add a New Bookmark >>'" End With '-- Create the label for the bookmark combo. Set ctlLabel = CreateControl(strTarget, acLabel, Me!cboSection, _ Me!txtBMTName, , 60, 70, 1600, 300) ctlLabel.Caption = "Bookmark(s):" |
4. |
The routine gets a reference to the target form's module: Set mdl = frmTarget.Module |
5. |
The code calls the CopyObjectsToDatabase routine, which is shown in Listing 17.11. Listing 17.11. BMTWiz.mdb: Copying Objects from the Add-In Database to the Current Application DatabaseListing 17.11 first checks to see whether the class module clsBookmarkManagement exists in the target application. If the class module exists, the code doesn't bother to copy the objects into the target database. If clsBookmarkManagement doesn't exist, the code copies two class modules into the target database, and then it sets them as hidden by using the SetHiddenAttribute method. |
6. |
The routine in Listing 17.10 generates the code line needed in the Declarations section of the target form by calling CreateDeclarationsArea and passing it a reference to the target form's module. You can see the code for CreateDeclarationsArea here: Sub CreateDeclarationsArea(mdl As Module) mdl.AddFromString vbCrLf & _ "'-- Declare a clsBookmarkManagement variable." & _ vbCrLf & "Private bmtForm As clsBookmarkManagement" End Sub |
Note
By using the module's AddFromString method, VBA places the supplied string at the bottom of the Declarations section. Is that convenient, or what?
7. |
Calling
CreateFormLoadEvent, it's time to create the Load event for the target form, if it doesn't already exist. Otherwise, CreateFormLoadEvent adds to the existing code base in Listing 17.12. Listing 17.12. BMTWiz.mdb: Creating the Target Form's Load EventListing 17.12, after declaring some variables, first tries to see whether the text "Form_Load" is already in the module. If it doesn't find it, the CreateFormLoadEvent routine creates the event by using the module's CreateEventProc method. In either case, lngStartLine gets updated to the location of the "Form_Load" line so that the routine can then use the InsertLines method to add the remaining lines of code to the Form_Load event procedure. |
8. |
The CreateComboAfterUpdateEvent routine does just what its name implies—it creates the AfterUpdate event procedure for the Bookmark Tracker combo box: Sub CreateComboAfterUpdateEvent(mdl As Module) Dim lngStartLine As Long lngStartLine = mdl.CreateEventProc("AfterUpdate", Me!txtBMTName) mdl.InsertLines lngStartLine + 2, _ " '-- Call the BookmarkAction method, for all actions." mdl.InsertLines lngStartLine + 3, _ " bmtForm.BookmarkAction" End Sub By now, the statements used in the CreateComboAfterUpdateEvent routine should be old hat. It creates the event procedure by using CreateEventProc, and then it inserts the necessary code lines. |
9. |
The final step in cmdFinish_Click in Listing 17.10 is to clean up. This is performed by closing the forms. If chkDesign was True (now assigned to blnDesign), the code reopens the form in Design view; otherwise, it hides the VBE as well. DoCmd.Close acForm, Me!lstForms, acSaveYes DoCmd.Close acForm, Me.Name Set frmTarget = Nothing Set mdl = Nothing If blnDesign Then DoCmd.OpenForm strTarget, acDesign Else Application.VBE.MainWindow.Visible = False End If |
Note
You may have noticed that the code simply sets the VBE to Not Visible and doesn't close it. If you try to code the VBE while code is executing, you will get an error.
That's it for the Bookmark Tracker Wizard. I hope this gives you enough to start creating your own wizards and add-ins. It's time now to look at issues regarding creating your own code libraries.
18.216.190.167