免责声明 我写了文章Victor K链接到。我拥有那个博客,并管理它所属的开源VBIDE插件项目。
你提出的两种方案都不是理想的。回归基础。
您的规格要求用户能够选择不同的过滤器,并选择使用
UserForm
实现UI。到目前为止,一切都很好......但从那时起,一切都是下坡路。使表单负责除演示之外的任何事情都是一个常见的错误,并且它有一个名称:这是
智能UI[反]模式,它的问题在于
它不能扩展。它非常适合原型制作(即快速制作“可行”的东西-请注意引号),但对于需要在多年内维护的任何内容来说都不太适用。您可能已经看到了这些表格,其中包含160个控件、217个事件处理程序和3个私有过程,每个过程关闭近2000行代码:这就是
智能UI扩展的糟糕结果,也是这条路上唯一可能的结果。您知道,
UserForm
是一个类模块:它定义了一个对象的蓝图。对象通常希望被实例化,但随后有人想出了给所有
MSForms.UserForm
实例授予一个
预声明ID的天才想法,在COM术语中,这意味着您基本上可以免费获得全局对象。太好了!不是吗?不是的。
UserForm1.Show
decisionInput1 = UserForm1.decision
If decisionInput1 Then
UserForm2.Show
Else
UserForm3.Show
End If
如果
UserForm1
被"X'd-out"或
UserForm1
被卸载,会发生什么?如果该表单未处理其
QueryClose
事件,则对象将被销毁 - 但由于这是默认实例,VBA会自动/静默地为您创建一个新实例,就在您的代码读取
UserForm1.decision
之前 - 因此,您得到的是
UserForm1.decision
的初始全局状态。
如果它不是默认实例,并且未处理QueryClose
,那么访问已销毁对象的.decision
成员将为访问空对象引用而给出经典的运行时错误91。
UserForm2.Show
和UserForm3.Show
都做同样的事情:点燃并忘记 - 不管发生什么,要想知道确切的内容,需要在各自的代码后台中挖掘它们。
换句话说,这些表单正在主导。它们负责收集数据、呈现数据、收集用户输入、以及执行必须完成的任何工作。这就是为什么它被称为“智能UI”:UI知道一切。
有更好的方法。MSForms是.NET WinForms UI框架的COM祖先,与.NET后继者所共有的是它特别适用于著名的模型-视图-展示器(MVP)模式。
模型
这是您的数据。基本上,它是您的应用程序逻辑需要知道的内容。
UserForm1.decision
我们采用这个。
添加一个新类,称之为,比如,FilterModel
。应该是一个非常简单的类:
Option Explicit
Private Type TModel
SelectedFilter As String
End Type
Private this As TModel
Public Property Get SelectedFilter() As String
SelectedFilter = this.SelectedFilter
End Property
Public Property Let SelectedFilter(ByVal value As String)
this.SelectedFilter = value
End Property
Public Function IsValid() As Boolean
IsValid = this.SelectedFilter <> vbNullString
End Function
这就是我们所需的:一个封装表单数据的类。该类可以负责一些验证逻辑,或其他操作 - 但它不会收集数据,也不会向用户展示数据,也不会消耗数据。它就是数据。
这里只有1个属性,但您可以拥有更多:一个表单字段 => 一个属性。
模型也是表单需要从应用程序逻辑中了解的内容。例如,如果表单需要显示一个下拉菜单,其中包含许多可能的选项,则模型将是公开它们的对象。
视图
这是你的表单。它负责了解控件,写入和读取模型,并且...就是这样。我们在这里看到一个对话框:我们打开它,用户填写它,关闭它,然后程序根据它采取行动 - 表单本身不会对收集的数据进行任何操作。模型可能会对其进行验证,表单可能会决定禁用其Ok按钮,直到模型表示其数据有效并可以继续,但UserForm
绝不会在任何情况下从工作表、数据库、文件、URL或任何其他地方读取或写入。
表单的代码很简单:它将UI与模型实例连接起来,并根据需要启用/禁用其按钮。
要记住的重要事项:
Hide
,不要Unload
:视图是一个对象,对象不会自我销毁。
- 永远不要引用表单的默认实例。
- 始终处理
QueryClose
,同样是为了避免自我销毁的对象(否则,“X”掉表单将销毁该实例)。
在这种情况下,代码可能如下所示:
Option Explicit
Private Type TView
Model As FilterModel
IsCancelled As Boolean
End Type
Private this As TView
Public Property Get Model() As FilterModel
Set Model = this.Model
End Property
Public Property Set Model(ByVal value As FilterModel)
Set this.Model = value
Validate
End Property
Public Property Get IsCancelled() As Boolean
IsCancelled = this.IsCancelled
End Property
Private Sub TextBox1_Change()
this.Model.SelectedFilter = TextBox1.Text
Validate
End Sub
Private Sub OkButton_Click()
Me.Hide
End Sub
Private Sub Validate()
OkButton.Enabled = this.Model.IsValid
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
Me.Hide
End Sub
那个表单只是做这些事情。它不负责知道数据来自哪里或者该怎么处理它。
Presenter
这是连接各个点的“粘合”对象。
Option Explicit
Public Sub DoSomething()
Dim m As FilterModel
Set m = New FilterModel
With New FilterForm
Set .Model = m 'set the model
.Show 'display the dialog
If Not .IsCancelled Then 'how was it closed?
'consume the data
Debug.Print m.SelectedFilter
End If
End With
End Sub
如果模型中的数据需要来自数据库或工作表,它使用一个类实例(是的,又是一个对象!)负责执行这个任务。
调用代码可以是您的ActiveX按钮的单击处理程序,通过
New
创建Presenter并调用其
DoSomething
方法。
这并不是关于VBA中面向对象编程的全部知识(我甚至没有提到接口、多态、测试存根和单元测试),但如果您想要客观可伸缩的代码,您将需要深入探索MVP兔子洞,探索真正面向对象编程带给VBA的可能性。
简而言之:
在任何需要跨越数年并具有可扩展性的代码库中,编码(“业务逻辑”)根本不应该存在于表单的代码后面。
在“变体1”中,代码难以跟踪,因为您要在模块之间跳转,并且呈现问题与应用程序逻辑混合在一起:表单不知道在按下按钮A或按钮B后显示其他表单的工作。相反,它应该让presenter知道用户想做什么,并相应地采取行动。
在“变体2”中,代码难以跟踪,因为所有内容都隐藏在用户表单的代码后面:除非我们深入了解该代码,否则我们不知道应用程序逻辑是什么,这现在故意混合了表示和业务逻辑问题。那就是“智能UI”反模式所做的exactly。
换句话说,“变体1”略好于“变体2”,因为至少逻辑不在代码后面,但它仍然是一个“智能UI”,因为它正在运行演出,而不是告诉其调用者正在发生什么。
在两种情况下,针对表单的默认实例进行编码是有害的,因为它将状态放在全局范围内(任何人都可以访问默认实例并从代码的任何地方对其状态进行任何操作)。
将表单视为对象:实例化它们!
在这两种情况下,由于表单的代码与应用逻辑紧密耦合并与表示层问题交织在一起,因此不可能编写一个单元测试来覆盖正在进行的任何一个方面。使用MVP模式,您可以完全解耦组件,将它们抽象为接口,隔离职责,并编写数十个自动化单元测试,涵盖每个功能模块,并准确记录规范 - 而无需编写任何文档:
代码成为自己的文档。