Practical Standards for Microsoft Visual Basic .NET (Pro-Developer)
[Previous] [Next]
10.1 Use For Next to loop a specific number of times.
The most basic type of loop is the For Next loop, which loops a specific number of times. When the maximum number of iterations is reached, execution continues with the statement following the Next statement. When you create a For Next loop, you designate the starting number to use for counting as well as the ending number. You can also specify an increment by using the Step keyword.
The For Next construct has the following syntax:
For counter = start To end [ Step step ] [ statements ] [ ExitFor ] [ statements ] Next [ counter ] |
The For Next construct has these parameters:
- counter A required numeric variable used as a loop counter. The variable can't be a Boolean or an array element.
- start A required initial value of counter.
- end A required final value of counter . When counter reaches the end number, the statements within the loop are executed one last time and then execution continues with the line following the Next statement. If counter exceeds the end number, execution continues with the line following the Next statement.
- step An optional number that specifies the amount counter is incremented each time through the loop. If you omit step , the counter variable is incremented by 1.
Most of the programming work in a For Next loop is done in the For statement that starts the loop. The majority of For Next loops are fairly simple ”the loop counter proceeds from one number to another by a step of 1. When you want a step of 1, you should omit the Step component. For instance, the following statement loops from 1 to 10 with a step of 1:
Dim intCounter AsInteger For intCounter=1 To 10 |
To increment the counter by a step other than 1, you must supply the Step component in the For statement. For example, to loop from 1 to 10 with a step of 2, you can use this For statement:
Dim intCounter AsInteger For intCounter=1 To 10 Step 2 Debug.Print intCounter Next intCounter |
To create loops that perform as you expect them to, you must fully understand the Step component. If you were asked to count to 10 by 2s, you'd probably count as follows :
2,4,6,8,10 |
However, the code shown above actually prints the following in the debug window:
1,3,5,7,9 |
When you start a loop with a For statement, the counter variable is initialized to the start value. In this example, the start value is 1. Once the loop executes, the counter variable is incremented (or decremented, in the case of a negative step value) by the value of step . This continues until the value of the counter variable exceeds the end value. When the counter variable reaches 9 and the loop completes, the counter variable is incremented to 11. Since 11 exceeds the end value of 10, the loop does not repeat again. Some developers don't understand this behavior. For example, what value is printed to the debug window when the following code executes?
Dim intCounter AsInteger '* Loop10times. For intCounter=1 To 10 |
If your first instinct is to say 10, you're mistaken but you're not alone. When the Next statement is reached and intCounter is 9, intCounter is incremented to 10 and the loop executes again. When the Next statement is encountered while intCounter has a value of 10, the loop does not immediately terminate. Instead, intCounter is incremented to 11 and execution again returns to the For statement. When the For statement evaluates intCounter and finds that it exceeds the end value, code execution moves to the statement immediately following the Next statement, which prints the value of intCounter . The value of intCounter that prints is 11.
NOTE
In Chapter 5, "Using Constants and Enumerations," I discussed why you should eliminate magic (hard-coded) numbers in favor of constants. Loops are good places to look for opportunities to use constants.
Practical Application 10.1.1 shows how a procedure is more readable when you replace hard-coded numbers in a For statement with constants. In addition to magic numbers and constants, you can also use variables or control values for the start and end parameters. For instance, the following code uses the value of a variable for the upper limit:
Dim intCounter AsInteger Dim intUpperLimit AsInteger '* Initializeupperlimit.Thisvaluecancomefromanywhere, '* suchasfromacontrolonaformorfromatextfile. intUpperLimit=100 For intCounter=1 To intUpperLimit |
When you use variable data for the start and end values in a For Next loop, you must use valid values. For instance, the following procedure is the same as the previous one, with one small but important change: intUpperLimit is set to -5.
Dim intCounter AsInteger Dim intUpperLimit AsInteger '* Initializeupperlimit.Thisvaluecancomefromanywhere, '* suchasfromacontrolonaformorfromatextfile. intUpperLimit=-5 For intCounter=1 To intUpperLimit |
This loop doesn't execute at all. When the For statement is first encountered, intCounter is initialized to 1 and compared to intUpperLimit . Since it has a value greater than that of intUpperLimit , code execution moves to the statement immediately following the Next statement. In some processes, this might be acceptable behavior; in others, it might not.
Every For statement must have a corresponding Next statement. You technically don't have to include the counter variable in the Next statement, but you should. Omitting the counter variable isn't much of a problem in a simple procedure with only one loop. But as a procedure grows in size and complexity or as nested loops are created, it becomes much more important that the counter variable be used. For example, look at these nested loops:
Dim intCounter AsInteger Dim intSecondCounter AsInteger '* Createtheouterloop. For intCounter=1 To 100 '* Createanestedloopalongwithanewcounter. For intSecondCounter=1 To 100 '* Printthevalueofthesecondcounter. Debug.Print intSecondCounter Next Next |
This example has a loop within a loop. Because the counter variables are omitted from the Next statements, it's less clear which Next statement is closing which loop. If there were many statements between the For and Next statements, it would be even more difficult to discern the closing of the loops.
You should indent all statements between the For and Next statements at least one tab stop to make the structure of the code apparent. (Chapter 8, "Formatting Code," discusses indentation in detail, but it's also worthwhile describing its importance here.) Indentation helps to display the flow or structure of code. Loops have a definite structure ”a beginning, a body, and an end. The beginning and end statements appear at the same level in the hierarchy, with the body statements subordinate. The first statement after a For statement must be indented one tab stop. The indentation of the rest of the body statements depends on their relationship to this first statement.
While most For Next loops are designed to loop a designated number of times, sometimes you need to end a For Next loop prematurely. Many developers make the mistake of using GoTo and a label to exit a loop, as shown in Practical Application 10.1.5. The correct method of leaving a For Next loop is to use the Exit For statement:
Dim intCounter AsInteger '* Loopthroughthepossibledaysofthemonth. For intCounter=1 To 31 '* Ifthecounterisequaltothecurrentdayofthemonth, '* exittheloop. If intCounter=Format(Date,"d") ThenExitFor Debug.Print intCounter Next intCounter |
The preceding code is not particularly functional, but it illustrates a point. If you must exit the loop early, do it with the Exit For statement.
Incorrect:
PrivateFunction RawNumber(strNumber AsString ) AsString '* Purpose:Stripsanalphanumericstringdowntojust '* itsnumericcomponenttobeusedforfaxing. '* Accepts:strNumber-astringcontainingaphonenumber, '* e.g.,(402)555-1212. '* Returns:Thestringwithallnonnumericelementsremoved. OnErrorGoTo PROC_ERR Dim strRaw AsString Dim intLocation AsInteger intLocation=1 '* Loopthroughallthecharactersinthestring. DoWhile intLocation<=Len(strNumber) '* Determinewhetherthecurrentcharacterisnumeric. If IsNumeric(Mid$(strNumber,intLocation,1)) Then strRaw=strRaw&Mid$(strNumber,intLocation,1) EndIf intLocation=intLocation+1 Loop RawNumber=strRaw PROC_EXIT: ExitFunction PROC_ERR: Call ShowError(Me.Name,"RawNumber",Err.Number,Err.Description) GoTo PROC_EXIT EndFunction |
Correct:
PrivateFunction RawNumber(strNumber AsString ) AsString '* Purpose:Stripsanalphanumericstringdowntojust '* itsnumericcomponenttobeusedforfaxing. '* Accepts:strNumber-astringcontainingaphonenumber, '* e.g.,(402)555-1212. '* Returns:Thestringwithallnonnumericelementsremoved. OnErrorGoTo PROC_ERR Dim strRaw AsString Dim intLocation AsInteger '* Loopthroughallthecharactersinthestring. For intLocation=1 To Len(strNumber) '* Determinewhetherthecurrentcharacterisnumeric. If IsNumeric(Mid$(strNumber,intLocation,1)) Then strRaw=strRaw&Mid$(strNumber,intLocation,1) EndIf Next intLocation RawNumber=strRaw PROC_EXIT: ExitFunction PROC_ERR: Call ShowError(Me.Name,"RawNumber",Err.Number,Err.Description) GoTo PROC_EXIT EndFunction |
Practical Applications
10.1.1 Use a constant for step whenever possible. Since the value of step is always numeric and its meaning varies from process to process, you should use a named constant rather than a literal value.
Incorrect:
'* Drawverticalgridlinesonthecanvas. For intCount=0 To (intEditWidth+1) Step 4 '* UsetheAPItomovethedrawingpen. lngResult=MoveToEx(lngScratchPadDC,intCount,0, ByVal 0&) '* Drawastraightline. lngResult=LineTo(lngScratchPadDC,intCount,intEditHeight) Next intCount |
Correct:
Const c_Magnification=4 '* Drawverticalgridlinesonthecanvas. For intCount=0 To (intEditWidth+1) Step c_Magnification '* UsetheAPItomovethedrawingpen. lngResult=MoveToEx(lngScratchPadDC,intCount,0, ByVal 0&) '* Drawastraightline. lngResult=LineTo(lngScratchPadDC,intCount,intEditHeight) Next intCount |
10.1.2 Avoid referencing the counter variable after a loop ends. When a For Next loop ends, the final value of the counter variable is not equal to the value of end ”it's higher or lower based on the value of step . If you expect a loop to run its full number of times, don't use the counter variable after the loop ends.
Although the following procedure marked as incorrect appears acceptable, it contains a nasty bug. When this procedure executes, an error is generated on the statement that attempts to print in the debug window. The error is Error 9, Subscript out of range , and it occurs because intCounter is actually 11 when the loop terminates. (The last element in the array has an index of 10.)
Incorrect:
PrivateSub FillArray() '* Purpose:Fillanarraywithvalues,andprintthe '* valueinthelastelement. OnErrorGoTo PROC_ERR Dim intCounter AsInteger Dim aintArray(1 To 10) AsInteger '* Initializetherandomnumbergenerator. Randomize '* Populatethearrayelementswithrandomnumbers. For intCounter=1 To 10 aintArray(intCounter)=(10*Rnd)+1 Next intCounter '* Printthevalueofthelastelement. Debug.Print aintArray(intCounter) PROC_EXIT: ExitSub PROC_ERR: Call ShowError(Me.Name,"FillArray",Err.Number,Err.Description) GoTo PROC_EXIT EndSub |
Correct:
PrivateSub FillArray() '* Purpose:Fillanarraywithvaluesandprintthe '* valueinthelastelement. OnErrorGoTo PROC_ERR Dim intCounter AsInteger Const c_ArrayMax=10 Const c_ArrayMin=1 Dim aintArray(c_ArrayMin To c_ArrayMax) AsInteger '* Initializetherandomnumbergenerator. Randomize '* Populatethearrayelementswithrandomnumbers. For intCounter=c_ArrayMin To c_ArrayMax aintArray(intCounter)=(c_ArrayMax*Rnd)+1 Next intCounter '* Printthevalueofthelastelement. Debug.Print aintArray(c_ArrayMax) PROC_EXIT: ExitSub PROC_ERR: Call ShowError(Me.Name,"FillArray",Err.Number,Err.Description) GoTo PROC_EXIT EndSub |
10.1.3 Include the counter variable in all Next statements. Code will still execute if you omit the counter variable from the Next statement, but it will be less readable and therefore more difficult to maintain.
Incorrect:
Const c_MinPort=1 Const c_MaxPort=4 '* SettheCommportbasedontheselectedoptionbutton. For intCounter=c_MinPort To c_MaxPort '* IfaCommportoptionbuttonisselected,usethatCommport '* andexittheloop. If optCommPort(intCounter).value=True Then intCommPort=intCounter ExitFor EndIf Next |
Correct:
Const c_MinPort=1 Const c_MaxPort=4 '* SettheCommportbasedontheselectedoptionbutton. For intCounter=c_MinPort To c_MaxPort '* IfaCommportoptionbuttonisselected,usethatCommport '* andexittheloop. If optCommPort(intCounter).value=True Then intCommPort=intCounter ExitFor EndIf Next intCounter |
10.1.4 Indent body statements in a For Next loop for visual clarity. Whenever you have a code construct with a beginning and ending statement, you have a situation that calls for indentation.
Incorrect:
'* Fillthedateselectioncombobox. For intYear=1969 To 2020 cboYear.AddItemStr(intYear) Next intYear |
Correct:
'* Fillthedateselectioncombobox. For intYear=1969 To 2020 cboYear.AddItemStr(intYear) Next intYear |
10.1.5 If you must exit a For Next loop early, use an Exit For statement. Avoid using GoTo and a label to exit a loop.
Incorrect:
'* Selectthe"unassigned"categorynodeintheTreeviewcontrol. For lngIndex=1 To treCategory.Nodes.Count '* Checktoseewhetherthisisthe"unassigned"node. If treCategory.Nodes(lngIndex).Text=c_strUnassigned Then treCategory.Nodes(lngIndex).Selected= True '* Noneedtokeeplooking;getoutoftheloop. GoTo END_OF_LOOP EndIf Next lngIndex END_OF_LOOP: |
Correct:
'* Selectthe"unassigned"categorynodeintheTreeviewcontrol. For lngIndex=1 To treCategory.Nodes.Count '* Checktoseewhetherthisisthe"unassigned"node. If treCategory.Nodes(lngIndex).Text=c_strUnassigned Then treCategory.Nodes(lngIndex).Selected= True '* Noneedtokeeplooking;getoutoftheloop. ExitFor EndIf Next lngIndex |
10.2 Use Do Loop to loop an undetermined number of times.
Sometimes you don't know the exact number of times a loop needs to be executed when the loop starts. You can start a For Next loop with an upper limit that you know is larger than the number of iterations needed (if you know that value), check for a condition within the loop, and exit the loop with an Exit For statement when the condition is met. However, this is extremely inefficient, often impractical , and just plain wrong. When you need to create a loop and you don't know how many times it needs to execute, use Do Loop .
The Do Loop construct comes in a number of flavors. In its most basic form, it has the following syntax:
Do [ statements ] Loop |
This particular loop is endless ”there is no conditional clause to determine when to stop looping. You might need an endless loop on rare occasions ”game programming comes to mind ”but more often you'll want to exit a loop when certain conditions are met.
The evaluation of a condition to terminate a Do loop can happen at the beginning or the end of the loop. Also, you can specify that a loop continue when a condition is met or until a condition is met. These options allow you a great deal of flexibility when writing loops.
The following is the syntax of a Do loop that uses the While keyword:
DoWhile condition [ statements ] Loop |
Here's an example of a simple Do loop that uses this syntax:
'* PopulateaTreeviewcontrolwithitemsfromthemarketing '* codestable. DoWhileNot (rstCodes.EOF) Set vntNode=treMarketing.Nodes.Add(intNodeIndex,tvwChild,,_ rstCodes![MarketingCode],"CodeImage") rstCodes.MoveNext Loop |
As long as condition evaluates to True ”in this case, as long as the recordset's EOF (end-of-file) property is not True ”the loop continues to execute. Note that if condition evaluates to False when the loop first starts, the code between the Do and the Loop statements never executes ”not even once.
The Until keyword keeps a Do loop going until a condition is met. The following code shows the same loop rewritten using the Until keyword:
'* PopulateaTreeviewcontrolwithitemsfromthemarketing '* codestable. DoUntil rstCodes.EOF Set vntNode=treMarketing.Nodes.Add(intNodeIndex,tvwChild,,_ rstCodes![MarketingCode],"CodeImage") rstCodes.MoveNext Loop |
This code example works exactly like the previous one, but it offers a better solution because it eliminates the need to negate the value of rstCodes.EOF . To use the Do While loop, you have to use Not , which requires extra processing and is a little more difficult to understand.
You can use the While or Until keywords in the Loop statement (the closing statement of the loop) rather than as part of the Do statement. When you place them in the Loop statement, condition is evaluated at the end of the loop rather than at the start. This ensures that the loop always executes at least once. You must be careful when you create a loop that checks the exit condition at the end of the loop, as shown here:
'* PopulateaTreeviewcontrolwithitemsfromthemarketing '* codestable. Do Set vntNode=treMarketing.Nodes.Add(intNodeIndex,tvwChild,,_ rstCodes![MarketingCode],"CodeImage") rstCodes.MoveNext LoopUntil rstCodes.EOF |
Note that this code suffers from the extreme possibility of failure. It will execute at least once regardless of whether the recordset is at end-of-file. If the recordset is at end-of-file when the loop starts, an attempt to reference the MarketingCode field causes a run-time error.
As you design a loop, consider whether it needs to execute at least once. If it does, evaluate condition at the end of the loop.
As with the For Next loop, you can exit a Do loop without using GoTo and a label. To exit a Do loop at any time, use Exit Do . The following procedure creates a loop that would be endless without the Exit Do statement. In each pass through the loop, the program looks in the list box for the string "ContactNumber" . If the string is found, it is removed from the list box. When no more occurrences of the string are found, the loop is exited. Note that using Exit Do is often inferior to placing an effective exit condition at the beginning or the end of the loop.
'* Thevalue"ContactNumber"mightappearmultipletimesinthelist '* box.Usealooptoremovealloccurrencesofthisvalue. '* SelectListItemusestheAPItoperformaquicksearch. Do lngIndex=SelectListItem(lstAvailableFields,"ContactNumber") '* Checktoseewhetherthevalue"ContactNumber"wasfound. If lngIndex>-1 Then '* Thevaluewasfound;removeitfromthelist. lstAvailableFields.RemoveItemlngIndex Else '* Itemnotfound.Alloccurrenceshavebeenremoved,so '* exittheloop. ExitDo EndIf Loop |
Generally, the expression that determines when to exit a loop uses a variable that's modified somewhere within the Do loop. Obviously, if the expression never changes, the loop is never exited.
Practical Applications
10.2.1 Evaluate the exit condition of a Do Loop at the start of the loop unless you have a reason to do otherwise . Often you can choose whether to place the exit condition at the beginning or at the end of a loop. However, loops whose exit conditions are evaluated at the beginning are a bit easier to understand, and they will not be executed even once if the condition is False.
Incorrect:
IfNot (rstReports.EOF) Then '* LoopthroughtheRecordsetofreports,addingeachreporttothe '* ListViewcontrol. Do Set objItem=lvwReports.ListItems.Add() objItem.Text=rstReports![Title] objItem.SmallIcon="Report" rstReports.MoveNext LoopUntil rstReports.EOF EndIf |
Correct:
'* LoopthroughtheRecordsetofreports,addingeachreporttothe '* ListViewcontrol. DoUntil rstReports.EOF Set objItem=lvwReports.ListItems.Add() objItem.Text=rstReports![Title] objItem.SmallIcon="Report" rstReports.MoveNext Loop |
10.2.2 When choosing between While and Until , use the one that allows the simplest exit condition. You can use either While or Until in most loops. However, depending on the situation, one of them (and not the other) will require that you use the Not operator to evaluate the opposite of an expression. You should use the value of a simple expression (rather than its opposite ) whenever possible.
Incorrect:
PrivateSub cboContacts_Requery() '* Purpose:Fillthecontactscomboboxwithallcontacts '* forthecurrentaccount. OnErrorGoTo PROC_ERR Dim strSQL AsString Dim rstContact As Recordset '* Retrievethenameofallcontactsfortheaccount. strSQL="SELECT[ContactName]FROMtblContacts"&_ "WHERE[AccountNumber]="&m_lngAccountNumber&""&_ "ORDERBY[ContactName];" cboContacts.Clear SetrstContact=dbSales.OpenRecordset(strSQL,dbOpenForwardOnly) '* Populatethecombobox. DoWhileNot (rstContact.EOF) cboContacts.AddItemrstContact![ContactName] rstContact.MoveNext Loop PROC_EXIT: '* ChecktoseewhetherrstContactissettoaninstance '* ofaRecordset. IfNot (rstContact Is Nothing ) Then rstContact.Close Set rstContact= Nothing EndIf ExitSub PROC_ERR: Call ShowError(Me.Name,"cboContacts_Requery",Err.Number,_ Err.Description) ResumeNext EndSub |
Correct:
PrivateSub cboContacts_Requery() '* Purpose:Fillthecontactscomboboxwithallcontacts '* forthecurrentaccount. OnErrorGoTo PROC_ERR Dim strSQL AsString Dim rstContact As Recordset '* Retrievethenameofallcontactsfortheaccount. strSQL="SELECT[ContactName]FROMtblContacts"&_ "WHERE[AccountNumber]="&m_lngAccountNumber&""&_ "ORDERBY[ContactName];" cboContacts.Clear SetrstContact=dbSales.OpenRecordset(strSQL,dbOpenForwardOnly) '* Populatethecombobox. DoUntil rstContact.EOF cboContacts.AddItemrstContact![ContactName] rstContact.MoveNext Loop PROC_EXIT: '* ChecktoseewhetherrstContactissettoaninstance '* ofaRecordset. IfNot (rstContact Is Nothing ) Then rstContact.Close Set rstContact= Nothing EndIf ExitSub PROC_ERR: Call ShowError(Me.Name,"cboContacts_Requery",Err.Number,_ Err.Description) ResumeNext EndSub |
10.2.3 Use a Do loop or a For Next loop instead of GoTo and a label whenever possible. In many situations, it's tempting to create a loop by using GoTo and a label. But this technique creates code that is difficult to understand and often less efficient.
Incorrect:
PrivateFunction StripDoubleQuotes( ByVal strString AsString )_ AsString '* Purpose:Locatealldoublequoteswithinastringandchange '* themtosinglequotes. '* Accepts:strString-thestringinwhichtosearchfor '* doublequotes. '* Returns:ThestringpassedhereasstrString,withdouble '* quotesreplacedbysinglequotes. Const c_DoubleQuote="""" Const c_SingleQuote="'" Dim intLocation AsInteger StartCheck: '* Lookforadoublequote. intLocation=InStr(strString,c_DoubleQuote) '* Ifadoublequoteisfound,replaceitwithasinglequote. If intLocation>0 Then '* Adoublequotehasbeenfound.Replaceitwith '* asinglequote. Mid$(strString,intLocation,1)=c_SingleQuote '* Lookforanotherdoublequote. GoTo StartCheck Else '* Nomoredoublequoteswerefound.Returnthenewstring. StripDoubleQuotes=strString EndIf EndFunction |
Correct:
PrivateFunction StripDoubleQuotes( ByVal strString AsString )_ AsString '* Purpose:Locatealldoublequoteswithinastringandchange '* themtosinglequotes. '* Accepts:strString-thestringinwhichtosearchfor '* doublequotes. '* Returns:ThestringpassedhereasstrString,withdouble '* quotesreplacedbysinglequotes. Const c_DoubleQuote="""" Const c_SingleQuote="'" Dim intLocation AsInteger Do '* Lookforadoublequote. intLocation=InStr(strString,c_DoubleQuote) '* Ifadoublequoteisfound,replaceitwithasinglequote. If intLocation>0 Then '* Adoublequotehasbeenfound.Replaceitwith '* asinglequote. Mid$(strString,intLocation,1)=c_SingleQuote EndIf LoopWhile intLocation>0 '* Returntheresult. StripDoubleQuotes=strString EndFunction |
10.3 Use Do Loop in place of While Wend .
The While Wend loop has been around for a long time, and its retirement is long overdue. Do Loop is more flexible, so you should use it instead. (Also, Wend just sounds ridiculous.) The While Wend loop looks like this:
While condition [ statements ] Wend |
The While Wend loop behaves exactly like the following Do loop:
DoWhile condition [ statements ] Loop |
There isn't much more to say about the While Wend loop. The exit condition is evaluated when the loop is started, and if the condition evaluates to True the code within the While Wend loop is executed. When the Wend statement (Middle English, anyone ?) is reached, execution jumps back to the While statement and the exit condition is reevaluated.
10.4 Use For Each Next to loop through all members of a collection.
A For Each Next loop is a powerful loop that programmers often overlook. It loops through all the members of an array or a collection of objects. Many programmers who do use For Each Next for collections don't realize that it works on arrays as well ”this is fortunate, because using For Each Next on arrays causes a noticeable decrease in performance.
For Each Next is designed and optimized to loop through collections of objects. You create an element variable, whose data type must be Variant, Object, or some specific object type, and then initiate the loop. During each pass through the loop, your element variable is set to reference an element in the collection. Manipulating the element variable directly manipulates the element in the collection. Using a For Each Next loop is often faster than using a For Next loop that utilizes the indexes of the items in a collection.
The following is the syntax for a For Each Next loop:
ForEach element In group [ statements ] [ ExitFor ] [ statements ] Next [ element ] |
The element variable, which you define, must have a Variant or Object (generic or specific) data type. You should use the specific object type that is most suitable for all objects in the group (collection). For instance, when you loop through the Controls collection on a form, you can use Variant, Object, or Control as the data type. However, you can't necessarily use the TextBox, ListBox, or ComboBox data types unless you are sure that only controls of the specified type are on the form. Therefore, the logical choice is the Control data type.
Incorrect:
PublicPropertyGet SpecialIsFormDirty(frmForm As Form) AsBoolean '* Purpose:Determinewhetheranyofthedataontheformhas '* changed. '* Accepts:frmForm-thenameoftheformtotest. '* Returns:Trueifevenonecontrol'sDataChangedproperty '* isTrue;otherwiseFalse. '* NOTE:Errorsaretrappedbecauseattemptingtoaccess '* theDataChangedpropertyofacontrolthatdoesn't '* haveone(suchasaFrame)causesarun-timeerror. OnErrorResumeNext Dim intIndex AsInteger Dim blnDataChanged AsBoolean Const c_NoSuchProperty=438 Const c_NoError=0 '* Assumethattheformisdirty. SpecialIsFormDirty= True '* Loopthroughallcontrolsontheform. For intIndex=0 To frmForm.Controls.Count-1 Err.Clear blnDataChanged=frmForm.Controls(intIndex).DataChanged '* Determinethetypeoferror(ifany)thatwasgenerated. SelectCase Err.Number CaseIs =c_NoError '* ThiscontrolhasaDataChangedproperty. If blnDataChanged ThenGoTo PROC_EXIT CaseIs =c_NoSuchProperty '* ThiscontroldoesnothaveaDataChangedproperty. CaseElse '* Legitimateerror.Sendtoerrorhandler. GoTo PROC_ERR EndSelect Next intIndex SpecialIsFormDirty= False PROC_EXIT: ExitProperty PROC_ERR: Call ShowError("clsApplication","Get_IsFormDirty",Err.Number,_ Err.Description) GoTo PROC_EXIT EndProperty |
Correct:
PublicPropertyGet SpecialIsFormDirty(frmForm As Form) AsBoolean '* Purpose:Determinewhetheranyofthedataontheformhas '* changed. '* Accepts:frmForm-thenameoftheformtotest. '* Returns:Trueifevenonecontrol'sDataChangedproperty '* isTrue;otherwiseFalse. '* NOTE:Errorsaretrappedbecauseattemptingtoaccess '* theDataChangedpropertyofacontrolthatdoesn't '* haveone(suchasaFrame)causesarun-timeerror. OnErrorResumeNext Dim ctl As Control Dim blnDataChanged AsBoolean Const c_NoSuchProperty=438 Const c_NoError=0 '* Assumethattheformisdirty. SpecialIsFormDirty= True '* Loopthroughallcontrolsontheform. ForEach ctl In frmForm.Controls '* Resettheerrorobject. Err.Clear blnDataChanged=ctl.DataChanged '* Determinethetypeoferror(ifany)thatwasgenerated. SelectCase Err.Number CaseIs =c_NoError '* ThiscontrolhasaDataChangedproperty. If blnDataChanged ThenGoTo PROC_EXIT CaseIs= c_NoSuchProperty '* ThiscontroldoesnothaveaDataChangedproperty. CaseElse '* Legitimateerror.Sendtoerrorhandler. GoTo PROC_ERR EndSelect Next ctl SpecialIsFormDirty= False PROC_EXIT: ExitProperty PROC_ERR: Call ShowError("clsApplication","Get_IsFormDirty",Err.Number,_ Err.Description) GoTo PROC_EXIT EndProperty |
Practical Applications
10.4.1 Don't use For Each Next to loop through arrays. When you use a For Each Next loop to manipulate the elements of an array, your element variable must have type Variant. If the elements of the array are not Variants, Visual Basic must coerce them to type Variant to use For Each . The result is much slower code.
Incorrect:
Dim lngResult AsLong Dim vntUndoBitMap AsVariant '* PopulateallUndomemorybitmapswiththecurrentimage. ForEach vntUndoBitMap In m_alngUndo() With vntUndoBitMap .UndoDC=CreateCompatibleDC(frmEditor.picPreview.hdc) .UndoHandle=CreateCompatibleBitmap(Me.picPreview.hdc,_ c_ImageWidth,c_ImageHeight) lngResult=SelectObject(.UndoDC,.UndoHandle) EndWith Next vntUndoBitMap |
Correct:
Dim lngResult AsLong Dim intCounter AsInteger '* PopulateallUndomemorybitmapswiththecurrentimage. For intCounter=c_MinUndo To c_MaxUndo With m_alngUndo(intCounter) .UndoDC=CreateCompatibleDC(frmEditor.picPreview.hdc) .UndoHandle=CreateCompatibleBitmap(Me.picPreview.hdc,_ c_ImageWidth,c_ImageHeight) lngResult=SelectObject(.UndoDC,.UndoHandle) EndWith Next intCounter |
10.4.2 Use the most specific data type possible in a For Each Next loop. The element variable in a For Each Next loop must be a Variant or some type of Object (generic or specific) variable. Variants have many drawbacks (as discussed in Chapter 6, "Variables"). If you use a Variant or generic Object when a specific Object type would work, you prevent Visual Basic from determining errors at compile time (such as referencing properties that don't exist for that type).
Incorrect:
Dim vntControl AsVariant '* Forcearefreshofalltextboxesontheform. ForEach vntControl In Me.Controls '* Checktoseewhetherthiscontrolisatextbox. If TypeName(vntControl)="TextBox" Then vntControl.Refresh EndIf Next vntControl |
Also incorrect:
Dim objControl AsObject '* Forcearefreshofalltextboxesontheform. ForEach objControl In Me.Controls '* Checktoseewhetherthiscontrolisatextbox. If TypeName(objControl)="TextBox" Then objControl.Refresh EndIf Next objControl |
Correct:
Dim ctl As Control '* Forcearefreshofalltextboxesontheform. ForEach ctl In Me.Controls '* Checktoseewhetherthiscontrolisatextbox. If TypeName(ctl)="TextBox" Then ctl.Refresh EndIf Next ctl |