Sub函数显示UserForm

tee*_*pee 5 excel vba excel-vba

我有一个包含多个UserForms的excel文件.要打开UserForm我有代码,如

Sub runAdjuster()
   Adjuster.Show
End Sub
Run Code Online (Sandbox Code Playgroud)

其中大约有5个.在保留此代码的位置方面,什么是最佳实践?我最初在一个模块中使用它,但已决定将其移动到ThisWorkbook对象.寻找通常用于保持代码清洁的提示.

Mat*_*don 12

假设Adjuster是表单的名称,您在这里使用默认实例,这不是理想的.

这已经更好了:

Dim view As Adjuster
Set view = New Adjuster
view.Show
Run Code Online (Sandbox Code Playgroud)

是的,这是更多的代码.但是你正在使用一个专用对象(即view),如果该对象的状态被修改,这些更改不会影响默认实例.将该默认实例视为全局对象:它是全局的,不是非常OOP.

现在,您可能会争辩,为什么不在声明的同一行"新建"对象呢?

考虑一下:

Sub DoSomething()
    Dim c As New Collection
    Set c = Nothing
    c.Add "test"
End Sub
Run Code Online (Sandbox Code Playgroud)

此代码是否访问了空引用并且导致运行时错误91?没有!混乱?是! 因此,请避免使用As New快捷方式,除非您希望让VBA自动执行隐藏的操作.


所以,你问的是最佳实践 ...我倾向于将VBA UserForms视为的早期.NET版本,而WinForms的最佳实践设计模式是Model-View-Presenter模式(又名"MVP") .

遵循此模式,您将拥有严格负责演示的 UserForms ,并且您将在演示者对象或演示者使用的专用对象中实现业务逻辑.像这样的东西:

课程模块:MyPresenter

演示类接收来自事件模型,并且根据所述模型的状态来执行应用逻辑.它知道一个概念一的观点,但并不必须紧密结合一个具体的执行方式(例如MyUserForm) -适当的工具,你可以编写单元测试,以编程的方式验证你的逻辑,而无需实际运行代码,并显示表单并随处点击.

Option Explicit

Private Type TPresenter
    View As IView
End type

Public Enum PresenterError
    ERR_ModelNotSet = vbObjectError + 42
End Enum

Private WithEvents viewModel As MyModel
Private this As TPresenter

Public Sub Show()
    If viewModel Is Nothing Then
        Err.Raise ERR_ModelNotSet, "MyPresenter.Show", "Model is not set to an object reference."
    End If
    'todo: set up model properties
    view.Show
    If Not view.IsCancelled Then DoSomething
End Sub

Public Property Get View() As IView
    Set View = this.View
End Property

Public Property Set View(ByVal value As IView)
    Set this.View = value
    If Not this.View Is Nothing Then Set this.View.Model = viewModel
End Property

Public Property Get Model() As MyModel
    Set Model = viewModel
End Property

Public Property Set Model(ByVal value As MyModel)
    Set viewModel = value
    If Not this.View Is Nothing Then Set this.View.Model = viewModel        
End Property

Private Sub Class_Terminate()
    Set this.View.Model = Nothing
    Set this.View = Nothing
    Set viewModel = Nothing
End Sub

Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
    'todo: execute logic that needs to run when something changes in the form
End Sub

Private Sub DoSomething()
    'todo: whatever needs to happen after the form closes
End Sub
Run Code Online (Sandbox Code Playgroud)

课程模块:IView

这就是抽象表示概念一的暴露一切演示需要了解的任何用户窗体-请注意,它需要知道的一切,是没有多大:

Option Explicit

Public Property Get Model() As Object
End Property

Public Property Set Model(ByVal value As Object)
End Property

Public Property Get IsCancelled() As Boolean
End Property

Public Sub Show()
End Sub
Run Code Online (Sandbox Code Playgroud)

类模块:MyModel

