Wednesday, September 2, 2009

Adding and Removing Visual Basic .NET Controls and Events Dynamically at Runtime

Questions:

-How do you add two controls at runtime with one of them reacting to the other?
-How do you add a ComboBox and ProgressBar pair at runtime that are linked?
-How do you remove controls at runtime and adjust the position of other controls?

I'm demonstrating this with a sample program, PAM Tasks, a project status manager designed to get a project name left in the VB settings section of the registry by another program called PAM, then load, change, save, and print (to HTML), a task completion list.

Code is given further below. I apologize the ends of some lines may not be visible here. If you paste them into your editor, you will see the full code.

This is the design form. It contains no task rows:



This is the runtime form with 7 task rows added by the code. These rows could be added using the Add Task button or automatically based on the contents of the data file for the project. Since this was a class project, and the rest of the class wasn't interested in adding and removing controls dynamically, the code is "hobbled" to always have exactly 7 tasks (rows). Comments in the code make this clear.

Note that the key problem here we are examining is how to get each ProgressBar to react when the ComboBox next to it is changed. Likewise, how to remove all the right controls and make all the right positioning adjustments when the Remove tasks button is clicked.



The executable is (hopefully) here. It won't load or save data because you don't have the PAM program, but you can add and remove tasks and change completions.

OK, let's look at how to add the controls. First, here is a snippet from the AddTask sub routine. Note that the names of the controls being added are structured for systematic reference:


ProgressBarI.Name = "Task" + TaskCounter.ToString + "ProgressBar"
ProgressBarI.Width = 140
ProgressBarI.Height = 20
ProgressBarI.Top = FirstY + TaskCounter * RowHeight
ProgressBarI.Left = 280
ProgressBarI.Value = Val(TaskCompletion)
Me.Controls.Add(ProgressBarI)

ComboBoxI.Name = "Task" + TaskCounter.ToString + "ComboBox"
ComboBoxI.Height = 20
ComboBoxI.Width = 45
ComboBoxI.Top = FirstY + TaskCounter * RowHeight
ComboBoxI.Left = 430
ComboBoxI.Items.Add("0")
ComboBoxI.Items.Add("10")
ComboBoxI.Items.Add("20")
ComboBoxI.Items.Add("30")
ComboBoxI.Items.Add("40")
ComboBoxI.Items.Add("50")
ComboBoxI.Items.Add("60")
ComboBoxI.Items.Add("70")
ComboBoxI.Items.Add("80")
ComboBoxI.Items.Add("90")
ComboBoxI.Items.Add("95")
ComboBoxI.Items.Add("100")
ComboBoxI.Text = TaskCompletion
Me.Controls.Add(ComboBoxI)

AddHandler ComboBoxI.SelectedIndexChanged, AddressOf ChangeProgressBar


The most interesting lines of code in the snippet above are the three shown below. What they do is add a handler to each ComboBox that will (further below) change it's respective ProgressBar:

ProgressBarI.Name = "Task" + TaskCounter.ToString + "ProgressBar"

ComboBoxI.Name = "Task" + TaskCounter.ToString + "ComboBox"

AddHandler ComboBoxI.SelectedIndexChanged, AddressOf ChangeProgressBar


OK. Now let's look at the ChangeProgressBar subroutine. It loops through every control in the form, and when it finds the right ProgressBar, it sets its value to match the ComboBox (the sender).


Private Sub ChangeProgressBar(ByVal sender As Object, ByVal e As System.EventArgs)
Dim ControlI As Object
For Each ControlI In Controls
If TypeOf ControlI Is ProgressBar Then
If (ControlI.Name.Substring(4, 1) = sender.Name.Substring(4, 1)) Then
ControlI.Value = sender.Text
End If
End If
Next
End Sub


OK. Now how do we remove tasks? I think this function is self-explanatory. The RemoveAll argument of the sub routine is used to empty (then in another location refill) the box when a new project is selected in the PAM program.


