Click here to Skip to main content
15,888,521 members
Articles / Programming Languages / Visual Basic
Tip/Trick

Alternative to BeginEdit/EndEdit Pattern

Rate me:
Please Sign up or sign in to vote.
3.67/5 (2 votes)
4 Nov 2015CPOL5 min read 8.4K   1
A class for implementing BeginEdit/EndEdit functionality in a "Using" Block

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:

VB.NET
Public Sub ExampleMethod()
    Try
        BeginEdit()
        'Do Work
    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.

VB.NET
Public Sub BetterExampleMethod()    
    Using GetEventSuppressionLock
        'Do Work
    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:

VB.NET
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:

VB.NET
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 LockTokens. 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.

VB.NET
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:

VB.NET
Public Sub UpdateExample()
    Dim person As New ExamplePersonClass
 
    Using person.GetEventSuppressionLock
        person.FirstName = "John" 'no event is raised
        person.LastName = "Doe" 'no event is raised
    End Using 'ResetBindings will raise PropertyChangedEvent
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

  • 5 Nov 2015: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseMy vote of 5! Pin
jediYL5-Nov-15 9:04
professionaljediYL5-Nov-15 9:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.