免责声明我写了article Victor K linked to。我拥有该博客,并管理它的开源 VBIDE 插件项目。
您的选择都不理想。回归基础。
要选择不同的过滤器,我有不同的(原文如此)用户表单。
您的规范要求用户需要能够选择不同的过滤器,而您选择使用 UserForm 为其实现 UI。到目前为止,一切都很好......从那里开始走下坡路。
让表单负责表示关注以外的任何事情是一个常见的错误,它有一个名称:它是智能 UI [anti-]模式,而问题在于 它无法扩展。它非常适合原型设计(即制作一个“有效”的快速东西 - 请注意吓人的引号),而不是需要多年维护的任何东西。
您可能已经看过这些表单,其中包含 160 个控件、217 个事件处理程序和 3 个私有过程,每个过程都接近 2000 行代码:这就是 Smart 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 是Unloaded?如果表单未处理其 QueryClose 事件,则该对象将被销毁 - 但因为这是 默认实例,VBA 会在您的代码读取 @987654331 之前自动/静默地为您创建一个新实例@ - 结果,您将获得 UserForm1.decision 的初始全局状态。
如果它不是一个默认实例,并且QueryClose 没有被处理,那么访问被破坏对象的.decision 成员会给你经典的运行时错误91访问空对象引用。
UserForm2.Show 和 UserForm3.Show 都做同样的事情:即发即弃 - 无论发生什么,要准确找出其中的内容,您需要在表单的相应代码隐藏中挖掘它.
换句话说,表单正在发挥作用。他们负责收集数据、呈现数据、收集用户输入,并使用它完成任何需要完成的工作。这就是为什么它被称为“智能 UI”:UI 无所不知。
有更好的方法。 MSForms 是 .NET 的 WinForms UI 框架的 COM 祖先,其祖先与其 .NET 继任者的共同点是它与著名的 Model-View-Presenter (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
这就是我们真正需要的:一个封装表单数据的类。该类可以负责某些验证逻辑或其他任何事情 - 但它不收集数据,它不呈现它给用户,它不' t 消费它。它是数据。
这里只有 1 个属性,但您可以拥有更多:想想表单上的一个字段 => 一个属性。
模型也是表单需要从应用程序逻辑中了解的内容。例如,如果表单需要一个下拉菜单来显示许多可能的选择,那么模型就是展示它们的对象。
观点
那是你的表格。它负责了解控件、写入和读取 模型,并且......仅此而已。我们在这里查看一个对话框:我们打开它,用户填写它,关闭它,然后程序对其进行操作 - 表单本身不会对它收集的数据做任何事情。模型可能会验证它,表单可能会决定禁用它的 Ok 按钮,直到模型说它的数据有效并且可以使用,但是在任何情况下UserForm从工作表、数据库、文件、URL 或任何内容中读取或写入。
表单的代码隐藏非常简单:它将 UI 与模型实例连接起来,并根据需要启用/禁用其按钮。
要记住的重要事项:
-
Hide,不要Unload:视图是对象,对象不会自毁。
-
从不引用表单的默认实例。
- 始终处理
QueryClose,再次,以避免自毁对象(“X-ing out”表单会破坏实例)。
在这种情况下,代码隐藏可能如下所示:
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
这就是表单的全部功能。 它不负责了解数据来自何处或如何处理数据。
演示者
那是连接点的“胶水”对象。
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-启动演示者并调用其 DoSomething 方法。
这并不是关于 VBA 中 OOP 的所有知识(我什至没有提到接口、多态性、测试存根和单元测试),但是如果你想要客观可扩展的代码,你会想要了解MVP 兔子洞,探索真正面向对象的代码给 VBA 带来的可能性。
TL;DR:
代码(“业务逻辑”)根本不属于表单的代码隐藏,也不属于任何意味着在几年内扩展和维护的代码库。
在“变体 1”中,代码难以理解,因为您在模块之间跳转,并且表示关注点与应用程序逻辑混合在一起:知道在给定按钮 A 或按钮 B 的情况下要显示什么其他表单不是表单的工作被按下。相反,它应该让 presenter 知道用户想要做什么,并采取相应的行动。
在“变体 2”中,代码难以理解,因为所有内容都隐藏在用户窗体的代码隐藏中:我们不知道应用程序逻辑是什么,除非我们深入研究该代码,现在 故意 em> 混合了表示和业务逻辑问题。这正是“智能 UI”反模式所做的。
换句话说,变体 1 比变体 2 稍好一些,因为至少逻辑不在代码隐藏中,但它仍然是“智能 UI”,因为它运行节目 告诉它的调用者发生了什么。
在这两种情况下,针对表单的默认实例进行编码都是有害的,因为它将状态置于全局范围内(任何人都可以从代码中的任何位置访问默认实例并对其状态执行任何操作)。
像对待对象一样对待表单:实例化它们!
在这两种情况下,由于表单的代码与应用程序逻辑紧密耦合并与表示关注点交织在一起,因此完全不可能编写一个单元测试来涵盖正在发生的事情的一个方面。使用 MVP 模式,您可以完全解耦组件,将它们抽象到接口后面,隔离职责,并编写数十个涵盖每一个功能的自动化单元测试,并准确记录规范是什么——而无需编写任何文档: 代码成为它自己的文档。