Private Sub RemoveTasks(ByVal RemoveAll As Boolean)
Dim Removals As String = ""
Dim ControlI As Object
Dim RowsUpward As Integer
'Get the list of the tasks to delete
For Each ControlI In Controls
If (TypeOf ControlI Is CheckBox And ControlI.Name.Substring(0, 4) = "Task") Then
If (ControlI.Checked Or RemoveAll = True) Then
Removals = Removals + ControlI.Name.Substring(4, 1)
End If
End If
Next
'Remove the requested task controls
For controlIndex As Integer = Me.Controls.Count - 1 To 0 Step -1
ControlI = Me.Controls(controlIndex)
If (ControlI.Name.Substring(0, 4) = "Task") Then
If (InStr(Removals, ControlI.Name.Substring(4, 1)) > 0) Then
Me.Controls.Remove(ControlI)
End If
End If
Next
'Move controls up as needed
'Any task needs to move up the number of rows of removal indices that are lower than its own
For Each ControlI In Controls
RowsUpward = 0
If (ControlI.Name.Substring(0, 4) = "Task") Then
For CharacterIndex As Integer = 0 To Removals.Length - 1 Step 1
If (Removals.Substring(CharacterIndex, 1) < ControlI.Name.Substring(4, 1)) Then
RowsUpward = RowsUpward + 1
End If
Next
If RowsUpward > 0 Then
ControlI.Top = ControlI.Top - RowHeight * RowsUpward
ControlI.Name = "Task" + (Val(ControlI.Name.Substring(4, 1)) - RowsUpward).ToString + ControlI.Name.Substring(5)
End If
End If
Next
'Shrink the form
Me.Height = Me.Height - RowHeight * Removals.Length
'Reset the TaskCounter
TaskCounter = TaskCounter - Removals.Length
End Sub


Finally, here is the full code:


Option Explicit On
Imports System.IO
Public Class FormMain
'These variables will be used among Sub routines of this form, so we declare them here.
Dim RowHeight As Integer = 30
Dim FirstY As Integer = 60
'This is the currently selected string in PAM
Dim PAMProjectString As String = ""
'PT stands for Pam Tasks
Dim PTProjectString As String = ""
Dim PTProjectName As String
Dim PTFolder As String = GetPTFolder()
Dim PTDataFileName As String
Dim PTPrintFileName As String
Dim DismissedPAMProjectString As String = ""
Dim SaveTasksReponse As String
Dim TasksExist As Boolean = True
Dim TaskCounter As Integer
Private Sub FormMain_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Click

'GetProject gets the current PAM project and its data if it has a data file
GetProject()
'If there's a PAM project
'And it's not already loaded in PAM Tasks
'And it's not already been dismissed
If PAMProjectString <> "" And PAMProjectString <> PTProjectString And PAMProjectString <> DismissedPAMProjectString Then
If PTProjectString = "" Then 'If there is no project already current in PAM Tasks
ChangeProjectLabel() 'just quietly change the label to match PAM
AddAllTasks() ' and add any tasks
Else
GetSaveTasksReponse() 'Otherwise ask Save? Yes, No ,Cancel
If SaveTasksReponse = vbCancel Then 'If user says Cancel
DismissedPAMProjectString = PAMProjectString 'Note PAM project as dismissed
Else 'Otherwise go ahead and save tasks and change project
If SaveTasksReponse = vbYes Then
SaveTasks()
End If
RemoveTasks(True)
ChangeProjectLabel()
AddAllTasks()
End If
End If
End If
End Sub
Private Sub GetProject()
PAMProjectString = GetSetting("PRARMan", "Startup", "SelectedProject", "")
End Sub
Private Function GetPTFolder()
Dim PAMFolder As String = GetSetting("PRARMan", "Startup", "NetPath", "")
If Directory.Exists(PAMFolder + "Tasks") = False Then
Directory.CreateDirectory(PAMFolder + "Tasks")
End If
Return PAMFolder + "Tasks\"
End Function
Private Sub ChangeProjectLabel()
PTProjectString = PAMProjectString
PTProjectName = PTProjectString.Split("|"c)(0).Trim
PTDataFileName = PTFolder + PTProjectName + ".dat"
PTPrintFileName = PTFolder + PTProjectName + ".htm"
If PTProjectName = "" Then
LabelSelectedProject.Text = "Please select a project in PAM."
Else
LabelSelectedProject.Text = PTProjectString
End If
End Sub
Private Sub GetSaveTasksReponse()
'If there are tasks already in the form, ask user for permission to save and change
If TasksExist = True Then
SaveTasksReponse = MsgBox("Changing project to " + PAMProjectString + vbCrLf + vbCrLf + _
"Save tasks first?", MsgBoxStyle.YesNoCancel, "Save")
Else
SaveTasksReponse = vbYes
End If
End Sub
Private Sub SaveTasks()
Dim ControlI As Object
'Make sure this is always 7 tasks long for the Hubbard training standard
Dim TaskDataArray(7, 2) As String
Dim TaskDataString As String = ""
'Put task data into an array
For Each ControlI In Controls
If (ControlI.Name.Substring(0, 4) = "Task") Then
If (ControlI.Name.Substring(5, 4) = "Text") Then
TaskDataArray(Val(ControlI.Name.Substring(4, 1)), 0) = ControlI.Text
End If
End If
If (ControlI.Name.Substring(0, 4) = "Task") Then
If (ControlI.Name.Substring(5, 4) = "Comb") Then
TaskDataArray(Val(ControlI.Name.Substring(4, 1)), 1) = ControlI.Text
End If