模型类封装了表单所需和操作的数据.它不了解视图,也不知道演示者:它只是封装数据的容器,具有简单的逻辑,使得视图和演示者在修改任何属性时都能执行代码.

Option Explicit

Private Type TModel
    MyProperty As String
    SomeOtherProperty As String
    'todo: wrap members here
End Type

Public Enum ModelProperties
    MyProperty
    SomeOtherProperty
    'todo: add enum values here for each monitored property
End Enum

Public Event PropertyChanged(ByVal changedProperty As ModelProperties)
Private this As TModel

Public Property Get MyProperty() As String
    MyProperty = this.MyProperty
End Property

Public Property Let MyProperty(ByVal value As String)
    If this.MyProperty <> value Then
        this.MyProperty = value
        RaiseEvent PropertyChanged(MyProperty)
    End If
End Property

Public Property Get SomeOtherProperty() As String
    SomeProperty = this.SomeOtherProperty
End Property

Public Property Let SomeOtherProperty(ByVal value As String)
    If this.SomeOtherProperty <> value Then
        this.SomeOtherProperty = value
        RaiseEvent PropertyChanged(SomeOtherProperty)
    End If
End Property

'todo: expose other model properties
Run Code Online (Sandbox Code Playgroud)

UserForm:MyUserForm

UserForm严格负责视觉呈现; 它的所有事件处理程序都是改变模型中属性的值 - 然后模型告诉主持人"嘿,我已被修改!",并且演示者相应地行动.表单还会侦听模型上的已修改属性,因此当演示者更改模型时,视图可以执行代码并相应地更新自身.这是一个简单的形式将MyProperty模型属性"绑定" 到某些文本的示例TextBox1; 我添加了一个监听器,SomeOtherProperty仅用于说明在模型更改时也可以间接更新视图.

显然,视图不会对作为演示者改变的相同属性做出反应,否则你会进入无休止的回调,这最终会炸毁堆栈......但是你明白了.

请注意,表单实现了IView界面,以便演示者可以与之交谈,而无需真正了解其内部工作原理.接口实现只是指具体成员,但具体成员甚至不需要实际存在,因为它们甚至不会被使用!

Option Explicit
Implements IView

Private Type TView
    IsCancelled As Boolean
End Type

Private WithEvents viewModel As MyModel
Private this As TView

Private Property Get IView_Model() As Object
    Set IView_Model = Model
End Property

Private Property Set IView_Model(ByVal value As Object)
    Set Model = value
End Property

Private Property Get IView_IsCancelled() As Boolean
    IView_IsCancelled = IsCancelled
End Property

Private Sub IView_Show()
    Show vbModal
End Sub

Public Property Get Model() As MyModel
    Set Model = viewModel
End Property

Public Property Set Model(ByVal value As MyModel)
    Set viewModel = value
End Property

Public Property Get IsCancelled() As Boolean
    IsCancelled = this.IsCancelled
End Property

Private Sub CancelButton_Click()
    this.IsCancelled = True
    Me.Hide
End Sub

Private Sub OkButton_Click()
    Me.Hide
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    '"x-ing out" of the form is like clicking the Cancel button
    If CloseMode = VbQueryClose.vbFormControlMenu Then
        this.IsCancelled = True
    End If
End Sub

Private Sub UserForm_Activate()
    If viewModel Is Nothing Then
        MsgBox "Model property must be assigned before the view can be displayed.", vbCritical, "Error"
        Unload Me
    Else
        Me.TextBox1.Text = viewModel.MyProperty
        Me.TextBox1.SetFocus
    End If
End Sub

Private Sub TextBox1_Change()
    'UI elements update the model properties
    viewModel.MyProperty = Me.TextBox1.Text
End Sub

Private Sub viewModel_PropertyChanged(ByVal changedProperty As ModelProperties)
    If changedProperty = SomeOtherProperty Then
        Frame1.Caption = SomeOtherProperty
    End If
