Content of the ZIP file
The ZIP file contains a demonstration application (source code and binary) which was created with Visual Studio 2003 as well as an additional copy of the file CtrlActivator.vb which contains the complete source code of the CtrlActivator
class.
Introduction
I recently read a post by a fellow programmer who wanted to have a disabled TextBox
in a GroupBox
and to enable it when the user would double click on it. The question was how to do it while the TextBox
does not receive any messages as long as it is disabled. Well, I'm still not sure if that is a good UI design but I was curious what a decent solution might look like. So, I created this little class I'm going to introduce in this article.
First, I tried to generalize the problem a little bit. What the asker wanted to do was to have a disabled control on a container that should be activated on a double click. And that's what my CtrlActivator
class does.
How to use it: A simple example
Just add the file CtrlActivator.vb to your project. Now, let's assume you have a form MainForm
in your project and a button DisabledButton
(which of course is disabled). Since the CtrlActivator
class does not contain any static members, we need an instance of the class. The constructor of CtrlActivator
expects a reference of the type Control
, and because Form
and UserControl
are derived from Control
it also works fine with them. This control must be the parent of the controls we want to enable on a double click. Why the parent? Well, messages which would normally be sent to a control don't vanish when the particular control is disabled. They are send to the disabled control's parent (given that the parent is not disabled itself). This of course means that if our Button
is nested in a GroupBox
, we must pass a reference to the GroupBox
to the CtrlActivator
constructor. If you are unsure about the parent, you can use the Parent
property of the control you want to enable (for example, TextboxActivator = New CtrlActivator(DisabledButton.Parent)
).
Public Class MainForm
Inherits System.Windows.Forms.Form
Private TextboxActivator As CtrlActivator = New CtrlActivator(Me)
[Designer generated code]
End Class
To let the CtrlActivator
know what we want to enable, we call CtrlActivator.Register
and pass a reference to DisableButton
.
TextboxActivator.RegisterControl(DisabledButton)
That's all. When you execute the code, you can enable the DisableButton
by double-clicking on it. Because the class uses a collection to store the objects that were registered by the caller, you can register as many objects as you want. The only requirement is that they all have the same parent control. But what if you later want not to have a certain control handled by the CtrlActivator
object after you registered it? Fortunately, there is a correspondent UnRegisterControl
method. Just call it and pass a reference to the control you registered earlier. Again, that's all.
TextboxActivator.UnRegisterControl(DisabledButton)
There is just one thing you should keep in mind: if you continue reading, you will find out that the CtrlActivator
class relies on registering new event handlers to the parent control as well as, of course, keeping references to the controls it is supposed to enable. Therefore, you should always call CtrlActivator.Dispose
after you're done. In the following paragraphs, I will describe every public member of the class.
How to use it (public members): Sub New(ByVal Parent As Control)
The constructor of the class has one parameter which is used to pass a reference to the parent of the controls you want to enable. If you want to handle multiple parent controls, you need to create one instance of the CtrlActivator
class for each parent control. The constructor will throw a NullReferenceException
exception if Nothing
is passed as its parameter.
Sub New(ByVal Parent As Control)
How to use it (public members): Public Sub RegisterControl(ByVal Ctrl As Control)
RegisterControl
is used to register controls that will be enabled on a double click. No exception will be thrown when Nothing
or a reference to an already registered control is passed. However, an ObjectDisposedException
exception will be thrown when this method is called after Dispose
.
Public Sub RegisterControl(ByVal Ctrl As Control)
How to use it (public members): Public Sub UnRegisterControl(ByVal Ctrl As Object)
UnRegisterControl
is used to un-register a previously registered control. Like RegisterControl
, this method is fault tolerant ignoring Nothing
and references to controls which where not registered previously. However, an ObjectDisposedException
exception will be thrown when this method is called after Dispose
.
Public Sub UnRegisterControl(ByVal Ctrl As Object)
How to use it (public members): Public Function Exists(ByVal Ctrl As Control) As Boolean
Exists
checks if a given object has been registered and returns True
if it has been registered. Otherwise, the method returns False
.
Public Function Exists(ByVal Ctrl As Control) As Boolean
How to use it (public members): Public Overloads Sub Dispose()
Dispose
deletes all external references stored within the CtrlActivator
object and therefore should be called when the object is not needed anymore.
Public Overloads Sub Dispose() Implements IDisposable.Dispose
How to use it (public members): Public ReadOnly Property Count() As Integer
The property Count
returns the number of controls currently registered.
Public ReadOnly Property Count() As Integer
How it works: Global declarations
The class implements the IDisposable
interface to allow a controlled deconstruction of an instance. It also has four private members. p_CallingCtrl
is used to store a reference to the parent control of the controls we want to enable. p_CtrlCollection
is a reference to the collection that actually stores references to the controls we want to handle. p_MousePos
is used to buffer the current location of the cursor in the coordinate system of the parent control. p_Disposed
is a flag which is set to True
after Dispose
has been executed.
Implements IDisposable
Private p_CallingCtrl As Control = Nothing
Private p_CtrlCollection As Collection = New Collection
Private p_MousePos As Point = New Point(-1, -1)
Private p_Disposed As Boolean = False
How it works: Sub New(ByVal Parent As Control)
The constructor is responsible for copying a parent control reference to p_CallingCtrl
and registering the CtrlActivator
object's own DoubleClick
and MouseMove
event handlers to the parent control. The constructor throws a NullReferenceException
exception if Nothing
is passed to it.
Sub New(ByVal Parent As Control)
If Parent Is Nothing Then
Throw New NullReferenceException
End If
p_CallingCtrl = Parent
AddHandler p_CallingCtrl.DoubleClick, AddressOf p_CallingCtrl_DoubleClick
AddHandler p_CallingCtrl.MouseMove, AddressOf p_CallingCtrl_MouseMove
End Sub
How it works: Public Sub RegisterControl(ByVal Ctrl As Control)
RegisterControl
adds control references to the p_CtrlCollection
. If the method Dispose
has already been executed, an ObjectDisposedException
exception is thrown. Furthermore, a control reference is only added to the collection if it is not Nothing
and has not already been added to the collection before. However, it is not ensured that the control is indeed a child control of the control referenced by p_CallingCtrl
.
Public Sub RegisterControl(ByVal Ctrl As Control)
If Me.p_Disposed Then
Throw New ObjectDisposedException(Me.ToString())
End If
If Not (Ctrl Is Nothing) Then
If Not Exists(Ctrl) Then
p_CtrlCollection.Add(Ctrl)
End If
End If
End Sub
How it works: Public Sub UnRegisterControl(ByVal Ctrl As Object)
UnRegisterControl
removes controls from the collection and also throws an ObjectDisposedException
exception if it is called after Dispose
. However, no exception is thrown when the control reference passed to the method is not found in the collection or when it is Nothing
.
Public Sub UnRegisterControl(ByVal Ctrl As Object)
Dim Counter As Integer
If Me.p_Disposed Then
Throw New ObjectDisposedException(Me.ToString())
End If
For Counter = 1 To p_CtrlCollection.Count
If p_CtrlCollection.Item(Counter) Is Ctrl Then
p_CtrlCollection.Remove(Counter)
Exit For
End If
Next Counter
End Sub
How it works: Public ReadOnly Property Count() As Integer
After ensuring that Dispose
has not been executed before, the Count
property returns the number of controls in the collection by simply forwarding the value of p_CtrlCollection.Count
.
Public ReadOnly Property Count() As Integer
Get
If Me.p_Disposed Then
Throw New ObjectDisposedException(Me.ToString())
End If
Return p_CtrlCollection.Count
End Get
End Property
How it works: The Deconstructor Logic
The methods Dispose
, Dispose(ByVal disposing As Boolean)
, and Finalize
were created by implementing the Microsoft dispose pattern.
Public Overloads Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
If Not (Me.p_Disposed) Then
If disposing Then
RemoveHandler p_CallingCtrl.DoubleClick, AddressOf p_CallingCtrl_DoubleClick
RemoveHandler p_CallingCtrl.MouseMove, AddressOf p_CallingCtrl_MouseMove
p_CtrlCollection = Nothing
p_CallingCtrl = Nothing
End If
Me.p_Disposed = True
End If
End Sub
Protected Overrides Sub Finalize()
Dispose(False)
End Sub
How it works: Private Sub p_CallingCtrl_DoubleClick(ByVal sender As Object, ByVal e As System.EventArgs)
This event handler is triggered when the DoubleClick
event occurs at the parent control. A loop casts every item back to Control
and copies the reference to the local variable Ctrl
. After that Ctrl
can be used to call Bounds.Contains(Point)
. The Point
used here is p_MousePos
, our own buffer of the last cursor position on our parent control. If the cursor is above the control which is currently checked, the particular control is enabled by setting Ctrl.Enabled
to True
.
Private Sub p_CallingCtrl_DoubleClick(ByVal sender As Object, _
ByVal e As System.EventArgs)
Dim Counter As Integer
Dim Ctrl As Control = Nothing
For Counter = 1 To p_CtrlCollection.Count
Ctrl = CType(p_CtrlCollection.Item(Counter), Control)
If Ctrl.Bounds.Contains(p_MousePos) Then
Ctrl.Enabled = True
End If
Next Counter
End Sub
How it works: Private Sub p_CallingCtrl_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
This event handler is triggered when the MouseMove
event occurs at the parent control. This is necessary because of two reasons. First, we can't use the DoubleClick
event to get the cursor position because it only passes an EventArgs
object which does not contain this information. Second, it is possible to use the Cursor
object but this object returns the absolute cursor position on the screen, and frankly, I failed to manually calculate the relative cursor position (always had an offset of a few pixel on both axes) or find an appropriate class for getting the relative position (your feedback is welcome if you find one). Anyway, this solution might be a little bit dirty but at least it provides us exactly the position information we need when processing the DoubleClick
event.
Private Sub p_CallingCtrl_MouseMove(ByVal sender As Object, _
ByVal e As System.Windows.Forms.MouseEventArgs)
p_MousePos.X = e.X
p_MousePos.Y = e.Y
End Sub
How it works: Public Function Exists(ByVal Ctrl As Control) As Boolean
Last but not least, there is Exists
which checks whether a certain control has been registered or not. This is done by using a loop and comparing each reference with the Ctrl
parameter. Like any other public member, Exists
ensures that Dispose
has not been executed.
Public Function Exists(ByVal Ctrl As Control) As Boolean
Dim Counter As Integer
If Me.p_Disposed Then
Throw New ObjectDisposedException(Me.ToString())
End If
Exists = False
For Counter = 1 To p_CtrlCollection.Count
If p_CtrlCollection.Item(Counter) Is Ctrl Then
Exists = True
Exit For
End If
Next Counter
End Function
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.