Sentry objects in VBA

感情迁移 提交于 2020-02-25 01:30:53

问题


I see codes with the following pattern ubiquitously on the web and in my own projects:

Sub Func()
     Application.EnableEvents = False
     ' some code
     Application.EnableEvents = True
End Sub

Since the lifetime of VBA objects seems to be deterministic, I thought I could replace this pattern with the so-called sentry objects as we do in C++, so that the problem of unusual exit (err.raise) could be solved automatically.

But how? I have little idea because I'm new to VBA and not even fully aware of when a object is passed by reference. Ideally, I would like to have the code look something like this:

Sub Func()
     dim Sentry
     Set Sentry = CreateSentry(Application.EnableEvents,False)

     ' From now on we should not need to care if the variable was actually 
     ' True or False beforehand, what kind of error handling is used in this function, etc.
End Sub

回答1:


Application.EnableEvents is not a variable, it is a property. You cannot pass a property by reference like this in VB(A), the compiler will create a temporary copy of the current property value, and your sentry will be "closed" over the copy.

To manage object properties in this way you could do this:
Create a class, name it e.g. SentryForPropertiesVariant and use similar code:

Option Explicit

Private m_Obj As Object
Private m_PropertyName As String
Private m_OldValue As Variant


Public Sub Init(ByVal obj As Object, ByVal PropertyName As String, ByVal NewValue As Variant)
  Set m_Obj = obj
  m_PropertyName = PropertyName

  m_OldValue = CallByName(obj, m_PropertyName, VbGet)
  CallByName m_Obj, m_PropertyName, VbLet, NewValue
End Sub

Private Sub Class_Terminate()
  If Not m_Obj Is Nothing Then
    CallByName m_Obj, m_PropertyName, VbLet, m_OldValue
  End If
End Sub

Then use it:

Dim s As SentryForPropertiesVariant
Set s = New SentryForPropertiesVariant

s.Init Application, "EnableEvents", False

You can also have a helper function in a module:

Public Function CreateSentry(ByVal obj As Object, ByVal PropertyName As String, ByVal NewValue As Variant) As SentryForPropertiesVariant
  Set CreateSentry = New SentryForPropertiesVariant

  CreateSentry.Init obj, PropertyName, NewValue
End Function

at which point using becomes simpler:

Dim s As SentryForPropertiesVariant
Set s = CreateSentry(Application, "EnableEvents", False)

and in which case you probably want to replace Public Sub Init with Friend Sub Init.

If you plan to store your sentry class in a shared add-in (.xla), you will have to have such helper function anyway, because classes defined in add-ins cannot be created from code residing in other workbooks, so the solution is to also define a function in the same workbook as the class that would create the instance and return it to the external caller.


Finally, it is convenient to control the lifetime of such sentries with With (resembles C#'s using):

With CreateSentry(Application, "EnableEvents", False)
  'Here EnableEvents is False
End With

'Here it's True

However when doing so, you should keep in mind the With only resembles using. If you jump out of it prematurely with GoTo, the End With statement will not be executed, which means the temporary variable holding the sentry instance will live to the end of the procedure, and the property will not revert to its original value until then.

So don't jump out of those blocks. If you absolutely have to, create a label right before End With and jump to that.




回答2:


Class Modules can be used for this as you can specify code to run when a class module instance is created, and, importantly when the reference count on an object drops to zero.

Specifically, you can put code in

Private Sub Class_Initialize()

and

Private Sub Class_Terminate()


来源:https://stackoverflow.com/questions/20718029/sentry-objects-in-vba

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