SAX for Pascal Home   SAX for Pascal Documentation

Filling a TreeView

This demo is designed to show you how to fill a TreeView that shows the document structure. Primarily you should pay attention to how the items are added. Throughout the parse, the CurrNode is updated. This allows the program to know where it should add each item in the tree. In general, SAX does not maintain state information (where it is in the tree with regard to parent/child context relationships). This means that you have to keep track of that information if it is important to you.

In this demo, we will skip all of the basic project setup information. If you need to know more about the project setup, please see the "Choosing a Parser Demo" or the "Simple Parsing Demo".

Setting up the form

For this demo we will need to add a few components to the form. First we will need a ComboBox to choose our parser. Second we'll add a TreeView for our output. In our demo we have also added an ImageList so that our TreeView nodes will have images for attributes and elements. Next we will need a listbox to show our error messages. Next we will need a button to start our parsing. We also want to add an OpenDialog so that we can search for the file. Finally we will need to add a TSAXContentHandler and a TSAXErrorHandler. The SAX components are installed to the SAX tab in the Component Palette by default.

Events

Now that you have added all of the components let's set up some events. The event handlers for the TSAXErrorHandler will work exactly as they did in our "Simple Parsing Demo", so we won't explain those here. Our FormCreate event will work exactly as it did in the "Choosing a Parser Demo" as well. The important events for this demo are the OnClick event for Button1 and the OnStartElement and OnEndElement events for TSAXContentHandler1. Let's begin with the OnClick event handler:

        procedure TForm1.Button1Click(Sender: TObject);
        var
          XMLReader : IXMLReader;
          XMLBufReader: IBufferedXMLReader;
          Vendor: TSAXVendor;
          ContentHandler: IContentHandler;
        begin
          TreeView1.Items.Clear;
          Listbox1.Items.Clear;
          CurrNode:= nil;
          ContentHandler:= SAXContentHandler1;
          if (OpenDialog1.Execute) then
          begin
            Vendor:= GetSAXVendor(ComboBox1.Items[ComboBox1.ItemIndex]);
            if Vendor is TBufferedSAXVendor then
            begin
              XMLBufReader:= TBufferedSAXVendor(Vendor).BufferedXMLReader;
              XMLBufReader.setContentHandler(Adapt(ContentHandler, XMLBufReader));
              XMLBufReader.setErrorHandler(SAXErrorHandler1);
              XMLBufReader.parse(OpenDialog1.FileName);
              XMLBufReader:= nil;
            end else
            begin
              XMLReader:= Vendor.XMLReader;
              XMLReader.setContentHandler(ContentHandler);
              XMLReader.setErrorHandler(SAXErrorHandler1);
              XMLReader.parse(OpenDialog1.FileName);
              XMLReader:= nil;
            end;
          end;
        end;
      

We begin by clearing the TreeView and ListView so that our output is fresh. Next we set CurrNode to nil. Wait a second, what is CurrNode? CurrNode is our temporary TTreeNode that we will use while we are building the tree. We will add this variable declaration to our Form's private section:

        private
          CurrNode : TTreeNode;
        end;
      

Finally we get a reference to our ContentHandler as an interface so that we can use it later in the function. After we have initialized all of our variables, we are ready to begin parsing. The rest of the code works exactly as it did in the "Simple Parsing Demo." Remember, in order to use Buffered Vendors with our normal ContentHandler, we need to use an adapter from the SAXAdapters unit.

Now that we have created the parsing code we need to add the OnStartElement and OnEndElement event handlers. Let's look at the OnStartElement handler first:

        procedure TForm1.SAXContentHandler1StartElement(Sender: TObject;
          const NamespaceURI, LocalName, QName: WideString;
          const Atts: IAttributes);
        var I : Integer;
            ANode : TTreeNode;
        begin
          // Set the node
          if (CurrNode = nil) then
            CurrNode:= TreeView1.Items.Add(nil, qName)
          else
            CurrNode:= TreeView1.Items.AddChild(CurrNode, qName);
          // Set the image
          CurrNode.ImageIndex:= 0;
          // Add the attributes
          if (Atts <> nil) then
          begin
            for I:= 0 to Atts.getLength - 1 do
            begin
              ANode:= TreeView1.Items.AddChild(CurrNode, Atts.getQName(I));
              ANode.ImageIndex:= 1;
              ANode.SelectedIndex:= 1;
            end;
          end;
        end;
      

