把代码放入用户窗体而不是模块中是否有缺点?

27
在VBA用户窗体中放置代码是否有劣势,相对于将代码放置在“普通”模块中?
这可能是一个简单的问题,但是在搜索网络和stackoverflow时我没有找到确定性的答案。
背景: 我正在开发一个Excel-VBA数据库的前端应用程序。为了选择不同的过滤器,我使用了不同的用户窗体。我想知道哪种一般的程序设计更好:(1)将控制结构放入单独的模块中,还是(2)将下一个用户窗体或操作的代码放入用户窗体中。
让我们举个例子。我有一个Active-X按钮,触发我的过滤器和窗体。
变量1:模块
在CommandButton中:
Private Sub CommandButton1_Click()
  call UserInterfaceControlModule
End Sub

在模块中:

Sub UserInterfaceControllModule()
Dim decisionInput1 As Boolean
Dim decisionInput2 As Boolean

UserForm1.Show
decisionInput1 = UserForm1.decision

If decisionInput1 Then
  UserForm2.Show
Else
  UserForm3.Show
End If

End Sub
在变体1中,控制结构位于普通模块中。关于显示下一个用户窗体的决策与用户窗体分离。为了决定显示哪个用户窗体,需要从用户窗体中提取所需的任何信息。 变体2: 用户窗体 在CommadButton中:
Private Sub CommandButton1_Click()
  UserForm1.Show
End Sub
在Userform1中:
Private Sub ToUserform2_Click()
  UserForm2.Show
  UserForm1.Hide
End Sub

Private Sub UserForm_Click()
  UserForm2.Show
  UserForm1.Hide
End Sub

在变体2中,控制结构直接放置于用户窗体中,每个用户窗体都有有关其后面内容的说明。

我已经开始使用方法2进行开发。如果这是一个错误,并且这种方法存在一些严重的缺点,我希望尽早知道。


9
看这里:https://rubberduckvba.wordpress.com/2017/10/25/userform1-show/。它解释了“智能UI”和MVP设计模式之间的许多重要区别。 - Victor K
18
这将会很有趣...系好安全带,你将要体验一段旅程。(回答正在进行中,请给我几个小时。) - Mathieu Guindon
7
@Mat'sMug最好修复一些Rubberduck的问题,而不是别的。 - Shai Rado
4
当Mat's Mug正在寻找答案时,您可以查看其他人在Code Review上如何处理类似的编程任务(例如:这里或者这里)。 - Victor K
5
@JohnMuggins - 我使用Rubberduck... 值得推荐! - Automate This
显示剩余13条评论
1个回答

61

免责声明 我写了文章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.ShowUserForm3.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模式,您可以完全解耦组件,将它们抽象为接口,隔离职责,并编写数十个自动化单元测试,涵盖每个功能模块,并准确记录规范 - 而无需编写任何文档:代码成为自己的文档

4
非常感谢您详细的回答!我有一个后续问题:我有几个需要在用户输入后更新的列表框。例如,如果用户选择了筛选类型,那么筛选选项就需要调整。这是“视图”的工作,但它需要访问数据库,那么更新选择应该放在哪里呢? - Lucas Raphael Pianegonda
2
在这些情况下,视图不断地与主持人“交流”,例如通过引发事件(例如FilterUpdated - 在.NET中,您可以使用委托) - 主持人处理该事件并更新模型,视图刷新其数据。通过这样做,您现在可以使用完全虚构的数据启动和彻底测试视图的逻辑,而无需访问数据库:视图不需要关心其数据来自何处=) - Mathieu Guindon
2
@Mat'sMug,你说View从来不会从数据库中读取。在需要从记录集中填充表单列表的情况下,应该由谁处理读取/填充? - Victor K
2
我开始实施这些技术,现在才真正意识到这个答案的价值。非常感谢你! - Lucas Raphael Pianegonda
2
感谢@MathieuGuindon的帮助:在他的回答的帮助下,我终于开始理解MVP链接是如何工作以保持视图和逻辑分离的。我以前从未使用过OOP,因此无法想象如果“确定”按钮(例如)仅隐藏表单,该表单如何按要求运行。在这个答案的帮助下,我构建了两个可工作的MVP示例,即使我知道它在做什么,它仍然看起来像魔术。您认为MVP严格属于OOP模式吗?如果不是,那么没有对象该如何实现它? - Instant Breakfast
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接