将代码放入Userforms而不是模块是否有缺点?

Luc*_*nda 22 excel user-interface vba userform

将代码放入VBA Userform而不是"普通"模块中是否有缺点?

这可能是一个简单的问题,但我在搜索Web和stackoverflow时没有找到一个确定的答案.

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

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

Variant1:模块

在CommandButton中:

Private Sub CommandButton1_Click()
  call UserInterfaceControlModule
End Sub
Run Code Online (Sandbox Code Playgroud)

在模块中:

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
Run Code Online (Sandbox Code Playgroud)

在变体1中,控制结构在正常模块中.关于下一个要显示哪个userform的决定与userform分开.决定下一个要显示的用户表单所需的任何信息都必须从用户表单中提取.

Variant2:Userform

在CommadButton中:

Private Sub CommandButton1_Click()
  UserForm1.Show
End Sub
Run Code Online (Sandbox Code Playgroud)

在Userform1中:

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

Private Sub UserForm_Click()
  UserForm2.Show
  UserForm1.Hide
End Sub
Run Code Online (Sandbox Code Playgroud)

在变体2中,控制结构直接在用户表单中,每个用户表单都有关于它后面的内容的说明.

我已经开始使用方法2进行开发.如果这是一个错误,并且这个方法有一些严重的缺点,我想早点知道它.

Mat*_*don 48

免责声明我写了Victor K 链接文章.我拥有该博客,并管理它的开源VBIDE加载项目.

你的两种选择都不理想.回归本源.


要选择不同的过滤器,我有不同的(原文如此)用户形式.

您的规范要求用​​户需要能够选择不同的过滤器,并且您选择使用a来实现UI UserForm.到目前为止,这么好......而且从那里开始走下坡路.

使表单对表示问题以外的任何事情负责是一个常见的错误,它有一个名称:它是智能UI [反]模式,它的问题是它不能扩展.它非常适合原型制作(即快速制作"有效"的东西 - 请注意恐慌报价),而不是那些需要多年维护的东西.

您可能已经看到过这些表单,160个控件,217个事件处理程序和3个私有程序各自关闭2000行代码:Smart UI的扩展程度非常糟糕,而且这是唯一可能的结果.

你看,a UserForm是一个类模块:它定义了一个对象蓝图.对象通常希望被实例化,但后来有人曾授予的所有实例的天才的想法一个预先声明的ID,这在COM而言意味着你基本上得到一个全局对象是免费的.MSForms.UserForm

大!没有?没有.

UserForm1.Show
decisionInput1 = UserForm1.decision

If decisionInput1 Then
  UserForm2.Show
Else
  UserForm3.Show
End If
Run Code Online (Sandbox Code Playgroud)

如果UserForm1是"X'd-out" 会发生什么?或者如果UserForm1Unloaded?如果表单没有处理它的QueryClose事件,那么该对象将被销毁 - 但由于这是默认实例,VBA会自动/静默地为您创建一个新的实例,就在您的代码读取之前UserForm1.decision- 因此您可以获得初始全局状态对UserForm1.decision.

如果它不是默认实例,并且QueryClose未被处理,则访问已.decision销毁对象的成员将为您提供用于访问空对象引用的经典运行时错误91.

UserForm2.Show并且UserForm3.Show两者都做同样的事情:即发生 - 忘记 - 无论发生什么事情,并且要确切地找出它包含的内容,你需要在形式'各自的代码隐藏中挖掘它.

换句话说,表单正在运行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
Run Code Online (Sandbox Code Playgroud)

这就是我们所需要的:一个封装表单数据的类.该类可以负责某些验证逻辑,或者其他任何东西 - 但它不收集数据,它不会呈现给用户,也不会消耗它.这数据.

这里只有1个属性,但你可以有更多:想想表格上的一个字段=>一个属性.

该模型也是表单需要从应用程序逻辑中获知的内容.例如,如果表单需要一个显示许多可能选择的下拉列表,则模型将是暴露它们的对象.


风景

那是你的形式.它负责了解控件,写入和读取模型,以及......这就是全部.我们期待在这里的对话:我们要拿出来,用户填写起来,关闭它,程序作用于它-形式本身并不处理收集的数据什么.该模型可以验证它的形式可能会决定禁用它的Ok按钮,直到模型说,它的数据是有效的,好走,但在任何情况下一个UserForm读取或从一个工作表,数据库,文件,URL,或任何写.

