If you're like me and you started coding in VB.NET (or C#, or probably any other programming language) without ever having coded in VB6 then you've probably never felt the need for a control array, at least in the VB6 sense. To hear many migrating VB6 developers tell it though, VB6 control arrays are the greatest programming invention ever and living without them in VB.NET is nigh on unbearable.
The thing is, design-time support for control arrays was added to VB6 for a reason: to enable the same event to be handled for multiple controls with a single method. In VB.NET, you can do that simply by adding multiple controls to the Handles clause of a method. As such, design-time support for control arrays is not needed in VB.NET, so it was never added. Take away the need for handling events for multiple controls and control arrays are exactly like any other arrays, which is exactly how they are treated in VB.NET and all other .NET languages.
VB6 developers often contend that creating a control array in the designer is sooooo much more convenient than doing so in code. I don't necessarily disagree that it could be more convenient but I probably do disagree with how much more. With Intellisense you can generally create an array of controls in code in less than a minute, if not in seconds. If the array should contain a very large number of controls then you can always use a loop and get each control from the form's Controls collection by name. It's really not a big deal.
Anyway, rather than fight it any longer I decided to create something that would help these poor souls. I know that various people have posted various such things around the place. I haven't checked any of those out so I don't know they compare to this current implementation. I've also never used VB6 so I don't know exactly how this compares to how control arrays work in VB6. What I do know is that this current implementation allows you to set the Index property of a control in the designer to add it to a collection that you can access in code, either using a For Each loop or accessing a specific control by index.
So, the first order of business is design-time support. In order to be able to add something to the Toolbox in VS, it must implement the IComponent interface. You can implement IComponent yourself, but the more usual way is to inherit the Component class. As such, that was the first thing to do here:
vb.net Code:
Public Class ControlArray
Inherits System.ComponentModel.Component
End Class
The next thing to do is to provide the Index property on each control. If you've ever used a ToolTip, you know that this can be done, although you may not know how. The secret is the IExtenderProvider interface, along with a few associated tricks. To implement the interface, you must define the CanExtend method. This method takes an object and returns a Boolean that indicates whether the object can be extended or not:
Public Function CanExtend(ByVal extendee As Object) As Boolean Implements System.ComponentModel.IExtenderProvider.CanExtend
Return TypeOf extendee Is Control
End Function
End Class
That's not enough on its own though. There's two more elements required to make this class useful. First, we need to specify exactly what property eligible controls will extended with. After that, we need to actually provide an implementation for that property. A property is just a convenient way to package together two methods: one that gets a value and one that sets a value. In this case, we implement those as just regular methods:
Public ReadOnly Property Controls As List(Of Control)
Get
Return Me._controls
End Get
End Property
Public Function CanExtend(ByVal extendee As Object) As Boolean Implements System.ComponentModel.IExtenderProvider.CanExtend
Return TypeOf extendee Is Control
End Function
Public Function GetIndex(ByVal control As System.Windows.Forms.Control) As Integer
Return Me.controls.IndexOf(control)
End Function
Public Sub SetIndex(ByVal control As Control, ByVal index As Integer)
Me.controls.Insert(index, control)
End Sub
End Class
That's all the basics in place, but that implementation won't do it. The attached solution includes a much more rigorous implementation that addresses these issues. The ControlArray class included in that solution is actually a base class only, with various derived classes provided for various specific types of controls. The sample application includes a form that contains a ButtonArray and a TextBoxArray. When you run the project, you'll be shown each item in the collection.
In the designer, you can select a Button or a TextBox and you'll find an extra Index property at the bottom of the Properties window. Clearing the property will remove the control from the collection and setting it will add the control to the collection. Note that adding and removing controls will automatically adjust the Indexes of all other controls in the collection. Note also that you can set the Index of a control to a value beyond the size of the collection and it will automatically adjust to add the control to the end of the collection. A very interesting feature of this auto-adjusting behaviour is that you can select multiple controls in the designer and then set the index property to a single value for all of them and they will all be added to the collection with distinct Index values.
A further feature of the attached sample is an extended derived class for the RadioButton control. Any and all methods of the base class that add or remove items in the collection have been declared Overridable. This means that you can override all those members and provide extra processing when items are added and removed. The RadioButtonArray class provided adds and removes a handler on the CheckedChanged event of each item in the collection that will uncheck every other item when one RadioButton is checked. This may sound redundant as it's the default behaviour of RadioButton's anyway, but in this case the behaviour is independent of container. This means that RadioButtons in different containers can all act as a single group. The sample contains three groups of three RadioButtons contained in different Panels to demonstrate this.
Finally, the attached solution was created in VS 2010 so will only be able to be opened in VS 2010 or VB Express 2010. You can add the ControlArray.vb code file to any project in VB 2005 or later though. The one change that might be required is the addition of a few line continuation characters.
Looks good, although I don't have any need for it like yourself.
I do remember that in VB6 you would create control arrays by giving multiple controls the same name. They would then become a control array automatically if I remember correctly. That also means that copy/pasting a control in the designer would give the copy the same name, but naturally a different index. Since in .NET you can't have multiple controls with the same name I don't think implementing this feature is possible though. Perhaps with an extension for Visual Studio itself, but that would involve an effort much greater than just learning to live without control arrays
I missed control arrays for about 30 seconds after going into VB.Net (and/or C#). When I discovered that you can use a method for multiple controls that problem vanished.
-Max
The name's "Peck" .... "Max Peck"
"If you think it's expensive to hire a professional to do the job, wait until you hire an amateur." - Red Adair
Many thanks for your great app! I was just what I was looking for when I started a new project recently. The forms have multiple sets of checkboxes and radiobuttons and your class has simplified the design considerably.
However, I have had a problem with the sequence of the controls not following the index defined at the design stage. It appears to be related to the way the controls are defined in Designer.vb.
I solved the problem in my design by manually changing the designer code which, of course, is not ideal.
I can post a simple app showing the problem if required.
Also, is there a simple way at design time of removing the index from a group of selected controls?
I can post a simple app showing the problem if required.
That would be a good idea. I had a quick look at the code and didn;t see anything that would explain that.
Originally Posted by Exem
Also, is there a simple way at design time of removing the index from a group of selected controls?
Not as it stands. Because all controls must have different indexes, if you select multiple controls then the Index property will display blank. If you try to set a single value for multiple controls, which you could then clear, it will not work because the actual indexes used will be determined by the number of elements in the array. You might be able to change the implementation but you would lose something else as a result. You could add a designer function to the array itself to clear it. Maybe I'll try that some time as it's something I've never done before.
You will see I have attached two zips - a 'Good' and a 'Bad'.
In both apps the form displays eight radiobuttons with only the first four having an index set. In the 'Good' app they sequence properly but not in the 'Bad' app where the sequence is 1, 4, 2, 3.
You can apparently change the index in the designer but the sequence is still wrong. If you close the solution and re-open the design then shows the wrong sequence.
question... what do you mean by "Sequence" ? I'm asking because it sounds like you might be mistaking the Index value for the TabOrder value... but I haven't looked at your code yet either.
I decided to try to fix the issue myself and found that the order of the controls in the list did not agree with the sequence set in the designer.
Here is my modification:
Code:
Public Overridable Sub SetIndex(ByVal control As TControl, ByVal index As Integer?)
If Not index.Equals(Me.GetIndex(control)) Then
'The control is either being moved to a different index or removed altogether, so remove it from its current index.
Me.Remove(control)
End If
If index.HasValue Then
'Insert the control at its new index, or to the end of the list if the index is invalid.
'was - Me.Insert(Math.Min(index.Value, Me.Count), control)
If index.Value > Me.Count - 1 Then
For i = Me.Count To index.Value
Me.Add(control)
Next
End If
Me.RemoveAt(index.Value)
Me.Insert(index.Value, control)
End If
End Sub
There may be a more elegant way of fixing the problem but this does appear to work for now.
How to share one procedure for all control in an array?
I know i can do with
Code:
For Each b In Buttons
AddHandler b.Click, AddressOf Buttons_Click
Next
Is it possible in code editor if i select Buttons from controls dropdown list, all events related to Button control appear in events dropdown list, selecting one will auto generate a procedure (Buttons_Click, Buttons_MouseMove, Buttons_KeyDown etc...) that will used by all buttons in Buttons array.
How to share one procedure for all control in an array?
I know i can do with
Code:
For Each b In Buttons
AddHandler b.Click, AddressOf Buttons_Click
Next
Is it possible in code editor if i select Buttons from controls dropdown list, all events related to Button control appear in events dropdown list, selecting one will auto generate a procedure (Buttons_Click, Buttons_MouseMove, Buttons_KeyDown etc...) that will used by all buttons in Buttons array.
If you want to create a handler for the same event for multiple controls then simply select those multiple controls and then use the Properties window to create or select an event handler. This functionality has been available for over a decade and possibly back to the original VS.NET. The fact that it exists is one of the primary reasons that you don't need control arrays in VB.NET in the first place.
If you wanted this control array class to do it for you then you'd have to add a corresponding event to the array class and then use AddHandler as a control was added to the array and RemoveHandler as one was removed.
If you wanted this control array class to do it for you then you'd have to add a corresponding event to the array class and then use AddHandler as a control was added to the array and RemoveHandler as one was removed.
I do it like this, first added declaration for events i want
Code:
Public Event Click As EventHandler
Private Sub Click_internal(sender As Object, e As EventArgs)
RaiseEvent Click(sender, e)
End Sub
Public Event MouseMove As MouseEventHandler
Private Sub MouseMove_internal(sender As Object, e As MouseEventArgs)
RaiseEvent MouseMove(sender, e)
End Sub
Public Event KeyDown As KeyEventHandler
Private Sub KeyDown_internal(sender As Object, e As KeyEventArgs)
RaiseEvent KeyDown(sender, e)
End Sub
second added two methods to add/remove handlers
Code:
Private Sub AddEventHandelers(control As TControl)
AddHandler control.Click, AddressOf Click_internal
AddHandler control.MouseMove, AddressOf MouseMove_internal
AddHandler control.KeyDown, AddressOf KeyDown_internal
End Sub
Private Sub RemoveEventHandelers(control As TControl)
RemoveHandler control.Click, AddressOf Click_internal
RemoveHandler control.MouseMove, AddressOf MouseMove_internal
RemoveHandler control.KeyDown, AddressOf KeyDown_internal
End Sub
third call AddEventHandelers & RemoveEventHandelers from Add and SetIndex methods
Code:
Public Overridable Sub Add(ByVal item As TControl) Implements ICollection(Of TControl).Add
Me.items.Add(item)
AddEventHandelers(item)
End Sub
Public Overridable Sub SetIndex(ByVal control As TControl, ByVal index As Integer?)
If Not index.Equals(Me.GetIndex(control)) Then
'The control is either being moved to a different index or removed altogether, so remove it from its current index.
RemoveEventHandelers(control)
Me.Remove(control)
End If
If index.HasValue Then
'Insert the control at its new index, or to the end of the list if the index is invalid.
Me.Insert(Math.Min(index.Value, Me.Count), control)
AddEventHandelers(control)
End If
End Sub
It works well, but i wonder is this the correct way to do it?
You are so professional and your advise is appreciated
I do it like this, first added declaration for events i want
Code:
Public Event Click As EventHandler
Private Sub Click_internal(sender As Object, e As EventArgs)
RaiseEvent Click(sender, e)
End Sub
Public Event MouseMove As MouseEventHandler
Private Sub MouseMove_internal(sender As Object, e As MouseEventArgs)
RaiseEvent MouseMove(sender, e)
End Sub
Public Event KeyDown As KeyEventHandler
Private Sub KeyDown_internal(sender As Object, e As KeyEventArgs)
RaiseEvent KeyDown(sender, e)
End Sub
second added two methods to add/remove handlers
Code:
Private Sub AddEventHandelers(control As TControl)
AddHandler control.Click, AddressOf Click_internal
AddHandler control.MouseMove, AddressOf MouseMove_internal
AddHandler control.KeyDown, AddressOf KeyDown_internal
End Sub
Private Sub RemoveEventHandelers(control As TControl)
RemoveHandler control.Click, AddressOf Click_internal
RemoveHandler control.MouseMove, AddressOf MouseMove_internal
RemoveHandler control.KeyDown, AddressOf KeyDown_internal
End Sub
third call AddEventHandelers & RemoveEventHandelers from Add and SetIndex methods
Code:
Public Overridable Sub Add(ByVal item As TControl) Implements ICollection(Of TControl).Add
Me.items.Add(item)
AddEventHandelers(item)
End Sub
Public Overridable Sub SetIndex(ByVal control As TControl, ByVal index As Integer?)
If Not index.Equals(Me.GetIndex(control)) Then
'The control is either being moved to a different index or removed altogether, so remove it from its current index.
RemoveEventHandelers(control)
Me.Remove(control)
End If
If index.HasValue Then
'Insert the control at its new index, or to the end of the list if the index is invalid.
Me.Insert(Math.Min(index.Value, Me.Count), control)
AddEventHandelers(control)
End If
End Sub
It works well, but i wonder is this the correct way to do it?
You are so professional and your advise is appreciated
That's basically it but there are a couple of things to note there.
1. There are more locations than you have shown that you need to use AddHandler and RemoveHandler. Check out the RadioButtonControlArray class in the provided solution to see all those locations.
2. It might work for you to simply pass through the 'sender' and 'e' arguments from one event handler to another but you're actually breaking the usual convention by doing so. The 'sender' should be the object that raised the event so you should always pass Me to RaiseEvent. That would mean creating a new type for 'e' that had a property for the control that raised the original event. That's going to be more trouble but is more correct in that it follows the standard conventions for events.
2. It might work for you to simply pass through the 'sender' and 'e' arguments from one event handler to another but you're actually breaking the usual convention by doing so. The 'sender' should be the object that raised the event so you should always pass Me to RaiseEvent. That would mean creating a new type for 'e' that had a property for the control that raised the original event. That's going to be more trouble but is more correct in that it follows the standard conventions for events.
Have to do just to follow the standard conventions for events or there is performance issue?
What is the meaning of question mark used in GetIndex and SetIndex methods?
Code:
Public Function GetIndex(ByVal control As TControl) As Integer?
Return If(Me.Contains(control), Me.IndexOf(control), DirectCast(Nothing, Integer?))
End Function
What is the meaning of question mark used in GetIndex and SetIndex methods?
Code:
Public Function GetIndex(ByVal control As TControl) As Integer?
Return If(Me.Contains(control), Me.IndexOf(control), DirectCast(Nothing, Integer?))
End Function
Appending a question mark to a value type denotes that it is nullable. 'Integer?' is shorthand for 'Nullable(Of Integer)'.