End If
Next
'The number 7 in the following line should be TaskCounter, but set to 7 for Hubbard training standard.
For RowCounter As Integer = 0 To 7 - 1 Step 1
For FieldCounter As Integer = 0 To 1 Step 1
TaskDataString = TaskDataString + TaskDataArray(RowCounter, FieldCounter) + "|"
Next
Next
Try
My.Computer.FileSystem.WriteAllText(PTDataFileName, TaskDataString.Substring(0, TaskDataString.Length - 1), False, System.Text.Encoding.ASCII)
Catch ex As Exception
MsgBox("SaveTasks: " + ex.Message)
End Try
End Sub
Private Function ReadTasks()
Dim TaskData() As String = {"", ""}
Try
If File.Exists(PTDataFileName) Then
TaskData = My.Computer.FileSystem.ReadAllText(PTDataFileName, System.Text.Encoding.ASCII).Split("|c")
End If
Catch ex As Exception
End Try
'Fill in empty tasks if needed up to 7
While TaskData.Count < 14
ReDim Preserve TaskData(TaskData.Count + 1)
TaskData(TaskData.Count - 1) = ""
End While
Return TaskData
End Function
Private Sub PrintAllTasks()
'Removed for blogger.com compatibility
End Sub
Private Sub AddAllTasks()
Dim TaskData() As String = ReadTasks()
'The following line would be used if the data file were flexible in length.
'For Me.TaskCounter = 0 To TaskData.Count / 2 - 1 Step 1
For Me.TaskCounter = 0 To 6 Step 1
AddTask(TaskCounter, TaskData(TaskCounter * 2), TaskData(TaskCounter * 2 + 1))
Next
End Sub
Private Sub AddTask(ByRef TaskCounter As Integer, Optional ByRef TaskName As String = "", Optional ByRef TaskCompletion As String = "0")

Dim CheckBoxI As New CheckBox
Dim TextBoxI As New TextBox
Dim ProgressBarI As New ProgressBar
Dim ComboBoxI As New ComboBox
Me.Height = 160 + TaskCounter * RowHeight

CheckBoxI.Name = "Task" + TaskCounter.ToString + "CheckBox"
CheckBoxI.Width = 15
CheckBoxI.Height = 20
CheckBoxI.Top = FirstY + TaskCounter * RowHeight
CheckBoxI.Left = 14
Me.Controls.Add(CheckBoxI)

TextBoxI.Name = "Task" + TaskCounter.ToString + "TextBox"
TextBoxI.Width = 240
TextBoxI.Height = 20
TextBoxI.Top = FirstY + TaskCounter * RowHeight
TextBoxI.Left = 35
TextBoxI.Text = TaskName
Me.Controls.Add(TextBoxI)

ProgressBarI.Name = "Task" + TaskCounter.ToString + "ProgressBar"
ProgressBarI.Width = 140
ProgressBarI.Height = 20
ProgressBarI.Top = FirstY + TaskCounter * RowHeight
ProgressBarI.Left = 280
ProgressBarI.Value = Val(TaskCompletion)
Me.Controls.Add(ProgressBarI)