End Sub
Run Code Online (Sandbox Code Playgroud)

模块:宏

假设您的电子表格有一个形状,并且您希望在单击时运行该逻辑.你需要将一个宏附加到该形状 - 我喜欢重新组合一个名为"宏"的标准模块(.bas)中的所有宏,它只包含公共过程,它们都是这样的:

Option Explicit

Public Sub DoSomething()

    Dim presenter As MyPresenter 
    Set presenter = New MyPresenter

    Dim theModel As MyModel
    Set theModel = New MyModel

    Dim theView As IView
    Set theView = New MyUserForm

    Set presenter.Model = theModel
    Set presenter.View = theView
    presenter.Show

End Sub
Run Code Online (Sandbox Code Playgroud)

现在,如果您想以编程方式测试演示者逻辑而不显示表单,那么您需要做的就是实现"假"视图,并编写一个可以满足您需求的测试方法:

类:MyFakeView

Option Explicit
Implements IView

Private Type TFakeView
    IsCancelled As Boolean
End Type

Private this As TFakeView

Private Property Get IView_Model() As Object
    Set IView_Model = Model
End Property

Private Property Set IView_Model(ByVal value As Object)
    Set Model = value
End Property

Private Property Get IView_IsCancelled() As Boolean
    IView_IsCancelled = IsCancelled
End Property

Private Sub IView_Show()
    IsCancelled = False
End Sub

Public Property Get IsCancelled() As Boolean
    IsCancelled = this.IsCancelled
End Property

Public Property Let IsCancelled(ByVal value As Boolean)
    this.IsCancelled = value
End Property
Run Code Online (Sandbox Code Playgroud)

模块:TestModule1

可能还有其他工具,但是因为我实际上已经编写了这个工具而且我喜欢它如何工作而没有大量的样板设置代码或包含可执行指令的注释我将热烈推荐使用Rubberduck单元测试.这是一个[非常简单]测试模块的样子:

'@TestModule
Option Explicit
Option Private Module
Private Assert As New Rubberduck.AssertClass

'@TestMethod
Public Sub Model_SomePropertyInitializesEmpty()
    On Error GoTo TestFail

    'Arrange
    Dim presenter As MyPresenter 
    Set presenter = New MyPresenter

    Dim theModel As MyModel
    Set theModel = New MyModel

    Set presenter.Model = theModel
    Set presenter.View = New MyFakeView

    'Act
    presenter.Show

    'Assert
    Assert.IsTrue theModel.SomeProperty = vbNullString

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Run Code Online (Sandbox Code Playgroud)

Rubberduck单元测试允许您使用此解耦代码来测试您要测试的有关应用程序逻辑的所有内容 - 只要您保持应用程序逻辑解耦并编写可测试代码,您就会有单元测试来记录您的VBA应用程序的方式应该表现,测试记录规范是什么 - 就像你将它们用C#或Java,或任何其他OOP语言,可以编写单元测试.

重点是,VBA也可以做到.


矫枉过正?要看.规格一直在变化,代码会相应变化.在电子表格的代码隐藏中实现所有应用程序逻辑变得非常烦人,因为Project Explorer不会深入到模块成员,所以找到实现的地方很容易让人讨厌.

当逻辑在表单的代码隐藏中实现,然后你有Button_Click处理程序进行数据库调用或电子表格操作时,情况会更糟.

在具有尽可能少责任的对象中实现的代码,使代码可重用,并且更易于维护.

您的问题与"具有多个用户表单的Excel文件"的确切含义并不完全准确,但如果您需要,您可以拥有一个"主要"演示者类,可以接收4-5个"孩子"演示者,每个人都负责对于与每个"子"形式相关的特定逻辑.

也就是说,如果您有工作代码(完全符合预期),您希望重构并提高效率,或更容易阅读/维护,您可以将其发布在Code Review Stack Exchange上,这就是该网站的用途.


免责声明:我维护Rubberduck项目.