Advanced Basics

Windows Forms Controls: Z-order and Copying Collections

Ken Spencer

Code download available at:AdvancedBasics0401.exe(131 KB)

Q Are the SendToBack and BringToFront methods the only options for modifying the z-order of Windows® Forms controls?

Q Are the SendToBack and BringToFront methods the only options for modifying the z-order of Windows® Forms controls?

A It took a suggestion from one of my buddies at Microsoft to figure this stumper out. Of course, it's pretty simple once you know how to do it.

A It took a suggestion from one of my buddies at Microsoft to figure this stumper out. Of course, it's pretty simple once you know how to do it.

There is no property on a Windows Forms control to manipulate z-order as there was in previous versions of Visual Basic® (ZOrder). It turns out the method you need is in the Controls collection. The SetChildIndex method allows you to set the z-index of a particular control. The following line, from the click event in Figure 1, changes the z-index of Label2:

Me.Controls.SetChildIndex(Me.Label2, Label2NewIndex)

To retrieve the current index of a control call the GetChildIndex method, as shown in the ShowIndex method in Figure 1.

Figure 1 frmMain Move Controls

Private Sub cmdMove_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdMove.Click Dim Label2NewIndex As Integer Try Label2NewIndex = numIndex.Value Me.Controls.SetChildIndex(Me.Label2, Label2NewIndex) ShowIndex() Catch ex As Exception Throw New Exception(ex.Message) End Try End Sub Sub ShowIndex() txtLabel1Index.Text = Me.Controls.GetChildIndex(Me.Label1).ToString txtLabel2Index.Text = Me.Controls.GetChildIndex(Me.Label2).ToString End Sub

Q How can I use the CopyTo method of the Windows Forms controls collection to copy controls into an array?

Q How can I use the CopyTo method of the Windows Forms controls collection to copy controls into an array?

A The CopyTo method will take the current controls collection and copy all the controls into an array. In order to use it, you must specify the array and the starting point. For instance, the following code copies the controls to the MyArrayOfControls array starting at the first element:

Me.Controls.CopyTo(MyArrayOfControls, 0)

A The CopyTo method will take the current controls collection and copy all the controls into an array. In order to use it, you must specify the array and the starting point. For instance, the following code copies the controls to the MyArrayOfControls array starting at the first element:

Me.Controls.CopyTo(MyArrayOfControls, 0)

Follow along with me while I create a sample to illustrate this. First, I created a form named frmControlsToArrayMaster that contains a set of six label/textbox pairs and three buttons. Next, I created a second form, named frmArrayClient, which has no controls but is the same size as the first form. Then I was ready to write some code.

The first code I wrote wired up the cmdCopy_Click event (see Figure 2). This event redims the MyArrayOfControls array to the number of controls in the collection. I then used CopyTo to copy the controls to the array starting at the first position. I put this code inside a button click event to make it easier to see when you run it.

Figure 2 frmControlsToArrayMaster

Private Sub cmdCopy_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdCopy.Click ReDim MyArrayOfControls(Me.Controls.Count) Me.Controls.CopyTo(MyArrayOfControls, 0) End Sub Private Sub cmdNew_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdNew.Click Dim frm As New frmArrayClient Dim ct As Control frm.ShowDialog() Me.Controls.AddRange(MyArrayOfControls) For Each ct In Me.Controls If ct.GetType.ToString = "System.Windows.Forms.Button" Then ct.Visible = True End If Next End Sub Private Sub cmdNewForm2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdNewForm2.Click Dim frm As New frmArrayClient frm.ShowDialog() End Sub

Next, I wired up the cmdNew_Click event. The first version of the code looked like this:

Private Sub cmdNew_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdNew.Click Dim frm As New frmArrayClient frm.ShowDialog() End Sub

Then, I added the following code to frmArrayClient:

