First we need to define what a state is. A state means that the object has variable(s) that may cause it to behave differently, depending on the content of the variables. As an example, the Forms collection is stateful:
1 |
Forms("SomeForm").Visible = False |
This is legal code and will compile but we can’t be assured that this will work in all cases because it depends on having a form named SomeForm and that it is open at the time we run the code. Thus, that code is stateful.
Some code are inevitably stateful but if we want to write a maintainable program, it’s usually best to write code in a way to minimizing the interaction with the state. Here are some guidelines to help do so:
- Prefer passing parameters to procedures
- Prefer writing pure functions
- Prefer creating objects over holding global variables
Pure Functions
A “pure function” is a procedure that has no state at all. A good example is this:
1 2 3 |
Public Function Add(LeftSide As Long, RightSide As Long) As Long Add = LeftSide + RightSide End Function |
There is only inputs and outputs; there are no changes made to existing variables, nor does it look at anything else. This is the ideal outcome because that means the code is very reliable and can be guaranteed to work the same way regardless of how or when it was called.
Unfortunately, we cannot always have pure functions. This would be not be a pure function:
1 2 3 |
Public Sub HideIt(TargetForm As Access.Form) TargetForm.Visible = False End Sub |
This has a effect of altering the form’s Visible property, which persists outside the procedure’s scope. If we wanted to make it pure again, we would need to do something like this instead:
1 |
Me.Visible = ShouldBeVisible(Me) |
In which case, the function only evaluates whether it should return either a True or False result and leave it up to the form to alter its own state.
Detailed Example
Consider this example:
1 2 3 4 |
Private Sub SomeButton_Click() TempVars.Add "SomeData", Me.SomeData.Value DoCmd.OpenForm "SomeForm" End Sub |
We create a TempVar which may be then later used in form SomeForm. As long nothing else changes the TempVar, this might work just fine. However, if we develop the program and now we need to use the TempVar in other places, and it happens to change before the SomeForm gets to read it, then what? We’ve created a time bomb in the code that may be hard to find because there may be many things happening in the program before we realize what really happens. We can improve this somehow by doing:
1 2 3 |
Private Sub SomeButton_Click() DoCmd.OpenForm "SomeForm", OpenArgs:=Me.SomeData.Value End Sub |
By using the OpenArgs, we now can just pass the variable directly to the SomeForm wtihout anyone else knowing about it and thus ensure that it will not get changed in the interim. But suppose we need to pass more than one piece?
1 2 3 4 5 6 7 8 9 |
Private Sub SomeButton_Click() Dim SomeValue As String Dim OtherValue As String SomeValue = Me.SomeData.Value OtherValue = Me.OtherData.Value DoCmd.OpenForm "SomeForm", OpenArgs:=SomeValue & ";" & OtherValue End Sub |
This will work but the problem is that you’ve shifted the burden to the SomeForm to parse the OpenArgs, which can only be a single string as it now contains a CSV of multiple values, and it must be in right order. Though not as stateful as the first example, this is still stateful because you are providing it with data in a format that requires a convention between the 2 classes to work with. Can we avoid the need to parse the OpenArgs?
Maybe we can improve with this:
1 2 3 4 5 6 7 8 9 10 11 |
Private Sub SomeButton_Click() Dim SomeForm As Form_frmSomeForm DoCmd.OpenForm "SomeForm" Set SomeForm = Forms("SomeForm") With SomeForm .SomeData.Value = Me.SomeData.Value .OtherData.Value = Me.OtherData.Value End With End Sub |
Actually, no this is not an improvement for the following reasons:
- We are now directly manipulating the contents of SomeForm from another form.
- The changes may be happening too late — by time we do so, SomeForm‘s Load and Current events already has fired.
- We still are doing DoCmd.OpenFormand then referring to the form.
Can we do better? Yes.
1 2 3 4 5 6 |
Private SomeForm As Form_frmSomeForm Private Sub SomeButton_Click() Set SomeForm = New Form_frmSomeForm SomeForm.Initialize Me.SomeData.Value, Me.OtherData.Value End Sub |
The changes we did was:
- Create our own instance of the form, rather than using the DoCmd.OpenForm; the SomeForm form is now associated with this form’s lifetime. If it closes, the SomeForm goes away, too.
- The SomeForm form now has an Initialize method which encapsulates the internal behavior of the SomeForm; we are now just passing parameters as opposed to rummaging inside other form’s guts. Note that this still doesn’t address the original issue where Load and Current events won’t have that data available.
- We are no long using DoCmd.OpenFormnor Forms to aid with our interaction with the SomeForm.