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

问题描述 投票:19回答:1

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

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

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

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

Variant1:模块

在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中,控制结构在正常模块中。关于下一个要显示哪个userform的决定与userform分开。决定下一个要显示的用户表单所需的任何信息都必须从用户表单中提取。

Variant2:Userform

在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进行开发。如果这是一个错误,并且这个方法有一些严重的缺点,我想早点知道它。

excel vba user-interface userform
1个回答
46
投票

免责声明我写了article Victor K linked to。我拥有该博客,并管理它的开源VBIDE加载项目。

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


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

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

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

您可能已经看到过这些表单,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会自动/静默地为您创建一个新的实例,就在您的代码读取UserForm1.decision之前 - 因此您可以获得最初的全局国家是为UserForm1.decision

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

UserForm2.ShowUserForm3.Show都做同样的事情:火与忘 - 无论发生什么事情,并且要确切地知道它包含什么,你需要在形式'各自的代码隐藏中挖掘它。

换句话说,表单正在运行show。他们负责收集数据,呈现数据,收集用户输入以及完成需要完成的任何工作。这就是为什么它被称为“智能UI”:UI知道一切。

还有更好的方法。 MSForms是.NET的WinForms UI框架的COM祖先,它的祖先与其.NET继承者的共同之处在于它与着名的模型 - 视图 - 展示器(MVP)模式一起工作得特别好。


The Model

那是你的数据。从本质上讲,这是您的应用程序逻辑需要知道的形式。

  • 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个属性,但你可以有更多:想想表格上的一个字段=>一个属性。

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


The View

那是你的形式。它负责了解控件,写入和读取模型,以及......这就是全部。我们在这里看一个对话框:我们提出它,用户填写它,关闭它,程序对它进行操作 - 表单本身对它收集的数据没有任何作用。模型可能会对其进行验证,表单可能会决定禁用其“确定”按钮,直到模型显示其数据有效且良好,但在任何情况下,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

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


The 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按钮的单击处理程序,qazxswing升级演示者并调用其New方法。


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


TL;DR:

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

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

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

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

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.