Sub函数来显示UserForm

我有一个多用户窗体的Excel文件。 要打开用户窗体我有代码如

Sub runAdjuster() Adjuster.Show End Sub 

大约有5个 根据代码的保存位置,什么被认为是最佳实践? 我原本是在一个模块中,但已决定将其移动到ThisWorkbook对象。 寻找关于通常做什么来保持代码清洁的提示。

假设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视为winforms早期的.NET版本,WinForms的最佳实践devise模式是Model-View-Presenter模式(又名“MVP”) 。

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

类模块:MyPresenter

演示者类从模型接收事件,并根据模型的状态执行应用程序逻辑。 它知道一个视图概念 ,但它不必与具体的实现(如MyUserForm紧密结合 – 通过适当的工具,您可以编写unit testing来以编程方式validation您的逻辑,而无需实际运行代码显示表单并点击无处不在。

 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需要了解的任何UserForm的一切信息 – 请注意, 它需要知道的一切并不多:

 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

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

 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 

用户窗体:MyUserForm

UserForm严格负责视觉呈现; 它的所有事件处理程序都会改变模型中某个属性的值 – 然后模型会告诉主持人“嘿,我已经被修改了!”,主持人会相应地采取行动。 表单还监听模型上的修改属性,因此,当演示者更改模型时,视图可以执行代码并相应地进行更新。 下面是一个简单的“绑定” MyProperty模型属性到一些TextBox1文本的例子; 我为SomeOtherProperty添加了一个监听器来说明视图也可以在模型更改时间接更新。

很显然,这个观点不会像主持人那样对相同的属性作出反应,否则你会进入一个无休止的callback乒乓球,最终会炸毁堆栈…但你明白了。

请注意,表单实现了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 

模块:macros

假设你的电子表格有一个形状,并且你想在被点击时运行这个逻辑。 你需要附加一个macros到这个形状 – 我喜欢重新组织一个叫做“macros”的标准模块(.bas)中的所有macros,除了公用程序,它们都是这样的:

 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 

现在,如果您想以编程的方式testing演示者逻辑,而不显示表单,则只需实现一个“假”视图,然后编写一个testing方法来执行您所需的操作:

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

也许还有其他的工具,但是因为我实际上是编写了这个工具,而且我喜欢它的工作方式,所以没有大量的样板代码或包含可执行指令的注释,我会热烈推荐使用Rubberduckunit testing。 以下是一个非常简单的testing模块:

 '@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 

使用Rubberduckunit testing,您可以使用此解耦代码来testing您要testing的有关应用程序逻辑的所有内容 – 只要保持应用程序逻辑解耦,并编写可testing的代码,就可以进行unit testing,loggingVBA应用程序应该是行为,testing,文件的规格是什么 – 就像你将他们在C#或Java,或任何其他OOP语言可以写unit testing。

要点是,VBA也可以做到这一点。


矫枉过正? 依靠。 规格一直在变化,代码也随之改变。 在电子表格的代码隐藏中实现所有的应用程序逻辑会变得非常烦人,因为Project Explorer不会钻取模块成员 ,所以find实现的地方很容易让人讨厌。

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

在具有尽可能less责任的对象中实现的代码使得可重用的代码更容易维护。

你的问题并不完全确切地说明你的意思是什么“一个Excel文件与多个用户forms”,但如果你需要,你可以有一个“主”演示类,接收4-5“小孩”演示者,每个负责为每个“孩子”forms绑定的具体逻辑。

也就是说,如果你有工作代码(工作完全按照预期工作),你想重构,使得更有效率,或更容易阅读/维护,你可以将它张贴在Code Review Stack Exchange上 ,这就是该网站的用途。


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

这取决于什么启动这些潜艇。 如果它们被附加到一个button或形状(这是我打算启动用户窗体),那么将它们放在包含形状的表单的模块中是有意义的。 如果几张纸上的button/形状指的是它 – 把它们放在一个通用的代码模块。 我不知道这里真的有没有“最佳做法”。 最重要的是要有一致性,以便你不必去寻找的东西。