Odd problem. Step through works, runtime doesn't - debugging

Morning all.
I've seen numerous posts about this problem on here, but nothing specific to my situation so would appreciate some assistance with this.
Essentially, I'm attempting to rearrange the order of tabpages on a tabcontrol, based on the order of entries in a listbox (lbxBuildings). The number of pages always matches the number of listbox entries, and their text values also match up.
Now I've written the following code which works perfectly when I step through it, but doesn't work (or error) at runtime.
Private Sub cmdBlgSetDown_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmdBlgSetDown.Click
'Make sure our item is not the last one on the list.
If lbxBuildings.SelectedIndex < lbxBuildings.Items.Count - 1 Then
'Insert places items above the index you supply, since we want
'to move it down the list we have to do + 2
Dim I = lbxBuildings.SelectedIndex + 2
Dim j As Integer = lbxBuildings.SelectedIndex
Dim NTP As TabPage = TabControl2.TabPages(j)
TabControl2.TabPages.Insert(I, NTP)
TabControl2.SelectedIndex = I - 1
TabControl2.TabPages.Remove(NTP)
lbxBuildings.Items.Insert(I, lbxBuildings.SelectedItem)
lbxBuildings.Items.RemoveAt(lbxBuildings.SelectedIndex)
lbxBuildings.SelectedIndex = I - 1
End If
End Sub
The rearranging of the listbox is controlled by two buttons (one up and one down) and that all works fine, so I now want the same buttons to rearrange the tabpages as well. There may well be information already entered on these pages prior to their being rearranged, so deleting one and adding a new one won't work for me (as far as I can tell).
My 'solution' of adding in a copy in the correct place then deleting the original makes perfect sense to me and as I say, it works when I step through. But at runtime, it seems to skip right over the 'insert' line and just deletes the orignal tab.
All suggestions welcome.
Many thanks!

Well after a load more digging I found mention of this specific problem on a C# site. More digging still and it turns out that there's a bug in the program with the following workaround.
Dim h As IntPtr = TabControl2.Handle
Call this 'before' doing anything with the tabcontrol and it works as expected. It's a bit odd but there it is.

Related

Missing events in properties for VB6

I am a self taught amateur Visual Basic programmer. My programs are for myself, friends and some non-profit organizations. I have a situation that I thought would be reasonably simple but doesn't seems the case.
I have a text box array of 6 elements (1 -6) named "txtBilled" .When the value is entered into any but element 6 I want to add the values in 1-5 and put result in element 6. My problems start due to the fact that the properties for the text array does not provide for a lost focus option. Searching the inter provides statements that this is normal, others say the "Lost Focus" should always be there.
As a second approach I tried to use the validate element. Never used this before created a sub as follows that I found on the web.
Private sub txtBilled__Validate(Cancel as Boolean)
Found that the Validate event is also not included in the properties for the array
I am using VB6 version 8176 under Windows 10.
Any ideal as to what I am doing incorrectly would be appreciated.
Can you not create your own LostFocus sub using the Index of the textbox array?
Private Sub txtBilled_LostFocus(Index As Integer)
Dim i As Integer
dim sngTotal As Single
' Calculate sum only if not in last textbox
If Index <> uBound(txtBilled) Then
For i = LBound(txtBilled) to UBound(txtBilled) - 1
sngTotal = sngTotal + txtBilled(i)
Next i
txtBilled(uBound(txtBilled)) = sngTotal
End If
End Sub

VB6, Adding an integer to a control name in a for loop

