Introduction
There are many types of classes that use BeginEdit
and EndEdit
methods to allow batch operations to be performed before raising an event handler.
The basic methodology is that a counter is used internally and each time BeginEdit
is called, the counter is incremented, and each time EndEdit
is called, the counter will be decremented (decreased). Before events are raised, the counter will be checked and the event will only be raised if the counter value is 0
.
This can be considered as a type of "lock
" because it will "lock" events from being raised.
The reason for the counter is that when using a long chain of methods, it may be possible that several of the methods in the chain may want to ensure the lock is in place so each should be able to add/remove locks (using BeginEdit
and EndEdit
), but the lock will only actually be removed when the last EndEdit
is called.
One signifigant drawback of this method is that the developer has to ensure that EndEdit
is called once for each BeginEdit
or the counter may never go back to 0
. This is especially important when it is possible that an exception may be thrown between BeginEdit
and EndEdit
, so a Try
/Finally
block should be used to ensure that EndEdit
is called in the Finally
block as shown in the following example:
Public Sub ExampleMethod()
Try
BeginEdit()
Finally
EndEdit()
End Try
End Sub
When implementing your own classes, a better alternative to what is described above is to set up your class to allow a "Using
" block to ensure that the counter is properly maintained. In this case, the code above would look like the following, and there is no need to pay attention to calling EndEdit
, and you can be ensured that the lock will always be removed at the end of the "Using
" block.
Public Sub BetterExampleMethod()
Using GetEventSuppressionLock
End Using
End Sub
This tip describes utility classes called VSLockBox
and VSLockToken
which can be used by any class to implement this functionality.
Background
This implementation uses two classes, VSLockBox
and VSLockToken
.
The VSLockBox
class is:
Public Class VSLockBox
Private _lockBox As HashSet(Of VSLockToken)
Public Event Locked()
Public Event UnLocked()
Public Sub New(Optional lockedHandler As LockedEventHandler = Nothing, _
Optional unlockedHandler As UnLockedEventHandler = Nothing)
If lockedHandler IsNot Nothing Then AddHandler Locked, lockedHandler
If unlockedHandler IsNot Nothing Then AddHandler UnLocked, unlockedHandler
End Sub
Public ReadOnly Property Count As Integer
Get
Return If(_lockBox Is Nothing, 0, _lockBox.Count)
End Get
End Property
Public ReadOnly Property IsLocked As Boolean
Get
Return _lockBox IsNot Nothing
End Get
End Property
Public Function GetLock() As VSLockToken
Dim lock = New VSLockToken(AddressOf ReleaseLock)
If _lockBox Is Nothing Then
_lockBox = New HashSet(Of VSLockToken)({lock})
RaiseEvent Locked()
Else
_lockBox.Add(lock)
End If
Return lock
End Function
Private Sub ReleaseLock(lock As VSLockToken, e As EventArgs)
_lockBox.Remove(lock)
If Count = 0 Then
_lockBox = Nothing
RaiseEvent UnLocked()
End If
End Sub
End Class
And the VSLockToken
class is:
Public Class VSLockToken
Implements IDisposable
Private ReadOnly _lockReleaseHandler As EventHandler
Friend Sub New(lockReleaseHandler As EventHandler)
_lockReleaseHandler = lockReleaseHandler
End Sub
Public Sub Release()
Dispose()
End Sub
Private _disposedValue As Boolean = False
Private Sub Dispose() Implements IDisposable.Dispose
If Not _disposedValue Then
_lockReleaseHandler.Invoke(Me, EventArgs.Empty)
_disposedValue = True
End If
GC.SuppressFinalize(Me)
End Sub
End Class
The metaphor for this pattern is based on the concept of a lockbox as used by electricians when multiple electricians need to put a lock an a circuit breaker to ensure the electricity is shut-off before doing work. A physical circuit breaker only has space for one lock, so the way multiple people can ensure the circuit breaker is locked is to put a single lock on the circuit breaker, and then put the key to that lock in a special box (the lockbox) which will allow multiple locks to lock it shut. Each person who puts a lock on the lockbox keeps the key to the lock they put on the lockbox ensuring that the circuit breaker can't be unlocked until they (and everyone else who put a lock on the lockbox) has removed their lock. Anybody who wants to ensure the circuit breaker is locked can put their lock on the lockbox, which will ensure that the circuit breaker can't be turned back on again until they remove their lock from the lockbox (using their own key).
As it relates to the code, the LockBox
class represents the lockbox described above, and the LockToken
class represents a lock that has been placed on the lockbox. The LockBox
class uses an internal HashSet
to keep track of the LockToken
s. As long as there is one or more LockToken
in the HashSet
, the LockBox
is considered locked. So, for example, if you were using this pattern to suppress events, the method that raises the event would check the IsLocked
property of LockBox
and only raise the event of the result is False
.
The LockBox
class has a method GetLock
which will return a LockToken
(after automatically adding it to the internal hashset). When the LockBox
class creates a new LockToken
, it passes the LockToken
constructor the address of an EventHandler
in the LockBox
class that the LockToken
can call to remove itself from the LockBox
's hashset of LockTokens
.
The key to everything working is that the LockToken
implements the IDisposable
pattern and will call the handler passed to its constructor the first time Dispose
is called on the LockToken
.
A couple of additional notes about this implementation are:
- The
LockToken
class includes a Release
method which can be used to release the lock before the end of the using
block.
- The
LockBox
constructor accepts two optional parameters lockedHandler
and unlockedHandler
. If a handler is provided for the lockedHandler
argument, that handler will be called each time the first lock is added to the LockBox
. Likewise for the unlockedHandler
, except the handler will be called each time the last lock is released.
- In order to conserve memory, the internal
HashSet
is created each time the first lock is added, and deleted each time the lock is removed.
Using the Code
The following code shows an example of how to integrate this into a custom class. In this case, it is a really simple example person
class with FirstName
and LastName
properties which implements the INotifyPropertyChanged
pattern.
Public Class ExamplePersonClass
Implements INotifyPropertyChanged
Private ReadOnly _LockBox As New VSLockBox(unlockedHandler:=AddressOf ResetBindings)
Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) _
Implements INotifyPropertyChanged.PropertyChanged
Protected Sub OnPropertyChanged(<Runtime.CompilerServices.CallerMemberName> _
Optional propName As String = Nothing)
If Not _LockBox.IsLocked Then RaiseEvent PropertyChanged_
(Me, New PropertyChangedEventArgs(propName))
End Sub
Protected Sub ResetBindings()
If Not _LockBox.IsLocked Then RaiseEvent PropertyChanged(Me, PropertyChangedEventArgs.Empty)
End Sub
Public Function GetEventSuppressionLock() As VSLockToken
Return _LockBox.GetLock()
End Function
Private _FirstName As String
Public Property FirstName As String
Get
Return _FirstName
End Get
Set(value As String)
_FirstName = value
OnPropertyChanged()
End Set
End Property
Private _LastName As String
Public Property LastName As String
Get
Return _LastName
End Get
Set(value As String)
_LastName = value
OnPropertyChanged()
End Set
End Property
End Class
The sections of code specific to the LockBox
are in bold and italicized and include:
- A
private
Field for the LockBox
(note the address of the ResetBindings
method is passed as an argument and that this handler will be called each time the last lock is removed.
- The two utility methods
OnPropertyChanged
and ResetBindings
check whether the LockBox
is locked before raising events.
- A new method
GetEventSuppressionLock
is included for getting the lock token to use in a using
block.
So an example showing how to edit both the FirstName
property and also the LastName
property as a batch and only raise one PropertyChanged
event is:
Public Sub UpdateExample()
Dim person As New ExamplePersonClass
Using person.GetEventSuppressionLock
person.FirstName = "John" person.LastName = "Doe" End Using End Sub
Further Thoughts
I know that this example is a little contrived because normally you would implement this type of locking on the container holding the records and not on the individual records, but I wanted to keep the examples simple.
Finally, suppressing events is just one example where this type of locking is useful and there are other applications for this pattern.
History