ComboBoxI.Name = "Task" + TaskCounter.ToString + "ComboBox"
ComboBoxI.Height = 20
ComboBoxI.Width = 45
ComboBoxI.Top = FirstY + TaskCounter * RowHeight
ComboBoxI.Left = 430
ComboBoxI.Items.Add("0")
ComboBoxI.Items.Add("10")
ComboBoxI.Items.Add("20")
ComboBoxI.Items.Add("30")
ComboBoxI.Items.Add("40")
ComboBoxI.Items.Add("50")
ComboBoxI.Items.Add("60")
ComboBoxI.Items.Add("70")
ComboBoxI.Items.Add("80")
ComboBoxI.Items.Add("90")
ComboBoxI.Items.Add("95")
ComboBoxI.Items.Add("100")
ComboBoxI.Text = TaskCompletion
Me.Controls.Add(ComboBoxI)

AddHandler ComboBoxI.SelectedIndexChanged, AddressOf ChangeProgressBar

End Sub
Private Sub ChangeProgressBar(ByVal sender As Object, ByVal e As System.EventArgs)
Dim ControlI As Object
For Each ControlI In Controls
If TypeOf ControlI Is ProgressBar Then
If (ControlI.Name.Substring(4, 1) = sender.Name.Substring(4, 1)) Then
ControlI.Value = sender.Text
End If
End If
Next
End Sub
Private Sub RemoveTasks(ByVal RemoveAll As Boolean)
Dim Removals As String = ""
Dim ControlI As Object
Dim RowsUpward As Integer
'Get the list of the tasks to delete
For Each ControlI In Controls
If (TypeOf ControlI Is CheckBox And ControlI.Name.Substring(0, 4) = "Task") Then
If (ControlI.Checked Or RemoveAll = True) Then
Removals = Removals + ControlI.Name.Substring(4, 1)
End If
End If
Next
'Remove the requested task controls
For controlIndex As Integer = Me.Controls.Count - 1 To 0 Step -1
ControlI = Me.Controls(controlIndex)
If (ControlI.Name.Substring(0, 4) = "Task") Then
If (InStr(Removals, ControlI.Name.Substring(4, 1)) > 0) Then
Me.Controls.Remove(ControlI)
End If
End If
Next
'Move controls up as needed
'Any task needs to move up the number of rows of removal indices that are lower than its own
For Each ControlI In Controls
RowsUpward = 0
If (ControlI.Name.Substring(0, 4) = "Task") Then
For CharacterIndex As Integer = 0 To Removals.Length - 1 Step 1
If (Removals.Substring(CharacterIndex, 1) < ControlI.Name.Substring(4, 1)) Then
RowsUpward = RowsUpward + 1
End If
Next
If RowsUpward > 0 Then
ControlI.Top = ControlI.Top - RowHeight * RowsUpward
ControlI.Name = "Task" + (Val(ControlI.Name.Substring(4, 1)) - RowsUpward).ToString + ControlI.Name.Substring(5)
End If
End If
Next
'Shrink the form
Me.Height = Me.Height - RowHeight * Removals.Length
'Reset the TaskCounter
TaskCounter = TaskCounter - Removals.Length
End Sub
Private Sub ButtonRemoveTasks_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonRemoveTasks.Click
RemoveTasks(False)
End Sub
Private Sub ButtonAddTask_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonAddTask.Click
AddTask(TaskCounter)
TaskCounter = TaskCounter + 1
End Sub
Private Sub ButtonPrint_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButtonPrint.Click
PrintAllTasks()
End Sub
Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
SaveTasks()
SaveSetting("PRARMan", "Task", "Top", Me.Top)
SaveSetting("PRARMan", "Task", "Left", Me.Left)
End Sub
Private Sub FormMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Me.Top = GetSetting("PRARMan", "Task", "Top", "100")
Me.Left = GetSetting("PRARMan", "Task", "Left", "100")
If Me.Top < 1 Then
Me.Top = 100
End If
If Me.Left < 1 Then
Me.Left = 100
End If
End Sub
End Class

No comments: