The Globe Example project demonstrates many of the techniques we’ve discussed so far. It’s not the simplest example of a TreeView control, and its code is lengthy, but it will help you understand how to manipulate nodes at runtime. Because TreeView is not a simple control, before ending this section I want to show you a nontrivial example that you can use as a starting point for your own custom applications.
The Globe project consists of a single form, which is shown in Figure 4.27. The TreeView control at the left contains a rather obvious tree structure that shows continents, countries, and cities. The control is initially populated with the continents, which were added at design time. The countries and cities are added from within the form’s Load event handler. Although the continents were added at design time, there’s no particular reason not to add them to the control at runtime. It would have been simpler to add all the nodes at runtime by using the TreeNode Editor, but I decided to add a few nodes at design time just for demonstration purposes.
Figure 4.27 – TreeView Globe Example project
When a node is selected from the TreeView control, its text is displayed in the TextBox controls at the bottom of the form. When a continent name is selected, the continent’s name appears in the first TextBox, and the other two TextBoxes are empty. When a country is selected, its name appears in the second TextBox, and its continent appears in the first TextBox. Finally, when a city is selected, it appears in the third TextBox, along with its country and continent in the other two TextBoxes.
You can also use the same TextBox controls to add new nodes. To add a new continent, just supply the name of the continent in the first TextBox and leave the other two empty. To add a new country, supply its name in the second TextBox and the name of the continent it belongs to in the first one. Finally, to add a city, supply a continent, country, and city name in the three TextBoxes. The program will add new nodes as needed.
Run the Globe application and expand the continents and countries to see the tree structure of the data stored in the control. Add new nodes to the control, and enumerate these nodes by clicking the buttons on the right-hand side of the form. These buttons list the nodes at a given level (continents, countries, and cities). When you add new nodes, the code places them in their proper place in the list. If you specify a new city and a new country under an existing continent, a new country node will be created under the specified continent, and a new city node will be inserted under the specified country.
Adding New Nodes
Let’s take a look at the code of the Globe project. We’ll start by looking at the code that populates the TreeView control. The root node (GLOBE) and the continent names were added at design time through the TreeNode Editor.
When the application starts, the code adds the countries to each continent and adds the cities to each country. The code in the form’s Load event goes through all the continents already in the control and examines their Text properties. Depending on the continent represented by the current node, the code adds the corresponding countries and some city nodes under each country node.
If the current node is Africa, the first country to be added is Egypt. The Egypt node is added to the ContinentNode variable. The new node is returned as a TreeNode object and is stored in the CountryNode variable. Then the code uses this object to add nodes that correspond to cities under the Egypt node. The form’s Load event handler is quite lengthy, so I’m showing only the code that adds the first country under each continent and the first city under each country (see Listing 4.39). The variable GlobeNode is the root node of the TreeView control, and it was declared and initialized with the following statement:
Dim GlobeNode As TreeNode = GlobeTree.Nodes(0)Code language: VB.NET (vbnet)
Listing 4.39: Adding the Nodes of Africa
For Each ContinentNode In GlobeNode.Nodes Select Case ContinentNode.Text Case "Europe" CountryNode = ContinentNode.Nodes.Add("Germany") CountryNode.Nodes.Add("Berlin") Case "Asia" CountryNode = ContinentNode.Nodes.Add("China") CountryNode.Nodes.Add("Beijing") Case "Africa" CountryNode = ContinentNode.Nodes.Add("Egypt") CountryNode.Nodes.Add("Cairo") CountryNode.Nodes.Add("Alexandria") Case "Oceania" CountryNode = ContinentNode.Nodes.Add("Australia") CountryNode.Nodes.Add("Sydney") Case "N. America" CountryNode = ContinentNode.Nodes.Add("USA") CountryNode.Nodes.Add("New York") Case "S. America" CountryNode = ContinentNode.Nodes.Add("Argentina") End Select NextCode language: VB.NET (vbnet)
The remaining countries and their cities are added via similar statements, which you can examine if you open the Globe project. Notice that the GlobeTree control could have been populated entirely at design time, but this wouldn’t be much of a demonstration. Let’s move on to a few more interesting aspects of programming the TreeView control.
Retrieving the Selected Node
The selected node is given by the property SelectedNode. After retrieving the selected node, you can also retrieve its parent node and the entire path to the root node. The parent node of the selected node is TreeView1.SelectedNode.Parent. If this node has a parent, you can retrieve it by calling the Parent property of the previous expression. The FullPath property of a node retrieves the selected node’s full path. The FullPath property of the Rome node is as follows:
GLOBE\Europe\Italy\RomeCode language: VB.NET (vbnet)
The slashes separate the segments of the node’s path. As mentioned earlier, you can specify any other character for this purpose by setting the control’s PathSeparator property. To remove the selected node from the tree, call the Remove method:
TreeView1.SelectedNode.RemoveCode language: VB.NET (vbnet)
If the selected node is a parent control for other nodes, the Remove method will take with it all the nodes under the selected one. To select a node from within your code, set the control’s SelectedNode property to the TreeNode object that represents the node you want to select.
One of the operations you’ll want to perform with the TreeView control is to capture the selection of a node. The TreeView control fires the BeforeSelect and AfterSelect events, which notify your application about the selection of another node. If you need to know which node was previously selected, you must use the BeforeSelect event. The second argument of both events has two properties, TreeNode and Action, which let you find out the node that fired the event and the action that caused it. The e.Node property is a TreeViewNode object that represents the selected node. Use it in your code as you would use any other node of the control. The e.Action property is a member of the TreeViewAction enumeration (ByKeyboard, ByMouse, Collapse, Expand, Unknown). Use this property to find out the action that caused the event. The actions of expanding and collapsing a tree branch fire their own events, which are the BeforeExpand/AfterExpand and the BeforeCollapse/AfterCollapse events, respectively.
The Globe project retrieves the selected node and extracts the parts of the node’s path. The individual components of the path are displayed in the three TextBox controls at the bottom of the form. Listing 4.40 shows the event handler for the TreeView control’s AfterSelect event.
Listing 4.40: Processing the Selected Node
Private Sub GlobeTree AfterSelect(...) _ Handles GlobeTree.AfterSelect If GlobeTree.SelectedNode Is Nothing Then Exit Sub Dim components() As String txtContinent.Text = "" txtCountry.Text = "" txtCity.Text = "" Dim separators() As Char separators = GlobeTree.PathSeparator.ToCharArray components = _ GlobeTree.SelectedNode.FullPath. _ ToString.Split(separators) If components.Length > 1 Then _ txtContinent.Text = components(1) If components.Length > 2 Then _ txtCountry.Text = components(2) If components.Length > 3 Then _ txtCity.Text = components(3) End SubCode language: VB.NET (vbnet)
The Split method of the String data type extracts the parts of a string that are delimited by the PathSeparator character (the backslash character). If any of the captions contain this character, you should change the default to a different character by setting the PathSeparator property to some other character.
The code behind the Delete Current Node and Expand Current Node buttons is simple. To delete a node, call the selected node’s Remove method. To expand a node, call the selected node’s Expand method.
Processing Multiple Selected Nodes
The GlobeTree control has its ShowCheckBoxes property set to True so that users can select multiple nodes. I added this feature to demonstrate how you can allow users to select any number of nodes and then process them.
As you will notice by experimenting with the TreeView control, you can select a node that has subordinate nodes, but these nodes will not be affected; they will remain deselected (or selected, if you have already selected them). In most cases, however, when we select a parent node, we actually intend to select all the nodes under it. When you select a country, for example, you’re in effect selecting not only the country, but also all the cities under it. The code of the Process Selected Nodes button assumes that when a parent node is selected, the code must also select all the nodes under it.
Let’s look at the code that iterates through the control’s nodes and isolates the selected ones. It doesn’t really process them; it simply prints their captions in the ListBox control. However, you can call a function to process the selected nodes in any way you like. The code behind the Process Selected Nodes button starts with the continents. It creates a TreeNodeCollection with all the continents and then goes through the collection with a For Each. . .Next loop. At each step, it creates another TreeNodeCollection, which contains all the subordinate nodes (the countries under the selected continent) and goes through the new collection. This loop is also interrupted at each step to retrieve the cities in the current country and process them with another loop. The code behind the Process Selected Nodes button is straightforward, as you can see in Listing 4.41.
Listing 4.41: Processing All Selected Nodes
Protected Sub bttnProcessSelected Click(...) _ Handles bttnProcessSelected.Click Dim continent, country, city As TreeNode Dim Continents, Countries, Cities As TreeNodeCollection ListBox1.Items.Clear() Continents = GlobeTree.Nodes(0).Nodes For Each continent In Continents If continent.Checked Then ListBox1.Items.Add(continent.FullPath) Countries = continent.Nodes For Each country In Countries If country.Checked Or country.Parent.Checked Then _ ListBox1.Items.Add(" " & country.FullPath) Cities = country.Nodes For Each city In Cities If city.Checked Or city.Parent.Checked Or _ city.Parent.Parent.Checked Then _ ListBox1.Items.Add(" " & city.FullPath) Next Next Next End SubCode language: VB.NET (vbnet)
The code examines the Checked property of the current node, as well as the Checked property of the parent node, all the way to the root node. If any of them is True, the node is considered selected. You should try to add the appropriate code to select all subordinate nodes of a parent node when the parent node is selected (whether you deselect the subordinate nodes when the parent node is deselected is entirely up to you and depends on the type of application you’re developing). The Nodes collection exposes the GetEnumerator method, and you can revise the last listing so that it uses an enumerator in place of each For Each. . .Next loop. If you want to retrieve the selected nodes only, and ignore the unselected child nodes of a selected parent node, use the CheckedNodes collection.
Adding New Nodes
The Add This Node button lets the user add new nodes to the tree at runtime. The number and type of the node(s) added depend on the contents of the TextBox controls:
- If only the first TextBox control contains text, a new continent will be added.
- If the first two TextBox controls contain text:
- If the continent exists, a new country node is added under the specified continent.
- If the continent doesn’t exist, a new continent node is added, and then a new country node is added under the continent’s node.
- If all three TextBox controls contain text, the program adds a continent node (if needed), then a country node under the continent node (if needed), and finally, a city node under the country node.
Obviously, you can omit a city, or a city and country, but you can’t omit a continent name. Likewise, you can’t specify a city without a country, or a country without a continent. The code will prompt you accordingly when it detects any condition that prevents it from adding the new node. If the node exists already, the program selects the existing node and doesn’t issue any warnings. The Add This Node button’s code is shown in Listing 4.42.
Listing 4.42: Adding Nodes at Runtime
Private Sub bttnAddNode_Click_1(...) _ Handles bttnAddNode.Click Dim nd As TreeNode = Nothing Dim Continents As TreeNode If txtContinent.Text.Trim <> "" Then Continents = GlobeTree.Nodes(0) Dim ContinentFound, CountryFound, CityFound As Boolean Dim ContinentNode, CountryNode, CityNode As TreeNode For Each nd In Continents.Nodes If nd.Text.ToUpper = txtContinent.Text.ToUpper Then ContinentFound = True Exit For End If Next If Not ContinentFound Then nd = Continents.Nodes.Add(txtContinent.Text) nd.NodeFont = continentfont End If ContinentNode = nd If txtCountry.Text.Trim <> "" Then Dim Countries As TreeNode Countries = ContinentNode If Not Countries Is Nothing Then For Each nd In Countries.Nodes If nd.Text.ToUpper = txtCountry.Text.ToUpper Then CountryFound = True Exit For End If Next End If If Not CountryFound Then nd = ContinentNode.Nodes.Add(txtCountry.Text) nd.NodeFont = countryfont End If CountryNode = nd If txtCity.Text.Trim <> "" Then Dim Cities As TreeNode Cities = CountryNode If Not Cities Is Nothing Then For Each nd In Cities.Nodes If nd.Text.ToUpper = txtCity.Text.ToUpper Then CityFound = True Exit For End If Next End If If Not CityFound Then nd = CountryNode.Nodes.Add(txtCity.Text) nd.NodeFont = cityfont End If CityNode = nd End If End If End If End SubCode language: VB.NET (vbnet)
The listing is quite lengthy, but it’s not hard to follow. First, it attempts to find a continent that matches the name in the first TextBox. If it succeeds, it does not need to add a new continent node. If not, a new continent node must be added. To avoid simple data-entry errors, the code converts the continent names to uppercase before comparing them to the uppercase of each node’s name. The same happens with the countries and the cities. As a result, each node’s pathname is unique — you can’t have the same city name under the same country more than once. It is possible, however, to add the same city name to two different countries.
The three buttons ListContinents, ListCountries, and ListCities populate the ListBox control with the names of the continents, countries, and cities, respectively. The code is straightforward and is based on the techniques discussed in previous sections. To print the names of the continents, it iterates through the children of the GLOBE node. Listing 4.43 shows the complete code of the ListContinents button.
Listing 4.43: Retrieving the Continent Names
Private Sub bttnListContinents_Click(...) _ Handles bttnListContinents.Click Dim Nd As TreeNode, continentNode As TreeNode Dim continent As Integer, continents As Integer ListBox1.Items.Clear() Nd = GlobeTree.Nodes(0) continents = Nd.Nodes.Count continentNode = Nd.Nodes(0) For continent = 1 To continents ListBox1.Items.Add(continentNode.Text) continentNode = continentNode.NextNode Next End SubCode language: VB.NET (vbnet)
The code behind the ListCountries button is equally straightforward, although longer. It must scan each continent, and within each continent, it must scan in a similar fashion the continent’s child nodes. To do this, you must set up two nested loops: the outer one to scan the continents, and the inner one to scan the countries. The complete code for the ListCountries button is shown in Listing 4.44. Notice that in this example, I used For. . .Next loops to iterate through the current level’s nodes, and I also used the NextNode method to retrieve the next node in the sequence.
Listing 4.44: Retrieving the Country Names
Private Sub bttnListCountries_Click(...) _ Handles bttnListCountries.Click Dim Nd As TreeNode, CountryNode As TreeNode, ContinentNode As TreeNode Dim continent As Integer, continents As Integer Dim country As Integer, countries As Integer ListBox1.Items.Clear() Nd = GlobeTree.Nodes.Item(0) continents = Nd.Nodes.Count ContinentNode = Nd.Nodes(0) For continent = 1 To continents countries = ContinentNode.Nodes.Count CountryNode = ContinentNode.Nodes(0) For country = 1 To countries ListBox1.Items.Add(CountryNode.Text) CountryNode = CountryNode.NextNode Next ContinentNode = ContinentNode.NextNode Next End SubCode language: VB.NET (vbnet)
When the ContinentNode.Next method is called, it returns the next node in the Continents level. Then the property ContinentNode.Nodes(0) returns the first node in the Countries level. As you can guess, the code of the ListCities button uses the same two nested lists as the previous listing and an added inner loop, which scans the cities of each country.
Listing 4.45 – Retrieving the City Names
Private Sub bttnListCities_Click(...) _ Handles bttnListCities.Click Dim Nd As TreeNode, countryNode As TreeNode, _ continentNode As TreeNode, cityNode As TreeNode Dim continent As Integer, continents As Integer Dim country As Integer, countries As Integer Dim city As Integer, cities As Integer ListBox1.Items.Clear() Nd = GlobeTree.Nodes(0) continents = Nd.Nodes.Count continentNode = Nd.Nodes(0) For continent = 1 To continents countries = continentNode.Nodes.Count countryNode = continentNode.Nodes(0) For country = 1 To countries cities = countryNode.Nodes.Count cityNode = countryNode.Nodes(0) For city = 1 To cities ListBox1.Items.Add(cityNode.Text) cityNode = cityNode.NextNode Next countryNode = countryNode.NextNode Next continentNode = continentNode.NextNode Next End SubCode language: VB.NET (vbnet)
The code behind these command buttons requires some knowledge of the information stored in the tree. The code will work with trees that have two or three levels of nodes such as the Globe tree, but what if the tree’s depth is allowed to grow to a dozen levels? A tree that represents the structure of a folder on your hard disk, for example, might easily contain a dozen nested folders. Obviously, to scan the nodes of this tree, you can’t put together unlimited nested loops. The next section describes a technique for scanning any tree, regardless of how many levels it contains. Finally, the application’s File menu contains commands for storing the nodes to a file and loading the same nodes in a later session. These commands use serialization, a topic that’s discussed in detail in Chapter, “XML and Object Serialization.” For now, you can use these commands to persist the edited nodes to a disk file and read them back.