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 ExampleModel
:
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.