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

谁说我不能喝 提交于 2020-01-02 07:09:58

问题


I am trying to further build on this excellent example, which already implements the best practices discussed in these very insightful RubberduckVBA.com articles:

  1. Abstracting away from the Excel workbook/worksheet through proxy classes;
  2. Utilizing the UserForm control without messing with the state of its default instance;
  3. Adding "Apply" logic to #2.

I would like to add to the existing example an event handler which (for simplicity's sake) reports the value of the upper-left cell of Sheet2's "changed" range in Sheet1's "A1" cell, along with the time of the change in "A2". I would normally set about doing this in Sheet2's code-behind like this:

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

but I want to know how to best achieve this in the given example, considering it is designed around the MVP pattern and utilizes workbook&worksheet abstraction through proxy interfaces - meaning zero/minimal sheet code-behind is expected.

I was able to make sense of how event-handling is implemented in the awesome Battleship tutorial but its design differs in some significant ways:

  1. "Battleship" follows the MVC design pattern while I would like to stick with MVP as in the example;
  2. "Battleship" abstracts away from its worksheets through a "View" class while I would like to have a separate proxy interface+class for each sheet;
  3. "Battleship" deploys the Adapter Pattern while I am fine with having my view and sheet proxy implementations coupled with the presenter (if possible with regards to event handling).

With that in mind I would absolutely love to see a code sample which adds the "Worksheet_Change" event I described above to the base project which has already implemented Workbook and Worksheet proxies and follows the MVP pattern.

Even without a code sample it would be of great help if I get those questions cleared up:

  1. Does the Worksheet proxy approach dictate that there should be absolutely zero sheet code-behind? Would it be a step in the wrong direction if I begin my "Worksheet_Change" event implementation inside Sheet2 (not its proxy) like this:
Public Event SheetChanged(ByVal changedRange As Range)

Private Sub Worksheet_Change(ByVal Target As Range)
    RaiseEvent SheetChanged(Target)
End Sub
  1. If it is not absolutely necessary to use the Adapter Pattern for event handling, is it still a good idea to have the "IViewCommands" and "IViewEvents" interfaces for listing all commands sent from the Presenter to the View and events raised from the View and sent to the Presenter respectively?
  2. I assume I would need to use Lazy Object/Weak Reference to be able to expose events. If so, and assuming I can get the job done without an Adapter (see #2 above), does that mean that my "Sheet2Proxy" class will have to hold a weak reference to the Presenter, through its "IViewEvents" (again see #2 above) interface?

回答1:


You're abstracting the Worksheet behind a "proxy" class; by definition, it's coupled with the worksheet, and what you want is to make sure the abstraction is air-tight, lest you're looking at a leaky abstraction and end up coupling other code with the Excel.Worksheet type, which defeats the entire purpose.

For the rest of the project, the worksheet proxy class is acting as a facade that manipulates and understands everything there is to know about a specific Excel.Worksheet: a consequence of this is that you can now use two modules to abstract worksheet things - the worksheet itself, and the proxy class:

  • Worksheet code-behind can abstract things like ListObject/tables, named ranges, etc.; using Property Get members that the proxy can consume.
  • Worksheet proxy class abstracts worksheet manipulations from the rest of the code.

Indeed, this approach doesn't leave much room/need for actual worksheet code-behind: I would start coding everything in the proxy class, and if that module gets too wordy, or if I'm finding its abstraction level needs to get a bit higher, then I'd move the lower-level stuff to the worksheet's code-behind itself.

Workheet modules and other document modules should not implement interfaces - making a worksheet implement an interface is a good way to confuse and crash VBA: don't do it. So this might be your code-behind:

Option Explicit

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

Then the proxy class can do this:

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

So the proxy class can handle worksheet events just fine without the whole adapter plumbing. It can also handle commands coming from your presenter, through its exposed Public members.

But the proxy class aka "abstracted worksheet" isn't the right place to respond to events: it's the presenter that needs to be running the show.

So you make the proxy fire an event in response to worksheet events, wrapping up and forwarding the message to the presenter:

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

The presenter can then handle SomeSpecificRangeChanged off the proxy class - bring up some UserForm, fire up some database query, whatever the requirements are:

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

The problem is that the proxy class is coupled with the worksheet, and now the presenter is coupled with the proxy: we've abstracted a lot of things, but there's still no way to swap the worksheet/proxy dependency for something else and test the presenter logic without involving the worksheet.

So we make an interface to decouple the presenter from the proxy - say, ISheet1Proxy... and now we're stuck, because we can't expose events on an interface.

This is where the adapter pattern comes into play and allows us to formalize interfaces for "commands" (presenter -> view) and "events" (view -> presenter).

With an adapter, the worksheet/proxy and the presenter are now fully decoupled, and now you can implement the presenter logic without any knowledge of any Excel.Worksheet, and ideally any Excel.Range or Excel.*: every single worksheet interaction is formalized as some "command" that's sent to the view/worksheet/proxy, or some "event" that sent to the presenter, exactly like in the Battleship project.

Side note, I've found that the WeakReference stuff wasn't always needed for properly tearing down the object hierarchies: that's why it's no longer in use in the current version of the Battleship code.


Obviously this is a lot of work. It's great practice for OOP principles and learning to write decoupled code that can be unit-tested... but for a small VBA project, it's wildly overkill IMO.


All this treats Excel.* classes as concrete types, which as far as VBA is concerned, might as well be the case. However the Excel interop types are all interfaces as far as .NET is concerned, so Rubberduck is about to tremendously simplify everything, by providing a wrapper API for Moq, a wildly popular .NET mocking framework:

This will remove the need to fully decouple worksheets from user code in order to make it fully testable - the only requirement will be dependency injection, i.e. prefer this:

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

Over this:

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


来源:https://stackoverflow.com/questions/55689220/properly-handle-worksheet-events-when-sheets-code-behind-is-implemented-in-a-p

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!