I am currently trying you learn VB6 and came across this issue.
I wanted to loop through a for loop and adding a number to a control name.
Dim I As Integer
For I = 1 To 5
S = CStr(I)
If TextS.Text = "" Then
LabelS.ForeColor = &HFF&
Else
LabelS.ForeColor = &H80000012
End If
Next I
This S needs to be added to Text and Label so the colour will be changed without needing to use 5 If Else statements
I hope you can help me with this.
From your comment below:
What i mean is this: If Text1.text = "" Then I need this 1 to be replaced with the variable I, so the for loop can loop through my 5 textboxes and the same for my Labels.
You can't do that (look up a variable using an expression to create its name) in VB6. (Edit: While that statement is true, it's not true that you can't look up form controls using a name from an expression. See "alternative" below.)
What you can do is make an array of your textboxes, and then index into that array. The dev env even helps you do that: Open your form in the dev env and click the first textbox. Change its name to the name you want the array to have (perhaps TextBoxes). Then click the next textbox and change its name to the same thing (TextBoxes). The dev env will ask you:
(Don't ask me why I have a VM lying around with VB6 on it...)
Click Yes, and then you can rename your other textboxes TextBoxes to add them to the array. Then do the same for your labels.
Then your code should look like this:
For I = TextBoxes.LBound To TextBoxes.UBound
If TextBoxes(I).Text = "" Then
Labels(I).ForeColor = &HFF&
Else
Labels(I).ForeColor = &H80000012
End If
Next
LBound is the lowest index of the control array, UBound is the highest. (You can't use the standard LBound and Ubound that take the array as an argument, because control arrays aren't quite normal arrays.) Note also that there's no need to put I on the Next line, that hasn't been required since VB4 or VB5. You can, though, if you like being explicit.
Just make sure that you have exactly the same number of TextBoxes as Labels. Alternately, you could create a user control that consisted of a label and a textbox, and then have a control array of your user control.
Alternative: : You can use the Controls array to look up a control using a name resulting from an expression, like this:
For I = 1 To 5
If Me.Controls("Text" & I).Text = "" Then
Me.Controls("Label" & I).ForeColor = &HFF&
Else
Me.Controls("Label" & I).ForeColor = &H80000012
End If
Next
This has the advantage of mapping over to a very similar construct in VB.Net, should you migrate at some point.
Side note:
I am currently trying you learn VB6...
(tl;dr - I'd recommend learning something else instead, VB6 is outdated and the dev env hasn't been supported in years.)
VB6's development environment has been discontinued and unsupported for years (since 2008). The runtime is still (I believe) supported because of the sheer number of apps that use it, although the most recent patch seems to be from 2012. But FWIW, you'd get a better return on your study time learning VB.net or C#.Net (or any of several non-Microsoft languages), rather than VB6...

Allow editing in a sheet, but protect it from Sorting

I have a sheet that is unprotected so that users can enter information in every field, but I need to protect the sheet from being sorted. Sorting seems to screw up the formulas and links that are in the sheet. I have seen a lot of help on how to protect the sheet but allow sorting, but not my problem which is the opposite of that.
It seems like it should be pretty straightforward; here's what I have now:
Sub ProtectRevHistory()
Worksheets("Revision History").Protect Contents:=False, AllowSorting:=False, UserInterfaceOnly:=True
End Sub
I don't get any errors with this, but it doesn't protect the Sorting. Is that even possible to do?
Ultimately, if I can get it working, I would like to run it from the Workbook_Open() event but when I placed this code in the ThisWorkbook object, it asks me for a password on open, even though I left the Password parameter off. Just hitting enter for the password gives me an error and also doesn't protect the Sorting.
So I broke down and used the macro editor to see if that would help me figure this out and I did get it working. After that, I was able to put it in the Workbook Open event so that it runs on open. Here's what it looks like:
Private Sub Workbook_Open()
Sheets("Revision History").Protect Password:="StrongPasswordHere", DrawingObjects:=False, Contents:=True, Scenarios:= _
False, AllowFormattingCells:=True, AllowFormattingColumns:=True, _
AllowFormattingRows:=True, AllowInsertingColumns:=True, AllowInsertingRows _
:=True, AllowInsertingHyperlinks:=True, AllowDeletingColumns:=True, _
AllowDeletingRows:=True, AllowFiltering:=True, AllowUsingPivotTables:= _
True
End Sub
Maybe there is something about the parameters that I don't fully understand that makes it work when all of them are listed. I'm not sure. But at least this is out there now for anyone else who needs it! The nice thing about it is that all of the Sorting functions on the Data tab are greyed out. And if you try to sort in the Filter, it doesn't work either. Success!!

Confusing with Approaching If statements and Debugging

I'm extremely new to Visual Studio 2010.
For my class, I need to make a calculator. The requirement for this problem is that if A or B is zero, then for the program to not actually calculate A or B; instead, the program assigns the value of the variable that is not zero to eh result, and if they're both zero, then just make the answer zero.
Here is my code:
Public Class Form1
Private Sub AddBox_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles AddButton.Click
Dim A As Decimal
Dim B As Decimal
Dim Result As Decimal
A = ABox.Text
B = BBox.Text
A = Decimal.Parse(ABox.Text)
B = Decimal.Parse(BBox.Text)
If (ABox.Text = 0) Then
ResultLabel.Text = BBox.Text
End If
If (BBox.Text = 0) Then
ResultLabel.Text = ABox.Text
End If
If (BBox.Text = 0 And ABox.Text = 0) Then
ResultLabel.Text = 0
End If
Result = A + B
ResultLabel.Text = Result.ToString("N2")
End Sub
My questions are as follows:
Are if statements good for this, or would Try/Catch be better?
Since the answer will automatically be right, even if the code is incorrect (e.g. 9+0 will be nine regardless, whether or not the If or Try/Catch actually works), they key is proper step by step debugging. What's the optimal way to do this? I had the one menu displaying everything step by step with how the program functioned before, but for some reason, I can't seem to find the window after I closed it. I want to see it step by step as I debug with break points.
Any other tips on good syntax for these type of conditional operators?
Sorry for sounding stupid; I couldn't find the topics on here that would cover this type of problem.
OK...first off, you don't really need the Ifs at all. (Identity Property of Addition: For any x, x + 0 = x.) Any error that's going to happen is going to happen before you even get to them, so the only thing you've done is say that if ABox parses to zero and BBox's value is something like "5.000000", all those zeros will get copied into the result. Likewise if the boxes are reversed.
Also, if you use Decimal.TryParse instead of Decimal.Parse, un-number-like stuff in the input boxes won't kill your program. They'll just get turned into 0.
You'd get similar results (minus the extra zeros) with some code like
Private Sub AddBox_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles AddButton.Click
Dim A as Decimal, B as Decimal
Decimal.TryParse(ABox.Text, A)
Decimal.TryParse(BBox.Text, B)
ResultBox.Text = (A + B).ToString("N2")
End Sub
If for some stupid reason you really, really, really need all that If crap, you will want to use ElseIf to ensure that the default case doesn't run.
Private Sub AddBox_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles AddButton.Click
Dim A as Decimal
Dim B as Decimal
Decimal.TryParse(ABox.Text, A)
Decimal.TryParse(BBox.Text, B)
If A = 0 then
ResultBox.Text = BBox.Text
ElseIf B = 0 then
ResultBox.Text = ABox.Text
Else
Dim result as Decimal = A + B
ResultBox.Text = result.ToString("N2")
End If
End Sub
But it's a waste -- and what's worse, if the point is to add two numbers, it's incorrect. Someone could put "I Like Cheese" in the first box and "YAAAAAAAAY!!!" in the other, and the result would be "YAAAAAAAAY!!!". Clearly not what should happen.
Private Sub AddBox_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles AddButton.Click
Dim A as Decimal
Dim B as Decimal
Try
A = Decimal.Parse(ABox.Text)
B = Decimal.Parse(BBox.Text)
ResultBox.Text = (A + B).ToString("N2")
Catch ex as FormatException
MessageBox.Show("Exactly two numbers (one per box), please!")
End Try
End Sub
But i detest doing that for user input. You should expect users to be malicious and/or idiotic, and count on them doing everything they can to mess up your app. It'll save you lots of debugging later on. But if we assume the worst, is malformed input really an exceptional condition? I say no. (Others may disagree. But they're wrong. :) )
If you want to notify the user of an input error, Decimal.TryParse also returns a boolean (true/false) saying whether it succeeded. If it failed, you can show a message telling the user to quit being an idiot. (Slightly more diplomatically, if you want.)
Secondly...turn Option Strict on. Please. All this implicit casting gives me the willies. Note that that will probably add like a dozen errors to your list...but that's a good thing. One should be aware when they're trying to put a number in a string variable and so on, cause lots of magic happens there that (in my opinion) people need to be more aware of.
K, now, as for debugging...if you've followed my advice up to this point, you shouldn't really need to debug. The exceptions are gone, and the code's going to be more correct before you can even compile it. But if you still have to, one way to go step by step through the program is to set a breakpoint on either the Function line itself, or the first line that isn't a declaration. (Click the left margin next to the line. A red ball should appear in the margin about where you clicked. That lets you know there's a breakpoint there.) You can probably try to set a breakpoint on the declarations too, but i seem to remember that causing the breakpoint to be either on the next line that actually does something, or on the beginning of the function. I forget which, and don't have VS on this machine to check.)
When you run the program from VS, the program will start, and when it hits the breakpoint, VS will pop back into the foreground with a yellow arrow where the red ball is. That yellow arrow points at the next line to be executed. From there, you can use the debug toolbar (look in your toolbars; you should have extra buttons, blue i think, that look like play/stop/etc buttons) to step through the code. You should also see windows labeled 'Locals', 'Watches', and other windows you'd expect to see in a worthwhile debugger. :)
Just be aware that while the program's stopped at a breakpoint, or stepping through the code, the app will appear frozen. UI updates probably will not appear right away, and consequently, you won't be able to type anything in the input boxes or hit the button til you resume (hit the "play' button).

What's so bad about using button captions as variables in VB6?

I received some justified critical feedback on my last question (How to gracefully exit from the middle of a nested subroutine when user cancels?) for using the caption of a command button as a state variable. I did it because it's efficient, serving two or three purposes at once with very little code, but I understand how it could also cause problems, particularly in the slightly sloppy way I originally presented it.
I feel like this deserves its own discussion, so here's the same idea cleaned up a bit and modified to do it "right" (which basically means defining the strings in a single place so your code won't start failing because you simply changed the text of a command button). I know my variable and control naming convention is poor (OK, nonexistent), so apologies in advance. But I'd like to stay focused on the caption as state variable discussion.
So here we go:
' Global variables for this form
Dim DoTheThingCaption(1) As String
Dim UserCancel, FunctionCompleted As Boolean
Private Sub Form_Initialize()
' Define the possible captions (is there a #define equivalent for strings?)
DoTheThingCaption(0) = "Click to Start Doing the Thing"
DoTheThingCaption(1) = "Click to Stop Doing the Thing"
' Set the caption state when form initializes
DoTheThing.Caption = DoTheThingCaption(0)
End Sub
Private Sub DoTheThing_Click() ' Command Button
If DoTheThing.Caption = DoTheThingCaption(0) Then
UserCancel = False ' this is the first time we've entered this sub
Else ' We've re-entered this routine (user clicked on button again
' while this routine was already running), so we want to abort
UserCancel = True ' Set this so we'll see it when we exit this re-entry
DoTheThing.Enabled = False 'Prevent additional clicks
Exit Sub
End If
' Indicate that we're now Doing the Thing and how to cancel
DoTheThing.Caption = DoTheThingCaption(1)
For i = 0 To ReallyBigNumber
Call DoSomethingSomewhatTimeConsuming
If UserCancel = True Then Exit For ' Exit For Loop if requested
DoEvents ' Allows program to see GUI events
Next
' We've either finished or been canceled, either way
' we want to change caption back
DoTheThing.Caption = DoTheThingCaption(0)
If UserCancel = True Then GoTo Cleanup
'If we get to here we've finished successfully
FunctionCompleted = True
Exit Sub '******* We exit sub here if we didn't get canceled *******
Cleanup:
'We can only get to here if user canceled before function completed
FunctionCompleted = False
UserCancel = False ' clear this so we can reenter later
DoTheThing.Enabled = True 'Prevent additional clicks
End Sub '******* We exit sub here if we did get canceled *******
So there it is. Is there still anything really that bad about doing it this way? Is it just a style issue? Is there something else that would give me these four things in a more desirable or maintainable way?
Instant GUI feedback that user's button press has resulted in action
Instant GUI feedback in the location where user's eyes already are on how to CANCEL if action is not desired
A one-button way for users to start/cancel an operation (reducing the amount of clutter on the GUI)
A simple, immediate command button disable to prevent multiple close requests
I can see one concern might be the close coupling (in several ways) between the code and the GUI, so I could see how that could get to be a big problem for large projects (or at least large GUIs). This happens to be a smaller project where there are only 2 or 3 buttons that would receive this sort of "treatment".
The single biggest problem with this technique is that it uses a string as a boolean. By definition, a boolean variable can have only two states, while a string can have any number of states.
Now, you've mitigated the danger inherent in this somewhat by relying on an array of predefined strings to define allowed values for the command button text. This leaves a handful of lesser issues:
Logic is less-than-explicit regarding current and available states (there are actually four possible states for the form: not-started, started, completed, started-but-canceling) - maintenance will require careful observation of the potential interactions between button text and boolean variable states to determine what the current state is / should be. A single enumeration would make these states explicit, making the code easier to read and understand, thereby simplifying maintenance.
You're relying on the behavior of a control property (button text) to remain consistent with that of the exposed property value type (string). This is the sort of assumption that makes migrating old VB6 code to newer languages / platforms absolute hell.
String comparison is much, much slower than a simple test of a boolean variable. In this instance, this won't matter. In general, it's just as easy to avoid it.
You're using DoEvents to simulate multi-threading (not directly relevant to the question... but, ugh).
The biggest issue i've come accross when working on (very old) code like this [button captions as variables] is that globalisation is a nightmare.... I had to move a old vb6 app to use English and German... it took weeks, if not months.
You're using goto's as well..... a bit of refactoring needed perhaps to make the code readable??
**Edit in response to comments
I'd only use a goto in vb6 at the top of each proc;
on error goto myErrorHandler.
then at the very bottom of the proc i'd have a one liner that would pass err to a global handler, to log the error.
Ignoring the general architecture/coupling problems because you are aware of those issues, one problem with your approach is because VB6 controls do magic stuff when you set properties.
You may think you are just setting a property but in many cases you are causing events to fire also. Setting a checkbox value to true fires the click event. Setting the tabindex on a tab control causes a click event. There are many cases.
If I remember correctly I also think there are scenarios where if you set a property, and then read it immediately, you will not see the update. I believe a screen refresh has to occur before you see the new value.
I have seen too much horrible VB6 code that uses control properties as storage. If you ever find this kind of code you will recognize it because it is scattered with redundant calls to Refresh methods, DoEvents and you will frequently see the UI get hung. This is often caused by infinite loops where a property is set, an event is fired and then another property is set and eventually someone writes a line of code that updates the first property again.
If those issues don't scare you enough then think of this. Some of us just are not that smart. I've been coding in VB6 for over 10 years and have personally written probably around 750K LOC and I keep staring at your example above and I find it very difficult to understand what it going on. Assume that all the people that will need to read your code in the future will be really dumb and make us happy by writing really simple looking code.
I think it's better to decouple the caption text from the state of processing. Also the goto's make it hard to read. Here is my refactored version...
Private Const Caption_Start As String = "Click to Start Doing the Thing"
Private Const Caption_Stop As String = "Click to Stop Doing the Thing"
Private Enum eStates
State_Initialized
State_Running
State_Canceled
State_Completed
End Enum
Private Current_State As eStates
Private Sub Form_Initialize()
DoTheThing.Caption = Caption_Start
Current_State = State_Initialized
End Sub
Private Sub DoTheThing_Click()
If Current_State = State_Running Then
'currently running - so set state to canceled, reset caption'
'and disable button until loop can respond to the cancel'
Current_State = State_Canceled
DoTheThing.Caption = Caption_Start
DoTheThing.Enabled = False
Else
'not running - so set state and caption'
Current_State = State_Running
DoTheThing.Caption = Caption_Stop
'do the work'
For i = 0 To ReallyBigNumber
Call DoSomethingSomewhatTimeConsuming
'at intervals check the state for cancel'
If Current_State = State_Canceled Then
're-enable button and bail out of the loop'
DoTheThing.Enabled = True
Exit For
End If
DoEvents
Next
'did we make it to the end without being canceled?'
If Current_State <> State_Canceled Then
Current_State = State_Completed
DoTheThing.Caption = Caption_Start
End If
End If
End Sub
Apart from removing the GOTos as DJ did in his answer, there is nothing really wrong about your approach. The button caption can have only two states, and you use those two states to define the flow in your code.
I have however two reasons why I would do it differently:
Your method creates problems when you want to translate your program into a different language (in my experience you should always plan for that), because the captions would change in another language
It goes against the principle of seperating the user interface from the program flow. This may not be an important thing for you, but when a program gets bigger and more complex, having a clear seperation of the UI from the logic makes things much easier.
To sum it up, for the case at hand your solution certainly works, and there is no reason why it shouldn't. But on the other hand experience has taught us that with more complex programs, this way can cause problems which you can easily avoid by using a slightly different approach.
Also, I think it is safe to assume that everybody who criticised your example did so because they made a simnilar choice at some point, and later realised that it was a mistake.
I know I did.
This ties your underlying algorithm to specific behavior in your UI. Now, if you want to change either one of them, you have to make changes to both. As your app grows in size, if you don't keep your changes local by encapsulating logic, maintenance will become a nightmare.
If anyone for any reason ever needs to work on your code, they won't find practices and conventions they are familiar and comfortable with, so the boundaries of functionality won't exist. In other words, you are headed in the wrong direction on the Coupling/Cohesion trail. Functionally integrating State management with the UI is the classic poster child for this issue.
Do you understand OOP at all? (Not a criticism, but a legitimate question. If you did, this would be a lot clearer to you. Even if it's only VB6 OOP.)
Localization has the biggest impact on the type of logic OP is presenting. As several people mentioned it - what if you need to translate the app into Chinese? And German? And Russian?
You'd have to add additional constants covering those languages too... pure hell. GUI data should remain what it is, a GUI data.
The method OP describes here reminded me what Henry ford said: "Any customer can have a car painted any color that he wants so long as it is black".

Resources