Dim ct As Control If MyArrayOfControls.GetUpperBound(0) > 0 Then Me.Controls.AddRange(MyArrayOfControls) For Each ct In Me.Controls If ct.GetType.ToString = "System.Windows.Forms.Button" Then ct.Visible = False End If Next End If

This code adds the controls from the first form by calling Controls.AddRange. Then it sets the Visible property of any buttons to False because I don't want the same command buttons on this form. Pretty cool? Let's see. Take a look at Figure 3.

Figure 3 Reparented Controls

Figure 3** Reparented Controls **

The top form is frmControlsToArrayMaster, which has the controls on it. But where are these controls? When AddRange was called in frmArrayClient, it actually loaded the controls from my array on the new form. Since a control can only have one parent, the controls were reassigned as children of the new form (frmArrayClient). This means the controls are no longer on the first form (frmControlsToArrayMaster).

The cmdNew_Click code shown in Figure 2 takes a stab at fixing this. It puts the controls back on frmControlsToArrayMaster. You can prove this to yourself by running the sample application, putting data in two controls, then clicking the New button. Now add data to two more controls and close the new form. All the data you entered is now on the original form. In most cases this is pretty cool, but what happens when you want to actually copy the controls onto the new form instead of moving them?

I played with this idea a bit, then called my buddy Bill Martschenko at NetEdge Software who sent me some code that elegantly solves the problem of copying (cloning) controls.

The goal is to take the controls in the array generated by CopyTo and create a new set of controls that have the same properties (or a subset of them) as the original control. Then you can change the new controls without impacting the original controls.

The form frmNewArrayClient2 takes the approach Bill suggested. Figure 4 shows this new form. As you can see, the original form (top) still has its controls but the new form has the same controls minus the command buttons (bottom).

Figure 4 Copied Controls

Figure 4** Copied Controls **

The form load event of frmNewArrayClient2, shows only this code:

Dim ctNew As Control Dim i As Integer If MyArrayOfControls.GetUpperBound(0) > 0 Then For i = 0 To MyArrayOfControls.GetUpperBound(0) ctNew = CloneControl(MyArrayOfControls(i)) If ctNew.GetType.ToString <> "System.Windows.Forms.Button" Then Me.Controls.Add(ctNew) End If Next End If

This code loops through the controls in MyArrayOfControls, creates a new control for each, and adds this new control to the form if it's not a button. The new controls are created by a call to the CloneControl function. This code is quite simple and performs the task of copying the controls. Now let's dig into the code that does the work behind CloneControl (see Figure 5). First, the CloneControl function executes this line of code to create a new control of the same type as the current control:

Dim newControl As Control = CType(NewAs(c), Control)

Figure 5 modControls Module

Module modControls Friend Function CloneControl(ByVal c As Control) As Control ' instantiate another instance of the given control and ' clone the common properties Dim newControl As Control = CType(NewAs(c), Control) CloneProperties(newControl, c, "Visible", "Size", "Font", _ "Text", "Location", "BackColor", "ForeColor", "Enabled", _ "BackgroundImage") ' clone properties unique to specific controls If TypeOf newControl Is ButtonBase Then CloneProperties(newControl, c, "DialogResult", _ "BackgroundImage", "FlatStyle", "TextAlign", "Image", _ "ImageAlign", "ImageIndex", "ImageList") ElseIf TypeOf newControl Is LinkLabel Then CloneProperties(newControl, c, "VisitedLinkColor", _ "LinkVisited", "LinkColor", "LinkBehavior", "LinkArea", _ "FlatStyle", "BorderStyle", "DisabledLinkColor", _ "ActiveLinkColor", "Image", "ImageAlign", "ImageIndex", _ "ImageList") End If Return newControl End Function End Module Public Module ObjectFactory Public Function NewAs(ByVal t As Type) As Object Return t.Assembly.CreateInstance(t.FullName) End Function Public Function NewAs(ByVal x As Object) As Object Return ObjectFactory.NewAs(x.GetType()) End Function Public Sub CloneProperties( ByVal target As Object, ByVal source As _ Object, ByVal ParamArray propertyNames() As String) Dim sourceProperties As New PropertyAccessor(source) Dim targetProperties As New PropertyAccessor(target) Dim p As String For Each p In propertyNames targetProperties(p) = sourceProperties(p) Next End Sub End Module

