I’ve seen these tutorials. You’ve probably seen them too. They all go “see how easy it is?!” when they end with a glorious
UserForm1.Show without explaining anything about what it means for your code and your understanding of programming concepts, to use a form’s default instance like this. Most don’t even venture into explaining anything about that default instance – and off you go, see you on Stack Overflow.
Because if you don’t know what you’re doing, all you’ve learned is how to write code that, in the name of “hey look it’s so easy”, abstracts away crucially important concepts that will, sooner or later, come back to bite you in the …rear end.
What’s that default instance anyway?
A UserForm is essentially a class module with a designer and a
VB_PredeclaredId attribute. That PredeclaredId means VBA is automatically creating a global-scope instance of the class, named after that class. If the default instance is ever unloaded or set to
Nothing, its internal state gets reset, and automatically reinitialized as soon as the default instance is invoked again. You can
Set UserForm1 = Nothing all you want, you can never verify whether
UserForm1 Is Nothing, because that expression will always evaluate to
False. A default instance is nice for, say, exposing a factory method. But please, please don’t
Show the default instance.
Doing. It. Wrong.™
There are a number of red flags invariably raised in many UserForm tutorials:
Unload Me, or worse,
Unload UserForm1, in the form’s code-behind. The former makes the form instance a self-destructing object, the latter
destroysresets the default instance, and that’s not necessarily the executing instance – and that leads to all kinds of funky unexpected behavior, and embarrassing duplicate questions on Stack Overflow. Every day.
UserForm1.Showat the call site, where
UserForm1isn’t a local variable but the “hey look it’s free” default instance, which means you’re using an object without even realizing it (at least without
New-ing it up yourself) – and you’re storing state that belongs to a global instance, which means you’re using an object but without the benefits of object-oriented programming. It also means that…
- The application logic is implemented in the form’s code-behind. In programming this [anti-]pattern has a name: the “smart UI”. If a dialog does anything beyond displaying and collecting data, it’s doing someone else’s job. That piece of logic is now coupled with the UI, and it’s impossible to write a unit test for it. It also means you can’t possibly reuse that form for something else in the same project (heck, or for something similar in another project) without making considerable changes to the form’s code-behind. A form that’s used in 20 places and runs the show for 20 functionalities, can’t possibly be anything other than a spaghetti mess.
So that’s what not to do. Flipside.
Doing it right.
What you want at the call site is to show an instance of the form, let the user do its thing, and when the dialog closes, the calling code pulls the data from the form’s state. This means you can’t afford a self-destructing form that wipes out its entire state before the [Ok] button’s Click handler even returns.
Hide it, don’t Unload it.
In .NET’s Windows Forms UI framework (WinForms / the .NET successor of MSForms), a form’s
Show method is a function that returns a
DialogResult enum value, a bit like a
MsgBox does. Makes sense; that
Show method tells its caller what the user meant to do with the form’s state:
Ok being your green light to process it,
Cancel meaning the user chose not to proceed – and your program is supposed to act accordingly.
Show-ing a dialog isn’t some fire-and-forget business: if the caller is going to be responsible for knowing what to do when the form is okayed or cancelled, then it’s going to need to know whether the form is okayed or cancelled.
And a form can’t tell its caller anything if clicking the [Ok] button nukes the form object.
The basic code-behind for a form with an [Ok] and a [Cancel] button could look like this:
Option Explicit '@Folder("UI") Private cancelled As Boolean Public Property Get IsCancelled() As Boolean IsCancelled = cancelled End Property Private Sub OkButton_Click() Hide End Sub Private Sub CancelButton_Click() OnCancel End Sub Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer) If CloseMode = VbQueryClose.vbFormControlMenu Then Cancel = True OnCancel End If End Sub Private Sub OnCancel() cancelled = True Hide End Sub
Notice there are two ways to cancel the dialog: the [Cancel] button, and the [X] button, which would also nuke the object instance if
Cancel = True wasn’t specified in the
QueryClose handler. Handling
QueryClose is fundamental – not doing it means even if you’re not
Unload-ing it anywhere, [X]-ing out of the form will inevitably cause issues, because the calling code has all rights to not be expecting a self-destructing object – you need to have the form’s object reference around, for the caller to be able to verify if the form was cancelled when
The calling code looks like this:
With New UserForm1 .Show If Not .IsCancelled Then '... End If End With
Notice there’s no need to declare a local variable; the
With New syntax yields the object reference to the
With block, which properly destroys the object whenever the
With block is exited – hence why
GoTo-jumping out and then back into a
With block is never a good idea; this can happen accidentally, with a
Resume Next instruction in an error-handling subroutine.
A dialog displays and collects data. If the caller needs to know about a
UserName and a
Password, it doesn’t need to care about some
passwordBox textbox controls: what it cares about, is the
UserName and the
Password that the user provided in these controls – the controls themselves, the ability to hide them, move them, resize them, change their font and border style, etc., is utterly irrelevant. The calling code doesn’t need controls, it needs a model that encapsulates the form’s data.
In its simplest form, the model can take the shape of a few
Property Get members in the form’s code-behind:
Public Property Get UserName() As String UserName = userNameBox.Text End Property Public Property Get Password() As String Password = passwordBox.Text End Property
Or better, it could be a full-fledged class, exposing
Property Get and
Property Let members for every property.
The calling code can now get the form’s data without needing to care about controls and knowing that the
UserName was entered in a
TextBox control, or knowing the
Password without knowing that the
PasswordChar for the
passwordBox was set to
Except, it can – form controls are basically public instance fields on the form object: the caller can happily access them at will… and this makes the
Password interesting properties kind of lost in a sea of MSForms boilerplate in IntelliSense. So you implement the model in its own class module instead, and use composition to encapsulate it:
Private viewModel As LoginDialogModel Public Property Get Model() As LoginDialogModel Set Model = viewModel End Property Public Property Set Model(ByVal value As LoginDialogModel) Set viewModel = value End Property
The model could be updated by the textboxes – it could even expose
Boolean properties that can be used to enable/disable the [Ok] button, or show/hide a validation error icon:
Private Sub userNameBox_Change() viewModel.UserName = userNameBox.Text ValidateForm End Sub Private Sub passwordBox_Change() viewModel.Password = passwordBox.Text ValidateForm End Sub Private Sub ValidateForm() okButton.Enabled = viewModel.IsValidModel userNameValidationErrorIcon.Visible = viewModel.IsInvalidUserName passwordValidationErrorIcon.Visible = viewModel.IsInvalidPassword End Sub
Now, a problem remains: the caller doesn’t want to see the form’s controls.
So we have a model abstraction that the view can consume, but we don’t have an abstraction for the view. That should be simple enough – let’s add a new class module and define a general-purpose
Option Explicit '@Folder("Abstractions") '@Interface Public Function ShowDialog(ByVal viewModel As Object) As Boolean End Function
Now the form can implement that interface – and because the interface is exposing that
ShowDialog method, we don’t need a public
IsCancelled property anymore. I’m introducing a
Private Type at this point, because I like having only one private field:
Option Explicit Implements IView '@Folder("UI") Private Type TView IsCancelled As Boolean Model As LoginDialogModel End Type Private this As TView Private Sub OkButton_Click() Hide End Sub Private Sub CancelButton_Click() OnCancel End Sub Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer) If CloseMode = VbQueryClose.vbFormControlMenu Then Cancel = True OnCancel End If End Sub Private Sub OnCancel() this.IsCancelled = True Hide End Sub Private Function IView_ShowDialog(ByVal viewModel As Object) As Boolean Set this.Model = viewModel Show IView_ShowDialog = Not cancelled End Function
The interface can’t be general-purpose if the
Model property is of a type more specific than
Object, but it doesn’t matter: the code-behind gets IntelliSense and early-bound, compile-time validation of member calls against it because the
Private viewModel field is an implementation detail, and this particular
IView implementation is a “login dialog” with a
LoginDialogModel; the interface doesn’t need to know, only the implementation.
The [Ok] button will only ever be enabled if the model is valid – that’s one less thing for the caller to worry about, and the logic addressing that concern is neatly encapsulated in the model class itself.
The calling code is supplying the model, so its type is known to the caller – in fact that
Property Get member is just provided as a convenience, because it makes little sense to
Set a property without being able to
Get it later.
Speaking of the calling code, with the addition of a
Self property to the model class (
Set Self = Me), it could look like this now:
Public Sub Test() Dim view As IView Set view = New LoginForm With New LoginDialogModel If Not view.ShowDialog(.Self) Then Exit Sub 'consume the model: Debug.Print .UserName, .Password End With 'model goes out of scope End Sub 'view goes out of scope
If you read the previous article about writing unit-testable code, you’re now realizing (if you haven’t already) that this
IView interface could be implemented by some
MockLoginDialog class that implements
ShowDialog by returning a test-configured value, and unit tests could be written against any code that consumes an
IView rather than an actual
LoginForm, so long as you’ve written it in such a way that it’s the calling code that’s responsible for knowing what specific
IView implementation the code is going to be interacting with.
The model’s validation logic could be unit-tested, too:
Const value As String = "1234" With New LoginDialogModel .Password = value Assert.IsTrue(.IsInvalidPassword, "'" & value & "' should be invalid.") End With
With a Model and a View, you’re one step away from implementing the
New-ing-up a Presenter class, an abstraction that completes the MVP pattern, a much more robust way to write UI-involving code than a Smart UI is.