Team LiB
Previous Section Next Section

Speeding Up the Controls

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.

The VB 6.0 TreeView

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.

Listing 10-1: VB 6.0 Code for the Tree Tester Project
Start example

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
End example

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.

Click To expand
Figure 10-1: Adding 1,000 nodes to a VB 6.0 tree

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.

Click To expand
Figure 10-2: Clearing 1,000 nodes in VB 6.0 tree

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.

The .NET TreeView Tester

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:

  1. Add a TreeView control and call it Tree.

  2. Add a Label whose text reads Max nodes to fill in at one time below the TreeView.

  3. Add a TextBox called txtMax. Its text should read 1000.

  4. Add a Button called cmdFill. Its text should read Fill.

  5. Add a Label called lblFill. Set its BorderStyle to FixedSingle.

  6. Add a Button called cmdClear. Its text should read Clear.

  7. Add a Label called lblClear. Set its BorderStyle to FixedSingle.

Figure 10-3 shows what the form should look like.

Click To expand
Figure 10-3: Tree tester form

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.

Click To expand
Figure 10-4: Filling and clearing 1,000 nodes

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.

Click To expand
Figure 10-5: Adding 10,000 nodes

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.

Click To expand
Figure 10-6: Clearing 10,000 nodes

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.

Table 10-1: Adding and Clearing Nodes in a TreeView Control

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.

Clearing the TreeView in a Flash

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.

Click To expand
Figure 10-7: Clearing 25,000 nodes with one root node

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.

Increasing Apparent TreeView Fill Speed

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.

Click To expand
Figure 10-8: Showing some nodes

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.

Filling a True Tree Hierarchy

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.

Making a Virtual Tree

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:

  1. Add a TreeView control called Tree.

  2. Add a Button called cmdFill. Its text should read Fill Normal.

  3. Add a Button called cmdFillFast. Its text should read Fill Fast.

  4. Add a Button called cmdClear. Its text should read Clear.

Figure 10-9 shows what the form looks like.

Click To expand
Figure 10-9: The new tree fill form

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.

Listing 10-2a: C# Code for the Inventory Class
Start example
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()));
      }
    }
  }
}
End example
Listing 10-2b: VB Code for the Inventory Class
Start example
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
End example

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

VB

#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

VB

#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

VB

#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);
    }

VB

  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.

Running the Fast TreeView

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.


Team LiB
Previous Section Next Section