当 sheet 的代码隐藏在 "proxy" 接口+class 中实现时,正确处理 Worksheet 事件

Properly handle Worksheet events when sheet's code-behind is implemented in a "proxy" interface+class

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

  1. 从 Excel workbook/worksheet 到 proxy classes;
  2. 进行抽象
  3. Utilizing the UserForm control 不影响其默认实例的状态;
  4. Adding "Apply" logic to #2.

我想向 existing example 添加一个事件处理程序,它(为简单起见)报告 Sheet1 "A1" 中 Sheet2 "changed" 范围左上角单元格的值单元格,以及 "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 模式设计的,并通过代理接口利用 workbook&worksheet 抽象 - 意思是 zero/minimal sheet 代码隐藏是预期的。

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

  1. "Battleship" 遵循 MVC 设计模式,而我想像示例中那样坚持使用 MVP;
  2. "Battleship" 通过 "View" class 从它的工作 sheet 中抽象出来,而我想为每个有一个单独的代理接口+class sheet;
  3. "Battleship" 部署了 ,而我的观点和 sheet 代理实现与演示者相结合(如果可能的话,在事件处理方面)。

考虑到这一点,我绝对希望看到一个代码示例,它将我上面描述的 "Worksheet_Change" 事件添加到已经实现了 Workbook 和 Worksheet 代理的 base project并遵循 MVP 模式。

即使没有代码示例,如果我弄清楚这些问题也会有很大帮助:

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

Private Sub Worksheet_Change(ByVal Target As Range)
    RaiseEvent SheetChanged(Target)
End Sub
  1. 如果不是绝对需要使用 进行事件处理,使用 "IViewCommands" 和 "IViewEvents" 接口来列出从Presenter 到 View 以及从 View 引发并发送到 Presenter 的事件?
  2. 我假设我需要使用 Lazy Object/Weak Reference 才能公开事件。如果是这样,假设我可以在没有适配器的情况下完成工作(参见上面的#2),这是否意味着我的 "Sheet2Proxy" class 将必须通过其 "IViewEvents"(再次参见上面的#2)接口?

您正在抽象 "proxy" class 后面的 Worksheet;根据定义,它与工作表相结合,而您想要的是确保抽象是无懈可击的,以免您正在查看有漏洞的抽象并最终将 其他代码Excel.Worksheet 类型,它破坏了整个目的。

对于项目的其余部分,工作表代理 class 作为一个 门面 操作和理解有关特定 [=17] 的所有信息=]:这样做的结果是您现在可以使用两个模块来抽象工作表内容 - 工作表本身和代理 class:

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

事实上,这种方法不会为实际的工作表代码隐藏留下太多 room/need:我将开始在代理 class 中编码所有内容,如果该模块太冗长,或者如果我发现它的抽象级别需要更高一点,然后我会将较低级别的内容移动到工作表的代码隐藏本身。

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

Option Explicit

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

那么代理class可以这样做:

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

因此代理 class 可以在没有整个适配器管道的情况下很好地处理工作表事件。它还可以通过公开的 Public 成员处理来自演示者的命令。

但是代理 class 又名 "abstracted worksheet" 不是响应事件的正确位置:主持人需要 运行 表演。

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

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 关闭代理 class - 调出一些用户窗体,启动一些数据库查询,无论要求是什么:

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

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

所以我们创建了一个接口来将演示者与代理分离 - 比如,ISheet1Proxy...现在我们被卡住了,因为我们无法在接口上公开事件。

这就是适配器模式发挥作用的地方,它允许我们为 "commands"(演示者 -> 视图)和 "events"(视图 -> 演示者)形式化接口。

有了适配器,worksheet/proxy 和呈现器现在完全分离,现在您可以在不了解任何 Excel.Worksheet 的情况下实现呈现器逻辑,理想情况下是任何 Excel.RangeExcel.*:每个工作表交互都被形式化为一些 "command" 发送到视图/worksheet/proxy,或一些 "event" 发送给演示者,就像在 Battleship 中一样项目。

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


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


所有这些都将 Excel.* classes 视为具体类型,就 VBA 而言,情况可能也是如此。但是,就 .NET 而言,Excel 互操作类型都是接口,因此 Rubberduck 是 about to tremendously simplify everything, by providing a wrapper API for Moq,一个广受欢迎的 .NET 模拟框架:

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

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