The first thing we need to do is to check if CurrNode is nil. Remember we set CurrNode to nil before we began our parse. If CurrNode is nil then this is the first time that the OnStartElement callback has ocurred. This indicates that we need to add the root node to our TreeView. Otherwise we need to add a new node as a child of CurrNode. As we add the item, we also assign CurrNode so that we remember where we are at in the TreeView. Finally we set the ImageIndex so that we can tell that it is an element in the TreeView.

After we have created the basic TTreeNode for the element, we can loop through the attributes and add each of those. Again, we will add the items as children of CurrNode. Now let's look at the OnEndElement handler:

        procedure TForm1.SAXContentHandler1EndElement(Sender: TObject;
          const NamespaceURI, LocalName, QName: WideString);
        begin
          // Expand the item
          if (CurrNode <> nil) then
            CurrNode.Expand(false);
          // Set the node
          CurrNode:= CurrNode.Parent;
        end;
      

Again, our primary interest is tracking the CurrNode. All we need to do is set the CurrNode to it's Parent Node. Before we have done this though we expand that section of the TreeView.

How does it work?

Just in case you don't have a complete understanding of how this works, we will break it down into each step. Remember, as the SAX parser is parsing the document it will fire an OnStartElement event at the start of every element it encounters in the document. It will fire and OnEndElement at the end of each element it encounters. For now, we will not go in to detail about other events that are not being handled in this demo. The following is a summary of what happens when parsing Document1.xml included with this demo.

  1. The parser first encounters the root element Document in the XML document. The parser will immediately fire an OnStartElement event. Within the OnStartElement handler, CurrNode is initially nil so we add the root item to the TreeView. We add the QName (Qualified Name) which is a combination of the element name and namespace prefix. We also set CurrNode equal to the new TTreeNode. So at this point CurrNode points to the first and only node in the TreeView, "Document".
  2. Before we receive the OnEndElement event, the parser encounters another element in the XML document. This new element, "Element1", is a child element of "Document". Again we receive on OnStartElement event. Remember CurrNode points to the first TTreeNode in our TreeView, so instead of adding a root node, we will create a new item that is a child of our CurrNode. Again we assign CurrNode to our new item in the TreeView. This time, however, we must also add nodes for the attributes. Notice, that we do not change the value of CurrNode when adding the attribute items. So, now we have received two OnStartElement events but no OnEndElement events. Also we have five nodes in our TreeView: "Document", its child "Element1", and the three attribute nodes "attribute1", "attribute2" and "attribute3". CurrNode still points to "Element1".
  3. Again, before we receive any OnEndElement events, we receive another OnStartElement event for the element "Element2". Remember CurrNode points the "Element1" TTreeNode in our TreeView, so instead of adding a root node, we will create a new item that is a child of our CurrNode. Again we assign CurrNode to our new item in the TreeView, so CurrNode points to "Element2".
  4. At last we receive an OnEndElement event as the parser reaches the closing tag in the XML document. CurrNode is pointing to our "Element2" node in the TreeView, so we expand the item (here this does nothing as the "Element2" node has no children) and point CurrNode to its parent. Just to be clear: within the XML document the "Element2" element is complete because the parse has reached the closing tag. Because it is complete, it will have no other children elements-- any new elements will be children of "Element1". Therefore, in order to make the TreeView match we had to change the CurrNode to point to the "Element1" node within the TreeView.
  5. Next we receive an OnStartElement event as the parser reaches the start tag for "Element3" in the XML document. CurrNode is pointing to our "Element1" node in the TreeView, when we add the item. This means that the "Element3" node is a sibling to "Element2" in our TreeView (just as they are in the XML document). At the end of the event the CurrNode points to the "Element3" node in the TreeView.
  6. It is important to note that we receive an immediate OnEndElement event for "Element3". In the XML document it appears as <Element3/> which is the shorthand XML for an empty element. Even empty elements will trigger OnStartElement and OnEndElement events.

At this point you should see how the events are functioning to fill the TreeView. The events continue in this manner until the end of the document is reached. If the parser encounters an error, it will stop firing the events. The TreeView will be filled until the point of the error in the document.