A close examination of the CType call shows a call to the NewAs method. The parameter of this function is the control you want to copy. The NewAs function is also shown in Figure 5.

If you examine the NewAs function definitions (there are two in Figure 5), you will see that they make what looks like recursive calls to themselves, but they're merely overloaded. This handy trick allows you to pass something as an object, making the first function call clean. Then that function can use some type of operator (such as GetType) on the parameter and then call the second function. This makes it easier for developers to use the function since they don't need to know the details of how to call the private function. The second NewAs function makes a call to the CreateInstance method of the assembly class. This method creates a new instance of the control by looking up the control's type. This allows you to create an instance of the control you want to clone.

The second line of the CloneProperties function actually copies the properties of the control (c) and maps those properties to the new control (newControl), as shown here:

CloneProperties(newControl, c, "Visible", "Size", "Font", _ "Text", "Location", "BackColor", "ForeColor", "Enabled", _ "BackgroundImage")

The CloneControl function finishes with an If block that allows you to set up special handling for particular types of controls. For instance, the sample has an If block that will copy particular properties when the control is a button or linklabel. It also makes a second call to CloneProperties to grab the additional properties.

The CloneProperties function is also shown in Figure 5. This function takes an array of the properties you want to set. Then it creates an instance of the PropertyAccessor class for each control. Finally, the code loops through the parameter names array and for each sets the target control's property equal to the source control's property. This copies the properties from the source control to the new control one by one.

Let's walk through the PropertyAccessor class (see Figure 6). The constructor for the class takes one parameter—a reference to either the source or target control. This reference is then set to the target_ variable. The Target property allows you to retrieve a reference to the control.

Figure 6 PropertyAccessor Class

Imports System.Reflection Public Class PropertyAccessor Public Sub New(ByVal target As Object) Me.target_ = target End Sub Public ReadOnly Property Target() As Object Get Return Me.target_ End Get End Property Default Public Property Item(ByVal propertyName As String) As Object Get Dim prop As PropertyInfo = _ Me.Target.GetType().GetProperty(propertyName) Return prop.GetValue(Me.Target, Nothing) End Get Set(ByVal value As Object) Dim prop As PropertyInfo = _ Me.Target.GetType().GetProperty(propertyName) prop.SetValue(Me.Target, value, Nothing) End Set End Property #Region " Private State " Private target_ As Object #End Region End Class

The interesting code in this class defines the Item property. This property is declared as the Default property for the class. You can see that this property takes one parameter:

Default Public Property Item(ByVal propertyName As String) As Object

The Get method of this property returns the property you want to access. Then it creates a new instance of PropertyInfo and sets this to the property you requested:

Dim prop As PropertyInfo = Me.Target.GetType().GetProperty(propertyName)

Once you have the property, you can return its value by calling the GetValue method. The Set method is also pretty straightforward. The new value is passed, so all you need to do is create an instance of the PropertyInfo class as in the Get method:

Dim prop As PropertyInfo = Me.Target.GetType().GetProperty(propertyName)

Then you can call SetValue to actually set the value of the property:

prop.SetValue(Me.Target, value, Nothing)

That's it. Now you have the code to easily copy a set of controls from one form to another.

Both of this month's questions looked difficult to accomplish at first. But in both cases the Microsoft® .NET Framework provided a straightforward way to accomplish the task. Sometimes you just have to be persistent to find the solution you're looking for.

Send your questions and comments for Ken to  basics@microsoft.com.

Ken Spencer works for 32X Tech (https://www.32X.com), where he provides training, software development, and consulting services on Microsoft technologies.