tags:

views:

383

answers:

7

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?

  1. Instant GUI feedback that user's button press has resulted in action
  2. Instant GUI feedback in the location where user's eyes already are on how to CANCEL if action is not desired
  3. A one-button way for users to start/cancel an operation (reducing the amount of clutter on the GUI)
  4. 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".

+4  A: 

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).
Shog9
"You're using DoEvents to simulate multi-threading" - Well it *is* VB6 after all. =P
Erik Forbes
Yeah, there are a lot of reasons to hate VB6.
Shog9
Multithreading hard. DoEvents easier.
MarkJ
+3  A: 

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.

GordonB
I thought the one place 'goto's were OK is error traps like this. I see that I didn't need it in my example, but my actual code has many points where the cancel can originate, so it's either 'goto's or a ton of conditional testing to skip what's left after an abort at some random point in the flow.
Fred Hamilton
Oh, man... I'm having nightmare flashbacks here... Worked on a library a few years back that used strings to represent units of measure - "in" = inches, etc. Then, at some point, they'd hacked in support for SI UOMs... The lib was 1/10 of its original size by the time i got done with it...
Shog9
+2  A: 

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
DJ
+1  A: 

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:

  1. 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
  2. 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.

Treb
+1  A: 

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.

recursive
A: 

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, your 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.)

le dorfier
+3  A: 

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.

Darrel Miller