In section “The TreeView and ListView Controls“, you learned how to populate the TreeView and ListView controls, how to manipulate them at runtime, and how to sort the ListView control in any way you want. But what good are all these techniques unless you can save the tree’s nodes or the ListViewItems to a disk file and then reuse them in a later session?
It would be nice if the TreeNode object were serializable — you could serialize the root node and all the nodes under it with a single call to the Serialize method. Unfortunately, this is not the case. Well, how about subclassing the TreeNode object? Create a new class that inherits from the TreeNode class and is serializable. This is an option, but it’s not simple.
The main reason that the Nodes collection can’t be easily serialized is that a node’s Tag property can store an object that’s not serializable. To serialize the Nodes collection, we must make a few assumptions that are specific to an application, or a class of applications, but not to all TreeView controls. In this section, I assume that the Tag property won’t be persisted. You can easily modify the code to persist nodes whose tags are strings, numbers, or any serializable object. The code presented in this section can be used to persist most TreeView controls, but not any TreeView control you can throw at it. Just remember that serialization is limited to the objects that are themselves serializable, and not all classes are serializable.
To serialize the nodes of a TreeView control, we’ll store the individual nodes in an ArrayList and then serialize the ArrayList, as discussed earlier in this chapter. The code of this section serializes the strings displayed on a TreeView control. You know how to scan the nodes of a TreeView control, and the code for serializing the control’s nodes seems trivial. It’s not quite so. The ArrayList has a linear structure: Each item is independent of any other. The TreeView control, however, has a hierarchical structure. Most of its nodes are children of other nodes, as well as parents of other nodes. Therefore, we must store not only the data (strings), but also their structure. To store this information, we’ll create a new structure with two fields: one for the node’s value and another one for the node’s indentation:
<Serializable()> Structure sNode Dim node As String Dim level As Integer End StructureCode language: PHP (php)
We want to be able to serialize this structure, so we must prefix it with the <Serializable> attribute. The level field is the node’s indentation. The level field of all root nodes is zero. The nodes immediately under the root have a level of 1, and so on. To serialize the TreeView control, we’ll iterate through its nodes and store each node to an sNode variable. Each time we switch to a child node, we’ll increase the current value of the level variable by one; each time we move up to a parent node, we’ll decrease the same value accordingly. All the sNode structures will be added to an ArrayList, which will then be serialized.
Likewise, when we read the ArrayList from the disk file, we must reconstruct the original tree. Items with a level value of zero are root nodes. The first item with a level value of 1 is the first child node under the most recently added root node. As long as the level field doesn’t change, the new nodes are added under the same parent. When this value increases, we must create a new child node under the current node. When this value decreases, we must move up to the current node’s parent and create a new child under it. The only complication is that a level value might decrease by more than one. In this case, we must move up to the parent’s parent— or even higher in the hierarchy. Figure 12.3 shows a typical TreeView control and how its nodes are stored in the ArrayList.
The control on the left is a TreeView control, populated at design time. The control on the right is a ListBox control with the items of the ArrayList. The first column is the level field (the node’s indentation), whereas the second column is the node’s text.
Now we can look at the code for serializing the control. The code presented in this section is part of the Globe project— namely, it’s the code behind the Save Nodes and Load Nodes commands of the File menu. The File Save Nodes command prompts the user with the File Save dialog box for the path of a file in which the nodes will be stored. Then it calls the CreateList() subroutine, passing the root node of the control and the path of the file where the items will be stored. Listing 16.7 shows this menu item’s Click event handler.
Listing 12.7: File > Save NodesMenu Item’s Event Handler
Private Sub FileSave_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles FileSaveMenuItem.Click SaveFileDialog1.DefaultExt = "XML" If SaveFileDialog1.ShowDialog = DialogResult.OK Then CreateList(GlobeTree.Nodes(0), SaveFileDialog1.FileName) End If End SubCode language: PHP (php)
The CreateList() subroutine goes through the subnodes of the root node and stores them into the GlobeNodes ArrayList. This ArrayList is declared at the form level with the following statement:
Dim GlobeNodes As New ArrayList()Code language: PHP (php)
CreateList() is a recursive subroutine that scans the immediate children of the node passed as an argument. If a child node contains its own children, the subroutine calls itself to iterate through the children. This process may continue to any depth. The code of the subroutine is shown in Listing 12.8.
Listing 12.8: The CreateList() Subroutine
Sub CreateList(ByVal node As TreeNode, ByVal fName As String) Static level As Integer Dim thisNode As TreeNode Dim myNode As sNode Application.DoEvents() myNode.level = level myNode.node = node.Text GlobeNodes.Add(myNode) If node.Nodes.Count > 0 Then level = level + 1 For Each thisNode In node.Nodes CreateList(thisNode, fName) Next level = level - 1 End If SaveNodes(fName) End SubCode language: PHP (php)
After the ArrayList has been populated, the code calls the SaveNodes() subroutine, which persists the ArrayList to a disk file. The path of the file is the second argument of the CreateList() subroutine. SaveNodes(), shown in Listing 12.9, is a straightforward subroutine that serializes the GlobeNodes ArrayList to disk. (The process of serializing ArrayLists and other collections was discussed earlier in this chapter.)
Listing 12.9: The SaveNodes() Subroutine
Sub SaveNodes(ByVal fName As String) Dim formatter As SoapFormatter Dim saveFile As FileStream saveFile = File.Create(fName) formatter = New SoapFormatter formatter.Serialize(saveFile, GlobeNodes) saveFile.Close() End SubCode language: PHP (php)
The File > Load Nodes command prompts the user for a filename and then calls the LoadNodes() subroutine to read the ArrayList persisted in this file and load the control with its nodes. The Click event handler of the Load Nodes command is shown in Listing 12.10.
Listing 12.10: Reading the Persisted Nodes
Private Sub FileLoad_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles FileLoadMenuItem.Click OpenFileDialog1.DefaultExt = "XML" If OpenFileDialog1.ShowDialog = DialogResult.OK Then LoadNodes(GlobeTree, OpenFileDialog1.FileName) End If End SubCode language: PHP (php)
The LoadNodes() subroutine loads the items read from the file into the GlobeNodes ArrayList and then calls the ShowNodes() subroutine to load the nodes from the ArrayList onto the control. The LoadNodes() subroutine is shown in Listing 12.11.
Listing 12.11: Loading the GlobeNodes ArrayList
Sub LoadNodes(ByVal TV As TreeView, ByVal fName As String) TV.Nodes.Clear() Dim formatter As SoapFormatter Dim openFile As FileStream openFile = File.Open(fName, FileMode.Open) formatter = New SoapFormatter GlobeNodes = CType(formatter.Deserialize(openFile), ArrayList) openFile.Close() showNodes(TV) End SubCode language: PHP (php)
The most interesting code is in the ShowNodes() subroutine, which goes through the items in the ArrayList and re-creates the original structure of the TreeView control. At each iteration, the subroutine examines the value of the item’s level field. If it’s the same as the current node’s level, the new node is added under the same node as the current node (we’re on the same indentation level.) If the current item’s level field is larger than the current node’s level, the new node is added under the current node (it’s a child of the current node.) Finally, if the current item’s level field is smaller than the current node’s level, the code moves up to the parent of the current node. This step can be repeated several times, depending on the difference between the two levels. If the current node’s level is 4 and the level field of the new node is 1, the code will move up three levels. (It will actually be added under the most recent root node.) Listing 12.12 is the code of the ShowNodes() subroutine.
Listing 12.12: The ShowNodes() Subroutine
Sub showNodes(ByVal TV As TreeView) Dim o As Object Dim currNode As TreeNode = Nothing Dim level As Integer = 0 Dim i As Integer For i = 0 To GlobeNodes.Count - 1 o = GlobeNodes(i) Dim oNode As sNode = CType(o, sNode) If CType(oNode, sNode).level = level Then If currNode Is Nothing Then currNode = TV.Nodes.Add(oNode.node.ToString) Else currNode = currNode.Parent.Nodes.Add(oNode.node.ToString) End If Else If oNode.level > level Then currNode = currNode.Nodes.Add(oNode.node.ToString) level = oNode.level Else While oNode.level <= level currNode = currNode.Parent level = level - 1 End While currNode = currNode.Nodes.Add(oNode.node.ToString) End If End If TV.ExpandAll() Application.DoEvents() Next End SubCode language: PHP (php)
Why did I use a SoapFormatter and not a BinaryFormatter to persist the data? I just wanted to see the structure of the data in text format. You will probably change the code to save the data in binary format because it’s much more compact. Of course, XML and SOAP are quite fashionable these days. You can also claim that the data can be read on any other system and that you’re following industry standards. I suggest that you use mostly the binary format for storing application data. If you want to exchange data with another system, use the XmlSerialization class instead.
The technique shown here persists the strings displayed on the control, and it works with most applications. If you’re using a TreeView control to store objects, you must adjust the code of this section to persist the objects, not just strings. It goes without saying that all objects you store to the TreeView control must be serializable; if not, you won’t be able to serialize the Nodes collection.
If you’re wondering what the persisted nodes look like in the XML file, here’s how the first few items of the Globe tree are persisted.
<item xsi:type="a3:NodeSerializer+sNode" xmlns:a3="http://schemas.microsoft.com/clr/ nsassem/Globe/Globe%2C%20 Version%3D1.0.638.15776%2C%20 Culture%3Dneutral%2C%20 PublicKeyToken%3Dnull"> <node id="ref-4">Globe</node> <level>0</level> </item> <item xsi:type="a3:NodeSerializer+sNode" xmlns:a3="http://schemas.microsoft.com/clr/ nsassem/Globe/Globe%2C%20 Version%3D1.0.638.15776%2C%20 Culture%3Dneutral%2C%20 PublicKeyToken%3Dnull"> <node id="ref-5">Africa</node> <level>1</level> </item> <item xsi:type="a3:NodeSerializer+sNode" xmlns:a3="http://schemas.microsoft.com/clr/ nsassem/Globe/Globe%2C%20 Version%3D1.0.638.15776%2C%20 Culture%3Dneutral%2C%20 PublicKeyToken%3Dnull"> <node id="ref-6">Egypt</node> <level>2</level> </item> <item xsi:type="a3:NodeSerializer+sNode" xmlns:a3="http://schemas.microsoft.com/clr/ nsassem/Globe/Globe%2C%20 Version%3D1.0.638.15776%2C%20 Culture%3Dneutral%2C%20 PublicKeyToken%3Dnull"> <node id="ref-7">Alexandria</node> <level>3</level> </item>Code language: HTML, XML (xml)
Persisting the items of a ListView control is even simpler. You must create a new structure that reflects the structure of each row (the item and subitems of each row) and then create an ArrayList with items of this type. Persisting the ArrayList is straightforward, and so is the loading of the control, because the ListView control doesn’t have a hierarchical structure. Its items are organized in a linear fashion, just like the items of the ArrayList.
To reuse the subroutines that serialize and deserialize the nodes of a TreeView control, you can create a new class that exposes the CreateList() and LoadNodes() subroutines as methods. The other two subroutines that save the ArrayList to disk and load a disk file into the ArrayList are private to the class and can be called only from within the code of the two methods. The Globe sample project contains the NodeSerializer custom class. This class contains the code and the declarations discussed in this section, and I will not repeat the code here. To use this class in your code, you must create an instance of the class and call the appropriate method. To persist the TreeView control to a file, use the following statements:
Dim NS As New NodeSerializer() NS.CreateList(GlobeTree.Nodes(0), SaveFileDialog1.FileName)Code language: CSS (css)
To load a TreeView control previously saved to a file, use the following statements:
Dim NS As New NodeSerializer() NS.LoadNodes(GlobeTree, OpenFileDialog1.FileName)Code language: CSS (css)
I included these statements in the Globe project, but they’re commented out. To test the Globe application’s File menu commands, add a few items to the TreeView control (countries and cities) and save the tree to a disk file. Then select the root node and delete it by clicking the Delete Current Node button, and load the file you just saved to disk.
One last remark about the code that loads a TreeView control from a disk file: Because the TreeView is persisted to an XML file, the user might attempt to open an XML file that contains irrelevant data. You must insert a structured exception handler to avoid runtime errors or use a new extension for these files. After looking at the XML files generated by the Serialize method for a couple of TreeView controls, you should change the SoapFormatter to a Binary Formatter.
In this section, you learned how to serialize the TreeView control’s Nodes collection, which holds the control’s data. Youmight wish to persist the appearance of the control as well, by including each node’s font, background, and foreground colors, and so on. To serialize each node’s attributes, add more fields to the sNode structure. To store each node’s font along with the text, add a new member to the structure (the nodeFont field) and set this field to the current node’s Font property.