【问题标题】:Sub function to show UserForm显示用户窗体的子函数
【发布时间】:2016-01-28 05:32:05
【问题描述】:

我有一个包含多个用户窗体的 excel 文件。要打开用户窗体,我有诸如

之类的代码
Sub runAdjuster()
   Adjuster.Show
End Sub

其中大约有 5 个。就应保留此代码的位置而言,什么被认为是最佳实践?我最初将它放在一个模块中,但决定将它移动到 ThisWorkbook 对象。寻找有关通常如何保持代码清洁的提示。

【问题讨论】:

    标签: vba excel


    【解决方案1】:

    假设Adjuster 是表单的名称,您在此处使用默认实例,这并不理想。

    这样就更好了:

    Dim view As Adjuster
    Set view = New Adjuster
    view.Show
    

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

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

    考虑一下:

    Sub DoSomething()
        Dim c As New Collection
        Set c = Nothing
        c.Add "test"
    End Sub
    

    此代码是否访问空引用并因运行时错误 91 而爆炸?不!令人困惑?是的!因此,请避免使用 As New 快捷方式,除非您喜欢让 VBA 在背后自动执行隐式操作。


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

    按照这种模式,您将让 UserForms 对 presentation 严格负责,并且您的业务逻辑要么在 presenter 对象中实现,要么在专用的演示者使用的对象。像这样的:

    类模块:MyPresenter

    presenter 类从 model 接收事件,并根据模型的状态执行应用程序逻辑。它知道 viewconcept,但不必与具体实现(例如 MyUserForm紧密耦合 - 使用适当的工具,您可以 write unit tests 以编程方式验证您的逻辑,而无需实际运行代码并显示表单并单击各处。

    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
    

    类模块:IView

    这就是抽象,它代表了View概念,它揭示了Presenter需要知道的一切任何用户窗体 - 请注意,它需要知道的一切并不多:

    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
    

    类模块:MyModel

    模型类封装了表单需要和操作的数据。它不知道 view,也不知道 presenter:它只是一个封装数据的容器,具有启用两个视图的简单逻辑以及在修改任何属性时执行代码的演示者。

    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
    

    用户表单:我的用户表单

    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
    

    模块:宏

    假设您的电子表格有一个形状,并且您希望在单击它时运行该逻辑。您需要将宏附加到该形状 - 我喜欢将所有宏重新组合到一个名为“宏”的标准模块 (.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
    

    现在,如果您想在不显示表单的情况下以编程方式测试您的演示者逻辑,您需要做的就是实现一个“假”视图,并编写一个测试方法来满足您的需求:

    类: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
    

    模块:TestModule1

    可能还有其他工具,但由于我实际上编写了这个工具,而且我喜欢它的工作方式,而无需大量样板设置代码或包含可执行指令的 cmets,我将热烈推荐使用 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
    

    Rubberduck 单元测试允许您使用此解耦代码来测试您想要测试的有关应用程序逻辑的所有内容 - 只要您保持该应用程序逻辑 解耦 并且您编写 可测试的代码,您将拥有记录 VBA 应用程序应该如何运行的单元测试,记录规范是什么的测试 - 就像您在 C# 或 Java 或任何其他 OOP 语言中拥有它们一样编写单元测试。

    重点是,VBA 也可以。


    矫枉过正?要看。规格一直在变化,代码也随之变化。在电子表格的代码隐藏中实现所有应用程序逻辑非常烦人,因为the Project Explorer doesn't drill down to module members,所以查找在哪里实现的内容很容易让人烦。

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

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

    您的问题并不完全准确地说明“具有多个用户窗体的 Excel 文件”的确切含义,但如果需要,您可以有一个接收 4-5 个“子”演示者的“主”演示者类,每个人都负责与每个“子”表单相关的特定逻辑。

    也就是说,如果您有工作代码(完全按预期工作),您想重构并提高效率,或者更易于阅读/维护,您可以将其发布到 Code Review Stack Exchange,这就是该站点的内容为。


    免责声明I maintain the Rubberduck project.

    【讨论】:

      【解决方案2】:

      这取决于启动这些潜艇的原因。如果它们附加到按钮或形状(这是我在启动用户窗体时倾向于做的),那么将它们放在包含形状的工作表的模块中是有意义的。如果几张纸上的按钮/形状引用它 - 将它们放在通用代码模块中。我不知道这里是否真的有“最佳实践”。最重要的是保持一致性,这样你就不必去寻找东西了。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-04-25
        • 1970-01-01
        • 2017-06-15
        • 2019-03-13
        • 2020-05-31
        • 1970-01-01
        • 2019-07-23
        • 1970-01-01
        相关资源
        最近更新 更多