A recent comment on UserForm1.Show asked about how to extend that logic to a dialog that would have an “Apply” button. This article walks you through the process – and this time, there’s a download link!
The dialog is a simple UserForm with two textboxes and 3 buttons:
The Model for this dialog is a simple class exposing properties that the two textboxes manipulate – I’ve named the class
Option Explicit Private Type TModel field1 As String field2 As String End Type Private this As TModel Public Property Get field1() As String field1 = this.field1 End Property Public Property Let field1(ByVal value As String) this.field1 = value End Property Public Property Get field2() As String field2 = this.field2 End Property Public Property Let field2(ByVal value As String) this.field2 = value End Property
I also defined a simple
IDialogView interface, which can be implemented by any other dialog, since it passes the model as an
Object (i.e. it’s not tightly coupled with the
ExampleModel class in any way); the contract is simply “here’s your model, now show me a dialog and tell me if I can proceed to consume the model” – in other words, the caller provides an instance of the model, and the implementation returns
True unless the user cancelled the form.
Option Explicit Public Function ShowDialog(ByVal viewModel As Object) As Boolean End Function
The form’s code-behind therefore needs to implement the
IDialogView interface, and somehow store a reference to the
ExampleModel. And since we have cancellation logic but we’re not exposing it (we don’t need to – the
IDialogView.ShowDialog interface handles that concern, by returning
False if the dialog is cancelled), the
IsCancelled flag is just internal state.
As far as the “apply” logic is concerned, the thing to note here is the
Public Event ApplyChanges event, which we raise when the user clicks the “apply” button:
Option Explicit Public Event ApplyChanges(ByVal viewModel As ExampleModel) Private Type TView IsCancelled As Boolean Model As ExampleModel End Type Private this As TView Implements IDialogView Private Sub AcceptButton_Click() Me.Hide End Sub Private Sub ApplyButton_Click() RaiseEvent ApplyChanges(this.Model) End Sub Private Sub CancelButton_Click() OnCancel End Sub Private Sub Field1Box_Change() this.Model.field1 = Field1Box.value End Sub Private Sub Field2Box_Change() this.Model.field2 = Field2Box.value End Sub Private Sub OnCancel() this.IsCancelled = True Me.Hide End Sub Private Function IDialogView_ShowDialog(ByVal viewModel As Object) As Boolean Set this.Model = viewModel Me.Show vbModal IDialogView_ShowDialog = Not this.IsCancelled End Function Private Sub UserForm_Activate() Field1Box.value = this.Model.field1 Field2Box.value = this.Model.field2 End Sub Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer) If CloseMode = VbQueryClose.vbFormControlMenu Then Cancel = True OnCancel End If End Sub
The Presenter class does all the fun stuff. Here I’ve decided to allow the model’s data to be optionally supplied as parameters to the
Show method; the form handles its
Activate event to make sure the form controls reflect the model’s initial values when the form is displayed:
Option Explicit Private WithEvents view As ExampleDialog Private Property Get Dialog() As IDialogView Set Dialog = view End Property Public Sub Show(Optional ByVal field1 As String, Optional ByVal field2 As String) Set view = New ExampleDialog Dim viewModel As ExampleModel Set viewModel = New ExampleModel viewModel.field1 = field1 viewModel.field2 = field2 If Dialog.ShowDialog(viewModel) Then ApplyChanges viewModel Set view = Nothing End Sub Private Sub view_ApplyChanges(ByVal viewModel As ExampleModel) ApplyChanges viewModel End Sub Private Sub ApplyChanges(ByVal viewModel As ExampleModel) Sheet1.Range("A1").value = viewModel.field1 Sheet1.Range("A2").value = viewModel.field2 End Sub
So we have a
Private WithEvents field that gets assigned in the
Show method, and we handle the form’s
ApplyChanges event by invoking the
ApplyChanges logic, which, for the sake of this example, takes the two fields and writes them to A1 and A2 on Sheet1; if you’ve read There is no worksheet then you know how you can introduce an interface there to decouple the worksheet from the presenter, and then it doesn’t matter if you’re writing to a worksheet, a text file, or a database: the presenter doesn’t need to know all the details.
The calling code in
Module1 might look like this:
Option Explicit Public Sub ExampleMacro() With New ExamplePresenter .Show "test" End With End Sub
One problem here, is that the View implementation is coupled with the presenter (i.e. the presenter is creating the view): we need the concrete UserForm type in order for VBA to see the events; without further abstraction, we can’t quite pass a
IDialogView implementation to the presenter logic without popping up the actual dialog. Pieter Geerkens has a nice answer on Stack Overflow that describes how an Adapter Pattern can be used to solve this problem by introducing more interfaces, but covering this design pattern will be the subject of another article.