Before I delved into .NET, the most recent language I worked in was VB 6.0. I created some large GUI-based projects in VB 6.0, and along the way I came up against some frustrating speed barriers.
When I first started using .NET, I wanted to experiment with some of the controls common to both languages. I wanted to know if Microsoft removed some of those barriers I came across in VB 6.0. The answer is both yes and no.
I mainly wanted to experiment with the TreeView control. As I have mentioned before in this book, I like this control. I think it shows the most information in the smallest space. I also think it shows hierarchical relationships the best of all the controls. If you think about it, quite a bit of data is hierarchical in nature.
To start off, I will show you some code for a VB 6.0 project that tests the performance of the VB 6.0 TreeView. I want to give you a baseline to compare .NET to. After you have examined this project and its results, you will build the same project in .NET.
I don't expect you to have VB 6.0, so I just show you the code and the results in this section. Listing 10-1 shows the code for the VB 6.0 Tree Tester project.
Option Explicit Dim tmr As Single Private Sub cmdClear_Click() Dim Count As Long Count = Tree.Nodes.Count MousePointer = vbHourglass DoEvents tmr = Timer Tree.Nodes.Clear lblClear.Caption = (Timer - tmr) & " seconds to clear " & Count & " Nodes)" MousePointer = vbNormal End Sub Private Sub cmdFill_Click() Dim k As Integer Dim Count As Long Dim max As Long MousePointer = vbHourglass DoEvents max = Val(txtMax.Text) Count = Tree.Nodes.Count tmr = Timer For k = Count To Count + max Tree.Nodes.Add , , "Key" & k, "Node " & k Next lblFill.Caption = (Timer - tmr) & " seconds to fill " & max & " Nodes)" MousePointer = vbNormal End Sub Private Sub Form_Load() txtMax = 1000 End Sub Private Sub txtMax_KeyPress(KeyAscii As Integer) If KeyAscii < 48 Or KeyAscii > 57 Then KeyAscii = 0 End If End Sub
This code is fairly simple. It creates several thousand nodes in a tree and then clears the tree. The time to do both is displayed in some labels on the form. Figure 10-1 shows this form after adding 1,000 nodes.
You can see that it took just two-tenths of a second to fill the tree with 1,000 root nodes. Why root nodes? Suppose a corporation has 1,000 employees. Each employee would be a root node and each employee would have child nodes representing a range of items, such as subordinate personnel.
Anyway, you can see that this did not take long at all. I must tell you that 1,000 nodes is not much, though. I have programs that work with trees containing more than 40,000 nodes.[1] If you extrapolate, you will find that 40,000 nodes take 8 seconds to load. This is unacceptable in an enterprise program.
How long does it take to clear this tree? See Figure 10-2 for the answer.
Yup, that's right. It takes almost 40 seconds to clear 1,000 root nodes from a TreeView control in VB 6.0. For 40,000 nodes, it would take almost a half-hour to clear this screen. For 100,000 nodes, it would take 2.7 days to clear this control. Are you willing to wait that long? I would have rebooted the machine after a few seconds.
Now I know you may not believe me, so I included this VB 6.0 project in this book's code so you can try it yourself. You can download the code for this book from the Downloads section of the Apress Web site (http://www.apress.com).
You may be wondering how I solved the slowdown problem. I didn't. I was only able to ameliorate it by using some Windows API calls to freeze the tree window and manually clear the tree. This helped a lot, but the control is still unworkable for large amounts of data. I finally fixed this problem in VB 6.0 by buying a third- party TreeView control. It's lightning fast in both loading and clearing.
While I was playing around with the TreeView control in VB 6.0 trying to get it to run faster, I noticed something about the TreeView that I put to good use. I will tell you what it is when I cover the TreeView control for .NET.
In this section you will compare the performance of the VB 6.0 TreeView control and the .NET TreeView control.
Start a new VB or C# project. Mine is called "TreeTest." You will need to add a number of controls:
Add a TreeView control and call it Tree.
Add a Label whose text reads Max nodes to fill in at one time below the TreeView.
Add a TextBox called txtMax. Its text should read 1000.
Add a Button called cmdFill. Its text should read Fill.
Add a Label called lblFill. Set its BorderStyle to FixedSingle.
Add a Button called cmdClear. Its text should read Clear.
Add a Label called lblClear. Set its BorderStyle to FixedSingle.
Figure 10-3 shows what the form should look like.
You need to double-click the buttons to get the delegates generated for you. The code is exceedingly simple and follows the code for the VB 6.0 tester. The only thing you need to enter is the code for the button click delegates.
C#
private void cmdFill_Click(object sender, System.EventArgs e) { DateTime tmr; TimeSpan ts; int NumNodes = int.Parse(txtMax.Text); lblFill.Text = ""; lblClear.Text = ""; Application.DoEvents(); this.Cursor = Cursors.WaitCursor; tmr = DateTime.Now; for(int k=0; k< NumNodes; k++) Tree.Nodes.Add("Node " + k.ToString()); ts = DateTime.Now - tmr; lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + NumNodes.ToString() + " Nodes "; this.Cursor = Cursors.Arrow; } private void cmdClear_Click(object sender, System.EventArgs e) { DateTime tmr = DateTime.Now; TimeSpan ts; string NodeCount = Tree.Nodes.Count.ToString(); this.Cursor = Cursors.WaitCursor; Tree.Nodes.Clear(); this.Cursor = Cursors.Arrow; ts = DateTime.Now - tmr; lblClear.Text = ts.TotalSeconds.ToString() + " seconds to clear " + NodeCount + " Nodes "; }
VB
Private Sub cmdFill_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdFill.Click Dim tmr As DateTime = DateTime.Now Dim ts As TimeSpan Dim NumNodes As Int32 = Int32.Parse(txtMax.Text) Dim k As Int32 lblFill.Text = "" lblClear.Text = "" Application.DoEvents() Me.Cursor = Cursors.WaitCursor tmr = DateTime.Now ' Tree.BeginUpdate() For k = 0 To NumNodes Tree.Nodes.Add("Node " + k.ToString()) Next ' Tree.EndUpdate() ts = DateTime.Now.Subtract(tmr) lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + _ NumNodes.ToString() + " Nodes " Me.Cursor = Cursors.Arrow End Sub Private Sub cmdClear_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdClear.Click Dim tmr As DateTime = DateTime.Now Dim ts As TimeSpan Dim NodeCount As String = Tree.Nodes.Count.ToString() Me.Cursor = Cursors.WaitCursor ' Tree.BeginUpdate() Tree.Nodes.Clear() ' Tree.EndUpdate() Me.Cursor = Cursors.Arrow ts = DateTime.Now.Subtract(tmr) lblClear.Text = ts.TotalSeconds.ToString() + " seconds to clear " + _ NodeCount + " Nodes " End Sub
Notice that I change the cursor to an hourglass before I do any work. You should do this to indicate that your program has not locked up. Don't forget to change it back.
Run the program and click the Fill and Clear buttons. Figure 10-4 shows the results from my laptop.
The .NET version is significantly better than the VB 6.0 version in clearing nodes. In fact, the .NET control clears nodes some 99% faster than the VB 6.0 control.
I hope you noticed that filling the .NET control took more than twice as long as filling the VB 6.0 control. Now does this extrapolate linearly if you add, say, 10,000 nodes? Run your program and add an extra 0 to the text box. Click the Fill button. Figure 10-5 shows what happened on my machine.
You can see that it didn't take 4 seconds to add ten times the number of nodes— it took 18 seconds! Are your customers going to wait around for that? I think not.
I hesitate to click the Clear button, but what the hey. Figure 10-6 shows the results of this action.
Again, this task is not a linear extrapolation.
Remember that I said I was able to speed up the VB 6.0 TreeView control by using some API commands to freeze the window? Well, it just so happens that the .NET TreeView control has these commands built in as properties that greatly increase the speed of this control. These commands are the BeginUpdate and EndUpdate properties. Here is the code with the new properties entered.
C#
private void cmdFill_Click(object sender, System.EventArgs e) { DateTime tmr; TimeSpan ts; int NumNodes = int.Parse(txtMax.Text); lblFill.Text = ""; lblClear.Text = ""; Application.DoEvents(); this.Cursor = Cursors.WaitCursor; tmr = DateTime.Now; Tree.BeginUpdate(); for(int k=0; k< NumNodes; k++) Tree.Nodes.Add("Node " + k.ToString()); Tree.EndUpdate(); ts = DateTime.Now - tmr; lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + NumNodes.ToString() + " Nodes "; this.Cursor = Cursors.Arrow; } private void cmdClear_Click(object sender, System.EventArgs e) { DateTime tmr = DateTime.Now; TimeSpan ts; string NodeCount = Tree.Nodes.Count.ToString(); this.Cursor = Cursors.WaitCursor; Tree.BeginUpdate(); Tree.Nodes.Clear(); Tree.EndUpdate(); this.Cursor = Cursors.Arrow; ts = DateTime.Now - tmr; lblClear.Text = ts.TotalSeconds.ToString() + " seconds to clear " + NodeCount + " Nodes "; }
VB
Private Sub cmdFill_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdFill.Click Dim tmr As DateTime = DateTime.Now Dim ts As TimeSpan Dim NumNodes As Int32 = Int32.Parse(txtMax.Text) Dim k As Int32 lblFill.Text = "" lblClear.Text = "" Application.DoEvents() Me.Cursor = Cursors.WaitCursor tmr = DateTime.Now Tree.BeginUpdate() For k = 0 To NumNodes Tree.Nodes.Add("Node " + k.ToString()) Next Tree.EndUpdate() ts = DateTime.Now.Subtract(tmr) lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + _ NumNodes.ToString() + " Nodes " Me.Cursor = Cursors.Arrow End Sub Private Sub cmdClear_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdClear.Click Dim tmr As DateTime = DateTime.Now Dim ts As TimeSpan Dim NodeCount As String = Tree.Nodes.Count.ToString() Me.Cursor = Cursors.WaitCursor Tree.BeginUpdate() Tree.Nodes.Clear() Tree.EndUpdate() Me.Cursor = Cursors.Arrow ts = DateTime.Now.Subtract(tmr) lblClear.Text = ts.TotalSeconds.ToString() + " seconds to clear " + _ NodeCount + " Nodes " End Sub
Once you have entered these four lines of code, try running the program at 10,000 nodes again. Table 10-1 shows the results of my adding and clearing root nodes in the TreeView control after adding this code.
Number of Nodes |
Fill Time (Seconds) |
Clear Time (Seconds) |
---|---|---|
1,000 |
.08 |
.07 |
10,000 |
6.39 |
5.86 |
20,000 |
31.89 |
30.13 |
30,000 |
73.38 |
70.04 |
40,000 |
129.53 |
125.52 |
50,000 |
201.18 |
196.03 |
I was going to do 100,000 nodes, but I do not have the time. This book needs to get out.
Unfortunately, there is not much you can do to physically increase the speed of adding nodes to a tree. There is, however, one thing you can do to allow the tree to clear in an instant.
I have been stressing all along that I am adding root nodes to this tree. What would happen if you have a single root node that acts as a header for all the pretend users you are adding to this tree? Let's see.
You will comment out the code loop that generates the root nodes for the tree. You will then add some code to make a single root node and add the 10,000 or so nodes as child nodes to this one root node. Here is the code. The changed code is in bold.
C#
private void cmdFill_Click(object sender, System.EventArgs e) { DateTime tmr; TimeSpan ts; int NumNodes = int.Parse(txtMax.Text); lblFill.Text = ""; lblClear.Text = ""; Application.DoEvents(); this.Cursor = Cursors.WaitCursor; tmr = DateTime.Now; Tree.BeginUpdate(); //Add only root nodes // for(int k=0; k< NumNodes; k++) // Tree.Nodes.Add("Node " + k.ToString()); //Add a single root node and many child nodes. TreeNode HeaderNode = Tree.Nodes.Add("User Header Node"); for(int k=0; k< NumNodes; k++) HeaderNode.Nodes.Add("Node " + k.ToString()); HeaderNode.Expand(); Tree.EndUpdate(); ts = DateTime.Now - tmr; lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + NumNodes.ToString() + " Nodes "; this.Cursor = Cursors.Arrow; }
VB
Private Sub cmdFill_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdFill.Click Dim tmr As DateTime = DateTime.Now Dim ts As TimeSpan Dim NumNodes As Int32 = Int32.Parse(txtMax.Text) Dim k As Int32 lblFill.Text = "" lblClear.Text = "" Application.DoEvents() Me.Cursor = Cursors.WaitCursor tmr = DateTime.Now Tree.BeginUpdate() 'Add only root nodes 'For k = 0 To NumNodes ' Tree.Nodes.Add("Node " + k.ToString()) 'Next Dim HeaderNode As TreeNode = Tree.Nodes.Add("User Header Node") For k = 0 To NumNodes HeaderNode.Nodes.Add("Node " + k.ToString()) Next HeaderNode.Expand() Tree.EndUpdate() ts = DateTime.Now.Subtract(tmr) lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + _ NumNodes.ToString() + " Nodes " Me.Cursor = Cursors.Arrow End Sub
This is a simple change that has major ramifications for the speed of your program. Compile and run the code now. Figure 10-7 shows what happens when I add and clear 25,000 nodes in my TreeView.
Your eyes do not deceive you. This small code change cleared 25,000 nodes in the tree in under a second. Now we are getting somewhere.
The 30 seconds to fill 25,000 records, though, is really distracting and makes the TreeView control unusable for major databases.
Because I can't really increase the fill speed of the TreeView, how about fooling the user into thinking that the tree is already filled? If reality doesn't work, smoke and mirrors just might.
The first thing you can do is show the user that something is actually going on. Take the 10,000-node example. It takes almost 6 seconds to fill the TreeView with nodes. During this time you show an hourglass cursor. This is a start, but you can do more.
If the user is able to see some of the data, he or she could start viewing this data while the rest of the screen is filling up. You did this at the start of this example, before you added the BeginUpdate and EndUpdate commands. Each node was shown as it was created. The problem with this is that it greatly slowed down the system as the tree had to repaint while each node was being added.
The VisibleCount property in the TreeView control allows you to tell how many nodes it takes to fill the visible portion of the tree control. How would you use this property to your advantage?
I have made one last change to this example's tree fill code. The new code is in bold.
C#
private void cmdFill_Click(object sender, System.EventArgs e) { DateTime tmr; TimeSpan ts; int NumNodes = int.Parse(txtMax.Text); lblFill.Text = ""; lblClear.Text = ""; Application.DoEvents(); this.Cursor = Cursors.WaitCursor; tmr = DateTime.Now; // Tree.BeginUpdate(); //-------------------------------------------------------------------- //Add only root nodes // for(int k=0; k< NumNodes; k++) // Tree.Nodes.Add("Node " + k.ToString()); //-------------------------------------------------------------------- //--------------------------------------------------------------------- //Add a single root node and many child nodes. // TreeNode HeaderNode = Tree.Nodes.Add("User Header Node"); // for(int k=0; k< NumNodes; k++) // HeaderNode.Nodes.Add("Node " + k.ToString()); // HeaderNode.Expand(); //--------------------------------------------------------------------- //Add nodes and show them before shutting down the tree pane update bool AllowUpdate = true; TreeNode HeaderNode = Tree.Nodes.Add("User Header Node"); for(int k=0; k< NumNodes; k++) { if(AllowUpdate && HeaderNode.Nodes.Count > Tree.VisibleCount) { HeaderNode.Expand(); Application.DoEvents(); Tree.BeginUpdate(); AllowUpdate = false; } HeaderNode.Nodes.Add("Node " + k.ToString()); } Tree.EndUpdate(); ts = DateTime.Now - tmr; lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + NumNodes.ToString() + " Nodes "; this.Cursor = Cursors.Arrow; }
VB
Private Sub cmdFill_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdFill.Click Dim tmr As DateTime = DateTime.Now Dim ts As TimeSpan Dim NumNodes As Int32 = Int32.Parse(txtMax.Text) Dim k As Int32 lblFill.Text = "" lblClear.Text = "" Application.DoEvents() Me.Cursor = Cursors.WaitCursor tmr = DateTime.Now ' Tree.BeginUpdate() '-------------------------------------------------------------------- 'Add only root nodes 'For k = 0 To NumNodes ' Tree.Nodes.Add("Node " + k.ToString()) 'Next 'For k = 0 To NumNodes ' Tree.Nodes.Add("Node " + k.ToString()) 'Next '-------------------------------------------------------------------- 'Add a single root node and many child nodes 'Dim HeaderNode As TreeNode = Tree.Nodes.Add("User Header Node") 'For k = 0 To NumNodes ' HeaderNode.Nodes.Add("Node " + k.ToString()) 'Next 'HeaderNode.Expand() '-------------------------------------------------------------------- 'Add nodes and show them before shutting down the tree pane update Dim AllowUpdate As Boolean = True Dim HeaderNode As TreeNode = Tree.Nodes.Add("User Header Node") For k = 0 To NumNodes If AllowUpdate AndAlso HeaderNode.Nodes.Count > Tree.VisibleCount Then HeaderNode.Expand() Application.DoEvents() Tree.BeginUpdate() AllowUpdate = False End If HeaderNode.Nodes.Add("Node " + k.ToString()) Next Tree.EndUpdate() ts = DateTime.Now.Subtract(tmr) lblFill.Text = ts.TotalSeconds.ToString() + " seconds to add " + _ NumNodes.ToString() + " Nodes " Me.Cursor = Cursors.Arrow End Sub
Previous code has been commented out and new code has been added. What I am doing here is allowing the tree to refresh itself for every node added until I see that the visible portion of the tree has all the nodes it can display. At this point, I shut down the tree repainting and add nodes as before.
Compile and run this program now. Figure 10-8 shows my results.
When you run this example, you will see the nodes appear immediately, and the total fill time does not suffer. After all, you are showing the fill for only about 15 or so nodes.
Although you have not decreased the time to fill the tree, you have decreased the time the user waits before seeing something. Believe me, this makes a huge difference to the apparent speed of the program.
So far I have talked about a tree in terms of root nodes only. I showed you the problems with having thousands of root nodes added that were somewhat amelio-rated by using a master root node and many subnodes. This is a trick to get around a deficiency in the TreeView control that comes with .NET. In reality we are still talking about many single root nodes.
This is not real life, however. Real programs use many root nodes and many subnodes within subnodes. Data can be a complicated thing when you are trying to display interrelated information.
Here is a contrived but common scenario. A superstore has an inventory database that contains every item in the store. This store sells clothing and toys. This is what your data could look like:
A Toys Header Node Electronic Toys Many Brands Video Games Many Brands Battery-Powered Toys Many Brands Plush Toys Many Brands Action Figures Many Brands Board Games Many Brands Models Many Brands A Clothing Header Node Footwear Many Brands Jackets Many Brands Tops Many Brands Pants Many Brands Underwear Many Brands Gloves/Hats Many Brands Sweaters Many Brands
Each of these first-level subnodes could have hundreds of thousands of subnodes. It would not be unusual to end up with many thousands of total nodes in your tree.
When you have a situation like this, you need to sit and think a bit before you go about writing code to fill your tree. No matter what you do, there may be an unacceptable delay in getting and displaying the information. There is one approach, though, that can greatly reduce the time need to display information in a TreeView control: Don't get or show the information until needed.
If someone goes into your inventory program and wants to know only about board games, there is no need to waste time getting information about all the other inventory items. You can let the user know these items exist and then get the other items only when the user calls for them.
The trick to making the user think there is data when there is none yet to be shown is to use virtual nodes. This is not something included with the TreeView; you need to program it yourself.
This next example shows the store inventory problem. It has a TreeView that includes over 25,000 nodes. I will show you the "fill it all at once" method and the "smoke and mirrors" method of presenting the data.
Start a new C# or VB Windows program. Mine is called "QuickTreeFill." Next, add these items:
Add a TreeView control called Tree.
Add a Button called cmdFill. Its text should read Fill Normal.
Add a Button called cmdFillFast. Its text should read Fill Fast.
Add a Button called cmdClear. Its text should read Clear.
Figure 10-9 shows what the form looks like.
Before you add any code to the form, you will need to add a new class called "Inventory." This class holds some classes and structures that represent the data you will be working with. Listings 10-2a and 10-2b show the complete code for this class.
using System; using System.Collections; namespace QuickTreeFill_c { /// <summary> /// Summary description for Inventory. /// </summary> /// public struct Brand { public Brand(string name) { BrandName = name; } public string BrandName; } public class Toys { public Items BatteryPowered = new Items(1000, "BatteryPowered"); public Items Electronic = new Items(500, "Electronic"); public Items BoardGames = new Items(1000, "BoardGames"); public Items Video = new Items(2000, "Video"); public Items Models = new Items(1000, "Models"); public Items Plush = new Items(3000, "Plush"); public Items ActionFigures = new Items(250, "ActionFigures"); public struct Items { public ArrayList Brands; public Items(int amount, string kind) { Brands = new ArrayList(); for(int k=0; k<amount; k++) Brands.Add(new Brand(kind + " Brand " + k.ToString())); } } } public class Clothing { public Items Footwear = new Items(500, "Footwear"); public Items Jackets = new Items(600, "Jackets"); public Items Tops = new Items(4800, "Tops"); public Items Pants = new Items(1000, "Pants"); public Items Underwear = new Items(100, "Underwear"); public Items GlovesHats = new Items(5000, "GlovesHats"); public Items Sweaters = new Items(2000, "Sweaters"); public struct Items { public ArrayList Brands; public Items(int amount, string kind) { Brands = new ArrayList(); for(int k=0; k<amount; k++) Brands.Add(new Brand(kind + " Brand " + k.ToString())); } } } }
Option Strict On Imports System.Collections Public Structure Brand Public Sub New(ByVal name As String) BrandName = name End Sub Public BrandName As String End Structure Public Class Toys Public BatteryPowered As Items = New Items(1000, "BatteryPowered") Public Electronic As Items = New Items(500, "Electronic") Public BoardGames As Items = New Items(1000, "BoardGames") Public Video As Items = New Items(2000, "Video") Public Models As Items = New Items(1000, "Models") Public Plush As Items = New Items(3000, "Plush") Public ActionFigures As Items = New Items(250, "ActionFigures") Public Structure Items Public Brands As ArrayList Public Sub New(ByVal amount As Int32, ByVal kind As String) Brands = New ArrayList() Dim k As Int32 For k = 0 To amount Brands.Add(New Brand(kind + " Brand " + k.ToString())) Next End Sub End Structure End Class Public Class Clothing Public Footwear As Items = New Items(500, "Footwear") Public Jackets As Items = New Items(600, "Jackets") Public Tops As Items = New Items(4800, "Tops") Public Pants As Items = New Items(1000, "Pants") Public Underwear As Items = New Items(100, "Underwear") Public GlovesHats As Items = New Items(5000, "GlovesHats") Public Sweaters As Items = New Items(2000, "Sweaters") Public Structure Items Public Brands As ArrayList Public Sub New(ByVal amount As Int32, ByVal kind As String) Brands = New ArrayList() Dim k As Int32 For k = 0 To amount Brands.Add(New Brand(kind + " Brand " + k.ToString())) Next End Sub End Structure End Class
I use a class to hold an ArrayList containing thousands of items. Although this is not reality, it does serve the purpose.
Now it is time to add some code to the main form. Make sure that you have a reference to the System.Collections namespace at the top of your code. First off, you will need some class local variables.
C#
#region class local variables private Toys toys; private Clothing Clothes; private enum NodeLevel { AllToys, AllClothes, ToyBrand, ClothingBrand, BatteryToys, ElectronicToys, BoardGameToys, VideoToys, PlushToys, ModelToys, FigureToys, ClothingFootwear, ClothingTops, ClothingJackets, ClothingSweaters, ClothingPants, ClothingGloves } #endregion
VB
#Region "class local variables" Private toys As Toys Private Clothes As Clothing Private Enum NodeLevel AllToys AllClothes ToyBrand ClothingBrand BatteryToys ElectronicToys BoardGameToys VideoToys PlushToys ModelToys FigureToys ClothingFootwear ClothingTops ClothingJackets ClothingSweaters ClothingPants ClothingGloves End Enum #End Region
There is a property in the TreeNode class that lets you get the full path of where it is in the tree. I prefer not to use this, though. I want to know exactly what kind of node I am on at any moment. I use the NodeLevel enum for that. You will see just how I use it soon.
Next, you need to add some helper functions.
C#
#region Helper functions private void ClearTree(object sender, EventArgs e) { Tree.Nodes.Clear(); cmdFill.Enabled = true; cmdFillFast.Enabled = true; } private void UpdateTree(ArrayList Brands, TreeNode ClickedNode) { bool AllowUpdate = true; this.Cursor = Cursors.WaitCursor; foreach(Brand x in Brands) { ClickedNode.Nodes.Add(x.BrandName); if(AllowUpdate && ClickedNode.Nodes.Count > Tree.VisibleCount) { AllowUpdate = false; ExpandThisNode(ClickedNode); Tree.BeginUpdate(); } } Tree.EndUpdate(); this.Cursor = Cursors.Arrow; } private void ExpandThisNode(TreeNode node) { Tree.BeforeExpand -= new TreeViewCancelEventHandler(FillSubNodes); node.Expand(); Tree.BeforeExpand += new TreeViewCancelEventHandler(FillSubNodes); Application.DoEvents(); } #endregion
#Region "Helper functions" Private Sub ClearTree(ByVal sender As Object, ByVal e As EventArgs) Tree.Nodes.Clear() cmdFill.Enabled = True cmdFillFast.Enabled = True End Sub Private Sub UpdateTree(ByVal Brands As ArrayList, ByVal ClickedNode As TreeNode) Dim AllowUpdate As Boolean = True Me.Cursor = Cursors.WaitCursor Dim x As Brand For Each x In Brands ClickedNode.Nodes.Add(x.BrandName) If AllowUpdate AndAlso ClickedNode.Nodes.Count > Tree.VisibleCount Then AllowUpdate = False ExpandThisNode(ClickedNode) Tree.BeginUpdate() End If Next Tree.EndUpdate() Me.Cursor = Cursors.Arrow End Sub Private Sub ExpandThisNode(ByVal node As TreeNode) RemoveHandler Tree.BeforeExpand, New TreeViewCancelEventHandler(AddressOf FillSubNodes) node.Expand() AddHandler Tree.BeforeExpand, New TreeViewCancelEventHandler(AddressOf FillSubNodes) Application.DoEvents() End Sub #End Region
The ClearTree function is a delegate that handles the Clear button click event. The UpdateTree function adds subnodes to a node that is passed in by argument. You can see that I take each brand and add it to the ClickedNode. While I do this, I keep track of the number of nodes, and when I get past the VisibleCount, I turn off the TreeView repainting. This allows me to show something to the user while he or she waits for data to appear.
Note the ExpandThisNode routine. Why do I need this? I have a delegate assigned to the Expand event (you will see this code in a bit). When I call the node.Expand() function, it will fire this event and I will be back in my delegate. I end up in a circular loop. Before I call the node.Expand method, I need to turn off the Expand delegate and then turn it back on afterward.
The form contains two buttons. The first is the Clear button. You just coded the delegate for that button. The second is the Fill Normal button. Clicking this button gets all the data and fills in all the nodes. The code for these buttons follows.
C#
#region fill the tree slow private void FillWholeTree(object sender, EventArgs e) { Tree.BeforeExpand -= new TreeViewCancelEventHandler(FillSubNodes); cmdFill.Enabled = false; cmdFillFast.Enabled = false; this.Cursor = Cursors.WaitCursor; Tree.Nodes.Clear(); Tree.BeginUpdate(); //------ Do Toys ------- TreeNode ThisNode; TreeNode AllToys = Tree.Nodes.Add("All Toys"); AllToys.Tag = NodeLevel.AllToys; TreeNode node = AllToys.Nodes.Add("Action Figures"); node.Tag = NodeLevel.FigureToys; foreach(Brand x in toys.ActionFigures.Brands) ThisNode = node.Nodes.Add(x.BrandName); node = AllToys.Nodes.Add("Battery Powered Toys"); foreach(Brand x in toys.BatteryPowered.Brands) node.Nodes.Add(x.BrandName); node = AllToys.Nodes.Add("Board Games"); foreach(Brand x in toys.BoardGames.Brands) node.Nodes.Add(x.BrandName); node = AllToys.Nodes.Add("Electronic Games"); foreach(Brand x in toys.Electronic.Brands) node.Nodes.Add(x.BrandName); node = AllToys.Nodes.Add("Models"); foreach(Brand x in toys.Models.Brands) node.Nodes.Add(x.BrandName); node = AllToys.Nodes.Add("Plush Toys"); foreach(Brand x in toys.Plush.Brands) node.Nodes.Add(x.BrandName); node = AllToys.Nodes.Add("Video Games"); foreach(Brand x in toys.Video.Brands) node.Nodes.Add(x.BrandName); // --------- Do Clothing --------- TreeNode AllClothes = Tree.Nodes.Add("All Clothes"); node = AllClothes.Nodes.Add("Footwear"); foreach(Brand x in Clothes.Footwear.Brands) node.Nodes.Add(x.BrandName); node = AllClothes.Nodes.Add("Gloves and Hats"); foreach(Brand x in Clothes.GlovesHats.Brands) node.Nodes.Add(x.BrandName); node = AllClothes.Nodes.Add("Jackets"); foreach(Brand x in Clothes.Jackets.Brands) node.Nodes.Add(x.BrandName); node = AllClothes.Nodes.Add("Pants"); foreach(Brand x in Clothes.Pants.Brands) node.Nodes.Add(x.BrandName); node = AllClothes.Nodes.Add("Sweaters"); foreach(Brand x in Clothes.Sweaters.Brands) node.Nodes.Add(x.BrandName); node = AllClothes.Nodes.Add("Tops"); foreach(Brand x in Clothes.Tops.Brands) node.Nodes.Add(x.BrandName); Tree.EndUpdate(); this.Cursor = Cursors.Arrow; } #endregion
#Region "fill the tree slow" Private Sub FillWholeTree(ByVal sender As Object, ByVal e As EventArgs) Dim x As Brand RemoveHandler Tree.BeforeExpand, New TreeViewCancelEventHandler(AddressOf FillSubNodes) Me.Cursor = Cursors.WaitCursor Tree.Nodes.Clear() Tree.BeginUpdate() '------ Do Toys ------- Dim ThisNode As TreeNode Dim AllToys As TreeNode = Tree.Nodes.Add("All Toys") AllToys.Tag = NodeLevel.AllToys Dim node As TreeNode = AllToys.Nodes.Add("Action Figures") node.Tag = NodeLevel.FigureToys For Each x In toys.ActionFigures.Brands ThisNode = node.Nodes.Add(x.BrandName) Next node = AllToys.Nodes.Add("Battery Powered Toys") For Each x In toys.BatteryPowered.Brands node.Nodes.Add(x.BrandName) Next node = AllToys.Nodes.Add("Board Games") For Each x In toys.BoardGames.Brands node.Nodes.Add(x.BrandName) Next node = AllToys.Nodes.Add("Electronic Games") For Each x In toys.Electronic.Brands node.Nodes.Add(x.BrandName) Next node = AllToys.Nodes.Add("Models") For Each x In toys.Models.Brands node.Nodes.Add(x.BrandName) Next node = AllToys.Nodes.Add("Plush Toys") For Each x In toys.Plush.Brands node.Nodes.Add(x.BrandName) Next node = AllToys.Nodes.Add("Video Games") For Each x In toys.Video.Brands node.Nodes.Add(x.BrandName) Next ' --------- Do Clothing --------- Dim AllClothes As TreeNode = Tree.Nodes.Add("All Clothes") node = AllClothes.Nodes.Add("Footwear") For Each x In Clothes.Footwear.Brands node.Nodes.Add(x.BrandName) Next node = AllClothes.Nodes.Add("Gloves and Hats") For Each x In Clothes.GlovesHats.Brands node.Nodes.Add(x.BrandName) Next node = AllClothes.Nodes.Add("Jackets") For Each x In Clothes.Jackets.Brands node.Nodes.Add(x.BrandName) Next node = AllClothes.Nodes.Add("Pants") For Each x In Clothes.Pants.Brands node.Nodes.Add(x.BrandName) Next node = AllClothes.Nodes.Add("Sweaters") For Each x In Clothes.Sweaters.Brands node.Nodes.Add(x.BrandName) Next node = AllClothes.Nodes.Add("Tops") For Each x In Clothes.Tops.Brands node.Nodes.Add(x.BrandName) Next Tree.EndUpdate() Me.Cursor = Cursors.Arrow End Sub #End Region
This code should not be new to you. It simply runs through each collection of inventory items and adds nodes to the tree. I shut down the update of the tree at the start and give the user a wait cursor to look at while I am adding nodes. The next region of code is the interesting one.
Enter the following code, which handles the smoke and mirrors action to make the user believe the data is all there and ready to view.
C#
#region Smoke and Mirrors private void FillTreeFast(object sender, EventArgs e) { cmdFill.Enabled = false; cmdFillFast.Enabled = false; Tree.BeforeExpand += new TreeViewCancelEventHandler(FillSubNodes); Tree.Nodes.Clear(); Tree.BeginUpdate(); TreeNode node = Tree.Nodes.Add("All Toys"); node.Tag = NodeLevel.AllToys; node.Nodes.Add("VirtualNode"); node = Tree.Nodes.Add("All Clothes"); node.Tag = NodeLevel.AllClothes; node.Nodes.Add("VirtualNode"); Tree.EndUpdate(); } private void FillSubNodes(object sender, TreeViewCancelEventArgs e) { TreeNode ClickedNode = e.Node; TreeNode node; NodeLevel l = (NodeLevel)ClickedNode.Tag; ClickedNode.Nodes.Clear(); switch(l) { case NodeLevel.AllToys: node = ClickedNode.Nodes.Add("Battery Powered Toys"); node.Tag = NodeLevel.BatteryToys; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Board Games"); node.Tag = NodeLevel.BoardGameToys; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Electronic Games"); node.Tag = NodeLevel.ElectronicToys; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Models"); node.Tag = NodeLevel.ModelToys; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Plush Toys"); node.Tag = NodeLevel.PlushToys; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Video Games"); node.Tag = NodeLevel.VideoToys; node.Nodes.Add("VirtualNode"); break; case NodeLevel.AllClothes: node = ClickedNode.Nodes.Add("Gloves and Hats"); node.Tag = NodeLevel.ClothingGloves; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Jackets"); node.Tag = NodeLevel.ClothingJackets; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Pants"); node.Tag = NodeLevel.ClothingPants; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Sweaters"); node.Tag = NodeLevel.ClothingSweaters; node.Nodes.Add("VirtualNode"); node = ClickedNode.Nodes.Add("Tops"); node.Tag = NodeLevel.ClothingTops; node.Nodes.Add("VirtualNode"); break; case NodeLevel.ModelToys: UpdateTree(toys.Models.Brands, ClickedNode); break; case NodeLevel.BatteryToys: UpdateTree(toys.BatteryPowered.Brands, ClickedNode); break; case NodeLevel.BoardGameToys: UpdateTree(toys.BoardGames.Brands, ClickedNode); break; case NodeLevel.ElectronicToys: UpdateTree(toys.Electronic.Brands, ClickedNode); break; case NodeLevel.FigureToys: UpdateTree(toys.ActionFigures.Brands, ClickedNode); break; case NodeLevel.PlushToys: UpdateTree(toys.Plush.Brands, ClickedNode); break; case NodeLevel.VideoToys: UpdateTree(toys.Video.Brands, ClickedNode); break; case NodeLevel.ClothingFootwear: UpdateTree(Clothes.Footwear.Brands, ClickedNode); break; case NodeLevel.ClothingGloves: UpdateTree(Clothes.GlovesHats.Brands, ClickedNode); break; case NodeLevel.ClothingJackets: UpdateTree(Clothes.Jackets.Brands, ClickedNode); break; case NodeLevel.ClothingPants: UpdateTree(Clothes.Pants.Brands, ClickedNode); break; case NodeLevel.ClothingSweaters: UpdateTree(Clothes.Sweaters.Brands, ClickedNode); break; case NodeLevel.ClothingTops: UpdateTree(Clothes.Tops.Brands, ClickedNode); break; } } #endregion
#Region "Smoke and Mirrors" Private Sub FillTreeFast(ByVal sender As Object, ByVal e As EventArgs) cmdFill.Enabled = False cmdFillFast.Enabled = False AddHandler Tree.BeforeExpand, _ New TreeViewCancelEventHandler(AddressOf FillSubNodes) Tree.Nodes.Clear() Tree.BeginUpdate() Dim node As TreeNode = Tree.Nodes.Add("All Toys") node.Tag = NodeLevel.AllToys node.Nodes.Add("VirtualNode") node = Tree.Nodes.Add("All Clothes") node.Tag = NodeLevel.AllClothes node.Nodes.Add("VirtualNode") Tree.EndUpdate() End Sub Private Sub FillSubNodes(ByVal sender As Object, _ ByVal e As TreeViewCancelEventArgs) Dim ClickedNode As TreeNode = e.Node Dim node As TreeNode Dim l As NodeLevel = CType(ClickedNode.Tag, NodeLevel) ClickedNode.Nodes.Clear() Select Case l Case NodeLevel.AllToys node = ClickedNode.Nodes.Add("Battery Powered Toys") node.Tag = NodeLevel.BatteryToys node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Board Games") node.Tag = NodeLevel.BoardGameToys node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Electronic Games") node.Tag = NodeLevel.ElectronicToys node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Models") node.Tag = NodeLevel.ModelToys node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Plush Toys") node.Tag = NodeLevel.PlushToys node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Video Games") node.Tag = NodeLevel.VideoToys node.Nodes.Add("VirtualNode") Case NodeLevel.AllClothes node = ClickedNode.Nodes.Add("Gloves and Hats") node.Tag = NodeLevel.ClothingGloves node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Jackets") node.Tag = NodeLevel.ClothingJackets node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Pants") node.Tag = NodeLevel.ClothingPants node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Sweaters") node.Tag = NodeLevel.ClothingSweaters node.Nodes.Add("VirtualNode") node = ClickedNode.Nodes.Add("Tops") node.Tag = NodeLevel.ClothingTops node.Nodes.Add("VirtualNode") Case NodeLevel.ModelToys UpdateTree(toys.Models.Brands, ClickedNode) Case NodeLevel.BatteryToys UpdateTree(toys.BatteryPowered.Brands, ClickedNode) Case NodeLevel.BoardGameToys UpdateTree(toys.BoardGames.Brands, ClickedNode) Case NodeLevel.ElectronicToys UpdateTree(toys.Electronic.Brands, ClickedNode) Case NodeLevel.FigureToys UpdateTree(toys.ActionFigures.Brands, ClickedNode) Case NodeLevel.PlushToys UpdateTree(toys.Plush.Brands, ClickedNode) Case NodeLevel.VideoToys UpdateTree(toys.Video.Brands, ClickedNode) Case NodeLevel.ClothingFootwear UpdateTree(Clothes.Footwear.Brands, ClickedNode) Case NodeLevel.ClothingGloves UpdateTree(Clothes.GlovesHats.Brands, ClickedNode) Case NodeLevel.ClothingJackets UpdateTree(Clothes.Jackets.Brands, ClickedNode) Case NodeLevel.ClothingPants UpdateTree(Clothes.Pants.Brands, ClickedNode) Case NodeLevel.ClothingSweaters UpdateTree(Clothes.Sweaters.Brands, ClickedNode) Case NodeLevel.ClothingTops UpdateTree(Clothes.Tops.Brands, ClickedNode) End Select End Sub #End Region
Here is an explanation of what happens when the user clicks the Fill Fast button. First, I fill only the top two nodes of the tree, the Toys and Clothing header nodes. While I do this, I add a virtual node under each of these two header nodes. This forces the TreeView control to add a plus sign (+) next to the two header nodes. As all computer users from Windows 95 on know, clicking the plus sign shows more data below the node. I have effectively given the user the impression of speed (showing only two nodes is fast) and access to further data.
The next method, FillSubNodes, is where all the real action happens. This is the delegate for the TreeView control's BeforeExpand event (you will wire this up shortly). The first thing I do here is prevent updating of the tree. I then clear out all the subnodes from this node. This does two things. On start-up, it gets rid of the virtual node. If this was expanded and collapsed previously, it gets rid of all the subnodes under this node. I then get the data again from the database (my collections in this case). The data is always "live."
Depending on which node was clicked, I add a single subnode with a virtual node or I add all the nodes contained in the appropriate collection. Notice that I use the NodeLevel enumeration to determine which node was clicked. Whenever I create a node, I save the node's type in the node's Tag property.
Now it is time to wire up the delegates in the form's constructor.
C#
public Form1() { InitializeComponent(); toys = new Toys(); Clothes = new Clothing(); cmdClear.Click += new EventHandler(ClearTree); cmdFill.Click += new EventHandler(FillWholeTree); cmdFillFast.Click += new EventHandler(FillTreeFast); }
Public Sub New() MyBase.New() InitializeComponent() toys = New Toys() Clothes = New Clothing() AddHandler cmdClear.Click, New EventHandler(AddressOf ClearTree) AddHandler cmdFill.Click, New EventHandler(AddressOf FillWholeTree) AddHandler cmdFillFast.Click, New EventHandler(AddressOf FillTreeFast) End Sub
I instantiate the data classes first. This takes almost no time at all. I then wire up the delegates for the button click events.
Compile and run the program. Click the Fill Normal button and note the wait necessary for the tree to fill.
Now click the Clear button and then the Fill Fast button. You will see the top two nodes appear instantly. Each time you go deeper into the tree by expanding nodes, you are getting data as you need it. The data is obtained and put in the tree very fast. Any slowdown is negligible because you are getting at most 5,000 nodes instead of 25,000 nodes, as is the case with the fill slow method.
Using virtual nodes is a good way to break up the data presentation task into small chunks. Chances are that a user will want to see only a small subset of data anyway, and you may never need to display most of the data you have. The point is, the user does not know this and is generally happy with the speed.
Tip |
I have used this technique many times in my code. I recently did a project, however, that had the potential of over 100,000 nodes, and this method started to get slow. I bought a third-party TreeView control that is the best thing since sliced bread. It is very fast in adding nodes and instantaneous in deleting them. |
So, is this it for speeding up the interface? No. I used a TreeView control as an example. You can use some of the same techniques with other data presentation controls. The most powerful tool you can use to speed up the GUI is threading.
[1]Think large colleges.