当工作表的代码隐藏在“代理”接口+类中实现时正确处理工作表事件

Posted

技术标签:

【中文标题】当工作表的代码隐藏在“代理”接口+类中实现时正确处理工作表事件【英文标题】:Properly handle Worksheet events when sheet's code-behind is implemented in a "proxy" interface+class 【发布时间】:2019-04-15 12:28:43 【问题描述】:

我正在尝试在this excellent example 的基础上进一步发展,它已经实施了这些非常有见地的 RubberduckVBA.com 文章中讨论的最佳实践:

    通过proxy classes从Excel工作簿/工作表中抽象出来; Utilizing the UserForm control 不会弄乱其默认实例的状态; Adding "Apply" logic to #2。

我想向existing example 添加一个事件处理程序,它(为简单起见)报告Sheet1 的“A1”单元格中Sheet2 的“更改”范围左上角单元格的值,以及时间“A2”的变化。我通常会在 Sheet2 的代码隐藏中这样做:

Private Sub Worksheet_Change(ByVal Target As Range)
    Sheet1.Cells(1, 1).Value2 = Target.Cells(1, 1).Value2
    Sheet1.Cells(1, 2).Value2 = CStr(Now)
End Sub

但我想知道如何在 given example 中最好地实现这一点,考虑到它是围绕 MVP 模式设计的,并通过代理接口利用工作簿和工作表抽象——这意味着需要零/最小工作表代码隐藏。

我能够理解在很棒的 Battleship tutorial 中是如何实现事件处理的,但它的设计在一些重要方面有所不同:

    “战舰”遵循 MVC 设计模式,而我想像示例中一样坚持使用 MVP; “Battleship”通过“View”类从其工作表中抽象出来,而我希望为每个工作表提供一个单独的代理接口+类; “Battleship”部署Adapter Pattern,而我可以将视图和工作表代理实现与演示者结合使用(如果可能的话,在事件处理方面)。

考虑到这一点,我非常希望看到一个代码示例,它将我上面描述的“Worksheet_Change”事件添加到已经实现工作簿和工作表代理并遵循 MVP 模式的base project。

即使没有代码示例,如果我能解决这些问题也会有很大帮助:

    Worksheet proxy approach 是否规定绝对零表代码隐藏?如果我像这样在 Sheet2(不是它的代理)内开始我的“Worksheet_Change”事件实现,会不会是朝着错误方向迈出的一步:
Public Event SheetChanged(ByVal changedRange As Range)

Private Sub Worksheet_Change(ByVal Target As Range)
    RaiseEvent SheetChanged(Target)
