A collection of recommendations, conventions and some scaffolding, for naming and crafting solutions in classic (VB5/6) and Visual Basic for Applications, that the code on this website, francescofoti.com, as the code in the applications I develop and commercialize thru my company devinfo.net, adheres to.
Foreword
Please note that these coding guidelines in no way pretend to be canon, they’re just my personal preferences and they reflect the coding style that has grown to be mine.
I wrote the first version of these guidelines back in 2014, because I needed to document and share my code with coworkers (I so always liked to document my work). As I’m going to share more of my codebase in the coming year(s – hopefully), I’m now (end of 2019) publishing this more complete version, to smooth your dive into the code you may encounter around here, and possibly get the most out of it.
I only added to these guidelines since I first defined them, and I still stick to them today.
I know every one of them is debatable and every developer has his or her own style and convictions.
However, these guidelines, as they are, and even since before I put them on paper, faithfully helped me acquire and maintain consistency and a high level of code maintainability throughout the years. I hope they can at least encourage you to build or step your own guidelines up, if you’ve not already done so.
Naming conventions
Hungarian notation
The”Hungarian notation” principle basically consists in using a set of standard prefixes (all in lower case), before each variable name to indicate its scope and type. Use it.
Exceptions to this rule, or when it may not be appropriate to use the Hungarian notation to name things, are indicated along, and there’s a paragraph about those exceptions later.
Constants
- Do not use Hungarian for constants (and there’s your first exception).
- All constant names are in upper case and, if they consist of multiple “words”, they may use an underscore to separate them.
Examples:
1 2 3 4 5 6 7 8 9 10 |
'Like API constants: Private Const SW_MAXIMIZE As Long = 3& Private Const SW_NORMAL As Long = 1& Private Const SW_HIDE As Long = 0& Private Const SW_SHOW As Long = 5& 'And we may do: Private Const COLFLAG_DATETIME As Long = 1& 'Which I prefer over: Private Const COL_FLAG_DATETIME As Long = 1& |
All constants should declared with their type (As …)
Examples:
1 2 3 4 5 |
'DON'T DO THAT: Private Const SW_MAXIMIZE = 3 'BUT INSTEAD, DO: Private Const SW_MAXIMIZE As Long = 3& |
Note the ampersand (“&”) after the “3”, that matches the type of the constant (here a Long).
Variables
As long as we’re talking about where a variable is defined inside a project, scope in Visual Basic is simple. A variable can be global to the entire application, or local to a class module or standard module (or any other document provided by the development environment like a Form or a Sheet), or local to a function (or Sub).
Prefix | Scope |
---|---|
g | Application global scope |
m | Class, module or other document (Form, Report, Sheet, etc…). “m” stands for “Member” variable. |
(none) | Local to the current function (or Sub), or see [Exceptions] |
There is no underscore following this 1 letter prefix.
Visual Basic native data types prefixes
Visual Basic data type | Prefix |
---|---|
Integer | “i” |
Long | “l” (minus L). Although in rare cases I don’t mind loosely using “i” when confusion wouldn’t matter so much. |
Variant | “v” |
String | “s” (and not that horrible “str”) |
Boolean | “f” (stands for “flag”, while I use “b” for the Byte data type) |
Byte | “b” |
Single | “sng” |
Double | “dbl” |
Currency | “c” or “cur” |
Object | “o” (and not “obj”) |
Date | “dt” (I use the Date datatype in very rare occurrences, I mostly use Variants for dates) |
Collection | “col” |
Local variables used as loop iterators (i, j, k, …)
There’s nothing wrong in using one letter variable names for Integer or Long indices (or indexes), whether it is in loops (For, Do, While, Repeat, …) or for index access to an item. But it may help using longer indices names (like iCustomer, iLine, iCol) when it helps disambiguate heavily packed code. For iteration on other data types (object reference, interface, structures, etc…), avoid one letter iterator variable names.
#personal: Use “i” for rows, and “j” for columns, then “k” for anything else. If nesting more, think of using more explicit variable names.
UI elements classes
UI element | Prefix |
---|---|
Form | “frm” |
Report | “rpt” |
Form used as a (modal or modeless) dialog | “frm” + name + “Dialog”, (eg. frmLoginDialog) |
Subform (Access) | “sfrm” |
Class module as an interface | “I” |
Textbox | “txt” |
Combobox | “cbo” |
Listbox | “lst” |
Button | “cmd” |
Label | “lbl” |
Tab control | “tab” |
Checkbox | “chk” |
Option button | “opt” |
Scrollbar | “hsb” or “vsb” (horizontal, respectively vertical) |
Other datatypes
Data type | Prefix |
---|---|
Type definition (structures) | “T” |
Object of any other class | “o” or a 2 or 3 letter prefix for the class, like “cn” for and ADODB connection, or “rs” for a Recordset, etc… |
Standard module | “M” |
Class module | “C”. This is unless you expose by any means the class outside of your project or database, in which case you should use a non prefixed class name (also see [Exceptions]). |
Class module, used as an interface | “I” (uppercase “i”) |
Defined Type | “t” or “tag” |
Inside a type definition, use Hungarian notation for member variables, example:
1 2 3 4 5 6 7 8 9 10 |
'Inside the CQueue class module (https://francescofoti.com/...) & Private Type TQHeader iQCount As Integer iQBack As Integer iQFront As Integer iMaxCapacity As Integer End Type 'The queue header holds runtime information about the queue Private mTHeader As TQHeader |
Function parameters
Every function / sub parameter variable is prefixed with a “p”. That’s another instance of an invaluable indicator in the body of the function’s code that allows to quickly identify if a variable is a parameter that has been passed to the function, among the ones that have been declared in its body. Without this prefix, it may be sometimes very difficult to distinguish between the origin of variables while reading the function’s code.
#personal: You’ll also quite commonly see the prefix “Ret” inserted after the variable scope and type prefix, when a function parameter variable (ByRef) is going to be modified in a meaningful way for the caller, by the function.
I also insert “New” after “Ret” if the (ByRef) parameter variable is an object that will be created inside the function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
'This function expects a reference to an existing CList instance in poRetList, 'and it will modify the passed object, but will not create it. 'Note that I crafted the "lst" prefix for a CList object reference variable in plstParams. 'I also crafted the "cn" prefix for an ADODB connection object reference. 'But I kept the generic "o" preifx in poRetList; it's OK to mix the two as they come. Public Function ADOGetSnapshotList(ByRef pcnDatabase As ADODB.Connection, _ ByRef psSQL As String, _ ByRef poRetList As CList, _ Optional ByRef plstParams As CList) As Boolean 'etc... End Function 'Source: https://www.tek-tips.com/viewthread.cfm?qid=1060175 'Restyled and modified to populate a CList. 'Returns True if successful or False and sets the module error context 'Returned list (poRetNewList) will have 2 columns: ' PaperNumber : Integer ' PaperName : String 'Sorted on PaperNumber 'Note that here I use the more generic "o" prefix in poRetNewList again. Private Function GetPaperList(ByVal psPrinterName As String, ByRef poRetNewList As CList) As Boolean Dim lPaperCount As Long Dim lCounter As Long Dim hPrinter As Long 'etc... End Function |
Arrays
Every array variable is prefixed with an “a”, after the scope letter prefix, examples:
1 2 3 4 5 6 7 8 9 |
'A global array of 250 strings Public gasLabels(1 To 250) As String 'A local array of strings Private Sub Foo() Dim asUnit(1 To 5) as String End Sub 'In a standard module Private masText(1 To 100) As String |
Enumerations
Enumeration definitions are prefixed with a minus “e”, as are the enumeration value names. Try to choose value names that can tip enough about what type of enumeration the values refer to, to mitigate confusion. In the sample below, the “eLeft”, “eCenter” and “eRight” symbolic names, may still be slightly ambiguous; this is acceptable if there is no near possibility of ambiguity.
Examples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
'We're inside the CCGrid class module (a [C]ustom, [C]haracter (or [C]onsole) based [Grid]) Public Enum eGridViewColAlign eLeft eCenter eRight End Enum Public Enum eGridViewCellWrap eWrapNone eWrapText eFoldText End Enum Public Enum eTableChar eCharE_W eCharN_S eCharS_E eCharS_W eCharN_E echarN_W eCharS_E_W echarN_E_W echarN_S_E echarN_S_W eCharEllipsis echarN_S_E_W End Enum Private masChar(eCharE_W To echarN_S_E_W) As String 'Sample usage (focus on enum usage, don't worry about the CCGrid class): Set oGrid = New CCGrid fOK = ADOGetSnapshotList(cnData, SQL, lstData) 'Get some db data oGrid.FormatCols "Title::10;ContactID:#:3;Zip:6:right;" & _ "City:10;Address:22" Set oGrid.DataList = lstData 'use of enums; full qualification "eGridViewCellWrap." is not required. oGrid.ColWrap(lstData.ColPos("Address")) = eGridViewCellWrap.eWrapText oGrid.ColWrap(lstData.ColPos("Mnemonic")) = eFoldText oGrid.ColWrap(lstData.ColPos("Floor")) = eFoldText |
Functions names
Let’s assume that when we refer to functions, we’re also talking about procedures (sub).
- Use Pascal case convention, where each word in the function name is capitalized.
No Camel case (first letter of the function in lower case, the other uppercase). - Never separate words with an underscore. This is because underscores are automatically added by Visual Basic in event procedures (and interface implementations), so they would only add unnecessary confusion.
Name of functions in standard modules
API and module mnemonic
When crafting a standard module to handle a specific set of tasks as an API, prefix all the public functions in the module with more or less one to five uppercase characters that qualify the module where they belong: the module mnemonic.
Don’t prefix the private functions.
Example:
Module name is: MADOAPI (which I prefer over MAdoApi as ADO and API are rarely seen in lower case).
The chosen module mnemonic is “ADO”, it will prefix each module member function.
Sample of some of the functions signatures, prefixed by “ADO”:
1 2 3 4 5 6 7 |
Public Function ADOGetAccessConnString(ByVal psDatabasePathname As String) As String End Function Public Function ADOOpenConnection(ByRef psConnString As String, ByRef psDatabase As String) As ADODB.Connection End Function Public Function ADOExecSQL(pcnExecute As ADODB.Connection, ByRef psSQL As String, Optional ByRef plRetAffectedCt As Long) As Boolean End Function |
Collection of related functions
If the module is just a collection of helper functions that are logically grouped together in the module, don’t prefix the module’s member function with a module mnemonic.
Example:
Module name is: MStrings (Not an API, a collection of functions helping with string manipulations). No Prefix:
1 2 3 4 5 |
Public Function ssprintf(ByRef psModel As String, ParamArray pavParams()) As String End Function Function UUCase(ByVal psSource As String) As String End Function |
(You can find articles explaining both these functions on my personal website)
Exceptions
Do not use Hungarian notation for variable names, in these two cases:
- Variables used as Public properties in a class module:
This happens when a variable, declared as Public, in a class module, is not wrapped with get/let/set property accessors, but is directly exposed (non managed R/W/Set).
Sample, in which we see a part of the declaration section of a class module:
1 2 3 4 5 6 7 8 9 10 11 |
'Extract from the header section (declarations) of CConsoul(.cls) Public FontName As String Public FontSize As Integer Public WindowTitle As String Public BackColor As Long Public ForeColor As Long Public MaxCapacity As Integer Public WithFrame As Boolean Public RefreshOnAutoRedraw As Boolean |
- Global objects:
1 2 3 4 5 6 7 |
'In the declaration section of a *standard* module. Public MyApp as New CMyApp 'And not "goMyApp" or something like "gappMyApp" 'Then, as we may use an ini file, it could be a public property in CMyApp: Dim asSectionName() As String Dim lSectionCt As Long '"Ct" stands for "Count", that's another personal habit lSectionCt = MyApp.IniFile.GetSectionNames(asSectionName()) |
Error management
Recommendations
- Return only basic native types, or objects references from functions (and property procedures).
- Do not return arrays, pass them by reference.
- Return True to indicate success, False to indicate failure. In case of failure, use the following guidelines to provide more contextual information.
- Avoid using error handlers to implement behavior as much as possible. Always jump from an error handler to an exit label in the same function, with VB resume label statement, don’t let the error handler be an exit point of your function, or jump conditionally at different points in the function.
- Do not raise errors, do not use Visual Basic Err.Raise method, instead trap errors in your error handler, return False from the function that trapped the error and set the module or class “error context” (see SetErr() function below).
In standard modules
This applies if the module represents an API (not for collection type modules). According to these guidelines, you should already have your public functions prefixed with the module mnemonic.
Add the following private member variables, in the declarations section of the module:
1 2 3 4 5 |
'Error context Private mlErr As Long Private msErr As String Private msErrCtx As String |
Add the following functions, prefixing them with your module mnemonic. As an example, they’re here prefixed with “ADO”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Private Sub ClearErr() mlErr = 0& msErr = "" msErrCtx = "" End Sub Private Sub SetErr(ByVal psErrCtx As String, ByVal plErr As Long, ByVal psErr As String) mlErr = plErr msErr = psErr msErrCtx = psErrCtx End Sub Public Function ADOLastErr() As Long ADOLastErr = mlErr End Function Public Function ADOLastErrDesc() As String ADOLastErrDesc = msErr End Function Public Function ADOLastErrCtx() As String ADOLastErrCtx = msErrCtx End Function |
In class modules
Add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
' Class error context Private mlErr As Long Private msErr As String Private msErrCtx As String Private Sub ClearErr() mlErr = 0& msErr = "" msErrCtx = "" End Sub Private Sub SetErr(ByVal psErrCtx As String, ByVal plErr As Long, ByVal psErr As String) mlErr = plErr msErr = psErr msErrCtx = psErrCtx End Sub Public Property Get LastErr() As Long LastErr = mlErr End Property Public Property Get LastErrDesc() As String LastErrDesc = msErr End Property Public Property Get LastErrCtx() As String LastErrCtx = msErrCtx End Property |
Trapping and handling errors
Once you’ve added the previous code, you can handle trappable errors in your functions, like in this example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Public Function DoSomething() As Boolean Const LOCAL_ERR_CTX As String = "DoSomething" On error goto DoSomething_Err ClearErr Dim oAnyRef As Object Set oAnyRef = CreateAnObject() 'Actually do something... DoSomething = True DoSomething_Exit: Set oAnyRef = Nothing Exit Function DoSomething_Err: Seterr LOCAL_ERR_CTX, Err.Number, Err.Description Resume DoSomething_Exit End Function |
Then
Thank you for reading or skimming these guidelines. Now that the boring stuff is behind us, I’ll try to bring more fun stuff in the next posts.
Please rate and comment here freely or tweet me @unraveledbytes, I’ll be happy to read you.