表单的代码隐藏很简单:它使用模型实例连接UI,并根据需要启用/禁用其按钮.

要记住的重要事项:

  • Hide,不要Unload:视图是一个对象,对象不会自毁.
  • 永远不要参考表单的默认实例.
  • 始终处理QueryClose,以避免自毁对象("X-out 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
Run Code Online (Sandbox Code Playgroud)

这就是所有形式的确如此.它不负责了解数据来自何处或如何处理数据.


演讲者

这是连接点的"胶水"对象.

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
Run Code Online (Sandbox Code Playgroud)

如果模型中的数据需要来自数据库或某个工作表,那么它使用的是一个类实例(是的,另一个对象!),它负责这样做.

调用代码可以是您的ActiveX按钮的单击处理程序, - 调New出演示者并调用其DoSomething方法.


这并不是VBA中有关OOP的所有知识(我甚至没有提到接口,多态,测试存根和单元测试),但是如果你想要客观可扩展的代码,你会想要走下MVP兔子洞并探索真正面向对象的代码为VBA带来的可能性.


TL; DR:

代码("业务逻辑")根本不属于表单的代码隐藏,在任何代码库中都意味着可以在几年内进行扩展和维护.

在"变体1"中,代码​​很难遵循,因为您在模块之间跳转并且演示问题与应用程序逻辑混合在一起:在按下按钮A或按钮B的情况下知道要显示的其他表单不是表单的工作.相反,它应该让演示者知道用户的意思,并采取相应的行动.

在"变体2"中,代码​​很难遵循,因为一切都隐藏在userforms的代码隐藏中:我们不知道应用程序逻辑是什么,除非我们深入研究代码,现在故意混合表示和业务逻辑问题.这正是 "智能UI"反模式的作用.

换句话说,变体1稍微好于变体2,因为至少逻辑不在代码隐藏中,但它仍然是"智能UI",因为它正在运行节目而不是告诉其调用者发生了什么.

在这两种情况下,对表单的默认实例进行编码都是有害的,因为它将状态置于全局范围内(任何人都可以从代码中的任何位置访问默认实例并对其状态执行任何操作).

像对象一样处理表单:实例化它们!

在这两种情况下,由于表单的代码与应用程序逻辑紧密耦合并与表示问题交织在一起,因此完全不可能编写单个单元测试,甚至涵盖正在发生的事情的一个方面.使用MVP模式,您可以完全解耦组件,在接口后面抽象它们,隔离职责,编写几十个自动化单元测试,涵盖每一项功能并准确记录规范是什么 - 无需编写一些文档:代码成为自己的文档.

  • 非常感谢您的详细解答!我有一个跟进问题:我有几个Listbox,需要在用户输入后更新.例如,如果用户选择过滤器类型,则需要调整过滤器的选项.现在这是"视图"的工作,但它需要访问数据库,以便更新选择属于哪里? (3认同)
  • 在这些情况下,视图不断地与演示者“对话”,例如,通过引发一个事件(例如`FilterUpdated` - 在 .net 中,您可以使用委托) - 演示者处理该事件并更新模型,然后视图刷新其数据. 通过这样做,您现在可以使用完全不访问数据库的完全虚构的数据启动并彻底测试您的视图逻辑:视图不需要关心它的数据来自哪里=) (2认同)
  • @ Mat'sMug你说View永远不会从数据库中读取.如果必须从记录集填充表单列表,谁应该处理读取/填充? (2认同)
  • 我开始实施这些技术,直到现在我才开始尽可能地欣赏这个答案。非常感谢! (2认同)
  • 感谢@MathieuGuindon:在这个答案的帮助下,我终于开始了解MVP链接如何工作以使视图和逻辑保持分离。我从来没有使用过OOP,因此,如果“确定”按钮(例如)仅隐藏表单,那么就无法考虑表单如何按要求运行。在这个答案的帮助下,我建立了两个有效的MVP示例,即使我知道它在做什么,它仍然看起来像魔术。您是否认为MVP严格来说是OOP模式?如果没有,一个没有对象的人怎么实现呢? (2认同)