End Sub
    如果不是绝对需要使用Adapter Pattern 进行事件处理,使用“IViewCommands”和“IViewEvents”接口列出所有从 Presenter 发送到 View 的命令和引发的事件是否仍然是一个好主意分别从 View 和 Presenter 发送? 我认为我需要使用Lazy Object/Weak Reference 才能公开事件。如果是这样,并且假设我可以在没有适配器的情况下完成工作(参见上面的#2),这是否意味着我的“Sheet2Proxy”类将不得不通过它的“IViewEvents”来持有对 Presenter 的弱引用(再次参见 # 2) 接口?

【问题讨论】:

肯定会回到这篇文章,但现在我只想说工作表代码隐藏可以很好地包含代码 - 只是,它是与 UI/表示问题严格相关的代码( c.f. BattleShip:工作表代码隐藏完全是一团糟,但它只处理 UI 的东西,没有任何“业务逻辑”)。 【参考方案1】:

您正在抽象“代理”类后面的Worksheet;根据定义,它与工作表耦合,您需要确保抽象是密封的,以免您看到泄漏的抽象并最终将 其他代码 与 @987654325 耦合@type,这违背了整个目的。

对于项目的其余部分,工作表代理类充当外观,它操纵和理解有关特定Excel.Worksheet 的所有信息:这样做的结果是您可以现在使用两个模块来抽象工作表的东西 - 工作表本身和代理类:

工作表代码隐藏可以抽象ListObject/tables、命名范围等内容;使用代理可以使用的Property Get 成员。 工作表代理类从其余代码中抽象出工作表操作。

确实,这种方法不会为实际的工作表代码隐藏留下太多空间/需求:我会开始在代理类中编写所有内容,如果该模块过于冗长,或者我发现它的抽象级别需要为了提高一点,我会将较低级别的东西移到工作表本身的代码隐藏中。

Workheet 模块和其他文档模块不应该实现接口 - 让工作表实现接口是混淆和崩溃 VBA 的好方法:不要这样做。所以这可能是你的代码隐藏:

Option Explicit

Public Property Get SomeSpecificRange() As Range
    Set SomeSpecificRange = Me.Names("SomeSpecificRange").RefersToRange
End Property

那么代理类可以这样做:

Option Explicit
Private sheetUI As Sheet1
Private WithEvents sheet As Worksheet

Private Sub Class_Initialize()
    Set sheet = Sheet1
    Set sheetUI = Sheet1
End Sub

Private Sub sheet_Change(ByVal Target As Range)
    If Intersect(Target, sheetUI.SomeSpecificRange) Then
        '...
    End If
End Sub

因此,代理类可以很好地处理工作表事件,而无需整个适配器管道。它还可以通过其公开的Public 成员处理来自演示者的命令。

但是代理类又名“抽象工作表”不是响应事件的正确位置:它是需要运行节目的演示者。

因此,您可以让代理触发一个事件以响应工作表事件,将消息打包并转发给演示者:

Option Explicit
Public Event SomeSpecificRangeChanged()
Private sheetUI As Sheet1
Private WithEvents sheet As Worksheet

Private Sub Class_Initialize()
    Set sheet = Sheet1
    Set sheetUI = Sheet1
End Sub

Private Sub sheet_Change(ByVal Target As Range)
    If Intersect(Target, sheetUI.SomeSpecificRange) Then
        RaiseEvent SomeSpecificRangeChanged
    End If
End Sub

然后演示者可以处理代理类之外的SomeSpecificRangeChanged - 调出一些用户窗体,启动一些数据库查询,无论要求是什么:

Private WithEvents proxy As Sheet1Proxy

Private Sub Class_Initialize()
    Set proxy = New Sheet1Proxy
End Sub

Private Sub proxy_SomeSpecificRangeChanged()
    'business logic to run when SomeSpecificRange is changed
End Sub

问题是代理类与工作表耦合,现在presenter与代理耦合:我们已经抽象了很多东西,但是仍然没有办法将工作表/代理依赖换成别的东西并在不涉及工作表的情况下测试演示者逻辑。

所以我们创建了一个接口来将演示者与代理解耦 - 例如,ISheet1Proxy... 现在我们陷入困境,因为我们无法在接口上公开事件。

这是适配器模式发挥作用的地方,它允许我们为“命令”(演示者 -> 视图)和“事件”(视图 -> 演示者)的接口形式化。

使用适配器,工作表/代理和演示者现在完全解耦,现在您可以在不了解任何Excel.Worksheet,理想情况下任何Excel.RangeExcel.* 的情况下实现演示者逻辑:每个工作表交互被形式化为一些发送到视图/工作表/代理的“命令”,或发送给演示者的一些“事件”,就像在战舰项目中一样。

旁注,我发现WeakReference 的东西并不总是需要正确地拆除对象层次结构:这就是为什么它不再在当前版本的战舰代码中使用。


显然这是很多的工作。这是 OOP 原则和学习编写可进行单元测试的解耦代码的绝佳实践……但对于小型 VBA 项目,IMO 太过分了。


所有这一切都将Excel.* 类视为具体类型,就VBA 而言,也可能是这种情况。然而,就 .NET 而言,Excel 互操作类型都是接口,因此 Rubberduck 是 about to tremendously simplify everything,通过为广受欢迎的 .NET 模拟框架 Moq 提供包装 API:

这将消除将工作表与用户代码完全分离以使其完全可测试的需要 - 唯一的要求是依赖注入,即更喜欢这样:

Public Sub DoSomething(ByVal target As Range)
    target.Value = 42
End Sub

关于这个:

Public Sub DoSomething()
    Dim target As Range
    Set target = Sheet1.Range("A1")
    target.Value = 42
End Sub

【讨论】:

一如既往的好帖子,真的帮助我对我应该如何继续我的项目有了一些看法。关于 Rubberduck 模拟框架诞生的非常激动人心的消息!我偷偷地希望有一种方法可以在不走适配器路线的情况下保持单元可测试性,现在如果我正确理解你最后几段的话,这似乎是可能的:) 值得一提的是——如果你想的话,适配器方法很棒如 Battleships 代码所示,只需对现有代码进行少量更改即可将 Excel 工作表 UI 替换为其他内容。

以上是关于当工作表的代码隐藏在“代理”接口+类中实现时正确处理工作表事件的主要内容,如果未能解决你的问题,请参考以下文章

DialogFragment 中实现的 videoview 的 MediaController 未正确更新

在python中实现类接口的正确方法是啥

java之动态代理

java中的动态代理

代码块是否完全取代了代理?

使用代理模式来给交易模块添加缓存功能