【问题标题】:Are there disadvantages in putting code into Userforms instead of modules?将代码放入用户表单而不是模块中是否有缺点?
【发布时间】:2018-04-27 13:28:57
【问题描述】:

将代码放入 VBA 用户窗体而不是“普通”模块是否有缺点?

这可能是一个简单的问题,但我在搜索 web 和 stackoverflow 时还没有找到确切的答案。

背景:我正在 Excel-VBA 中开发数据库的前端应用程序。要选择不同的过滤器,我有不同的用户表单。我问什么通用程序设计更好:(1) 将控制结构放入单独的模块中(2) 将下一个用户窗体或操作的代码放在用户窗体中 .

让我们举个例子。我有一个 Active-X 按钮,可以触发我的过滤器和表单。

变体 1:模块

在命令按钮中:

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 中,控制结构位于普通模块中。并且关于接下来显示哪个用户表单的决定与用户表单分开。决定接下来显示哪个用户表单所需的任何信息都必须从用户表单中提取。

Variant2:用户表单

在 CommadButton 中:

Private Sub CommandButton1_Click()
  UserForm1.Show
End Sub

在用户窗体 1 中:

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

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

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

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

【问题讨论】:

  • 看看这里:rubberduckvba.wordpress.com/2017/10/25/userform1-show。它解释了“智能 UI”和 MVP 设计模式之间的一些重要区别。
  • 这会很有趣...系好安全带,你要兜风了(回答中,给我几个小时)。
  • @Mat'sMug 更好地解决了一些 Rubberduck 问题;)
  • Mat's Mug 正在研究答案时,您可以查看 Code Review 中其他人如何处理类似的编程任务(f.eks.:hereor here
  • @JohnMuggins - 我用的是 Rubberduck...值得推动!

标签: excel vba user-interface userform


【解决方案1】:

免责声明我写了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”会发生什么?或者如果UserForm1Unloaded?如果表单未处理其 QueryClose 事件,则该对象将被销毁 - 但因为这是 默认实例,VBA 会在您的代码读取 @987654331 之前自动/静默地为您创建一个新实例@ - 结果,您将获得 UserForm1.decision 的初始全局状态。

如果它不是一个默认实例,并且QueryClose 没有被处理,那么访问被破坏对象的.decision 成员会给你经典的运行时错误91访问空对象引用。

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

【讨论】:

  • 非常感谢您的详细回答!我有一个后续问题:我有几个列表框需要在用户输入后更新。例如,如果用户选择过滤器类型,则过滤器的选项需要调整。现在是“视图”的工作,但它需要访问数据库,所以更新选择属于哪里?
  • 在这些情况下,视图不断与演示者“对话”,例如,通过引发事件(例如 FilterUpdated - 在 .net 中您可以使用委托) - 演示者处理该事件并更新模型,视图刷新其数据。通过这样做,您现在可以启动并使用完全不访问数据库的完全虚构的数据彻底测试视图的逻辑:视图不需要关心其数据来自哪里=)跨度>
  • @Mat'sMug 你说 View 从不从数据库中读取数据。如果必须从记录集中填充表单列表,谁应该处理读取/填充?
  • 我开始实施这些技术,我现在才开始尽可能地欣赏这个答案。非常感谢!
  • 感谢@MathieuGuindon:在这个答案的帮助下,我终于开始掌握 MVP 链接如何工作以保持视图和逻辑分离。我从未使用过 OOP,因此无法想象如果 OK 按钮(例如)仅隐藏表单,表单如何按要求运行。在这个答案的帮助下,我构建了两个有效的 MVP 示例,即使我知道它在做什么,它仍然看起来很神奇。你认为 MVP 是一种严格的 OOP 模式吗?如果没有,没有对象如何实现它?
猜你喜欢
  • 2020-06-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-28
  • 1970-01-01
  • 2018-05-27
  • 2016-06-21
相关资源
最近更新 更多